SPB Git
12.8 KB · 169 lines python
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
Files · da8a4ee0304be0aa9c54ed0aad08d985fc4539fc
Menu

Theme