SPB Git
28.1 KB · 387 lines python
1#!/usr/bin/env python32"""Chapitre 41 — Plus haute et meilleure utilisation, terrains et développement : calculs de l'exemple intégral.34Sujet : le terrain de 620 m² du cas 1 (cottage de 1998, 165 m², secteur Hull de Gatineau ; DONNÉES SYNTHÉTIQUES,5data/synthetic/cas1_gatineau_*). Le zonage retenu pour l'exemple est FICTIF (zone « H-2f » : 1 à 3 logements).67Quatre usages sont confrontés :8  S0 — maison actuelle conservée (valeur dans l'usage actuel : conclusion du ch. 20, 600 000 $) ;9  U  — démolition et maison neuve de 165 m² (prix de sortie : modèle hédonique du cas 1, âge 0) ;10  D  — démolition et duplex (2 × 120 m², vendus en copropriété divise) ;11  T  — démolition et triplex (3 × 100 m²).12Pour chacun : quatre tests de la PHMU, méthode résiduelle de développement avec calendrier (financement intérimaire13explicite via traite.finance.interets_intermediaires, actualisation du résidu, contribution pour fins de parcs),14sensibilité, puis option réelle de redéveloppement (arbre binomial à deux périodes) et seuil de Capozza–Helsley.1516Réutilise : traite.cas1 (hedonique, predire), traite.finance (interets_intermediaires, marge_sur_prix, marge_sur_cout),17ch21_cout_neuf (devis du cas 1 : D + I), ch08_zonage (densite_admissible, contribution_parcs, prime_option,18valeur_residuelle_redeveloppement pour la version statique).1920Usage : python3 analysis/python/cases/ch41_phmu.py21"""22from __future__ import annotations23import sys, pathlib, math24import numpy as np25import pandas as pd2627ROOT = pathlib.Path(__file__).resolve().parents[3]28sys.path.insert(0, str(ROOT / "analysis/python")); sys.path.insert(0, str(ROOT / "analysis/python/cases"))29from traite import cas1, finance as fin  # noqa: E40230import ch21_cout_neuf as c21              # noqa: E40231import ch08_zonage as c08                 # noqa: E4023233# ----------------------------------------------------------------------------- constantes partagées (provenance déclarée)34V_ACT = 600_000.0        # ch. 20 : valeur la plus probable conclue pour la maison du cas 1 (fourchette 575 000 – 640 000 $)35V_L_MARCHE = 277_000.0   # ch. 24/25 : valeur du terrain du sujet retenue par comparaison/extraction (fourchette 240 000 – 285 000 $)36IPCB_MAISON_T3_2026 = 116.5    # Statistique Canada 18-10-0289-01, Montréal, maison individuelle, T3 2026 (SRC-E-017)37IPCB_APPART_T3_2026 = 118.5    # idem, immeuble d'appartements de moins de cinq étages38TAUX_PRET = 0.0619       # taux hypothécaire affiché 5 ans, prêt conventionnel, 28 octobre 2026 (Banque du Canada, série V80691335 ; ch. 23)39PART_PRET = 0.75         # prêt de construction : 75 % des coûts (hypothèse, comme au ch. 23)40EFFICACITE = 0.85        # superficie habitable / aire brute de plancher (hypothèse, comme au ch. 23)4142# zone FICTIVE « H-2f » : hypothèses de zonage déclarées (grille calquée sur la colonne H2 de Val-Fictif, ch. 8, plafond ramené à 3 logements)43GRILLE = {"s_min_lot": 450.0, "s_min_par_logement": 150.0, "logements_max": 3, "cos_max": 0.60, "hauteur_max_etages": 3,44          "stationnement_par_logement": 1, "marge_avant": 6.0, "marge_lat": 2.0, "marge_arr": 9.0}4546# hypothèses communes du promoteur (déclarées dans le texte ; sensibilité à la fin)47HYP = {"demolition": 30_000.0,        # démolition, désamiantage sommaire, préparation du site (hypothèse, idem ch. 8)48       "frais_vente": 0.04,           # courtage, notaire, mise en marché : 4 % du produit de la vente (hypothèse)49       "marge": 0.15, "marge_base": "prix",   # marge du promoteur-marchand : 15 % du produit de la vente (hypothèse, idem ch. 8)50       "taux_parcs": 0.10,            # contribution pour fins de parcs exigée au redéveloppement : 10 % de la valeur du site (LAU 117.4, plafond)51       "taux_pret": TAUX_PRET, "part_pret": PART_PRET, "r_portage": TAUX_PRET}5253# variante « construction sur commande » (acheteur-occupant qui fait bâtir) : la convention du ch. 21 — incitatif de 10 % des coûts de54# construction (directs, indirects, démolition, financement), pas de frais de vente (rien n'est vendu), pas de contribution pour fins de parcs55HYP_OCCUPANT = {**HYP, "frais_vente": 0.0, "marge": 0.10, "marge_base": "cout", "taux_parcs": 0.0}5657# programmes de construction (hypothèses) ; facteur_prix = décote déclarée d'une unité superposée/contiguë vendue en copropriété divise58# par rapport à la maison isolée neuve de même superficie que prédit le modèle hédonique (1,00 = maison isolée)59PROGRAMMES = {60    "S0": {"nom": "Maison actuelle conservée", "n": 1, "s_unite": 165.0, "garage": 1, "sous_sol": 1, "facteur_prix": 1.0, "duree_ans": 0.0, "neuf": False},61    "U": {"nom": "Maison neuve de 165 m²", "n": 1, "s_unite": 165.0, "garage": 1, "sous_sol": 1, "facteur_prix": 1.00, "duree_ans": 1.00, "neuf": True},62    "D": {"nom": "Duplex, 2 × 120 m²", "n": 2, "s_unite": 120.0, "garage": 0, "sous_sol": 1, "facteur_prix": 0.90, "duree_ans": 1.25, "neuf": True},63    "T": {"nom": "Triplex, 3 × 100 m²", "n": 3, "s_unite": 100.0, "garage": 0, "sous_sol": 1, "facteur_prix": 0.85, "duree_ans": 1.50, "neuf": True},64    "Q": {"nom": "Quadruplex, 4 × 85 m²", "n": 4, "s_unite": 85.0, "garage": 0, "sous_sol": 1, "facteur_prix": 0.80, "duree_ans": 1.75, "neuf": True},65}666768# ----------------------------------------------------------------------------- intrants : coûts (ch. 21) et prix de sortie (cas 1)6970def couts_unitaires() -> dict:71    """Coût direct + indirect par m² habitable du devis du cas 1 (ch. 21), et transposition aux plex par le ratio IPCB (ch. 23)."""72    devis, sujet = c21.charger(); cn = c21.cout_neuf_sujet(devis)73    hab = float(sujet["superficie_habitable_m2"])74    di_m2 = (cn["direct"] + cn["indirects"]) / hab75    ratio = IPCB_APPART_T3_2026 / IPCB_MAISON_T3_202676    return {"direct": cn["direct"], "indirects": cn["indirects"], "incitatif": cn["incitatif"], "cout_neuf": cn["total"],77            "di_m2_maison": di_m2, "ratio_ipcb_plex": ratio, "di_m2_plex": di_m2 * ratio, "cn_m2_maison": cn["total"] / hab, "sujet": sujet}787980def modele_cas1() -> dict:81    d = cas1.charger(); m = cas1.hedonique(d["ventes"]); return {"modele": m, "sujet": d["sujet"]}828384def prix_unite(m: dict, sujet: dict, s_unite: float, n: int, garage: int, sous_sol: int, neuf: bool = True) -> float:85    """Prix prédit par le modèle hédonique du cas 1 pour une habitation isolée neuve de s_unite m² sur 620/n m² de terrain,86    à Hull, à la date d'évaluation (correction de Duan incluse). La décote d'unité superposée est appliquée ensuite (facteur_prix)."""87    x = dict(sujet); x.update({"superficie_habitable_m2": s_unite, "superficie_terrain_m2": sujet["superficie_terrain_m2"] / n,88                               "garage_places": garage, "sous_sol_fini": sous_sol})89    if neuf:90        x["age"] = 0; x["annee_construction"] = 202691    return float(cas1.predire(m, cas1.sujet_df(x))[0])929394def produit_vente(m: dict, sujet: dict, prog: dict) -> dict:95    p_isolee = prix_unite(m, sujet, prog["s_unite"], prog["n"], prog["garage"], prog["sous_sol"], prog["neuf"])96    p_unite = p_isolee * prog["facteur_prix"]97    return {"prix_isolee_equiv": p_isolee, "prix_unite": p_unite, "gdv": p_unite * prog["n"], "s_total": prog["s_unite"] * prog["n"]}9899100# ----------------------------------------------------------------------------- les quatre tests101102def test_legal(prog: dict, grille: dict = GRILLE, s_terrain: float = 620.0) -> dict:103    g = c08.densite_admissible(s_terrain, grille["s_min_par_logement"], grille["logements_max"], grille["cos_max"], grille["s_min_lot"])104    brut = prog["n"] * prog["s_unite"] / EFFICACITE105    ok_n = prog["n"] <= g["logements_admissibles"]; ok_cos = brut <= g["aire_plancher_max_m2"]106    return {"logements_admissibles": g["logements_admissibles"], "aire_max_m2": g["aire_plancher_max_m2"], "aire_brute_m2": brut,107            "ok_logements": ok_n, "ok_cos": ok_cos, "permis": bool(g["lot_conforme"] and ok_n and ok_cos)}108109110def test_physique(prog: dict, grille: dict = GRILLE, facade_m: float = 20.0, profondeur_m: float = 31.0) -> dict:111    """Enveloppe constructible après marges (façade et profondeur du lot : hypothèses, le dossier du cas ne les précise pas) ;112    stationnement : 1 case par logement de 2,6 m × 5,5 m en cour avant/latérale (hypothèse)."""113    largeur = facade_m - 2 * grille["marge_lat"]; prof = profondeur_m - grille["marge_avant"] - grille["marge_arr"]114    empreinte_max = largeur * prof115    brut = prog["n"] * prog["s_unite"] / EFFICACITE116    etages = math.ceil(brut / empreinte_max) if empreinte_max > 0 else 99117    ok_etages = etages <= grille["hauteur_max_etages"]118    cases = prog["n"] * grille["stationnement_par_logement"]; ok_stat = cases * 2.6 <= largeur + 2 * grille["marge_lat"]  # cases côte à côte en façade119    return {"empreinte_max_m2": empreinte_max, "etages_requis": etages, "ok_etages": ok_etages, "cases": cases, "ok_stationnement": ok_stat,120            "possible": bool(ok_etages and ok_stat)}121122123# ----------------------------------------------------------------------------- méthode résiduelle de développement (dynamique)124125def residuel_developpement(gdv: float, s_total: float, di_m2: float, duree_ans: float, hyp: dict = HYP, demolir: bool = True) -> dict:126    """Valeur résiduelle du terrain avec calendrier :127        V_L = [GDV(1 − c_v) − C − D − F − m·GDV] / [(1 + τ_parcs)(1 + r)^T]128    où C = coûts directs + indirects (di_m2 × s_total), D = démolition, F = intérêts intérimaires du prêt de construction129    (tirage uniforme, mise de fonds d'abord), m = marge sur le prix, τ_parcs = contribution pour fins de parcs (assise sur130    la valeur du site), r = taux de portage du terrain, T = durée totale (chantier + vente). r = 0 redonne la version131    statique du ch. 8 (contribution incluse) ; r = 0 et τ = 0 redonnent le résidu « de manuel »."""132    C = di_m2 * s_total; D = hyp["demolition"] if demolir else 0.0133    couts_fin = C + D134    pret = hyp["part_pret"] * couts_fin135    F = fin.interets_intermediaires(pret, hyp["taux_pret"], duree_ans, mode="continu", mise_de_fonds=couts_fin - pret, cout_total=couts_fin)["interets"] if duree_ans > 0 else 0.0136    frais = hyp["frais_vente"] * gdv137    marge = hyp["marge"] * gdv if hyp.get("marge_base", "prix") == "prix" else hyp["marge"] * (C + D + F)138    reste = gdv - frais - C - D - F - marge139    facteur_temps = (1.0 + hyp["r_portage"]) ** duree_ans140    v_l = reste / ((1.0 + hyp["taux_parcs"]) * facteur_temps)141    return {"gdv": gdv, "frais_vente": frais, "couts_construction": C, "demolition": D, "pret": pret, "financement": F, "marge": marge,142            "reste": reste, "facteur_temps": facteur_temps, "valeur_residuelle": v_l, "contribution_parcs": v_l * hyp["taux_parcs"],143            "reste_statique": reste, "v_l_statique": reste / (1.0 + hyp["taux_parcs"]), "cout_total_hors_terrain": C + D + F + frais + marge,144            "marge_sur_cout": marge / (C + D + F + frais + v_l) if (C + D + F + frais + v_l) > 0 else float("nan")}145146147def seuil_gdv(res: dict, v_cible: float, hyp: dict = HYP) -> float:148    """Produit de vente qui rend la valeur résiduelle égale à v_cible (résolution de la forme linéaire en GDV)."""149    a, b = _a_b(res, hyp)150    return (v_cible + b) / a151152153def _a_b(res: dict, hyp: dict) -> tuple[float, float]:154    """Forme linéaire du résidu : V_L = a·GDV − b. La marge entre dans a si elle est assise sur le prix, dans b si elle est assise sur le coût."""155    den = (1.0 + hyp["taux_parcs"]) * res["facteur_temps"]156    fixes = res["couts_construction"] + res["demolition"] + res["financement"]157    if hyp.get("marge_base", "prix") == "prix":158        return (1.0 - hyp["frais_vente"] - hyp["marge"]) / den, fixes / den159    return (1.0 - hyp["frais_vente"]) / den, fixes * (1.0 + hyp["marge"]) / den160161162# ----------------------------------------------------------------------------- option réelle : arbre binomial163164def parametres_binomiaux(sigma: float, dt: float, r: float, y: float) -> dict:165    """u = e^{σ√Δt}, d = 1/u ; probabilité risque-neutre d'un actif à rendement de détention y :166    q = (F − d)/(u − d), F = [(1 + r)/(1 + y)]^Δt (facteur de croissance à terme)."""167    u = math.exp(sigma * math.sqrt(dt)); d = 1.0 / u168    F = ((1.0 + r) / (1.0 + y)) ** dt169    q = (F - d) / (u - d)170    return {"u": u, "d": d, "F": F, "q": q, "disc": (1.0 + r) ** (-dt)}171172173def option_redeveloppement(gdv0: float, a: float, b: float, sigma: float, dt: float, r: float, y: float, n: int = 2) -> dict:174    """Option américaine de redéveloppement sur n périodes de Δt années. À chaque nœud, le propriétaire peut vendre à un175    promoteur (ou se faire promoteur) pour V_L(GDV) = a·GDV − b' ; la prime par rapport à la valeur en usage V_act est176    max(a·GDV − b, 0) avec b = b' + V_act. Rend la prime (américaine et européenne), la règle VAN immédiate et la politique177    d'exercice par nœud."""178    p = parametres_binomiaux(sigma, dt, r, y); u, d, q, disc = p["u"], p["d"], p["q"], p["disc"]179    payoff = lambda g: max(a * g - b, 0.0)180    noeuds = {}181    V = [payoff(gdv0 * u ** j * d ** (n - j)) for j in range(n + 1)]182    for j in range(n + 1):183        noeuds[(n, j)] = {"gdv": gdv0 * u ** j * d ** (n - j), "exercice": V[j], "continuation": None, "valeur": V[j]}184    V_eur = list(V)185    for t in range(n - 1, -1, -1):186        Vn, Vn_eur = [], []187        for j in range(t + 1):188            g = gdv0 * u ** j * d ** (t - j)189            cont = disc * (q * V[j + 1] + (1 - q) * V[j]); ex = payoff(g)190            Vn.append(max(ex, cont)); Vn_eur.append(disc * (q * V_eur[j + 1] + (1 - q) * V_eur[j]))191            noeuds[(t, j)] = {"gdv": g, "exercice": ex, "continuation": cont, "valeur": max(ex, cont), "exercer": ex > cont and ex > 0}192        V, V_eur = Vn, Vn_eur193    return {**p, "prime_americaine": V[0], "prime_europeenne": V_eur[0], "van_immediate": payoff(gdv0), "noeuds": noeuds,194            "gdv_seuil_van": b / a, "a": a, "b": b, "n": n, "dt": dt, "sigma": sigma, "r": r, "y": y}195196197def coefficients_option(res: dict, v_act: float, hyp: dict = HYP) -> tuple[float, float]:198    """V_L(GDV) = a·GDV − b' (forme linéaire du résidu) ; prime d'exercice = a·GDV − (b' + V_act)."""199    a, b_prime = _a_b(res, hyp)200    return a, b_prime + v_act201202203def frontiere_exercice_t1(a: float, b: float, sigma: float, dt: float, r: float, y: float) -> float:204    """Plus petit GDV au nœud t = 1 (arbre à deux périodes) où l'exercice immédiat vaut au moins la continuation (bissection)."""205    p = parametres_binomiaux(sigma, dt, r, y); u, d, q, disc = p["u"], p["d"], p["q"], p["disc"]206    f = lambda g: (a * g - b) - disc * (q * max(a * g * u - b, 0.0) + (1 - q) * max(a * g * d - b, 0.0))207    lo, hi = b / a, 50 * b / a208    if f(hi) < 0:209        return float("inf")210    for _ in range(200):211        mid = 0.5 * (lo + hi)212        if f(mid) >= 0:213            hi = mid214        else:215            lo = mid216    return hi217218219# ----------------------------------------------------------------------------- Capozza–Helsley (version simplifiée)220221def seuil_capozza_helsley(R_A: float, C: float, r: float, g: float, sigma: float) -> dict:222    """Rente de réserve R* déclenchant la conversion lorsque la rente du nouvel usage suit dR = g dt + σ dz :223        R* = R_A + rC − g/r + 1/α,  α = [−g + sqrt(g² + 2 r σ²)] / σ²   (σ → 0 : R* → R_A + rC).224    Prime d'irréversibilité = 1/α − g/r (nulle sans incertitude)."""225    if sigma <= 0:226        alpha = r / g if g > 0 else float("inf"); prime = 0.0227    else:228        alpha = 2.0 * r / (g + math.sqrt(g * g + 2.0 * r * sigma * sigma))   # forme rationalisée de [−g + √(g² + 2rσ²)]/σ², stable quand σ → 0229        prime = 1.0 / alpha - g / r230    R_cert = R_A + r * C231    return {"alpha": alpha, "R_certitude": R_cert, "prime_irreversibilite": prime, "R_seuil": R_cert + prime}232233234# ----------------------------------------------------------------------------- exemple intégral235236OPTION = {"sigma": 0.10, "dt": 5.0, "r": 0.02, "y": 0.045, "n": 2}   # volatilité réelle annuelle du prix des unités, pas de 5 ans, taux réel, rendement locatif net237238239def prix_logement_plex_existant() -> dict:240    """Prix par logement des triplex existants du cas 2 (Québec, Limoilou ; traite.cas2) — contrepoint « vente en bloc »."""241    from traite import cas2242    d = cas2.charger(); c = cas2.comparables_plex(d["ventes"], d["sujet"]); ind = cas2.indicateurs_marche(c)243    return {"prix_par_logement_triplex": ind["prix_par_logement_triplex"], "mrb_triplex": ind["mrb_median_triplex"], "n": ind["n"]}244245246def exemple_complet() -> dict:247    cu = couts_unitaires(); mc = modele_cas1(); m, sujet = mc["modele"], mc["sujet"]248    rows, res, tests = [], {}, {}249    for k, prog in PROGRAMMES.items():250        pv = produit_vente(m, sujet, prog) if prog["neuf"] else {"prix_isolee_equiv": float(cas1.predire(m, cas1.sujet_df(sujet))[0]), "prix_unite": V_ACT, "gdv": V_ACT, "s_total": 165.0}251        tl = test_legal(prog); tp = test_physique(prog)252        di = cu["di_m2_maison"] if prog["n"] == 1 else cu["di_m2_plex"]253        if prog["neuf"]:254            r_prom = residuel_developpement(pv["gdv"], pv["s_total"], di, prog["duree_ans"], HYP)255            r_occ = residuel_developpement(pv["gdv"], pv["s_total"], di, prog["duree_ans"], HYP_OCCUPANT)256        else:257            r_prom = r_occ = None258        res[k] = {"programme": prog, "vente": pv, "legal": tl, "physique": tp, "promoteur": r_prom, "occupant": r_occ, "di_m2": di}259        rows.append({"usage": k, "nom": prog["nom"], "n": prog["n"], "s_total": pv["s_total"], "prix_unite": pv["prix_unite"], "gdv": pv["gdv"],260                     "permis": tl["permis"], "possible": tp["possible"],261                     "v_l_promoteur": r_prom["valeur_residuelle"] if r_prom else float("nan"),262                     "v_l_occupant": r_occ["valeur_residuelle"] if r_occ else float("nan")})263    tableau = pd.DataFrame(rows)264    # contrepoint : triplex vendu en bloc à un investisseur au prix par logement des triplex existants du cas 2265    bloc = prix_logement_plex_existant(); uT = res["T"]266    gdv_bloc = bloc["prix_par_logement_triplex"] * 3267    res["T_bloc"] = {"gdv": gdv_bloc, "vente": {"gdv": gdv_bloc}, "promoteur": residuel_developpement(gdv_bloc, uT["vente"]["s_total"], uT["di_m2"], uT["programme"]["duree_ans"], HYP), **bloc}268    # PHMU du terrain vacant : usages permis et possibles, résidu positif ; maximalement productif = plus grande valeur résiduelle269    # (marge du promoteur pour les plex ; marge du constructeur sur commande pour la maison, usage dont l'acheteur type est un occupant)270    cand = tableau[(tableau.usage != "S0") & tableau.permis & tableau.possible].copy()271    cand["v_l_retenue"] = np.where(cand.n == 1, cand.v_l_occupant, cand.v_l_promoteur)272    cand["faisable"] = cand["v_l_retenue"] > 0273    cand_f = cand[cand.faisable]274    phmu_vacant = cand_f.loc[cand_f.v_l_retenue.idxmax(), "usage"]275    phmu_vacant_promoteur = cand_f.loc[cand_f.v_l_promoteur.idxmax(), "usage"]276    # PHMU du bien amélioré : max(V_act, V_L − D) — la démolition est déjà incluse dans V_L277    meilleure_redev = cand["v_l_promoteur"].max()278    primes = {u: c08.prime_option(V_ACT, v) for u, v in zip(cand.usage, cand.v_l_promoteur)}279    phmu_ameliore = "S0" if V_ACT >= meilleure_redev else cand.loc[cand.v_l_promoteur.idxmax(), "usage"]280    contribution_batiment = V_ACT - V_L_MARCHE281    # seuils de bascule (triplex, convention promoteur)282    rT = res["T"]["promoteur"]; seuil_T = seuil_gdv(rT, V_ACT); hausse_T = seuil_T / rT["gdv"] - 1283    seuil_T_marche = seuil_gdv(rT, V_L_MARCHE); hausse_T_marche = seuil_T_marche / rT["gdv"] - 1284    # option réelle (triplex) : arbre binomial à deux périodes de 5 ans285    a, b = coefficients_option(rT, V_ACT); O = OPTION286    opt = option_redeveloppement(rT["gdv"], a, b, O["sigma"], O["dt"], O["r"], O["y"], O["n"])287    front = frontiere_exercice_t1(a, b, O["sigma"], O["dt"], O["r"], O["y"])288    # sensibilité de l'option : σ et V_act289    sens_opt = {}290    for v_act in (600_000.0, 450_000.0, 300_000.0):291        a_, b_ = coefficients_option(rT, v_act)292        sens_opt[v_act] = {s: option_redeveloppement(rT["gdv"], a_, b_, s, O["dt"], O["r"], O["y"], O["n"])["prime_americaine"] for s in (0.05, 0.10, 0.15, 0.20, 0.25)}293    # Capozza–Helsley : rente implicite de la maison R_A = y·V_act ; rente du triplex R = y·GDV ; coût de conversion C = coûts + démolition + financement294    ch = {"R_A": O["y"] * V_ACT, "R_T": O["y"] * rT["gdv"], "C": rT["couts_construction"] + rT["demolition"] + rT["financement"]}295    ch["g"] = 0.01 * ch["R_T"]; ch["sigma"] = 0.10 * ch["R_T"]296    ch["resultat"] = seuil_capozza_helsley(ch["R_A"], ch["C"], O["y"], ch["g"], ch["sigma"])297    ch["hausse_requise_certitude"] = ch["resultat"]["R_certitude"] / ch["R_T"] - 1; ch["hausse_requise_incertitude"] = ch["resultat"]["R_seuil"] / ch["R_T"] - 1298    # sensibilité de la valeur résiduelle du triplex (convention promoteur)299    sens = sensibilite_triplex(res["T"], cu)300    return {"couts": cu, "modele": m, "sujet": sujet, "usages": res, "tableau": tableau, "candidats": cand, "phmu_vacant": phmu_vacant,301            "phmu_vacant_promoteur": phmu_vacant_promoteur, "phmu_ameliore": phmu_ameliore, "primes": primes, "contribution_batiment": contribution_batiment,302            "seuil_gdv_T": seuil_T, "hausse_requise_T": hausse_T, "seuil_gdv_T_marche": seuil_T_marche, "hausse_requise_T_marche": hausse_T_marche,303            "option": opt, "a": a, "b": b, "frontiere_t1": front, "sens_option": sens_opt, "capozza": ch, "sensibilite": sens, "v_act": V_ACT, "v_l_marche": V_L_MARCHE}304305306def sensibilite_triplex(uT: dict, cu: dict) -> pd.DataFrame:307    """Variations une à une autour du cas de base (triplex, convention promoteur)."""308    prog = uT["programme"]; pv = uT["vente"]; base = uT["promoteur"]["valeur_residuelle"]309    def run(gdv=pv["gdv"], di=cu["di_m2_plex"], duree=prog["duree_ans"], **kw):310        h = {**HYP, **kw}; return residuel_developpement(gdv, pv["s_total"], di, duree, h)["valeur_residuelle"]311    lignes = [312        ("Prix des unités", "−15 %", run(gdv=pv["gdv"] * 0.85), "+15 %", run(gdv=pv["gdv"] * 1.15)),313        ("Coût de construction", "−10 %", run(di=cu["di_m2_plex"] * 0.90), "+10 %", run(di=cu["di_m2_plex"] * 1.10)),314        ("Marge du promoteur", "10 % du prix", run(marge=0.10), "20 % du prix", run(marge=0.20)),315        ("Durée totale", "12 mois", run(duree=1.0), "24 mois", run(duree=2.0)),316        ("Taux (prêt et portage)", "5,00 %", run(taux_pret=0.05, r_portage=0.05), "7,50 %", run(taux_pret=0.075, r_portage=0.075)),317        ("Contribution pour fins de parcs", "0 %", run(taux_parcs=0.0), "10 %", base),318        ("Frais de vente", "2 %", run(frais_vente=0.02), "6 %", run(frais_vente=0.06)),319    ]320    return pd.DataFrame(lignes, columns=["parametre", "bas_etiquette", "bas", "haut_etiquette", "haut"]).assign(base=base)321322323def courbe_densite(res: dict, cu: dict, facteurs_prix=(0.85, 1.0, 1.15)) -> pd.DataFrame:324    """Valeur résiduelle (convention promoteur) selon le nombre de logements, pour plusieurs niveaux de prix."""325    rows = []326    for k in ("U", "D", "T", "Q"):327        u = res["usages"][k]; prog = u["programme"]328        for f in facteurs_prix:329            r = residuel_developpement(u["vente"]["gdv"] * f, u["vente"]["s_total"], u["di_m2"], prog["duree_ans"], HYP)330            rows.append({"usage": k, "n": prog["n"], "facteur_prix": f, "v_l": r["valeur_residuelle"], "permis": u["legal"]["permis"]})331    return pd.DataFrame(rows)332333334def surface_prix_cout(uT: dict, cu: dict, fp=np.linspace(0.80, 1.40, 61), fc=np.linspace(0.80, 1.20, 41)) -> tuple[np.ndarray, np.ndarray, np.ndarray]:335    prog = uT["programme"]; pv = uT["vente"]336    Z = np.array([[residuel_developpement(pv["gdv"] * p, pv["s_total"], cu["di_m2_plex"] * c, prog["duree_ans"], HYP)["valeur_residuelle"] for p in fp] for c in fc])337    return fp, fc, Z338339340def _d(x): return f"{x:,.0f} $".replace(",", " ")341342343def main() -> dict:344    R = exemple_complet(); cu = R["couts"]345    print("=== Ch. 41 — PHMU du terrain du cas 1 (620 m², Hull ; DONNÉES SYNTHÉTIQUES ; zonage FICTIF H-2f) ===")346    print(f"Coûts ch. 21 : D = {_d(cu['direct'])} ; I = {_d(cu['indirects'])} ; D+I/m² hab. = {cu['di_m2_maison']:.2f} $/m² ; ratio IPCB plex {cu['ratio_ipcb_plex']:.4f} → {cu['di_m2_plex']:.2f} $/m²")347    print(f"V_act (ch. 20) = {_d(R['v_act'])} ; V_L marché (ch. 24/25) = {_d(R['v_l_marche'])} ; contribution du bâtiment = {_d(R['contribution_batiment'])}")348    print("\n--- Programmes, tests et produits de vente ---")349    for k, u in R["usages"].items():350        if "legal" not in u:351            continue352        tl, tp, pv = u["legal"], u["physique"], u["vente"]353        print(f"{k:>2} {u['programme']['nom']:<28} n={u['programme']['n']} S={pv['s_total']:.0f} m² brut={tl['aire_brute_m2']:.0f} (max {tl['aire_max_m2']:.0f}) "354              f"permis={tl['permis']} possible={tp['possible']} (étages {tp['etages_requis']}, cases {tp['cases']}) | prix isolée équiv. {_d(pv['prix_isolee_equiv'])} → unité {_d(pv['prix_unite'])} ; GDV {_d(pv['gdv'])}")355    print("\n--- Résidu de développement (convention promoteur : marge 15 % du prix, frais de vente 4 %, parcs 10 %, prêt 75 % à 6,19 %, portage 6,19 %) ---")356    for k in ("U", "D", "T", "Q"):357        r = R["usages"][k]["promoteur"]358        print(f"{k}: GDV {_d(r['gdv'])} − frais {_d(r['frais_vente'])} − C {_d(r['couts_construction'])} − démol. {_d(r['demolition'])} − F {_d(r['financement'])} − marge {_d(r['marge'])} = reste {_d(r['reste'])} ; "359              f"/(1,10 × {r['facteur_temps']:.4f}) → V_L = {_d(r['valeur_residuelle'])} (parcs {_d(r['contribution_parcs'])}) ; statique {_d(r['v_l_statique'])}")360    print("--- Convention constructeur sur commande (incitatif 10 % des coûts de construction, pas de frais de vente ni de parcs) ---")361    for k in ("U", "D", "T"):362        r = R["usages"][k]["occupant"]; print(f"{k}: V_L = {_d(r['valeur_residuelle'])} (marge {_d(r['marge'])}, reste {_d(r['reste'])}, F {_d(r['financement'])})")363    tb = R["usages"]["T_bloc"]; rb = tb["promoteur"]364    print(f"--- Triplex vendu en bloc (cas 2 : {_d(tb['prix_par_logement_triplex'])} par logement, n = {tb['n']} ventes) : GDV {_d(tb['gdv'])} → reste {_d(rb['reste'])} → V_L = {_d(rb['valeur_residuelle'])}")365    print(f"\nPHMU du terrain vacant : {R['phmu_vacant']} (par résidu promoteur seul : {R['phmu_vacant_promoteur']}) ; PHMU du bien amélioré : {R['phmu_ameliore']}")366    print("Primes d'option immédiates max(0, V_L − V_act) : " + ", ".join(f"{u} {_d(p)}" for u, p in R["primes"].items()))367    print(f"Seuil GDV triplex pour égaler V_act : {_d(R['seuil_gdv_T'])} (+{R['hausse_requise_T']*100:.1f} %) ; pour égaler V_L marché : {_d(R['seuil_gdv_T_marche'])} ({R['hausse_requise_T_marche']*100:+.1f} %)")368    o = R["option"]369    print(f"\n--- Option réelle (triplex) : σ = {o['sigma']:.2f}, Δt = {o['dt']:.0f} ans, r = {o['r']:.3f}, y = {o['y']:.3f} ; u = {o['u']:.4f}, d = {o['d']:.4f}, F = {o['F']:.4f}, q = {o['q']:.4f}, disc = {o['disc']:.4f}")370    print(f"a = {R['a']:.4f} ; b = {_d(R['b'])} ; GDV* (VAN nulle) = {_d(o['gdv_seuil_van'])} ; frontière d'exercice à t = 1 : {_d(R['frontiere_t1'])}")371    for (t, j), nd in sorted(o["noeuds"].items()):372        print(f"  nœud t={t} j={j}: GDV {_d(nd['gdv'])} ; exercice {_d(nd['exercice'])} ; continuation {('—' if nd['continuation'] is None else _d(nd['continuation']))} ; valeur {_d(nd['valeur'])}")373    print(f"Prime américaine {_d(o['prime_americaine'])} ({o['prime_americaine']/R['v_act']*100:.2f} % de V_act) ; européenne {_d(o['prime_europeenne'])} ; VAN immédiate {_d(o['van_immediate'])}")374    print("Sensibilité de la prime (lignes V_act, colonnes σ = 5/10/15/20/25 %) :")375    for v, d in R["sens_option"].items():376        print(f"  V_act {_d(v)} : " + "  ".join(_d(x) for x in d.values()))377    ch = R["capozza"]; cr = ch["resultat"]378    print(f"\n--- Capozza–Helsley : R_A {_d(ch['R_A'])}/an ; R_T {_d(ch['R_T'])}/an ; C {_d(ch['C'])} ; g {_d(ch['g'])}/an ; σ {_d(ch['sigma'])}/an ; r = y = {OPTION['y']}")379    print(f"α = {cr['alpha']:.6e} ; R* certitude {_d(cr['R_certitude'])} (+{ch['hausse_requise_certitude']*100:.1f} %) ; prime d'irréversibilité {_d(cr['prime_irreversibilite'])} ; R* {_d(cr['R_seuil'])} (+{ch['hausse_requise_incertitude']*100:.1f} %)")380    print("\n--- Sensibilité V_L triplex (promoteur) ---")381    print(R["sensibilite"].assign(bas=lambda d: d.bas.round(0), haut=lambda d: d.haut.round(0), base=lambda d: d.base.round(0)).to_string(index=False))382    return R383384385if __name__ == "__main__":386    main()387
Files · da8a4ee0304be0aa9c54ed0aad08d985fc4539fc
Menu

Theme