spb/traite-evaluation-qc
Public
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