spb/traite-evaluation-qc
Public
1#!/usr/bin/env python32"""Chapitre 24 — dépréciation et désuétudes du sujet du cas 1 (Gatineau, DONNÉES SYNTHÉTIQUES).34Imprime tous les chiffres cités dans le chapitre 24 :5 * âges (chronologique, effectif selon la convention du ch. 11), vie économique et vie restante ;6 * dépréciation physique : âge-vie simple (âge chronologique / âge effectif), âge-vie modifiée, ventilée par composantes7 (fiche d'état et travaux du ch. 11, traite.cout.depreciation_ventilee) ;8 * désuétude fonctionnelle : carence curable du sujet (salle d'eau), exemples incurables (perte de loyer × MRB du cas 2 ;9 perte de RNE capitalisée) ; désuétude externe (ventes appariées, capitalisation) ;10 * extraction de marché sur les 600 ventes du cas 1 : coût neuf par vente (élasticité de taille du ch. 21), prix ramenés à la11 date d'évaluation (tendance du modèle hédonique), valeur de terrain par secteur extraite des ventes récentes, dépréciation12 par vente, profil en fonction de l'âge (médianes par tranche, ajustements linéaire et quadratique) ;13 * profils comparés : âge-vie linéaire, extraction, courbe hédonique (coefficients âge et âge² de traite.cas1.hedonique)14 et profil « vrai » du générateur (data/synthetic/README.md), convertis en dépréciation du bâtiment ;15 * valeur par le coût du sujet (traite.cout.valeur_par_cout) selon deux lectures de la dépréciation, et sensibilité.1617Usage (racine du dépôt) : PYTHONPATH=analysis/python python3 analysis/python/cases/ch24_depreciation.py18Toutes les formules de dépréciation proviennent de analysis/python/traite/cout.py (aucune réécriture)."""19from __future__ import annotations20import sys, pathlib21import numpy as np, pandas as pd22sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1]))23sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent))24from traite import cout as ct, cas125from traite import hedonique as h26import ch21_cout_neuf as c2127import ch11_inspection as c112829ROOT = pathlib.Path(__file__).resolve().parents[3]3031# --- conventions et hypothèses déclarées ---------------------------------------------------------------------32VIE_ECONOMIQUE = c11.VIE_ECONOMIQUE # 60 ans (convention des ch. 11 et 24, hypothèse pédagogique)33AGE_CHRONO = 28 # 1998 → 202634DATE_EVAL = "2026-09-01"35ELASTICITE_TAILLE = -0.10 # ch. 21 : coût unitaire ∝ (S/S_réf)^ε36FACTEUR_QUALITE = 0.05 # coût neuf ± 5 % par classe de qualité (hypothèse pédagogique)37AGE_MAX_RECENT = 10 # ventes « récentes » servant à extraire la valeur du terrain38S_REF = 165.039# Carence fonctionnelle curable du sujet (constat FICTIF du ch. 24) : aucune salle d'eau au rez-de-chaussée40SALLE_EAU_AJOUT = 16000.0 # soumission fictive : ajout a posteriori, installé (hypothèse pédagogique)41SALLE_EAU_POSTES = {"Plomberie brute et finition": 2.0, "Vanités et accessoires (2 sdb)": 0.5, "Portes intérieures et moulures": 1.0,42 "Gypse et tirage de joints": 12.0, "Peinture intérieure": 12.0, "Planchers céramique": 3.0} # quantités si incluse à neuf43# Exemples illustratifs (hypothèses pédagogiques, hors sujet)44TRIPLEX_PERTE_LOYER_MOIS = 75.0 # $/mois, logement sans entrée laveuse-sécheuse45RNE_PERTE_ANNUELLE = 6000.0 # $/an46TAUX_CAP_ILLUSTRATIF = 0.045 # milieu de la fourchette CBRE T3 2026 Montréal low-rise B (4,25-4,75 %), calibration.json SRC-E-04547PART_BATIMENT_ILLUSTRATIVE = 0.7048PAIRES_SANS, PAIRES_AVEC, PAIRES_PART_BAT = 520000.0, 490000.0, 0.60495051def charger() -> dict:52 devis, sujet, cn = c11.charger()53 d = cas1.charger()54 m = cas1.hedonique(d["ventes"])55 fiche = c11.fiche(devis, cn)56 trav = c11.travaux(devis, cn)57 return {"devis": devis, "sujet": sujet, "cn": cn, "ventes": d["ventes"], "modele": m, "fiche": fiche, "travaux": trav}585960# --------------------------------------------------------------------------------------------- âges et âge-vie6162def ages_sujet(fiche: pd.DataFrame) -> dict:63 s = fiche[fiche["longue_duree"]].iloc[0]64 pond = float((fiche["cout_neuf"] * fiche["ratio"]).sum() / fiche["cout_neuf"].sum())65 return {"chronologique": AGE_CHRONO, "effectif_structure": float(s.age_effectif), "vie_economique": VIE_ECONOMIQUE,66 "vie_restante": ct.vie_restante(VIE_ECONOMIQUE, float(s.age_effectif)),67 "ratio_pondere": pond, "age_effectif_pondere": pond * VIE_ECONOMIQUE, "indice_etat": c11.indice_etat(fiche)}686970def variantes_physique(cn: dict, fiche: pd.DataFrame, trav: pd.DataFrame) -> dict:71 """Quatre lectures de la dépréciation physique du sujet (toutes sur C_N = 397 567,33 $)."""72 a_eff = ages_sujet(fiche)["effectif_structure"]73 curable = float(trav["cout"].sum())74 simple_chrono = ct.depreciation_age_vie(cn["total"], AGE_CHRONO, VIE_ECONOMIQUE)75 simple_eff = ct.depreciation_age_vie(cn["total"], a_eff, VIE_ECONOMIQUE)76 modifiee = ct.depreciation_age_vie_modifiee(cn["total"], curable, a_eff, VIE_ECONOMIQUE)77 ventilee = c11.depreciation(fiche, trav, cn)78 return {"age_vie_chrono": simple_chrono, "age_vie_effectif": simple_eff, "modifiee": modifiee, "ventilee": ventilee,79 "curable": curable, "age_effectif": a_eff}808182# ------------------------------------------------------------------------------------------ désuétudes8384def carence_salle_eau(devis: pd.DataFrame, cn: dict) -> dict:85 """Désuétude fonctionnelle curable (carence) : coût d'ajout a posteriori − coût de l'élément s'il avait été inclus à neuf.86 Le coût « à neuf » est reconstitué à partir des prix unitaires du devis (quantités déclarées), majoré comme le coût neuf."""87 g = c11.facteur_majoration(cn)88 direct = 0.089 for poste, q in SALLE_EAU_POSTES.items():90 pu = float(devis.loc[devis["poste"] == poste, "prix_unitaire"].iloc[0])91 direct += pu * q92 cout_si_neuf = direct * g93 d = ct.desuetude_fonctionnelle_curable(SALLE_EAU_AJOUT, cout_si_neuf, 0.0)94 return {"cout_si_neuf_direct": direct, "cout_si_neuf": cout_si_neuf, "cout_ajout": SALLE_EAU_AJOUT, "desuetude": d}959697def exemples_incurables() -> dict:98 from traite import cas299 ind = cas2.indicateurs_marche(cas2.comparables_plex(cas2.charger()["ventes"], cas2.charger()["sujet"]))100 mrb_annuel = ind["mrb_median_triplex"]101 loyer = ct.desuetude_incurable_par_loyer(TRIPLEX_PERTE_LOYER_MOIS, mrb_annuel * 12)102 rne = ct.desuetude_incurable_par_rne(RNE_PERTE_ANNUELLE, TAUX_CAP_ILLUSTRATIF, PART_BATIMENT_ILLUSTRATIVE)103 rne_total = ct.desuetude_incurable_par_rne(RNE_PERTE_ANNUELLE, TAUX_CAP_ILLUSTRATIF, 1.0)104 paires = ct.desuetude_externe_ventes_appariees(PAIRES_SANS, PAIRES_AVEC, PAIRES_PART_BAT)105 return {"mrb_annuel": mrb_annuel, "mrb_mensuel": mrb_annuel * 12, "perte_loyer_capitalisee": loyer,106 "perte_rne_capitalisee_totale": rne_total, "perte_rne_batiment": rne, "paires": paires}107108109# ------------------------------------------------------------------------------------------ extraction de marché110111def cout_neuf_ventes(v: pd.DataFrame, cn_total: float, elasticite: float = ELASTICITE_TAILLE, fq: float = FACTEUR_QUALITE) -> pd.Series:112 """Coût neuf de chaque vente, transposé du sujet : C_N × (S/165)^(1+ε) × (1 + fq·(qualité − 3))."""113 unit = ct.cout_par_superficie_ajuste(cn_total / S_REF, S_REF, v["superficie_habitable_m2"].astype(float), elasticite)114 return unit * v["superficie_habitable_m2"].astype(float) * (1.0 + fq * (v["qualite_1a5"].astype(float) - 3.0))115116117def prix_a_la_date(v: pd.DataFrame, modele: dict, date_eval: str = DATE_EVAL) -> pd.Series:118 """Prix ramenés à la date d'évaluation par la tendance mensuelle du modèle hédonique (ch. 17-18)."""119 g = modele["croissance_mensuelle"]120 mois = (pd.Timestamp(date_eval) - pd.to_datetime(v["date_vente"])).dt.days / 30.4375121 return v["prix_vente"].astype(float) * (1.0 + g) ** mois122123124def valeur_terrain_par_secteur(v: pd.DataFrame, p_adj: pd.Series, cn_i: pd.Series, age_max: int = AGE_MAX_RECENT) -> pd.DataFrame:125 """Valeur du terrain par secteur, extraite des ventes récentes : V_L = P_adj − C_N (1 − âge/L) (extraction, ch. 25)."""126 r = v[v["age"] <= age_max].copy()127 r["p_adj"] = p_adj[r.index]; r["cn"] = cn_i[r.index]128 r["dep"] = [ct.depreciation_age_vie(c, a, VIE_ECONOMIQUE) for c, a in zip(r["cn"], r["age"])]129 r["vl"] = r["p_adj"] - (r["cn"] - r["dep"])130 r["part"] = r["vl"] / r["p_adj"]131 g = r.groupby("secteur").agg(n=("vl", "size"), vl_mediane=("vl", "median"), vl_moyenne=("vl", "mean"), vl_ecart_type=("vl", "std"),132 part_mediane=("part", "median"), terrain_m2_median=("superficie_terrain_m2", "median"))133 g["vl_par_m2"] = g["vl_mediane"] / g["terrain_m2_median"]134 return g135136137def extraction(v: pd.DataFrame, modele: dict, cn_total: float, elasticite: float = ELASTICITE_TAILLE, fq: float = FACTEUR_QUALITE,138 age_max: int = AGE_MAX_RECENT) -> tuple[pd.DataFrame, pd.DataFrame]:139 """Dépréciation totale extraite de chaque vente : D = C_N − (P_adj − V_L) (traite.cout.extraction_depreciation_marche)."""140 w = v.copy()141 w["cn"] = cout_neuf_ventes(w, cn_total, elasticite, fq)142 w["p_adj"] = prix_a_la_date(w, modele)143 vl = valeur_terrain_par_secteur(w, w["p_adj"], w["cn"], age_max)144 w["vl"] = w["secteur"].map(vl["vl_mediane"])145 ext = [ct.extraction_depreciation_marche(p, l, c, a) for p, l, c, a in zip(w["p_adj"], w["vl"], w["cn"], w["age"])]146 w["contribution_batiment"] = [e["contribution_batiment"] for e in ext]147 w["D"] = [e["depreciation"] for e in ext]148 w["taux"] = [e["taux"] for e in ext]149 w["taux_annuel"] = [e.get("taux_annuel", np.nan) for e in ext]150 return w, vl151152153def ajuster_profil(w: pd.DataFrame) -> dict:154 """Ajustements du taux de dépréciation extrait sur l'âge : linéaire (constante), quadratique, linéaire par l'origine."""155 a = w["age"].astype(float).values; y = w["taux"].values; n = len(w)156 lin = h.mco(np.column_stack([np.ones(n), a]), y)157 quad = h.mco(np.column_stack([np.ones(n), a, a ** 2]), y)158 orig = h.mco(a.reshape(-1, 1), y)159 return {"lineaire": lin, "quadratique": quad, "origine": orig,160 "taux_annuel_origine": float(orig["beta"][0]), "vie_implicite": 1.0 / float(orig["beta"][0])}161162163def taux_quadratique(quad: dict, age) -> np.ndarray:164 b = quad["beta"]; age = np.asarray(age, dtype=float)165 return b[0] + b[1] * age + b[2] * age ** 2166167168def tranches(w: pd.DataFrame, bornes=(0, 10, 20, 30, 40, 50, 60, 100)) -> pd.DataFrame:169 g = w.groupby(pd.cut(w["age"], bornes), observed=True)["taux"].agg(["size", "median", "mean", "std"])170 g.index = [f"{int(i.left) + (1 if i.left else 0)}-{int(i.right)}" for i in g.index]171 return g172173174# ------------------------------------------------------------------------------------------ profils comparés175176def prix_neuf_sujet(modele: dict, sujet: dict) -> float:177 """Prédiction hédonique du sujet s'il était neuf (âge 0), même date, correction de Duan."""178 s = dict(sujet); s["age"] = 0; s["annee_construction"] = 2026179 return float(cas1.predire(modele, cas1.sujet_df(s))[0])180181182def profil_total_hedonique(modele: dict, age) -> np.ndarray:183 """Baisse du prix TOTAL avec l'âge selon le modèle : 1 − exp(β₁ âge + β₂ âge²)."""184 b1 = float(modele["coef"].loc["age", "coef"]); b2 = float(modele["coef"].loc["age2_100", "coef"]) / 100.0185 age = np.asarray(age, dtype=float)186 return 1.0 - np.exp(b1 * age + b2 * age ** 2)187188189def profil_total_generateur(age) -> np.ndarray:190 """Loi de génération du cas 1 (data/synthetic/README.md) : −0,0055·âge + 0,00003·âge²."""191 age = np.asarray(age, dtype=float)192 return 1.0 - np.exp(-0.0055 * age + 0.00003 * age ** 2)193194195def total_vers_batiment(d_total, alpha: float) -> np.ndarray:196 """Conversion : si le terrain vaut une part α du prix neuf et ne se déprécie pas, d_B = d_total / (1 − α)."""197 return np.asarray(d_total, dtype=float) / (1.0 - alpha)198199200def profils(modele: dict, sujet: dict, vl: float, quad: dict, ages=np.arange(0, 81)) -> dict:201 p0 = prix_neuf_sujet(modele, sujet)202 alpha = vl / p0203 return {"ages": ages, "prix_neuf": p0, "alpha": alpha,204 "age_vie": np.minimum(ages / VIE_ECONOMIQUE, 1.0),205 "extraction": taux_quadratique(quad, ages),206 "hedonique_total": profil_total_hedonique(modele, ages), "generateur_total": profil_total_generateur(ages),207 "hedonique_bat": total_vers_batiment(profil_total_hedonique(modele, ages), alpha),208 "generateur_bat": total_vers_batiment(profil_total_generateur(ages), alpha)}209210211# ------------------------------------------------------------------------------------------ valeur par le coût212213def valeur_terrain_retenue(vl: pd.DataFrame, secteur: str = "Hull") -> dict:214 med = float(vl.loc[secteur, "vl_mediane"])215 return {"mediane": med, "retenue": round(med / 1000.0) * 1000.0, "n": int(vl.loc[secteur, "n"]), "part": float(vl.loc[secteur, "part_mediane"])}216217218def valeur_sujet(cn: dict, vl_retenue: float, quad: dict, fonc: float, variantes: dict) -> dict:219 """Deux lectures : (A) dépréciation physique ventilée (ch. 11) + désuétude fonctionnelle ; (B) dépréciation extraite du marché220 à l'âge chronologique + désuétude fonctionnelle spécifique au sujet."""221 t28 = float(taux_quadratique(quad, AGE_CHRONO))222 d_ext = t28 * cn["total"]223 A = ct.valeur_par_cout(vl_retenue, cn["total"], variantes["ventilee"]["total"], fonc, 0.0)224 B = ct.valeur_par_cout(vl_retenue, cn["total"], d_ext, fonc, 0.0)225 return {"taux_extrait_28": t28, "d_extraite": d_ext, "A_ventilee": A, "B_extraction": B,226 "ecart": B["valeur"] - A["valeur"], "valeur_retenue": B["valeur"], "valeur_arrondie": round(B["valeur"] / 5000.0) * 5000.0}227228229# ------------------------------------------------------------------------------------------ sensibilité230231def grille_age_vie(cn: dict, curable: float, vies=(50, 60, 70, 80, 100), ages_eff=(20, 25, 29, 33, 37)) -> pd.DataFrame:232 """Taux de dépréciation (âge-vie modifiée, curable fixé) pour chaque couple (vie économique, âge effectif)."""233 rows = {}234 for L in vies:235 rows[L] = {a: ct.depreciation_age_vie_modifiee(cn["total"], curable, a, L)["total"] / cn["total"] for a in ages_eff}236 return pd.DataFrame(rows).T237238239def sensibilite_extraction(v: pd.DataFrame, modele: dict, cn_total: float) -> pd.DataFrame:240 """Taux extrait à 28 ans et valeur du terrain (Hull) selon les choix de l'extraction."""241 rows = []242 for nom, kw in [("cas de base", {}), ("ventes récentes ≤ 5 ans", {"age_max": 5}), ("ventes récentes ≤ 15 ans", {"age_max": 15}),243 ("élasticité −0,05", {"elasticite": -0.05}), ("élasticité −0,15", {"elasticite": -0.15}),244 ("qualité ±0 %", {"fq": 0.0}), ("qualité ±10 %", {"fq": 0.10})]:245 w, vl = extraction(v, modele, cn_total, **kw)246 q = ajuster_profil(w)["quadratique"]247 rows.append({"variante": nom, "vl_hull": float(vl.loc["Hull", "vl_mediane"]), "taux_28": float(taux_quadratique(q, AGE_CHRONO)),248 "taux_annuel": ajuster_profil(w)["taux_annuel_origine"]})249 return pd.DataFrame(rows)250251252def tornade(cn: dict, vl: float, d_ext: float, fonc: float, variantes: dict, sens_ext: pd.DataFrame) -> list[dict]:253 base = ct.valeur_par_cout(vl, cn["total"], d_ext, fonc)["valeur"]254 out = []255 def add(param, bas, haut, etiquette):256 out.append({"param": param, "bas": bas, "haut": haut, "base": base, "etiquette": etiquette})257 add("Valeur du terrain ±15 %", ct.valeur_par_cout(vl * 0.85, cn["total"], d_ext, fonc)["valeur"], ct.valeur_par_cout(vl * 1.15, cn["total"], d_ext, fonc)["valeur"], f"{vl*0.85:,.0f} → {vl*1.15:,.0f} $".replace(",", " "))258 t = sens_ext["taux_28"]259 add("Choix d'extraction (taux à 28 ans)", ct.valeur_par_cout(vl, cn["total"], t.max() * cn["total"], fonc)["valeur"], ct.valeur_par_cout(vl, cn["total"], t.min() * cn["total"], fonc)["valeur"], f"{t.max()*100:.1f} % → {t.min()*100:.1f} %")260 add("Coût neuf ±8,6 % (date des prix)", ct.valeur_par_cout(vl, cn["total"] / 1.086, d_ext / 1.086, fonc)["valeur"], ct.valeur_par_cout(vl, cn["total"] * 1.086, d_ext * 1.086, fonc)["valeur"], "indice ×0,921 → ×1,086")261 add("Désuétude fonctionnelle 0 → 2×", ct.valeur_par_cout(vl, cn["total"], d_ext, 2 * fonc)["valeur"], ct.valeur_par_cout(vl, cn["total"], d_ext, 0.0)["valeur"], f"{2*fonc:,.0f} → 0 $".replace(",", " "))262 add("Lecture ventilée (ch. 11) au lieu de l'extraction", ct.valeur_par_cout(vl, cn["total"], variantes["ventilee"]["total"], fonc)["valeur"], base, f"{variantes['ventilee']['total']/cn['total']*100:.1f} % → {d_ext/cn['total']*100:.1f} %")263 return out264265266# ------------------------------------------------------------------------------------------ main267268def main() -> dict:269 d = charger()270 cn, fiche, trav, v, m, sujet, devis = d["cn"], d["fiche"], d["travaux"], d["ventes"], d["modele"], d["sujet"], d["devis"]271 f2 = lambda x: f"{x:,.2f}".replace(",", " "); f0 = lambda x: f"{x:,.0f}".replace(",", " ")272 print("== DONNÉES SYNTHÉTIQUES — sujet cas 1 :", sujet["type"], sujet["annee_construction"], sujet["superficie_habitable_m2"], "m² hab.,", sujet["secteur"], "; évaluation", DATE_EVAL)273 print(f"== Coût neuf C_N (ch. 21) = {f2(cn['total'])} $")274 ag = ages_sujet(fiche)275 print(f"== Âges : chronologique {ag['chronologique']} ans ; effectif de la structure {ag['effectif_structure']:.1f} ans (convention ch. 11, λ = {c11.LAMBDA}) ; "276 f"vie économique {ag['vie_economique']:.0f} ans ; vie restante {ag['vie_restante']:.1f} ans ; indice d'état {ag['indice_etat']:.3f} ; "277 f"âge effectif pondéré par les coûts {ag['age_effectif_pondere']:.1f} ans")278 var = variantes_physique(cn, fiche, trav)279 print("== Dépréciation physique — quatre lectures :")280 print(f" âge-vie simple, âge chronologique 28/60 : {f2(var['age_vie_chrono'])} $ ({var['age_vie_chrono']/cn['total']*100:.2f} %)")281 print(f" âge-vie simple, âge effectif {var['age_effectif']:.0f}/60 : {f2(var['age_vie_effectif'])} $ ({var['age_vie_effectif']/cn['total']*100:.2f} %)")282 mo = var["modifiee"]283 print(f" âge-vie modifiée : curable {f2(mo['curable'])} + incurable {f2(mo['incurable'])} = {f2(mo['total'])} $ ({mo['total']/cn['total']*100:.2f} %)")284 ve = var["ventilee"]285 print(f" ventilée (ch. 11) : curable {f2(ve['curable'])} + courte durée {f2(sum(ve['courte_duree'].values()))} + longue durée {f2(ve['longue_duree'])} "286 f"(base {f2(ve['base_longue'])} × {ve['age_effectif_structure']:.0f}/60) = {f2(ve['total'])} $ ({ve['taux']*100:.2f} %)")287 courtes = fiche[(~fiche["longue_duree"]) & (fiche["composante"] != "Entrée asphaltée")]288 print(f" composantes de courte durée : {len(courtes)} ; coût neuf {f2(courtes['cout_neuf'].sum())} $ ; dépréciation {f2(sum(ve['courte_duree'].values()))} $ "289 f"({sum(ve['courte_duree'].values())/courtes['cout_neuf'].sum()*100:.1f} % de leur coût neuf)")290 fin = [(k, x, fiche.loc[fiche['composante'] == k, 'cout_neuf'].iloc[0]) for k, x in ve["courte_duree"].items()]291 for k, x, c in sorted(fin, key=lambda t: -t[1]):292 print(f" {k:58s} coût neuf {f2(c):>11s} $ dépréciation {f2(x):>11s} $ ({x/c*100:5.1f} %)")293 se = carence_salle_eau(devis, cn)294 print("== Désuétude fonctionnelle curable du sujet (carence : salle d'eau au rez-de-chaussée, constat fictif) :")295 print(f" coût si incluse à neuf : direct {f2(se['cout_si_neuf_direct'])} $ → majoré {f2(se['cout_si_neuf'])} $ ; ajout a posteriori {f2(se['cout_ajout'])} $ ; désuétude = {f2(se['desuetude'])} $")296 ex = exemples_incurables()297 print("== Exemples incurables (hypothèses pédagogiques) :")298 print(f" triplex cas 2 : perte {TRIPLEX_PERTE_LOYER_MOIS:.0f} $/mois × MRB mensuel {ex['mrb_mensuel']:.2f} (= 12 × {ex['mrb_annuel']:.2f}) = {f2(ex['perte_loyer_capitalisee'])} $")299 print(f" perte de RNE {f0(RNE_PERTE_ANNUELLE)} $/an / {TAUX_CAP_ILLUSTRATIF*100:.2f} % = {f2(ex['perte_rne_capitalisee_totale'])} $ ; part bâtiment {PART_BATIMENT_ILLUSTRATIVE:.0%} → {f2(ex['perte_rne_batiment'])} $")300 print(f" paires : ({f0(PAIRES_SANS)} − {f0(PAIRES_AVEC)}) × {PAIRES_PART_BAT:.0%} = {f2(ex['paires'])} $")301 w, vl = extraction(v, m, cn["total"])302 print(f"== Extraction de marché sur {len(w)} ventes : tendance {m['croissance_mensuelle']*100:.3f} %/mois ; C_N par vente = C_N × (S/165)^(1{ELASTICITE_TAILLE:+.2f}) × (1 ± {FACTEUR_QUALITE:.0%}/classe)")303 print(f" Valeur du terrain par secteur (ventes ≤ {AGE_MAX_RECENT} ans) :")304 print(vl.round(3).to_string())305 vlr = valeur_terrain_retenue(vl)306 print(f" → Hull : médiane {f2(vlr['mediane'])} $ sur {vlr['n']} ventes (part médiane du prix {vlr['part']*100:.1f} %) ; VALEUR DE TERRAIN RETENUE V_L = {f0(vlr['retenue'])} $ ({vlr['retenue']/620:.0f} $/m² pour 620 m²)")307 tr = tranches(w)308 print(" Taux de dépréciation extrait par tranche d'âge :"); print(tr.round(3).to_string())309 aj = ajuster_profil(w)310 q = aj["quadratique"]; l = aj["lineaire"]311 print(f" linéaire : taux = {l['beta'][0]:.4f} + {l['beta'][1]:.5f}·âge (se {l['se_hc1'][1]:.5f}), R² {l['r2']:.3f}")312 print(f" quadratique : taux = {q['beta'][0]:.4f} + {q['beta'][1]:.5f}·âge {q['beta'][2]:+.7f}·âge² (se {q['se_hc1'][0]:.4f}, {q['se_hc1'][1]:.5f}, {q['se_hc1'][2]:.7f}), R² {q['r2']:.3f}")313 print(f" par l'origine : {aj['taux_annuel_origine']*100:.3f} % de C_N par année → vie économique linéaire implicite {aj['vie_implicite']:.0f} ans")314 for a in (10, 28, 40, 60):315 print(f" taux quadratique à {a:2d} ans : {float(taux_quadratique(q, a))*100:.2f} %")316 pr = profils(m, sujet, vlr["retenue"], q)317 print(f"== Profils : prix hédonique du sujet neuf {f0(pr['prix_neuf'])} $ ; part du terrain α = {pr['alpha']:.3f}")318 print(" âge | âge-vie | extraction | hédonique total → bâtiment | générateur total → bâtiment")319 for a in (10, 20, 28, 40, 60, 80):320 i = int(a)321 print(f" {a:3d} | {pr['age_vie'][i]*100:6.1f} | {pr['extraction'][i]*100:7.1f} | {pr['hedonique_total'][i]*100:6.1f} → {pr['hedonique_bat'][i]*100:6.1f} | {pr['generateur_total'][i]*100:6.1f} → {pr['generateur_bat'][i]*100:6.1f}")322 vs = valeur_sujet(cn, vlr["retenue"], q, se["desuetude"], var)323 print("== Valeur par le coût du sujet (traite.cout.valeur_par_cout) :")324 A, B = vs["A_ventilee"], vs["B_extraction"]325 print(f" (A) ventilée : V_L {f0(vlr['retenue'])} + C_N {f2(cn['total'])} − D_phys {f2(ve['total'])} − D_fonc {f2(se['desuetude'])} = {f2(A['valeur'])} $ (dépréciation totale {A['taux_depreciation']*100:.2f} %)")326 print(f" (B) extraction : D_extraite(28) = {vs['taux_extrait_28']*100:.2f} % × C_N = {f2(vs['d_extraite'])} $ ; V = {f0(vlr['retenue'])} + {f2(cn['total'])} − {f2(vs['d_extraite'])} − {f2(se['desuetude'])} = {f2(B['valeur'])} $ "327 f"(dépréciation totale {f2(B['depreciation_totale'])} $ = {B['taux_depreciation']*100:.2f} % ; contribution du bâtiment {f2(B['contribution_batiment'])} $)")328 print(f" écart B − A = {f2(vs['ecart'])} $ ; VALEUR RETENUE PAR LE COÛT {f2(vs['valeur_retenue'])} $, arrondie {f0(vs['valeur_arrondie'])} $")329 gr = grille_age_vie(cn, var["curable"])330 print("== Sensibilité âge-vie modifiée (taux en %) : vie économique (lignes) × âge effectif (colonnes)"); print((gr * 100).round(1).to_string())331 sx = sensibilite_extraction(v, m, cn["total"])332 print("== Sensibilité de l'extraction :"); print(sx.assign(taux_28=sx.taux_28 * 100, taux_annuel=sx.taux_annuel * 100).round(2).to_string())333 to = tornade(cn, vlr["retenue"], vs["d_extraite"], se["desuetude"], var, sx)334 print("== Tornade (valeur par le coût) :")335 for o in to:336 print(f" {o['param']:48s} {o['etiquette']:28s} bas {f0(o['bas'])} haut {f0(o['haut'])} (base {f0(o['base'])}) ; étendue {(o['haut']-o['bas'])/o['base']*100:.1f} %")337 return {"cn": cn, "fiche": fiche, "travaux": trav, "ventes": v, "modele": m, "sujet": sujet, "devis": devis, "ages": ag, "variantes": var,338 "salle_eau": se, "exemples": ex, "extraction": w, "vl": vl, "vl_retenue": vlr, "tranches": tr, "ajustements": aj, "profils": pr,339 "valeur": vs, "grille": gr, "sens_extraction": sx, "tornade": to}340341342if __name__ == "__main__":343 main()344