SPB Git
9.2 KB · 129 lines python
1"""Figures du chapitre 32 — Duplex à quintuplex (données SYNTHÉTIQUES du cas 2, sauf la figure des médianes APCIQ, citée).2Produit : assets/figures/ch32/ch32-fig-{mrb-taille-quartier,taux-loyer-relatif,prix-logement-quartier,plex-apciq-rmr,sensibilite-occupant}.pdf3Exécution : python3 analysis/python/figures/ch32_figures.py (depuis la racine)."""4from __future__ import annotations5import sys, pathlib6import numpy as np7import matplotlib.pyplot as plt8from matplotlib.ticker import FuncFormatter910ROOT = pathlib.Path(__file__).resolve().parents[3]11sys.path.insert(0, str(ROOT / "analysis/python")); sys.path.insert(0, str(ROOT / "analysis/python/cases"))12from traite.style import setup, palette, savefig, LARGEUR, DEMI, fmt_dollars, fmt_milliers  # noqa: E40213import ch32_plex as C  # noqa: E4021415QUARTIERS = ["Limoilou", "Saint-Roch", "Saint-Sauveur", "Maizerets"]161718def _boxstyle(bp, color):19    for k in ("boxes", "whiskers", "caps", "medians"):20        for a in bp[k]: a.set_color(color); a.set_linewidth(1.2)21    for a in bp["medians"]: a.set_color(palette.ink); a.set_linewidth(1.6)22    for a in bp["fliers"]: a.set(marker="o", markersize=3, markerfacecolor="none", markeredgecolor=color, alpha=0.7)232425def fig_mrb(R):26    c = R["comparables"]27    fig, axes = plt.subplots(1, 2, figsize=(LARGEUR, 3.0), gridspec_kw={"width_ratios": [1, 2]})28    ax = axes[0]29    data = [c.loc[c["nb_logements"] == n, "mrb"] for n in (2, 3)]30    bp = ax.boxplot(data, widths=0.55, patch_artist=False); _boxstyle(bp, palette.cat[0])31    ax.set_xticks([1, 2]); ax.set_xticklabels([f"Duplex\n(n = {len(data[0])})", f"Triplex\n(n = {len(data[1])})"])32    ax.set_ylabel("Multiplicateur du revenu brut (MRB)"); ax.set_title("Selon la taille", loc="left")33    for i, d in enumerate(data, 1):34        ax.text(i + 0.33, d.median(), f"{d.median():.1f}".replace(".", ","), va="center", fontsize=8, color=palette.ink)35    ax = axes[1]36    data = [c.loc[c["quartier"] == q, "mrb"] for q in QUARTIERS]37    bp = ax.boxplot(data, widths=0.55); _boxstyle(bp, palette.cat[1])38    ax.set_xticks(range(1, 5)); ax.set_xticklabels([f"{q}\n(n = {len(d)})" for q, d in zip(QUARTIERS, data)])39    ax.set_title("Selon le quartier (2 et 3 logements confondus)", loc="left")40    for i, d in enumerate(data, 1):41        ax.text(i + 0.33, d.median(), f"{d.median():.1f}".replace(".", ","), va="center", fontsize=8, color=palette.ink)42    ymin = min(c["mrb"]) - 1; ymax = max(c["mrb"]) + 143    for ax in axes: ax.set_ylim(ymin, ymax); ax.grid(axis="x", visible=False)44    fig.tight_layout(); savefig(fig, "ch32/ch32-fig-mrb-taille-quartier.pdf")454647def fig_taux_rho(R):48    g = R["decomposition"]; reg = R["regression"]; I = R["indications"]49    fig, ax = plt.subplots(figsize=(LARGEUR, 3.3))50    for n, col, lab in ((2, palette.cat[0], "Duplex"), (3, palette.cat[1], "Triplex")):51        s = g[g["nb_logements"] == n]52        ax.scatter(s["loyer_relatif"], 100 * s["taux_global"], s=16, color=col, alpha=0.75, label=lab, edgecolors="none")53    xs = np.linspace(g["loyer_relatif"].min() - 0.01, 1.01, 50)54    ax.plot(xs, 100 * (reg["a"] + reg["b_rho"] * (xs - 1)), color=palette.ink, linewidth=1.4, label="Régression $R = a + b(\\rho - 1)$")55    ax.axhline(100 * R["indicateurs"]["taux_median"], color=palette.ink2, linestyle=":", linewidth=1)56    ax.text(xs[0], 100 * R["indicateurs"]["taux_median"] + 0.04, "médiane des 127 ventes", fontsize=7.5, color=palette.ink2)57    ax.scatter([I["rho_sujet"]], [100 * I["taux_ajuste"]], marker="*", s=140, color=palette.cat[2], zorder=5, label="Sujet : $\\rho$ = 0,848, taux ajusté", edgecolors=palette.ink, linewidths=0.5)58    ax.set_xlabel("Loyer contractuel moyen / loyer de marché estimé ($\\rho$)"); ax.set_ylabel("Taux global extrait (%)")59    ax.yaxis.set_major_formatter(FuncFormatter(lambda x, p: f"{x:.1f}".replace(".", ",")))60    ax.xaxis.set_major_formatter(FuncFormatter(lambda x, p: f"{x:.2f}".replace(".", ",")))61    ax.legend(loc="upper left", fontsize=7.5, ncol=2); fig.tight_layout()62    savefig(fig, "ch32/ch32-fig-taux-loyer-relatif.pdf")636465def fig_prix_logement(R):66    c = R["comparables"]; comp5 = R["comparables5"]; I = R["indications"]67    fig, ax = plt.subplots(figsize=(LARGEUR, 3.2))68    data = [c.loc[c["quartier"] == q, "prix_par_logement"] / 1000 for q in QUARTIERS]69    bp = ax.boxplot(data, widths=0.55); _boxstyle(bp, palette.cat[0])70    rng = np.random.default_rng(20261031)71    for i, q in enumerate(QUARTIERS, 1):72        s = c[c["quartier"] == q]73        ax.scatter(i + rng.uniform(-0.18, 0.18, len(s)), s["prix_par_logement"] / 1000, s=9, color=palette.cat[0], alpha=0.35, edgecolors="none")74    ax.scatter(np.ones(len(comp5)) + 0.0, comp5["prix_par_logement"] / 1000, marker="D", s=30, color=palette.cat[2], zorder=5, label="Cinq comparables retenus (triplex, Limoilou)", edgecolors=palette.ink, linewidths=0.4)75    ax.axhline(I["prix_log_final"] / 1000, color=palette.ink, linestyle="--", linewidth=1.1)76    ax.text(4.45, I["prix_log_final"] / 1000 + 4, "valeur pondérée\ndu sujet, par logement", fontsize=7.5, ha="right", color=palette.ink)77    ax.set_xticks(range(1, 5)); ax.set_xticklabels([f"{q}\n(n = {len(d)})" for q, d in zip(QUARTIERS, data)])78    ax.set_ylabel("Prix par logement (milliers de $)"); ax.yaxis.set_major_formatter(FuncFormatter(fmt_milliers))79    ax.grid(axis="x", visible=False); ax.legend(loc="upper right", fontsize=7.5); fig.tight_layout()80    savefig(fig, "ch32/ch32-fig-prix-logement-quartier.pdf")818283# Médianes APCIQ (Centris) des plex de 2 à 5 logements, T3 2026 — chiffres cités (communiqué du 14 octobre 2026 et baromètres), sources/notes-E.md84APCIQ_PLEX_T3_2026 = [85    ("Province de Québec", 699_000), ("RMR de Montréal", 860_500), ("Île de Montréal", 900_000), ("RMR de Québec", 600_000),86    ("Agglomération de Québec", 618_500), ("Secteur La Cité–Basse-Ville et Limoilou", 647_500), ("RMR de Gatineau", 617_500),87    ("RMR de Sherbrooke", 544_500), ("RMR de Trois-Rivières", 420_000), ("RMR de Saguenay", 373_500)]888990def fig_apciq():91    fig, ax = plt.subplots(figsize=(LARGEUR, 3.1))92    labels = [l for l, _ in APCIQ_PLEX_T3_2026][::-1]; vals = [v for _, v in APCIQ_PLEX_T3_2026][::-1]93    cols = [palette.cat[1] if ("Québec" in l and "Province" not in l) or "Limoilou" in l else palette.cat[0] for l in labels]94    ax.barh(labels, vals, color=cols, height=0.65)95    for i, v in enumerate(vals): ax.text(v + 8000, i, fmt_dollars(v), va="center", fontsize=7.5, color=palette.ink)96    ax.set_xlim(0, 1_050_000); ax.xaxis.set_major_formatter(FuncFormatter(lambda x, p: fmt_milliers(x / 1000) + " k$"))97    ax.set_xlabel("Prix médian des plex (2 à 5 logements), 3ᵉ trimestre 2026"); ax.grid(axis="y", visible=False); fig.tight_layout()98    savefig(fig, "ch32/ch32-fig-plex-apciq-rmr.pdf")99100101def fig_sensibilite(R):102    S = R["sensibilite"]; base = R["base"]; I = R["indications"]; loyers = R["loyers"]103    fig, axes = plt.subplots(1, 2, figsize=(LARGEUR, 3.1))104    ax = axes[0]; L = S["loyers"]105    ax.plot(L["rho"], L["v_mrb"] / 1000, marker="o", color=palette.cat[0], label="MRB des comparables × RBP")106    ax.plot(L["rho"], L["v_cap_taux_median"] / 1000, marker="s", color=palette.cat[1], label="Capitalisation, taux médian fixe")107    ax.plot(L["rho"], L["v_cap_ajuste"] / 1000, marker="^", color=palette.cat[2], label="Capitalisation, taux ajusté à $\\rho$")108    ax.axvline(I["rho_sujet"], color=palette.ink2, linestyle=":", linewidth=1); ax.text(I["rho_sujet"] + 0.004, L["v_mrb"].max() / 1000, "sujet", fontsize=7.5, color=palette.ink2, va="top")109    ax.set_xlabel("Loyers contractuels / loyers de marché ($\\rho$)"); ax.set_ylabel("Indication de valeur (milliers de $)")110    ax.xaxis.set_major_formatter(FuncFormatter(lambda x, p: f"{x:.2f}".replace(".", ","))); ax.yaxis.set_major_formatter(FuncFormatter(fmt_milliers))111    ax.legend(fontsize=7, loc="upper left"); ax.set_title("Loyers sous le marché", loc="left")112    ax = axes[1]113    prix = I["valeur_arrondie"]; taux = np.arange(0.0312, 0.0713, 0.0025)114    for mf, col, lab in ((C.mise_de_fonds_minimale_assuree(prix), palette.cat[0], "Mise de fonds minimale assurée (7,1 %)"), (0.20 * prix, palette.cat[1], "Mise de fonds de 20 %")):115        y = [C.cout_occupation(C.financement_occupant(prix, mf, t), base, loyers, "RC")["cout_net_mensuel_hors_capital"] for t in taux]116        ax.plot(100 * taux, y, color=col, label=lab)117    ax.axhline(1430, color=palette.ink2, linestyle="--", linewidth=1); ax.text(100 * taux[0] + 0.05, 1430 + 40, "loyer de marché du logement occupé (1 430 $)", fontsize=7.5, color=palette.ink2)118    ax.axvline(100 * C.TAUX_ASSURE, color=palette.ink2, linestyle=":", linewidth=1); ax.text(100 * C.TAUX_ASSURE + 0.05, 3000, "4,12 %\n(assuré,\naoût 2026)", fontsize=7, color=palette.ink2)119    ax.set_xlabel("Taux hypothécaire (%)"); ax.set_ylabel("Coût net mensuel d'occupation, hors capital ($)")120    ax.xaxis.set_major_formatter(FuncFormatter(lambda x, p: f"{x:.1f}".replace(".", ","))); ax.yaxis.set_major_formatter(FuncFormatter(fmt_milliers))121    ax.legend(fontsize=7, loc="upper left"); ax.set_title("Financement de l'acheteur-occupant", loc="left")122    fig.tight_layout(); savefig(fig, "ch32/ch32-fig-sensibilite-occupant.pdf")123124125if __name__ == "__main__":126    setup(); R = C.calculer()127    fig_mrb(R); fig_taux_rho(R); fig_prix_logement(R); fig_apciq(); fig_sensibilite(R)128    print("figures ch32 écrites dans", ROOT / "assets/figures/ch32")129
Files · 577f3e95e830087bf4d5101ceb2925ae494ca15c
Menu

Theme