spb/traite-evaluation-qc
Public
1#!/usr/bin/env python32"""Chapitre 45 — figures du dossier (cas 1, DONNÉES SYNTHÉTIQUES) : carte des comparables, cascade des ajustements,3fourchettes par méthode et conclusion, profil de dépréciation par composante, financement de l'acheteur.4Tous les chiffres proviennent de analysis/python/cases/ch45_dossier.py (qui importe les scripts des chapitres de méthode).5Usage (racine du dépôt) : PYTHONPATH=analysis/python python3 analysis/python/figures/ch45_figures.py"""6from __future__ import annotations7import sys, pathlib, math8import numpy as np9import matplotlib.pyplot as plt10from matplotlib.ticker import FuncFormatter11sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1]))12sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "cases"))13from traite.style import setup, palette, savefig, LARGEUR14import ch45_dossier as ch15import ch17_ajustements as c171617def fmt_k(x, pos=None):18 return f"{x/1000:,.0f}".replace(",", " ") + " k$"1920def fmt_d(x):21 return f"{x:,.0f} $".replace(",", " ")222324def fig_carte(D):25 """Sujet, 16 candidats de l'entonnoir du ch. 16 et 5 comparables retenus ; distances en km autour du sujet (projection locale)."""26 s = D["sujet"]; cand = D["candidats"]; comps = D["comparables"]27 lat0, lon0 = s["lat"], s["lon"]; kx = 111.32 * math.cos(math.radians(lat0)); ky = 110.5728 fig, ax = plt.subplots(figsize=(LARGEUR, 3.6))29 for r_km, lab in [(1.5, "1,5 km"), (3.0, "3 km")]:30 ax.add_patch(plt.Circle((0, 0), r_km, fill=False, color=palette.ink2, lw=0.7, ls="--"))31 ax.text(r_km * 0.72, r_km * 0.72, lab, fontsize=7, color=palette.ink2)32 autres = cand[~cand["id_vente"].isin(comps["id_vente"])]33 ax.scatter((autres["lon"] - lon0) * kx, (autres["lat"] - lat0) * ky, s=22 + (autres["prix_vente"] - 450000) / 4000, color=palette.cat[0], alpha=0.55, label="11 autres candidats (entonnoir du ch. 16)", zorder=3)34 ax.scatter((comps["lon"] - lon0) * kx, (comps["lat"] - lat0) * ky, s=30 + (comps["prix_vente"] - 450000) / 4000, color=palette.cat[1], edgecolor="white", lw=0.6, label="5 comparables retenus", zorder=4)35 for _, r in comps.iterrows():36 ax.annotate(f"{r['id_vente']}\n{fmt_d(r['prix_vente'])}", ((r["lon"] - lon0) * kx, (r["lat"] - lat0) * ky), textcoords="offset points", xytext=(6, 4), fontsize=6.5, color=palette.ink)37 ax.scatter([0], [0], marker="*", s=160, color=palette.ink, zorder=5, label="sujet (adresse fictive, secteur Hull)")38 ax.set_aspect("equal"); ax.set_xlim(-3.4, 3.4); ax.set_ylim(-3.4, 3.4)39 ax.set_xlabel("Est–ouest (km, à partir du sujet)"); ax.set_ylabel("Nord–sud (km)")40 ax.legend(loc="upper left", fontsize=7, frameon=False)41 ax.set_title("Taille des points proportionnelle au prix de vente", loc="left", fontsize=8)42 fig.tight_layout()43 savefig(fig, "ch45/ch45-fig-carte-comparables.pdf")444546def fig_cascade(D):47 """Prix bruts → ajustements par groupe (empilés) → prix ajustés, pour les 5 comparables (grille du ch. 17), et valeur pondérée."""48 a = D["adj17"]; grp = [g for g in c17.GROUPES if (a[g].abs() > 1).any()]49 fig, ax = plt.subplots(figsize=(LARGEUR, 3.9))50 x = np.arange(len(a)); w = 0.2751 ax.bar(x - 0.3, a["prix_vente"], width=w, color=palette.cat[0], alpha=0.5, label="prix de vente")52 ax.bar(x + 0.3, a["prix_ajuste"], width=w, color=palette.cat[1], label="prix ajusté")53 cols = [palette.cat[(i + 2) % len(palette.cat)] for i in range(len(grp))]54 for i, (_, r) in enumerate(a.iterrows()):55 pos = r["prix_vente"]; neg = r["prix_vente"]56 for g, c in zip(grp, cols):57 v = r[g]58 if v >= 0:59 ax.bar(x[i], v, bottom=pos, width=w, color=c, edgecolor="white", lw=0.3, label=c17.LIBELLES[g] if i == 0 else None); pos += v60 else:61 ax.bar(x[i], v, bottom=neg, width=w, color=c, edgecolor="white", lw=0.3, label=c17.LIBELLES[g] if i == 0 else None); neg += v62 ax.plot([x[i] - 0.3 - w / 2, x[i] + w / 2], [r["prix_vente"]] * 2, color=palette.ink2, lw=0.5, ls=":")63 ax.text(x[i] + 0.3, r["prix_ajuste"] + 5000, fmt_d(r["prix_ajuste"]), ha="center", fontsize=6.3)64 ax.text(x[i] - 0.3, r["prix_vente"] + 5000, fmt_d(r["prix_vente"]), ha="center", fontsize=6.3, color=palette.ink2)65 ax.axhline(D["V_comp"], color=palette.ink, lw=1.0, ls="--"); ax.text(len(a) - 0.55, D["V_comp"] - 11000, f"valeur pondérée {fmt_d(D['V_comp'])}", ha="right", fontsize=7)66 ax.axhline(D["V_hed"], color=palette.ink2, lw=0.8, ls=":"); ax.text(len(a) - 0.55, D["V_hed"] + 4000, f"prédiction hédonique {fmt_d(D['V_hed'])}", ha="right", fontsize=7, color=palette.ink2)67 ax.set_xticks(x); ax.set_xticklabels([f"{r['id_vente']}\nbrut {r['ajust_brut_pct']*100:.1f} %\npoids {D['poids17'][i]:.3f}".replace(".", ",") for i, (_, r) in enumerate(a.iterrows())], fontsize=7)68 ax.set_ylim(440000, 770000); ax.yaxis.set_major_formatter(FuncFormatter(fmt_k))69 ax.set_ylabel("Prix ($)"); ax.legend(loc="upper left", fontsize=6.3, frameon=False, ncol=3)70 fig.tight_layout()71 savefig(fig, "ch45/ch45-fig-grille-cascade.pdf")727374def fig_fourchettes(D):75 """Indications ± 1 écart-type (ch. 20), IC et IP hédoniques (ch. 18), conclusion, fourchette conclue, bande bootstrap 90 %,76 rôle (fictif) et valeur réelle au 2024-07-01 (ch. 9), prêt assuré maximal."""77 fig, ax = plt.subplots(figsize=(LARGEUR, 3.5))78 lignes = [("Comparaison (ch. 17)", D["indications"]["comparaison"], D["ecarts_types20"]["comparaison"], None),79 ("Hédonique (ch. 18)", D["indications"]["hedonique"], D["ecarts_types20"]["hedonique"], (D["ip95"], D["ic95_valeur"])),80 ("Coût (ch. 24-25)", D["indications"]["cout"], D["ecarts_types20"]["cout"], None),81 ("Combinaison 0,40/0,45/0,15 (ch. 20)", D["V_raisonnee"], D["sd_conclusion"], None)]82 for i, (lab, v, sd, extra) in enumerate(lignes):83 y = len(lignes) - i84 if extra:85 ax.plot(extra[0], [y, y], color=palette.cat[0], lw=1.0, alpha=0.5); ax.text(extra[0][1] + 3000, y, "IP 95 % du prix", fontsize=6.5, va="center", color=palette.ink2)86 ax.plot(extra[1], [y, y], color=palette.cat[0], lw=5, alpha=0.35, solid_capstyle="butt")87 ax.errorbar([v], [y], xerr=[[sd], [sd]], fmt="o", color=palette.cat[1] if i < 3 else palette.ink, capsize=3, ms=5, lw=1.4)88 ax.text(v, y + 0.22, fmt_d(v), ha="center", fontsize=7)89 ax.set_yticks([len(lignes) - i for i in range(len(lignes))]); ax.set_yticklabels([l[0] for l in lignes], fontsize=7.5)90 ax.axvspan(*D["fourchette"], color=palette.cat[1], alpha=0.10)91 ax.axvspan(*D["bande90"], color=palette.cat[1], alpha=0.14)92 ax.axvline(D["conclusion"], color=palette.ink, lw=1.3); ax.text(D["conclusion"], 4.75, f"valeur la plus probable {fmt_d(D['conclusion'])}", ha="center", fontsize=7.5)93 ax.text(D["fourchette"][0] + 2000, 0.35, f"fourchette {fmt_d(D['fourchette'][0])} – {fmt_d(D['fourchette'][1])}", fontsize=7, ha="left")94 ax.text(D["bande90"][0], 0.08, f"bande bootstrap 90 % {fmt_d(D['bande90'][0])} – {fmt_d(D['bande90'][1])}", fontsize=6.5, ha="left", color=palette.ink2)95 ax.axvline(D["role"], color=palette.ink2, lw=0.8, ls=":"); ax.text(D["role"], 0.35, "rôle (fictif)\n" + fmt_d(D["role"]), ha="center", fontsize=6.5, color=palette.ink2)96 ax.axvline(D["valeur_reelle_2024"], color=palette.ink2, lw=0.8, ls=":"); ax.text(D["valeur_reelle_2024"] - 2500, 0.72, "valeur réelle au\n2024-07-01 (ch. 9)", ha="right", fontsize=6.5, color=palette.ink2)97 ax.set_ylim(-0.1, 5.1); ax.set_xlim(445000, 775000); ax.xaxis.set_major_formatter(FuncFormatter(fmt_k))98 ax.set_xlabel("Valeur ($) — points : indications ; barres : ± 1 écart-type attribué au ch. 20")99 fig.tight_layout()100 savefig(fig, "ch45/ch45-fig-fourchettes.pdf")101102103def fig_depreciation(D):104 """(a) Dépréciation ventilée par composante (ch. 11) contre dépréciation extraite du marché (ch. 24) ;105 (b) décomposition de la valeur par le coût : terrain + coût neuf − dépréciation."""106 f = D["fiche"].copy(); f["dep"] = f["ratio"] * f["cout_neuf"]107 f = f.sort_values("dep", ascending=True)108 fig, axes = plt.subplots(1, 2, figsize=(LARGEUR, 4.0), gridspec_kw={"width_ratios": [1.45, 1]})109 ax = axes[0]110 y = np.arange(len(f))111 ax.barh(y, f["cout_neuf"], color=palette.cat[0], alpha=0.25, label="coût neuf de la composante")112 ax.barh(y, f["dep"], color=palette.cat[1], label="dépréciation ventilée (âge effectif / vie utile)")113 ax.set_yticks(y); ax.set_yticklabels([c if len(c) < 34 else c[:32] + "…" for c in f["composante"]], fontsize=6)114 ax.xaxis.set_major_formatter(FuncFormatter(fmt_k)); ax.legend(loc="lower right", fontsize=6.5, frameon=False)115 ax.set_title(f"(a) Ventilée : {fmt_d(D['dv11']['total'])} ({D['dv11']['taux']*100:.1f} % du coût neuf)".replace(".", ","), loc="left", fontsize=8)116 ax = axes[1]117 cats = ["Terrain", "Coût\nneuf", "Dépréc.\nextraite", "Carence\nsalle d'eau", "Valeur\npar le coût"]118 vals = [D["V_L"], D["CN"], -D["D_ext"], -D["D_fonc"], D["V_cout"]]119 bottoms = [0, D["V_L"], D["V_L"] + D["CN"] - D["D_ext"], D["V_L"] + D["CN"] - D["D_ext"] - D["D_fonc"], 0]120 cols = [palette.cat[2], palette.cat[0], palette.cat[1], palette.cat[1], palette.ink]121 ax.bar(cats, [abs(v) for v in vals], bottom=bottoms, color=cols, edgecolor="white", lw=0.5)122 for i, (v, b) in enumerate(zip(vals, bottoms)):123 if v < 0:124 ax.text(i, b + abs(v) / 2 if i == 2 else b - 12000, ("−" + fmt_d(abs(v))), ha="center", va="center", fontsize=6.3, color=palette.ink if i == 3 else "white")125 else:126 ax.text(i, b + abs(v) + 8000, fmt_d(abs(v)), ha="center", fontsize=6.5)127 ax.axhline(D["V_L"] + D["CN"], color=palette.ink2, lw=0.7, ls=":"); ax.text(0.0, D["V_L"] + D["CN"] + 30000, f"terrain + coût neuf\n{fmt_d(D['V_L'] + D['CN'])}", fontsize=6.5, ha="left", color=palette.ink2)128 ax.yaxis.set_major_formatter(FuncFormatter(fmt_k)); ax.tick_params(axis="x", labelsize=6.3); ax.set_ylim(0, 790000)129 ax.set_title(f"(b) Extraction : {D['taux_tot']*100:.2f} % → {fmt_d(D['V_cout_arrondie'])}".replace(".", ","), loc="left", fontsize=8)130 fig.tight_layout(w_pad=1.5)131 savefig(fig, "ch45/ch45-fig-depreciation.pdf")132133134def fig_financement(D):135 """(a) Paiement mensuel et paiement au taux admissible selon la mise de fonds ; (b) paiement au renouvellement selon la hausse de taux."""136 F = D["financement"]; ren = D["renouvellement"]137 fig, axes = plt.subplots(1, 2, figsize=(LARGEUR, 3.3))138 ax = axes[0]139 keys = ["minimale", "10 %", "20 %", "minimale 30 ans"]; labs = ["5,87 %\n(min.)\n25 ans", "10 %\n25 ans", "20 %\n(non assuré)\n25 ans", "5,87 %\n30 ans"]140 x = np.arange(len(keys)); w = 0.38141 ax.bar(x - w / 2, [F[k]["paiement"] for k in keys], width=w, color=palette.cat[1], label="paiement au taux du contrat")142 ax.bar(x + w / 2, [F[k]["paiement_mqr"] for k in keys], width=w, color=palette.cat[0], alpha=0.6, label="paiement au taux admissible minimal")143 for i, k in enumerate(keys):144 ax.text(x[i] - w / 2, F[k]["paiement"] + 40, f"{F[k]['paiement']:,.0f}".replace(",", " "), ha="center", fontsize=6.5)145 ax.text(x[i] + w / 2, F[k]["paiement_mqr"] + 40, f"{F[k]['paiement_mqr']:,.0f}".replace(",", " "), ha="center", fontsize=6.5, color=palette.ink2)146 ax.text(x[i], 300, f"prime\n{fmt_d(F[k]['prime'])}" if F[k]["prime"] else "sans\nprime", ha="center", fontsize=6.3, color="white")147 ax.set_xticks(x); ax.set_xticklabels(labs, fontsize=7); ax.set_ylabel("Paiement mensuel ($)"); ax.set_ylim(0, 4600)148 ax.legend(loc="upper right", fontsize=6.5, frameon=False); ax.set_title(f"(a) Prix {fmt_d(D['prix'])}, taux {F['minimale']['taux']*100:.2f} % / {F['20 %']['taux']*100:.2f} %".replace(".", ","), loc="left", fontsize=8)149 ax = axes[1]150 ax.plot(ren["hausse_pb"], ren["paiement"], marker="o", color=palette.cat[1], lw=1.6, label="paiement mensuel")151 for _, r in ren.iterrows():152 ax.annotate(f"{r['paiement']:,.0f} $\n({r['hausse_paiement_pct']*100:+.1f} %)".replace(",", " ").replace(".", ","), (r["hausse_pb"], r["paiement"]), textcoords="offset points", xytext=(0, 8), ha="center", fontsize=6.5)153 ax.axhline(F["minimale"]["paiement_mqr"], color=palette.cat[0], lw=0.9, ls="--"); ax.text(0, F["minimale"]["paiement_mqr"] + 25, "paiement au taux admissible initial (6,12 %)", fontsize=6.5, ha="left", color=palette.cat[0])154 ax.set_xlabel("Hausse du taux au renouvellement (points de base)"); ax.set_ylabel("Paiement mensuel ($)"); ax.set_ylim(2900, 4450)155 ax.set_xticks(ren["hausse_pb"]); ax.set_title(f"(b) Solde à 60 mois {fmt_d(F['minimale']['solde_terme'])}, 20 ans restants", loc="left", fontsize=8)156 fig.tight_layout(w_pad=1.5)157 savefig(fig, "ch45/ch45-fig-financement.pdf")158159160def main():161 setup()162 D = ch.dossier()163 fig_carte(D); fig_cascade(D); fig_fourchettes(D); fig_depreciation(D); fig_financement(D)164 print("figures ch45 écrites dans assets/figures/ch45/")165166167if __name__ == "__main__":168 main()169