SPB Git
10.7 KB · 126 lines python
1#!/usr/bin/env python32"""Chapitre 46 — figures du dossier « triplex à Québec » (DONNÉES SYNTHÉTIQUES, graine 20261031).3Produit assets/figures/ch46/ch46-fig-{loyers,cascade,mrb,occupation,fourchettes}.pdf4Usage (racine) : python3 analysis/python/figures/ch46_figures.py"""5from __future__ import annotations6import sys, pathlib7import numpy as np8import matplotlib.pyplot as plt9from matplotlib.ticker import FuncFormatter1011ROOT = pathlib.Path(__file__).resolve().parents[3]12sys.path.insert(0, str(ROOT / "analysis/python")); sys.path.insert(0, str(ROOT / "analysis/python/cases"))13from traite.style import setup, palette, savefig, LARGEUR, fmt_dollars  # noqa: E40214import ch46_dossier as ch  # noqa: E402151617def fig_loyers(D):18    L = D["loyers"]; x = np.arange(len(L)); w = 0.3619    fig, axes = plt.subplots(1, 2, figsize=(LARGEUR, 2.9), gridspec_kw={"width_ratios": [1.6, 1]})20    ax = axes[0]21    ax.bar(x - w / 2, L["loyer_mensuel"], w, color=palette.cat[0], label="Loyer contractuel (bail 2026-2027)")22    ax.bar(x + w / 2, L["loyer_marche_estime"], w, color=palette.cat[1], label="Loyer de marché estimé")23    for i, r in L.iterrows():24        e = (r["loyer_marche_estime"] - r["loyer_mensuel"]) / r["loyer_marche_estime"]25        ax.text(x[i], r["loyer_marche_estime"] + 20, f"−{e*100:.1f} %".replace(".", ","), ha="center", va="bottom", fontsize=7.5, color=palette.ink)26    ax.set_xticks(x); ax.set_xticklabels([f"{l}\n{p}, {s} m²\nbail {a}" for l, p, s, a in zip(["Rez-de-ch.", "2e étage", "3e étage"], L["pieces"], L["superficie_m2"], L["locataire_depuis"])], fontsize=7.2)27    ax.set_ylabel("Loyer mensuel ($)"); ax.yaxis.set_major_formatter(FuncFormatter(fmt_dollars)); ax.set_ylim(0, 2050)28    ax.legend(loc="upper left", fontsize=7, ncol=1); ax.grid(axis="x", visible=False); ax.set_title("A. Loyer contractuel et loyer de marché", loc="left", fontsize=8.5)29    ax = axes[1]30    anc = 2026 - L["locataire_depuis"]; ecart = (1 - L["loyer_mensuel"] / L["loyer_marche_estime"]) * 10031    ax.scatter(anc, ecart, s=48, color=palette.cat[0], zorder=3)32    for a, e, l in zip(anc, ecart, ["RC", "2e", "3e"]): ax.annotate(l, (a, e), textcoords="offset points", xytext=(6, 2), fontsize=7.5, color=palette.ink)33    xx = np.linspace(0, 8, 20); ax.plot(xx, 1.5 * xx, color=palette.ink2, lw=1, ls="--", label="Loi de génération des ventes (1,5 %/an)")34    ax.plot(xx, 2.0 * xx, color=palette.cat[2], lw=1, ls=":", label="Décote du cas 3 (2 %/an)")35    ax.set_xlabel("Ancienneté du bail (années)"); ax.set_ylabel("Écart au marché (%)"); ax.set_xlim(0, 8); ax.set_ylim(0, 40)36    ax.legend(loc="upper left", fontsize=6.5); ax.set_title("B. Écart et ancienneté", loc="left", fontsize=8.5)37    fig.tight_layout(w_pad=1.5)38    savefig(fig, "ch46/ch46-fig-loyers.pdf")394041def fig_cascade(D):42    t = D["grille"]["tableau"]; r = t[t["id_vente"] == "Q0243"].iloc[0]43    etapes = [("Prix/log.\n2026-07-24", r["prix_logement"]), ("Marché\n(temps)", r["temps"]), ("Superficie\npar log.", r["superficie"]), ("Âge", r["age"]),44              ("État", r["etat"]), ("Station-\nnement", r["stationnement"]), ("Loyer de\nmarché", r["loyer_marche"]), ("Loyer relatif\n(non appl.)", r["loyer_relatif"])]45    fig, ax = plt.subplots(figsize=(LARGEUR, 3.2))46    cum = etapes[0][1]; ax.bar(0, cum, color=palette.cat[0], width=0.6)47    ax.text(0, cum + 2500, fmt_dollars(cum), ha="center", fontsize=7.5)48    for i, (lib, a) in enumerate(etapes[1:], 1):49        col = palette.cat[1] if a >= 0 else palette.cat[3]50        bas = cum if a >= 0 else cum + a51        ax.bar(i, abs(a) if abs(a) > 1 else 600, bottom=bas if abs(a) > 1 else cum, color=col if abs(a) > 1 else palette.grid, width=0.6)52        ax.text(i, max(cum, cum + a) + 2500, (f"+{a:,.0f}" if a >= 0 else f"−{abs(a):,.0f}").replace(",", " ") + " $", ha="center", fontsize=7.2)53        ax.plot([i - 0.7, i + 0.3], [cum, cum], color=palette.ink2, lw=0.6)54        cum += a55    ax.bar(len(etapes), cum, color=palette.cat[0], width=0.6); ax.text(len(etapes), cum + 2500, fmt_dollars(cum), ha="center", fontsize=7.5, fontweight="bold")56    ax.set_xticks(range(len(etapes) + 1)); ax.set_xticklabels([e[0] for e in etapes] + ["Prix\najusté"], fontsize=7)57    ax.set_ylim(300000, 400000); ax.yaxis.set_major_formatter(FuncFormatter(fmt_dollars)); ax.set_ylabel("$ par logement"); ax.grid(axis="x", visible=False)58    ax.text(0.99, 0.04, f"Ajustement brut {r['brut_pct']*100:.1f} % ; net +{r['net_pct']*100:.1f} %".replace(".", ","), transform=ax.transAxes, ha="right", fontsize=7.5, color=palette.ink2)59    savefig(fig, "ch46/ch46-fig-cascade.pdf")606162def fig_mrb(D):63    c = D["comparables127"]; tri = c[c["nb_logements"] == 3]; comp5 = D["comparables5"]; rec = D["reconciliation"]; rev = D["revenu"]64    fig, axes = plt.subplots(1, 2, figsize=(LARGEUR, 3.0), gridspec_kw={"width_ratios": [1.5, 1]})65    ax = axes[0]66    ax.hist(tri["mrb"], bins=np.arange(12, 32, 1.0), color=palette.cat[0], alpha=0.35, label="67 triplex (4 quartiers, 24 mois)")67    ax.hist(c.loc[(c["quartier"] == "Limoilou") & (c["nb_logements"] == 3), "mrb"], bins=np.arange(12, 32, 1.0), color=palette.cat[0], alpha=0.8, label="28 triplex de Limoilou")68    for i, (_, r) in enumerate(comp5.iterrows()):69        ax.axvline(r["mrb"], color=palette.cat[1], lw=1.2, label="5 comparables (MRB 20,75 à 23,35)" if i == 0 else None)70    ax.axvline(rec["mrb_implicite"], color=palette.ink, lw=1.6, ls="--", label=f"Sujet, valeur conclue ({rec['mrb_implicite']:.2f})".replace(".", ","))71    ax.axvline(rev["mrb_127"], color=palette.ink2, lw=1, ls=":", label=f"Médiane des 67 triplex ({rev['mrb_127']:.2f})".replace(".", ","))72    ax.set_xlabel("Multiplicateur du revenu brut (prix / RBP contractuel)"); ax.set_ylabel("Nombre de ventes"); ax.set_ylim(0, 15); ax.set_xlim(12, 31); ax.legend(loc="upper right", fontsize=6.3)73    ax.set_title("A. Les cinq comparables dans la distribution", loc="left", fontsize=8.5)74    ax = axes[1]75    rho5 = comp5["loyer_mensuel_moyen_contrat"] / comp5["loyer_mensuel_moyen_marche"]; rho_tri = tri["loyer_mensuel_moyen_contrat"] / tri["loyer_mensuel_moyen_marche"]76    ax.scatter(rho_tri, tri["mrb"], s=10, color=palette.cat[0], alpha=0.35, label="67 triplex")77    ax.scatter(rho5, comp5["mrb"], s=40, color=palette.cat[1], zorder=3, label="5 comparables")78    reg = D["regression_taux"]; xx = np.linspace(0.72, 1.0, 50); ax.plot(xx, np.exp(reg["mrb_a"] + reg["mrb_b_rho"] * (xx - 1)), color=palette.ink, lw=1.2, label="ln MRB = a + b(ρ − 1)")79    ax.axvline(D["base"]["rho"], color=palette.ink2, lw=0.8, ls="--"); ax.text(D["base"]["rho"] + 0.005, 13, "ρ du sujet", fontsize=7, color=palette.ink2)80    ax.set_xlabel("Loyer relatif ρ (contrat / marché)"); ax.set_ylabel("MRB"); ax.set_ylim(12, 31); ax.legend(loc="upper right", fontsize=6.8)81    ax.set_title("B. MRB et loyer relatif", loc="left", fontsize=8.5)82    fig.tight_layout(w_pad=1.5)83    savefig(fig, "ch46/ch46-fig-mrb.pdf")848586def fig_occupation(D):87    g = D["grille_occupation"]; occ = D["financement"]["occupation"]88    fig, ax = plt.subplots(figsize=(LARGEUR, 3.1))89    for i, (lib, lab) in enumerate((("minimale", "Mise de fonds minimale (7,3 %, assuré)"), ("10 %", "Mise de fonds de 10 % (assuré)"), ("15 %", "Mise de fonds de 15 % (assuré)"), ("20 %", "Mise de fonds de 20 % (non assuré, +46 pb)"))):90        s = g[g["mise"] == lib]; ax.plot(s["taux"] * 100, s["hors_capital"], color=palette.cat[i], label=lab)91    ax.axhline(occ["loyer_marche_logement"], color=palette.ink, lw=1.2, ls="--"); ax.text(3.15, occ["loyer_marche_logement"] + 25, "Loyer de marché du rez-de-chaussée (1 430 $)", fontsize=7.5, color=palette.ink)92    ax.axvline(ch.C.TAUX_ASSURE * 100, color=palette.ink2, lw=0.8, ls=":"); ax.text(ch.C.TAUX_ASSURE * 100 + 0.05, 1080, "Taux assuré, août 2026 (4,12 %)", fontsize=7, color=palette.ink2)93    ax.set_xlabel("Taux hypothécaire nominal (%, composition semestrielle)"); ax.set_ylabel("Coût économique mensuel hors capital ($)")94    ax.yaxis.set_major_formatter(FuncFormatter(fmt_dollars)); ax.set_xlim(3.1, 7.2); ax.set_ylim(1000, 3600); ax.legend(loc="upper left", fontsize=7.2)95    savefig(fig, "ch46/ch46-fig-occupation.pdf")969798def fig_fourchettes(D):99    rec = D["reconciliation"]; rev = D["revenu"]; gr = D["grille"]; co = D["cout"]; cs = D["cout_sensibilite"]; ch32 = D["ch32"]100    lignes = [101        ("Comparaison — prix par logement ajusté (5 comp.)", gr["valeur"], gr["reconciliation"]["min"] * 3, gr["reconciliation"]["max"] * 3, 0.40),102        ("Comparaison — MRB médian des comparables (22,05)", rev["v_mrb_comp"], float(D["comparables5"]["mrb"].min()) * D["base"]["rbp_contrat"], float(D["comparables5"]["mrb"].max()) * D["base"]["rbp_contrat"], 0.35),103        ("Revenu — capitalisation au taux ajusté (3,02 %)", rev["v_cap_ajuste"], rev["v_cap_q3"], rev["v_cap_q1"], 0.25),104        ("Repère — 127 ventes brutes (ch. 32)", ch32["v_log_127"], ch32["v_cap_127"], ch32["v_log_127"], 0.0),105        ("Repère — ch. 32 (médianes des comparables)", 872425.7581, 729549.19, 922395.6, 0.0),106        ("Contrôle — coût déprécié (hypothèses déclarées)", co["valeur"], float(cs["valeur"].min()), float(cs["valeur"].max()), 0.0),107    ]108    fig, ax = plt.subplots(figsize=(LARGEUR, 3.1))109    for i, (lib, v, lo, hi, w) in enumerate(lignes):110        y = len(lignes) - 1 - i; col = palette.cat[0] if w > 0 else palette.ink2111        ax.plot([lo, hi], [y, y], color=col, lw=1.4, alpha=0.6); ax.scatter([v], [y], s=46 if w > 0 else 26, color=col, zorder=3, marker="o" if w > 0 else "s")112        ax.text(hi + 12000, y, fmt_dollars(v) + (f"  (poids {int(w*100)} %)" if w > 0 else ""), va="center", fontsize=7.2, color=palette.ink)113    ax.axvspan(rec["fourchette_conclue"][0], rec["fourchette_conclue"][1], color=palette.cat[1], alpha=0.12, lw=0)114    ax.axvline(rec["valeur_finale"], color=palette.ink, lw=1.5, ls="--"); ax.text(rec["valeur_finale"], len(lignes) - 0.45, f"Valeur conclue {fmt_dollars(rec['valeur_finale'])}\nfourchette {fmt_dollars(rec['fourchette_conclue'][0])} – {fmt_dollars(rec['fourchette_conclue'][1])}", ha="center", fontsize=7.5, color=palette.ink)115    ax.axvline(D["sujet"]["valeur_role_2025"], color=palette.cat[3], lw=1, ls=":"); ax.text(D["sujet"]["valeur_role_2025"], -0.75, "Rôle 2025-2027\n(réf. 2023-07-01)", ha="center", fontsize=7, color=palette.cat[3])116    ax.set_yticks(range(len(lignes))); ax.set_yticklabels([l[0] for l in reversed(lignes)], fontsize=7.2)117    ax.xaxis.set_major_formatter(FuncFormatter(fmt_dollars)); ax.set_xlim(330000, 1480000); ax.set_ylim(-1.1, len(lignes) + 0.3); ax.grid(axis="y", visible=False)118    ax.set_xlabel("Indication de valeur ($)")119    savefig(fig, "ch46/ch46-fig-fourchettes.pdf")120121122if __name__ == "__main__":123    setup(); D = ch.dossier()124    fig_loyers(D); fig_cascade(D); fig_mrb(D); fig_occupation(D); fig_fourchettes(D)125    print("Figures écrites dans assets/figures/ch46/")126
Files · cd76781f94cbc7113926bd8a4bd920d14c17c982
Menu

Theme