#!/usr/bin/env python3 """Chapitre 45 — Cas 1 : maison unifamiliale à Gatineau (DONNÉES SYNTHÉTIQUES, propriété fictive) — dossier complet. Ce script N'INVENTE AUCUN CHIFFRE : il importe les fonctions des chapitres de méthode et de la bibliothèque partagée * sélection des comparables : traite.cas1.selection (ch. 16) ; * grille d'ajustements et valeur pondérée : traite.cas1.ajustements_hedoniques + ch17_ajustements.reconcilier (ch. 17) ; * modèle hédonique, prédiction, intervalles : traite.cas1.hedonique / predire / intervalle_prediction, ch18_hedonique.prediction_sujet (ch. 18) ; * mesurage : ch12_mesurage.superficies (ch. 12) ; inspection : ch11_inspection (ch. 11) ; * coût neuf : ch21_cout_neuf (ch. 21) ; dépréciation et valeur par le coût : ch24_depreciation.main (ch. 24) ; * terrain : ch25_terrain (comparaison directe + extraction du ch. 24) (ch. 25) ; * rôle, taxes, mutations : ch09_ratios (ch. 9) ; * réconciliation : ch20_reconciliation.tout (ch. 20), et n'y ajoute que ce qui est propre au dossier : le financement de l'acheteur (traite.finance), les droits de mutation à l'achat, le sommaire du rapport et les exercices. Toute divergence entre un chiffre d'ici et un chiffre des chapitres de méthode est une erreur à signaler : les tests (test_ch45.py) la détecteraient. Usage (racine du dépôt) : PYTHONPATH=analysis/python python3 analysis/python/cases/ch45_dossier.py""" from __future__ import annotations import sys, io, math, pathlib, contextlib import numpy as np, pandas as pd HERE = pathlib.Path(__file__).resolve() sys.path.insert(0, str(HERE.parents[1])); sys.path.insert(0, str(HERE.parent)) from traite import cas1, finance as fin, cout as ct, comparables as cp import ch16_selection as c16 import ch17_ajustements as c17 import ch18_hedonique as c18 import ch20_reconciliation as c20 import ch21_cout_neuf as c21 import ch24_depreciation as c24 import ch25_terrain as c25 import ch12_mesurage as c12 import ch11_inspection as c11 import ch09_ratios as c09 ROOT = HERE.parents[3] GRAINE = 20261031 DATE_EVAL = "2026-09-01" # ----------------------------------------------------------------------------- paramètres du financement (sources : notes-C, registre ch45) TAUX_ASSURE = 0.0412 # BdC, fonds avancés, prêts assurés à taux fixe ≥ 5 ans, août 2026 (bdctauxhypothecaires2026) TAUX_NON_ASSURE = 0.0458 # idem, prêts non assurés AMORTISSEMENT_MOIS = 300 # 25 ans (SCHL : maximum 25 ans ; 30 ans réservé aux premiers acheteurs / constructions neuves) TERME_ANS = 5 MQR_PLANCHER, MQR_COUSSIN = 0.0525, 0.02 # taux admissible minimal : max(taux + 2 %, 5,25 %) (SCHL Achat ; Finances Canada 2023-12-12 ; BSIF pour le non assuré) ABD_MAX, ATD_MAX = 0.39, 0.44 # SCHL : « restricts debt service ratios to 39 % (GDS) and 44 % (TDS) » PRIMES_SCHL = [(0.65, 0.0060), (0.75, 0.0170), (0.80, 0.0240), (0.85, 0.0280), (0.90, 0.0310), (0.95, 0.0400)] # SCHL Achat, propriétaires-occupants PLAFOND_ASSURABLE = 1_500_000.0 # prix maximal admissible (SCHL ; Finances Canada 2024-12-15) TAXE_PRIME_QC = 0.09 # taxe québécoise sur les primes d'assurance : NON VÉRIFIÉE sur une source primaire (Revenu Québec inaccessible le 2026-10-31) ; # la SCHL indique seulement que la prime est « subject to provincial sales tax » au Québec. Hypothèse déclarée. CHAUFFAGE_ANNUEL = 2400.0 # hypothèse pédagogique déclarée (coût de chauffage retenu par le prêteur dans l'ABD) — aucune source AUTRES_DETTES_MENSUELLES = 500.0 # hypothèse pédagogique déclarée (prêt auto, cartes) pour l'ATD SCENARIOS_MISE = {"minimale": None, "10 %": 0.10, "20 %": 0.20} # None → mise de fonds minimale réglementaire def mise_de_fonds_minimale(prix: float) -> float: """SCHL / Finances Canada : 5 % de la première tranche de 500 000 $ + 10 % du reste (prix < 1,5 M$).""" if prix >= PLAFOND_ASSURABLE: raise ValueError("prix non assurable (≥ 1,5 M$) : mise de fonds d'au moins 20 %") return 0.05 * min(prix, 500_000.0) + 0.10 * max(prix - 500_000.0, 0.0) def taux_prime(rpv: float) -> float: """Prime SCHL (propriétaire-occupant) selon le rapport prêt-valeur ; 0 si RPV ≤ 80 % (prêt conventionnel non assuré).""" if rpv <= 0.80 + 1e-12: return 0.0 for borne, t in PRIMES_SCHL: if rpv <= borne + 1e-12: return t raise ValueError("RPV > 95 % : non assurable") # ----------------------------------------------------------------------------- 1. héritages des chapitres de méthode def _silencieux(f, *a, **k): with contextlib.redirect_stdout(io.StringIO()): return f(*a, **k) def heritage() -> dict: """Tous les chiffres hérités, obtenus des scripts des chapitres (aucun recalcul « à la main »).""" t = cas1.tout() # sélection ch. 16, modèle ch. 18, grille ch. 17, prédiction, intervalle sel = t["selection"]; m = t["modele"]; s = t["sujet"] b17 = c17.base(); rec17 = c17.reconcilier(b17) # ch. 17 : poids inverse du brut, valeur pondérée pred18 = c18.prediction_sujet(m, s) # ch. 18 : prédiction, Duan, intervalles 95 et 80 %, prédiction au 2024-07-01 r20 = c20.tout() # ch. 20 : bootstrap, coût (ch. 24 importé), combinaison, vérité ic95 = (float(r20["exercices"]["ex2"]["ic_bas"]), float(r20["exercices"]["ex2"]["ic_haut"])) # IC 95 % de la valeur (ch. 20, eq. 20.6) r24 = r20["cout"]["r24"] # ch. 24 complet (main() déjà exécuté silencieusement par le ch. 20) devis, sujet, cn = c11.charger() # ch. 21 (coût neuf) et ch. 11 (fiche) fiche = c11.fiche(devis, cn); trav = c11.travaux(devis, cn); dv = c11.depreciation(fiche, trav, cn) sup12 = c12.superficies() # ch. 12 devis21, _ = c21.charger(); g21 = c21.regrouper(devis21); sup21 = c21.superficies(sujet, devis21) d09 = c09.charger(); et09 = c09.etude(d09); ds09 = c09.dossier_sujet(d09, et09) # ch. 9 comp25 = _silencieux(c25.comparaison) # ch. 25 : comparaison directe des terrains extr25 = _silencieux(c25.extraction_recente_ch24, t) cand = sel["candidats"] return {"t": t, "sujet": s, "ventes": t["ventes"], "modele": m, "selection": sel, "candidats": cand, "comparables": sel["comparables"], "etapes16": sel["etapes"], "adj17": b17["adj"], "rec17": rec17, "V_comp": rec17["inverse_brut"]["valeur_ponderee"], "poids17": rec17["poids_inverse_brut"], "pred18": pred18, "V_hed": t["prediction_hedonique"], "ip95": pred18["ic95"], "ip80": pred18["ic80"], "ic95_valeur": ic95, "r20": r20, "r24": r24, "cn": cn, "devis": devis, "fiche": fiche, "travaux": trav, "dv11": dv, "sup12": sup12, "groupes21": g21, "sup21": sup21, "d09": d09, "et09": et09, "ds09": ds09, "comp25": comp25, "extr25": extr25, "indication_brute16": float(sel["comparables"]["prix_m2"].median() * s["superficie_habitable_m2"])} # ----------------------------------------------------------------------------- 2. financement de l'acheteur (propre au ch. 45) def financement(prix: float, mise: float | None = None, amortissement_mois: int = AMORTISSEMENT_MOIS, taxes_annuelles: float = 0.0, droits_mutation: float = 0.0, chauffage: float = CHAUFFAGE_ANNUEL, autres_dettes_mensuelles: float = AUTRES_DETTES_MENSUELLES) -> dict: """Prêt assuré ou conventionnel selon la mise de fonds ; prime SCHL (ajoutée au prêt), taxe québécoise sur la prime (payée comptant, hypothèse), paiement mensuel (composition semestrielle), taux admissible minimal, revenu brut minimal selon l'ABD et l'ATD, liquidités à la signature (mise de fonds + droits de mutation + taxe sur la prime ; frais de notaire et d'inspection exclus).""" mise = mise_de_fonds_minimale(prix) if mise is None else mise pret_base = prix - mise; rpv = pret_base / prix assure = rpv > 0.80 tp = taux_prime(rpv); prime = pret_base * tp; taxe = prime * TAXE_PRIME_QC pret = pret_base + prime taux = TAUX_ASSURE if assure else TAUX_NON_ASSURE h = fin.Hypotheque(pret, taux, amortissement_mois) mqr = fin.taux_admissible_minimal(taux, MQR_PLANCHER, MQR_COUSSIN) hq = fin.Hypotheque(pret, mqr, amortissement_mois) pith_q = hq.service_dette_annuel + taxes_annuelles + chauffage revenu_abd = pith_q / ABD_MAX revenu_atd = (pith_q + 12 * autres_dettes_mensuelles) / ATD_MAX return {"prix": prix, "mise": mise, "mise_pct": mise / prix, "pret_base": pret_base, "rpv": rpv, "assure": assure, "taux_prime": tp, "prime": prime, "taxe_prime": taxe, "pret": pret, "rpv_avec_prime": pret / prix, "taux": taux, "amortissement_mois": amortissement_mois, "hypotheque": h, "paiement": h.paiement, "service_dette": h.service_dette_annuel, "constante": h.constante_hypothecaire, "interets_an1": h.interets_annee(1), "capital_an1": h.capital_annee(1), "solde_terme": h.solde(TERME_ANS * 12), "mqr": mqr, "paiement_mqr": hq.paiement, "pith_qualification": pith_q, "revenu_min_abd": revenu_abd, "revenu_min_atd": revenu_atd, "revenu_min": max(revenu_abd, revenu_atd), "contrainte_revenu": "ATD" if revenu_atd > revenu_abd else "ABD", "taxes_annuelles": taxes_annuelles, "chauffage": chauffage, "autres_dettes": autres_dettes_mensuelles, "droits_mutation": droits_mutation, "liquidites": mise + droits_mutation + taxe} def renouvellement(f: dict, hausses_pb=(0, 100, 200, 300), taxes_annuelles: float = 0.0) -> pd.DataFrame: """Au terme de 5 ans, le solde est refinancé au taux initial + hausse sur l'amortissement résiduel.""" solde = f["solde_terme"]; n = f["amortissement_mois"] - TERME_ANS * 12; rows = [] for pb in hausses_pb: taux = f["taux"] + pb / 10_000 h = fin.Hypotheque(solde, taux, n) pith = h.service_dette_annuel + taxes_annuelles + f["chauffage"] rows.append({"hausse_pb": pb, "taux": taux, "solde": solde, "paiement": h.paiement, "service_dette": h.service_dette_annuel, "hausse_paiement_pct": h.paiement / f["paiement"] - 1, "revenu_min_abd": pith / ABD_MAX}) return pd.DataFrame(rows) def valeur_d_emprunt(prix: float, valeur_preteur: float, mise_disponible: float) -> dict: """Le prêteur prête sur la moindre du prix et de la valeur d'emprunt (RPV maximal 95 %) ; la mise de fonds doit combler l'écart.""" base = min(prix, valeur_preteur) pret_max = 0.95 * base mise_requise = max(prix - pret_max, mise_de_fonds_minimale(prix)) return {"valeur_preteur": valeur_preteur, "base": base, "pret_max": pret_max, "mise_requise": mise_requise, "mise_supplementaire": mise_requise - mise_disponible} # ----------------------------------------------------------------------------- 3. dossier _CACHE: dict = {} def dossier() -> dict: if "D" in _CACHE: return _CACHE["D"] H = heritage() cb = H["r20"]["combinaison"]; co = H["r20"]["cout"]; bs = H["r20"]["bootstrap"] conclusion = float(cb["conclusion"]); fourchette = tuple(float(x) for x in cb["fourchette_conclue"]) prix = conclusion # prix supposé = valeur la plus probable (mandat de financement) role = float(H["sujet"]["valeur_role_2025"]) taxes = H["ds09"]["taxes_role"] # compte de taxes 2026 au rôle fictif (ch. 9) # droits de mutation à l'achat (Gatineau 2026, barème à compter du 25 février 2026) : base = max(prix ; rôle × facteur réel 1,02) base_mut = max(prix, role * c09.FACTEUR_GATINEAU_2026) droits = c09.droits_mutation(base_mut, c09.BAREME_MUTATION_GAT) droits_general = c09.droits_mutation(base_mut, c09.BAREME_MUTATION_QC) F = {k: financement(prix, None if v is None else v * prix, taxes_annuelles=taxes["total"], droits_mutation=droits) for k, v in SCENARIOS_MISE.items()} F["minimale 30 ans"] = financement(prix, None, 360, taxes_annuelles=taxes["total"], droits_mutation=droits) ren = renouvellement(F["minimale"], taxes_annuelles=taxes["total"]) ve = {v: valeur_d_emprunt(prix, v, F["minimale"]["mise"]) for v in (fourchette[0], conclusion, fourchette[1])} # sommaire : indications, poids, écarts-types (ch. 20) tab20 = cb["tableau"]; raisonnee = tab20[tab20["schema"] == "raisonnée (retenue)"].iloc[0] D = {**H, "conclusion": conclusion, "fourchette": fourchette, "V_raisonnee": float(cb["V_raisonnee"]), "sd_conclusion": float(raisonnee["ecart_type"]), "poids20": {"comparaison": 0.40, "hedonique": 0.45, "cout": 0.15}, "indications": {"comparaison": float(cb["V"][0]), "hedonique": float(cb["V"][1]), "cout": float(cb["V"][2])}, "ecarts_types20": {"comparaison": float(cb["s"][0]), "hedonique": float(cb["s"][1]), "cout": float(cb["s"][2])}, "R20": cb["R"], "bande90": tuple(float(x) for x in cb["fourchette90"]), "q20": cb["q"], "verite": H["r20"]["verite"], "tab20": tab20, "V_cout": float(co["valeur"]), "V_cout_arrondie": float(co["arrondie"]), "V_L": float(co["V_L"]), "CN": float(co["CN"]), "D_ext": float(co["D_ext"]), "taux_ext": float(co["taux_ext"]), "D_fonc": float(co["D_fonc"]), "D_tot": float(co["D_tot"]), "taux_tot": float(co["taux_tot"]), "V_ventilee": float(co["ventilee"]), "taux_ventilee": float(co["taux_ventilee"]), "contribution_batiment": float(co["contribution_batiment"]), "prix": prix, "role": role, "taxes": taxes, "base_mutation": base_mut, "droits_mutation": droits, "droits_mutation_general": droits_general, "financement": F, "renouvellement": ren, "valeur_emprunt": ve, "bootstrap": bs, "residu_moyen_retenus": float(np.mean(np.exp(H["r20"]["base"]["adj5"]["residu_log"]) - 1)), "residu_moyen_autres": float(np.mean(np.exp(H["r20"]["base"]["adj16"].loc[~H["r20"]["base"]["adj16"]["retenu"], "residu_log"]) - 1)), "V_16": float(H["r20"]["elargissement"]["valeur_ponderee"].iloc[-1]), "valeur_reelle_2024": float(H["ds09"]["valeur_reelle"]), "ratio_sujet_role": float(H["ds09"]["ratio_sujet"]), "PM_fictif": float(H["et09"]["PM"]), "F_fictif": float(H["et09"]["F"]), "valeur_a_inscrire": float(H["ds09"]["valeur_a_inscrire"]), "hed_2024": float(H["ds09"]["hedonique"])} D["ratio_prix_role"] = prix / role D["ratio_conclusion_brute16"] = conclusion / H["indication_brute16"] - 1 _CACHE["D"] = D return D # ----------------------------------------------------------------------------- 4. exercices (chiffres des solutions) def exercices(D: dict | None = None) -> dict: D = D or dossier(); ex = {} prix = D["prix"]; S = D["sujet"]["superficie_habitable_m2"]; sup = D["sup12"] # ex 1 : unités de comparaison de la valeur conclue ex["ex1"] = {"par_m2_hab": prix / S, "par_m2_brut": prix / sup["aire_etages_m2"], "par_pi2_hab": prix / (S * 10.7639), "par_m2_hab_plus_ss": prix / sup["habitable_sous_sol_m2"], "med_comparables_m2": float(D["comparables"]["prix_m2"].median()), "indication_brute": D["indication_brute16"], "ecart_pct": D["ratio_conclusion_brute16"]} # ex 2 : droits de mutation et liquidités au haut et au bas de la fourchette out = {} for p in (D["fourchette"][0], D["fourchette"][1]): base = max(p, D["role"] * c09.FACTEUR_GATINEAU_2026); dm = c09.droits_mutation(base, c09.BAREME_MUTATION_GAT) f = financement(p, None, taxes_annuelles=D["taxes"]["total"], droits_mutation=dm) out[p] = {"base": base, "droits": dm, "droits_general": c09.droits_mutation(base, c09.BAREME_MUTATION_QC), "mise": f["mise"], "prime": f["prime"], "taxe_prime": f["taxe_prime"], "liquidites": f["liquidites"]} ex["ex2"] = out # ex 3 : mise de fonds 10 % et 20 % ; coût d'opportunité au rendement des obligations 5 ans (3,58 %) ; patrimoine net à 60 mois R_OBLIG = 0.0358; f0 = D["financement"]["minimale"]; out = {} for k in ("minimale", "10 %", "20 %"): f = D["financement"][k]; sup = f["mise"] - f0["mise"] econ_mens = f0["paiement"] - f["paiement"] placement_5ans = sup * (1 + R_OBLIG) ** 5 # mise supplémentaire placée 5 ans (composition annuelle) # patrimoine net à 60 mois : valeur (constante) − solde + économies de paiement cumulées (sans intérêt) ; variante avec placement de la mise non versée patrimoine = prix - f["solde_terme"] + 60 * econ_mens out[k] = {kk: f[kk] for kk in ("mise", "pret_base", "rpv", "taux_prime", "prime", "taxe_prime", "pret", "taux", "paiement", "mqr", "paiement_mqr", "revenu_min_abd", "liquidites", "solde_terme")} out[k].update({"mise_supplementaire": sup, "economie_paiement_mensuel": econ_mens, "economie_paiement_5ans": 60 * econ_mens, "economie_prime": f0["prime"] - f["prime"], "cout_opportunite_annuel": sup * R_OBLIG, "placement_5ans": placement_5ans, "interets_placement_5ans": placement_5ans - sup, "patrimoine_net_60": patrimoine, "patrimoine_net_60_vs_min": patrimoine - (prix - f0["solde_terme"])}) ex["ex3"] = out # ex 4 : valeur d'emprunt du prêteur inférieure au prix → montage complet au prêt maximal assuré (prime sur le prêt de base) out = {} for v, ve in D["valeur_emprunt"].items(): pret = ve["pret_max"] if ve["mise_supplementaire"] > 0 else D["financement"]["minimale"]["pret_base"] mise = prix - pret; rpv = pret / prix; tp = taux_prime(rpv); prime = pret * tp; total = pret + prime h = fin.Hypotheque(total, TAUX_ASSURE, AMORTISSEMENT_MOIS); hq = fin.Hypotheque(total, fin.taux_admissible_minimal(TAUX_ASSURE, MQR_PLANCHER, MQR_COUSSIN), AMORTISSEMENT_MOIS) out[v] = {**ve, "pret": pret, "mise": mise, "rpv": rpv, "taux_prime": tp, "prime": prime, "pret_total": total, "paiement": h.paiement, "paiement_mqr": hq.paiement, "revenu_min_abd": (hq.service_dette_annuel + D["taxes"]["total"] + CHAUFFAGE_ANNUEL) / ABD_MAX, "liquidites": mise + D["droits_mutation"] + prime * TAXE_PRIME_QC} ex["ex4"] = out # ex 5 : renouvellement à +200 pb ; variante avec taxes +3 %/an, chauffage +2 %/an, revenu +2,5 %/an depuis le minimum initial r = D["renouvellement"]; ex["ex5"] = {int(k): dict(v) for k, v in r.set_index("hausse_pb").iterrows()} f0 = D["financement"]["minimale"]; row = ex["ex5"][200] taxes5 = D["taxes"]["total"] * 1.03 ** 5; chauff5 = CHAUFFAGE_ANNUEL * 1.02 ** 5; revenu5 = f0["revenu_min_abd"] * 1.025 ** 5 pith5 = row["service_dette"] + taxes5 + chauff5 ex["ex5"]["variante"] = {"taxes5": taxes5, "chauffage5": chauff5, "revenu5": revenu5, "pith5": pith5, "abd5": pith5 / revenu5, "revenu_min5": pith5 / ABD_MAX, "abd_initial_contrat": (f0["service_dette"] + D["taxes"]["total"] + CHAUFFAGE_ANNUEL) / f0["revenu_min_abd"]} # ex 6 : terrain aux bornes de la fourchette du ch. 25 → coût → réconciliation (poids fixes) out = {} for vl in (240_000.0, 277_000.0, 285_000.0): vc = ct.valeur_par_cout(vl, D["CN"], D["D_ext"], D["D_fonc"])["valeur"] comb = 0.40 * D["indications"]["comparaison"] + 0.45 * D["indications"]["hedonique"] + 0.15 * vc out[vl] = {"V_cout": vc, "combinaison": comb, "arrondie": round(comb / 5000) * 5000, "fourchette": (math.floor(min(D["indications"]["comparaison"], D["indications"]["hedonique"], vc) / 5000) * 5000, math.ceil(max(D["indications"]["comparaison"], D["indications"]["hedonique"], vc) / 5000) * 5000)} ex["ex6"] = out # ex 7 : hypothèse extraordinaire (drain français) : incidence 9 000 $, p = 0,8 ; effet sur le financement K = 9000.0; VH = D["conclusion"]; VnH = VH - K f_nH = financement(VnH, None, taxes_annuelles=D["taxes"]["total"], droits_mutation=D["droits_mutation"]) ex["ex7"] = {"V_H": VH, "V_nonH": VnH, "delta": K, "delta_pct": K / VH, "esperance_p80": VnH + 0.8 * K, "esperance_p50": VnH + 0.5 * K, "mise_nonH": f_nH["mise"], "pret_nonH": f_nH["pret"], "paiement_nonH": f_nH["paiement"], "paiement_H": D["financement"]["minimale"]["paiement"]} return ex # ----------------------------------------------------------------------------- 5. impression def _d(x): return f"{x:,.0f} $".replace(",", " ") def _d2(x): return f"{x:,.2f} $".replace(",", " ") def main() -> dict: D = dossier(); s = D["sujet"] print("=== DONNÉES SYNTHÉTIQUES — Cas 1 : cottage 1998, 165 m², Hull (Gatineau) ; évaluation au", DATE_EVAL, "===") print("--- Mesurage (ch. 12) :", {k: round(v, 2) for k, v in D["sup12"].items() if k in ("aire_sol_m2", "aire_etages_m2", "habitable_m2", "nette_m2", "habitable_sous_sol_m2", "garage_m2", "lot_m2")}) print("--- Coût neuf (ch. 21) : C_N", _d2(D["cn"]["total"]), "; D", _d2(D["cn"]["direct"]), "; I", _d2(D["cn"]["indirects"]), "; E", _d2(D["cn"]["incitatif"])) dv = D["dv11"] print(f"--- Inspection (ch. 11) : indice d'état {c11.indice_etat(D['fiche']):.3f} ; âge effectif structure {dv['age_effectif_structure']:.0f} ans ; curable {_d2(dv['curable'])} ; ventilée {_d2(dv['total'])} ({dv['taux']*100:.2f} %)") print(f"--- Rôle et taxes (ch. 9) : rôle fictif {_d(D['role'])} (réf. 2024-07-01) ; valeur réelle 2024-07-01 {_d(D['valeur_reelle_2024'])} (hédonique {_d(D['hed_2024'])}) ; ratio {D['ratio_sujet_role']:.4f} vs PM {D['PM_fictif']:.4f} ; " f"valeur à inscrire {_d(D['valeur_a_inscrire'])} ; compte 2026 {_d2(D['taxes']['total'])} (municipal {_d2(D['taxes']['municipal'])}, scolaire {_d2(D['taxes']['scolaire'])}, compensation {_d2(D['taxes']['compensations'])})") print("--- Sélection (ch. 16) :", D["etapes16"], "; comparables", list(D["comparables"]["id_vente"]), "; indication brute", _d(D["indication_brute16"])) print("--- Grille (ch. 17) :\n", D["adj17"][["id_vente", "prix_vente", "conditions_marche", "superficie", "terrain", "age", "qualite", "garage", "sous_sol", "prix_ajuste", "ajust_brut_pct", "ajust_net_pct", "residu_pct"]].round(3).to_string(index=False)) print(" poids", np.round(D["poids17"], 3), "; valeur pondérée", _d(D["V_comp"]), "; moyenne", _d(D["rec17"]["egal"]["valeur_ponderee"]), "; médiane", _d(D["rec17"]["egal"]["mediane"])) p = D["pred18"] print(f"--- Hédonique (ch. 18) : point {_d(p['naif'])} ; Duan {p['facteur_duan']:.5f} → {_d(D['V_hed'])} ; IP 95 % [{_d(D['ip95'][0])} ; {_d(D['ip95'][1])}] ; IP 80 % [{_d(D['ip80'][0])} ; {_d(D['ip80'][1])}] ; IC 95 % de la valeur [{_d(D['ic95_valeur'][0])} ; {_d(D['ic95_valeur'][1])}]") print(f"--- Coût (ch. 24/25) : V_L {_d(D['V_L'])} ; D extraite {_d2(D['D_ext'])} ({D['taux_ext']*100:.2f} %) ; D fonc {_d2(D['D_fonc'])} ; D totale {_d2(D['D_tot'])} ({D['taux_tot']*100:.2f} %) ; contribution bâtiment {_d2(D['contribution_batiment'])} ; V {_d2(D['V_cout'])} → {_d(D['V_cout_arrondie'])} ; ventilée {_d2(D['V_ventilee'])} ({D['taux_ventilee']*100:.2f} %)") c25r = D["comp25"]["reconciliation"] print(f" terrain ch. 25 : comparaison directe {_d(c25r['valeur_ponderee'])} ; hédonique terrains {_d(D['comp25']['prediction_hedonique'])} ; extraction ch. 24 médiane {_d(D['extr25']['mediane'])} ({D['extr25']['n']} ventes, part {D['extr25']['part']*100:.1f} %)") print(f"--- Réconciliation (ch. 20) : {D['indications']} ; é.-t. {D['ecarts_types20']} ; poids {D['poids20']} ; combinaison {_d2(D['V_raisonnee'])} (é.-t. {_d(D['sd_conclusion'])}) → CONCLUSION {_d(D['conclusion'])} ; fourchette {_d(D['fourchette'][0])} – {_d(D['fourchette'][1])} ; bande bootstrap 90 % {_d(D['bande90'][0])} – {_d(D['bande90'][1])}") print(f" résidu moyen retenus {D['residu_moyen_retenus']*100:.2f} % ; autres {D['residu_moyen_autres']*100:.2f} % ; k = 16 : {_d(D['V_16'])} ; vérité {_d(D['verite']['V_true'])} ; ratio prix/rôle {D['ratio_prix_role']:.3f}") print(D["tab20"].round(3).to_string(index=False)) print(f"--- Mutation (Gatineau 2026, dès le 25 février) : base {_d(D['base_mutation'])} ; droit {_d2(D['droits_mutation'])} (barème général : {_d2(D['droits_mutation_general'])})") print("--- Financement (prix = valeur la plus probable) :") for k, f in D["financement"].items(): print(f" {k:16s} mise {_d2(f['mise'])} ({f['mise_pct']*100:.2f} %) ; prêt {_d2(f['pret_base'])} RPV {f['rpv']*100:.2f} % ; prime {f['taux_prime']*100:.2f} % = {_d2(f['prime'])} (taxe hyp. 9 % {_d2(f['taxe_prime'])}) ; " f"prêt total {_d2(f['pret'])} (RPV {f['rpv_avec_prime']*100:.2f} %) ; taux {f['taux']*100:.2f} % ; {f['amortissement_mois']} mois ; paiement {_d2(f['paiement'])}/mois ; SD {_d2(f['service_dette'])} ; f {f['constante']*100:.3f} % ; " f"intérêts an 1 {_d2(f['interets_an1'])} ; capital an 1 {_d2(f['capital_an1'])} ; solde à 60 mois {_d2(f['solde_terme'])} ; MQR {f['mqr']*100:.2f} % → paiement {_d2(f['paiement_mqr'])} ; " f"revenu min ABD {_d(f['revenu_min_abd'])} ; ATD {_d(f['revenu_min_atd'])} ({f['contrainte_revenu']}) ; liquidités {_d2(f['liquidites'])}") print("--- Renouvellement (mise minimale, 25 ans) :\n", D["renouvellement"].round(4).to_string(index=False)) print("--- Valeur d'emprunt du prêteur :", {k: {kk: round(vv) for kk, vv in v.items()} for k, v in D["valeur_emprunt"].items()}) print("--- Exercices :") for k, v in exercices(D).items(): print(" ", k, v if not isinstance(v, dict) else {kk: (round(vv, 4) if isinstance(vv, (int, float, np.floating)) else vv) for kk, vv in v.items()}) return D if __name__ == "__main__": main()