#!/usr/bin/env python3 """Chapitre 24 — dépréciation et désuétudes du sujet du cas 1 (Gatineau, DONNÉES SYNTHÉTIQUES). Imprime tous les chiffres cités dans le chapitre 24 : * âges (chronologique, effectif selon la convention du ch. 11), vie économique et vie restante ; * dépréciation physique : âge-vie simple (âge chronologique / âge effectif), âge-vie modifiée, ventilée par composantes (fiche d'état et travaux du ch. 11, traite.cout.depreciation_ventilee) ; * désuétude fonctionnelle : carence curable du sujet (salle d'eau), exemples incurables (perte de loyer × MRB du cas 2 ; perte de RNE capitalisée) ; désuétude externe (ventes appariées, capitalisation) ; * extraction de marché sur les 600 ventes du cas 1 : coût neuf par vente (élasticité de taille du ch. 21), prix ramenés à la date d'évaluation (tendance du modèle hédonique), valeur de terrain par secteur extraite des ventes récentes, dépréciation par vente, profil en fonction de l'âge (médianes par tranche, ajustements linéaire et quadratique) ; * profils comparés : âge-vie linéaire, extraction, courbe hédonique (coefficients âge et âge² de traite.cas1.hedonique) et profil « vrai » du générateur (data/synthetic/README.md), convertis en dépréciation du bâtiment ; * valeur par le coût du sujet (traite.cout.valeur_par_cout) selon deux lectures de la dépréciation, et sensibilité. Usage (racine du dépôt) : PYTHONPATH=analysis/python python3 analysis/python/cases/ch24_depreciation.py Toutes les formules de dépréciation proviennent de analysis/python/traite/cout.py (aucune réécriture).""" from __future__ import annotations import sys, pathlib import numpy as np, pandas as pd sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1])) sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent)) from traite import cout as ct, cas1 from traite import hedonique as h import ch21_cout_neuf as c21 import ch11_inspection as c11 ROOT = pathlib.Path(__file__).resolve().parents[3] # --- conventions et hypothèses déclarées --------------------------------------------------------------------- VIE_ECONOMIQUE = c11.VIE_ECONOMIQUE # 60 ans (convention des ch. 11 et 24, hypothèse pédagogique) AGE_CHRONO = 28 # 1998 → 2026 DATE_EVAL = "2026-09-01" ELASTICITE_TAILLE = -0.10 # ch. 21 : coût unitaire ∝ (S/S_réf)^ε FACTEUR_QUALITE = 0.05 # coût neuf ± 5 % par classe de qualité (hypothèse pédagogique) AGE_MAX_RECENT = 10 # ventes « récentes » servant à extraire la valeur du terrain S_REF = 165.0 # Carence fonctionnelle curable du sujet (constat FICTIF du ch. 24) : aucune salle d'eau au rez-de-chaussée SALLE_EAU_AJOUT = 16000.0 # soumission fictive : ajout a posteriori, installé (hypothèse pédagogique) SALLE_EAU_POSTES = {"Plomberie brute et finition": 2.0, "Vanités et accessoires (2 sdb)": 0.5, "Portes intérieures et moulures": 1.0, "Gypse et tirage de joints": 12.0, "Peinture intérieure": 12.0, "Planchers céramique": 3.0} # quantités si incluse à neuf # Exemples illustratifs (hypothèses pédagogiques, hors sujet) TRIPLEX_PERTE_LOYER_MOIS = 75.0 # $/mois, logement sans entrée laveuse-sécheuse RNE_PERTE_ANNUELLE = 6000.0 # $/an TAUX_CAP_ILLUSTRATIF = 0.045 # milieu de la fourchette CBRE T3 2026 Montréal low-rise B (4,25-4,75 %), calibration.json SRC-E-045 PART_BATIMENT_ILLUSTRATIVE = 0.70 PAIRES_SANS, PAIRES_AVEC, PAIRES_PART_BAT = 520000.0, 490000.0, 0.60 def charger() -> dict: devis, sujet, cn = c11.charger() d = cas1.charger() m = cas1.hedonique(d["ventes"]) fiche = c11.fiche(devis, cn) trav = c11.travaux(devis, cn) return {"devis": devis, "sujet": sujet, "cn": cn, "ventes": d["ventes"], "modele": m, "fiche": fiche, "travaux": trav} # --------------------------------------------------------------------------------------------- âges et âge-vie def ages_sujet(fiche: pd.DataFrame) -> dict: s = fiche[fiche["longue_duree"]].iloc[0] pond = float((fiche["cout_neuf"] * fiche["ratio"]).sum() / fiche["cout_neuf"].sum()) return {"chronologique": AGE_CHRONO, "effectif_structure": float(s.age_effectif), "vie_economique": VIE_ECONOMIQUE, "vie_restante": ct.vie_restante(VIE_ECONOMIQUE, float(s.age_effectif)), "ratio_pondere": pond, "age_effectif_pondere": pond * VIE_ECONOMIQUE, "indice_etat": c11.indice_etat(fiche)} def variantes_physique(cn: dict, fiche: pd.DataFrame, trav: pd.DataFrame) -> dict: """Quatre lectures de la dépréciation physique du sujet (toutes sur C_N = 397 567,33 $).""" a_eff = ages_sujet(fiche)["effectif_structure"] curable = float(trav["cout"].sum()) simple_chrono = ct.depreciation_age_vie(cn["total"], AGE_CHRONO, VIE_ECONOMIQUE) simple_eff = ct.depreciation_age_vie(cn["total"], a_eff, VIE_ECONOMIQUE) modifiee = ct.depreciation_age_vie_modifiee(cn["total"], curable, a_eff, VIE_ECONOMIQUE) ventilee = c11.depreciation(fiche, trav, cn) return {"age_vie_chrono": simple_chrono, "age_vie_effectif": simple_eff, "modifiee": modifiee, "ventilee": ventilee, "curable": curable, "age_effectif": a_eff} # ------------------------------------------------------------------------------------------ désuétudes def carence_salle_eau(devis: pd.DataFrame, cn: dict) -> dict: """Désuétude fonctionnelle curable (carence) : coût d'ajout a posteriori − coût de l'élément s'il avait été inclus à neuf. Le coût « à neuf » est reconstitué à partir des prix unitaires du devis (quantités déclarées), majoré comme le coût neuf.""" g = c11.facteur_majoration(cn) direct = 0.0 for poste, q in SALLE_EAU_POSTES.items(): pu = float(devis.loc[devis["poste"] == poste, "prix_unitaire"].iloc[0]) direct += pu * q cout_si_neuf = direct * g d = ct.desuetude_fonctionnelle_curable(SALLE_EAU_AJOUT, cout_si_neuf, 0.0) return {"cout_si_neuf_direct": direct, "cout_si_neuf": cout_si_neuf, "cout_ajout": SALLE_EAU_AJOUT, "desuetude": d} def exemples_incurables() -> dict: from traite import cas2 ind = cas2.indicateurs_marche(cas2.comparables_plex(cas2.charger()["ventes"], cas2.charger()["sujet"])) mrb_annuel = ind["mrb_median_triplex"] loyer = ct.desuetude_incurable_par_loyer(TRIPLEX_PERTE_LOYER_MOIS, mrb_annuel * 12) rne = ct.desuetude_incurable_par_rne(RNE_PERTE_ANNUELLE, TAUX_CAP_ILLUSTRATIF, PART_BATIMENT_ILLUSTRATIVE) rne_total = ct.desuetude_incurable_par_rne(RNE_PERTE_ANNUELLE, TAUX_CAP_ILLUSTRATIF, 1.0) paires = ct.desuetude_externe_ventes_appariees(PAIRES_SANS, PAIRES_AVEC, PAIRES_PART_BAT) return {"mrb_annuel": mrb_annuel, "mrb_mensuel": mrb_annuel * 12, "perte_loyer_capitalisee": loyer, "perte_rne_capitalisee_totale": rne_total, "perte_rne_batiment": rne, "paires": paires} # ------------------------------------------------------------------------------------------ extraction de marché def cout_neuf_ventes(v: pd.DataFrame, cn_total: float, elasticite: float = ELASTICITE_TAILLE, fq: float = FACTEUR_QUALITE) -> pd.Series: """Coût neuf de chaque vente, transposé du sujet : C_N × (S/165)^(1+ε) × (1 + fq·(qualité − 3)).""" unit = ct.cout_par_superficie_ajuste(cn_total / S_REF, S_REF, v["superficie_habitable_m2"].astype(float), elasticite) return unit * v["superficie_habitable_m2"].astype(float) * (1.0 + fq * (v["qualite_1a5"].astype(float) - 3.0)) def prix_a_la_date(v: pd.DataFrame, modele: dict, date_eval: str = DATE_EVAL) -> pd.Series: """Prix ramenés à la date d'évaluation par la tendance mensuelle du modèle hédonique (ch. 17-18).""" g = modele["croissance_mensuelle"] mois = (pd.Timestamp(date_eval) - pd.to_datetime(v["date_vente"])).dt.days / 30.4375 return v["prix_vente"].astype(float) * (1.0 + g) ** mois def valeur_terrain_par_secteur(v: pd.DataFrame, p_adj: pd.Series, cn_i: pd.Series, age_max: int = AGE_MAX_RECENT) -> pd.DataFrame: """Valeur du terrain par secteur, extraite des ventes récentes : V_L = P_adj − C_N (1 − âge/L) (extraction, ch. 25).""" r = v[v["age"] <= age_max].copy() r["p_adj"] = p_adj[r.index]; r["cn"] = cn_i[r.index] r["dep"] = [ct.depreciation_age_vie(c, a, VIE_ECONOMIQUE) for c, a in zip(r["cn"], r["age"])] r["vl"] = r["p_adj"] - (r["cn"] - r["dep"]) r["part"] = r["vl"] / r["p_adj"] g = r.groupby("secteur").agg(n=("vl", "size"), vl_mediane=("vl", "median"), vl_moyenne=("vl", "mean"), vl_ecart_type=("vl", "std"), part_mediane=("part", "median"), terrain_m2_median=("superficie_terrain_m2", "median")) g["vl_par_m2"] = g["vl_mediane"] / g["terrain_m2_median"] return g def extraction(v: pd.DataFrame, modele: dict, cn_total: float, elasticite: float = ELASTICITE_TAILLE, fq: float = FACTEUR_QUALITE, age_max: int = AGE_MAX_RECENT) -> tuple[pd.DataFrame, pd.DataFrame]: """Dépréciation totale extraite de chaque vente : D = C_N − (P_adj − V_L) (traite.cout.extraction_depreciation_marche).""" w = v.copy() w["cn"] = cout_neuf_ventes(w, cn_total, elasticite, fq) w["p_adj"] = prix_a_la_date(w, modele) vl = valeur_terrain_par_secteur(w, w["p_adj"], w["cn"], age_max) w["vl"] = w["secteur"].map(vl["vl_mediane"]) 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"])] w["contribution_batiment"] = [e["contribution_batiment"] for e in ext] w["D"] = [e["depreciation"] for e in ext] w["taux"] = [e["taux"] for e in ext] w["taux_annuel"] = [e.get("taux_annuel", np.nan) for e in ext] return w, vl def ajuster_profil(w: pd.DataFrame) -> dict: """Ajustements du taux de dépréciation extrait sur l'âge : linéaire (constante), quadratique, linéaire par l'origine.""" a = w["age"].astype(float).values; y = w["taux"].values; n = len(w) lin = h.mco(np.column_stack([np.ones(n), a]), y) quad = h.mco(np.column_stack([np.ones(n), a, a ** 2]), y) orig = h.mco(a.reshape(-1, 1), y) return {"lineaire": lin, "quadratique": quad, "origine": orig, "taux_annuel_origine": float(orig["beta"][0]), "vie_implicite": 1.0 / float(orig["beta"][0])} def taux_quadratique(quad: dict, age) -> np.ndarray: b = quad["beta"]; age = np.asarray(age, dtype=float) return b[0] + b[1] * age + b[2] * age ** 2 def tranches(w: pd.DataFrame, bornes=(0, 10, 20, 30, 40, 50, 60, 100)) -> pd.DataFrame: g = w.groupby(pd.cut(w["age"], bornes), observed=True)["taux"].agg(["size", "median", "mean", "std"]) g.index = [f"{int(i.left) + (1 if i.left else 0)}-{int(i.right)}" for i in g.index] return g # ------------------------------------------------------------------------------------------ profils comparés def prix_neuf_sujet(modele: dict, sujet: dict) -> float: """Prédiction hédonique du sujet s'il était neuf (âge 0), même date, correction de Duan.""" s = dict(sujet); s["age"] = 0; s["annee_construction"] = 2026 return float(cas1.predire(modele, cas1.sujet_df(s))[0]) def profil_total_hedonique(modele: dict, age) -> np.ndarray: """Baisse du prix TOTAL avec l'âge selon le modèle : 1 − exp(β₁ âge + β₂ âge²).""" b1 = float(modele["coef"].loc["age", "coef"]); b2 = float(modele["coef"].loc["age2_100", "coef"]) / 100.0 age = np.asarray(age, dtype=float) return 1.0 - np.exp(b1 * age + b2 * age ** 2) def profil_total_generateur(age) -> np.ndarray: """Loi de génération du cas 1 (data/synthetic/README.md) : −0,0055·âge + 0,00003·âge².""" age = np.asarray(age, dtype=float) return 1.0 - np.exp(-0.0055 * age + 0.00003 * age ** 2) def total_vers_batiment(d_total, alpha: float) -> np.ndarray: """Conversion : si le terrain vaut une part α du prix neuf et ne se déprécie pas, d_B = d_total / (1 − α).""" return np.asarray(d_total, dtype=float) / (1.0 - alpha) def profils(modele: dict, sujet: dict, vl: float, quad: dict, ages=np.arange(0, 81)) -> dict: p0 = prix_neuf_sujet(modele, sujet) alpha = vl / p0 return {"ages": ages, "prix_neuf": p0, "alpha": alpha, "age_vie": np.minimum(ages / VIE_ECONOMIQUE, 1.0), "extraction": taux_quadratique(quad, ages), "hedonique_total": profil_total_hedonique(modele, ages), "generateur_total": profil_total_generateur(ages), "hedonique_bat": total_vers_batiment(profil_total_hedonique(modele, ages), alpha), "generateur_bat": total_vers_batiment(profil_total_generateur(ages), alpha)} # ------------------------------------------------------------------------------------------ valeur par le coût def valeur_terrain_retenue(vl: pd.DataFrame, secteur: str = "Hull") -> dict: med = float(vl.loc[secteur, "vl_mediane"]) return {"mediane": med, "retenue": round(med / 1000.0) * 1000.0, "n": int(vl.loc[secteur, "n"]), "part": float(vl.loc[secteur, "part_mediane"])} def valeur_sujet(cn: dict, vl_retenue: float, quad: dict, fonc: float, variantes: dict) -> dict: """Deux lectures : (A) dépréciation physique ventilée (ch. 11) + désuétude fonctionnelle ; (B) dépréciation extraite du marché à l'âge chronologique + désuétude fonctionnelle spécifique au sujet.""" t28 = float(taux_quadratique(quad, AGE_CHRONO)) d_ext = t28 * cn["total"] A = ct.valeur_par_cout(vl_retenue, cn["total"], variantes["ventilee"]["total"], fonc, 0.0) B = ct.valeur_par_cout(vl_retenue, cn["total"], d_ext, fonc, 0.0) return {"taux_extrait_28": t28, "d_extraite": d_ext, "A_ventilee": A, "B_extraction": B, "ecart": B["valeur"] - A["valeur"], "valeur_retenue": B["valeur"], "valeur_arrondie": round(B["valeur"] / 5000.0) * 5000.0} # ------------------------------------------------------------------------------------------ sensibilité def grille_age_vie(cn: dict, curable: float, vies=(50, 60, 70, 80, 100), ages_eff=(20, 25, 29, 33, 37)) -> pd.DataFrame: """Taux de dépréciation (âge-vie modifiée, curable fixé) pour chaque couple (vie économique, âge effectif).""" rows = {} for L in vies: rows[L] = {a: ct.depreciation_age_vie_modifiee(cn["total"], curable, a, L)["total"] / cn["total"] for a in ages_eff} return pd.DataFrame(rows).T def sensibilite_extraction(v: pd.DataFrame, modele: dict, cn_total: float) -> pd.DataFrame: """Taux extrait à 28 ans et valeur du terrain (Hull) selon les choix de l'extraction.""" rows = [] for nom, kw in [("cas de base", {}), ("ventes récentes ≤ 5 ans", {"age_max": 5}), ("ventes récentes ≤ 15 ans", {"age_max": 15}), ("élasticité −0,05", {"elasticite": -0.05}), ("élasticité −0,15", {"elasticite": -0.15}), ("qualité ±0 %", {"fq": 0.0}), ("qualité ±10 %", {"fq": 0.10})]: w, vl = extraction(v, modele, cn_total, **kw) q = ajuster_profil(w)["quadratique"] rows.append({"variante": nom, "vl_hull": float(vl.loc["Hull", "vl_mediane"]), "taux_28": float(taux_quadratique(q, AGE_CHRONO)), "taux_annuel": ajuster_profil(w)["taux_annuel_origine"]}) return pd.DataFrame(rows) def tornade(cn: dict, vl: float, d_ext: float, fonc: float, variantes: dict, sens_ext: pd.DataFrame) -> list[dict]: base = ct.valeur_par_cout(vl, cn["total"], d_ext, fonc)["valeur"] out = [] def add(param, bas, haut, etiquette): out.append({"param": param, "bas": bas, "haut": haut, "base": base, "etiquette": etiquette}) 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(",", " ")) t = sens_ext["taux_28"] 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} %") 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") 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(",", " ")) 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} %") return out # ------------------------------------------------------------------------------------------ main def main() -> dict: d = charger() cn, fiche, trav, v, m, sujet, devis = d["cn"], d["fiche"], d["travaux"], d["ventes"], d["modele"], d["sujet"], d["devis"] f2 = lambda x: f"{x:,.2f}".replace(",", " "); f0 = lambda x: f"{x:,.0f}".replace(",", " ") print("== DONNÉES SYNTHÉTIQUES — sujet cas 1 :", sujet["type"], sujet["annee_construction"], sujet["superficie_habitable_m2"], "m² hab.,", sujet["secteur"], "; évaluation", DATE_EVAL) print(f"== Coût neuf C_N (ch. 21) = {f2(cn['total'])} $") ag = ages_sujet(fiche) print(f"== Âges : chronologique {ag['chronologique']} ans ; effectif de la structure {ag['effectif_structure']:.1f} ans (convention ch. 11, λ = {c11.LAMBDA}) ; " f"vie économique {ag['vie_economique']:.0f} ans ; vie restante {ag['vie_restante']:.1f} ans ; indice d'état {ag['indice_etat']:.3f} ; " f"âge effectif pondéré par les coûts {ag['age_effectif_pondere']:.1f} ans") var = variantes_physique(cn, fiche, trav) print("== Dépréciation physique — quatre lectures :") print(f" âge-vie simple, âge chronologique 28/60 : {f2(var['age_vie_chrono'])} $ ({var['age_vie_chrono']/cn['total']*100:.2f} %)") 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} %)") mo = var["modifiee"] print(f" âge-vie modifiée : curable {f2(mo['curable'])} + incurable {f2(mo['incurable'])} = {f2(mo['total'])} $ ({mo['total']/cn['total']*100:.2f} %)") ve = var["ventilee"] 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'])} " f"(base {f2(ve['base_longue'])} × {ve['age_effectif_structure']:.0f}/60) = {f2(ve['total'])} $ ({ve['taux']*100:.2f} %)") courtes = fiche[(~fiche["longue_duree"]) & (fiche["composante"] != "Entrée asphaltée")] 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()))} $ " f"({sum(ve['courte_duree'].values())/courtes['cout_neuf'].sum()*100:.1f} % de leur coût neuf)") fin = [(k, x, fiche.loc[fiche['composante'] == k, 'cout_neuf'].iloc[0]) for k, x in ve["courte_duree"].items()] for k, x, c in sorted(fin, key=lambda t: -t[1]): print(f" {k:58s} coût neuf {f2(c):>11s} $ dépréciation {f2(x):>11s} $ ({x/c*100:5.1f} %)") se = carence_salle_eau(devis, cn) print("== Désuétude fonctionnelle curable du sujet (carence : salle d'eau au rez-de-chaussée, constat fictif) :") 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'])} $") ex = exemples_incurables() print("== Exemples incurables (hypothèses pédagogiques) :") 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'])} $") 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'])} $") print(f" paires : ({f0(PAIRES_SANS)} − {f0(PAIRES_AVEC)}) × {PAIRES_PART_BAT:.0%} = {f2(ex['paires'])} $") w, vl = extraction(v, m, cn["total"]) 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)") print(f" Valeur du terrain par secteur (ventes ≤ {AGE_MAX_RECENT} ans) :") print(vl.round(3).to_string()) vlr = valeur_terrain_retenue(vl) 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²)") tr = tranches(w) print(" Taux de dépréciation extrait par tranche d'âge :"); print(tr.round(3).to_string()) aj = ajuster_profil(w) q = aj["quadratique"]; l = aj["lineaire"] print(f" linéaire : taux = {l['beta'][0]:.4f} + {l['beta'][1]:.5f}·âge (se {l['se_hc1'][1]:.5f}), R² {l['r2']:.3f}") 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}") 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") for a in (10, 28, 40, 60): print(f" taux quadratique à {a:2d} ans : {float(taux_quadratique(q, a))*100:.2f} %") pr = profils(m, sujet, vlr["retenue"], q) print(f"== Profils : prix hédonique du sujet neuf {f0(pr['prix_neuf'])} $ ; part du terrain α = {pr['alpha']:.3f}") print(" âge | âge-vie | extraction | hédonique total → bâtiment | générateur total → bâtiment") for a in (10, 20, 28, 40, 60, 80): i = int(a) 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}") vs = valeur_sujet(cn, vlr["retenue"], q, se["desuetude"], var) print("== Valeur par le coût du sujet (traite.cout.valeur_par_cout) :") A, B = vs["A_ventilee"], vs["B_extraction"] 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} %)") 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'])} $ " f"(dépréciation totale {f2(B['depreciation_totale'])} $ = {B['taux_depreciation']*100:.2f} % ; contribution du bâtiment {f2(B['contribution_batiment'])} $)") print(f" écart B − A = {f2(vs['ecart'])} $ ; VALEUR RETENUE PAR LE COÛT {f2(vs['valeur_retenue'])} $, arrondie {f0(vs['valeur_arrondie'])} $") gr = grille_age_vie(cn, var["curable"]) print("== Sensibilité âge-vie modifiée (taux en %) : vie économique (lignes) × âge effectif (colonnes)"); print((gr * 100).round(1).to_string()) sx = sensibilite_extraction(v, m, cn["total"]) print("== Sensibilité de l'extraction :"); print(sx.assign(taux_28=sx.taux_28 * 100, taux_annuel=sx.taux_annuel * 100).round(2).to_string()) to = tornade(cn, vlr["retenue"], vs["d_extraite"], se["desuetude"], var, sx) print("== Tornade (valeur par le coût) :") for o in to: 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} %") return {"cn": cn, "fiche": fiche, "travaux": trav, "ventes": v, "modele": m, "sujet": sujet, "devis": devis, "ages": ag, "variantes": var, "salle_eau": se, "exemples": ex, "extraction": w, "vl": vl, "vl_retenue": vlr, "tranches": tr, "ajustements": aj, "profils": pr, "valeur": vs, "grille": gr, "sens_extraction": sx, "tornade": to} if __name__ == "__main__": main()