SPB Git
10.5 KB · 130 lines python
1#!/usr/bin/env python32"""Chapitre 33 — figures (4) : (1) SCHL, île de Montréal, loyers et inoccupation par taille d'immeuble et par année de3construction (DONNÉES RÉELLES, Enquête sur les logements locatifs d'octobre 2025) ; (2) cas 4 : valeur par logement et4taux global par RMR ; (3) taux global selon la taille : comparables du cas 3 et immeubles du cas 4 ; (4) sensibilité5de la valeur par logement de l'immeuble P45 (36 logements) à la RMR et à l'état (régression du cas 4).6Idempotent ; écrit dans assets/figures/ch33/ch33-fig-<slug>.pdf.7Usage (racine) : PYTHONPATH=analysis/python python3 analysis/python/figures/ch33_figures.py"""8from __future__ import annotations9import sys, pathlib10import numpy as np, pandas as pd11import matplotlib.pyplot as plt12from matplotlib.ticker import FuncFormatter13sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1]))14from traite.style import setup, palette, savefig, LARGEUR, fmt_milliers15sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "cases"))16import ch33_six_logements as ch1718ROOT = pathlib.Path(__file__).resolve().parents[3]19pct1 = FuncFormatter(lambda x, p: f"{x:.1f}".replace(".", ",") + " %")20pct2 = FuncFormatter(lambda x, p: f"{x*100:.1f}".replace(".", ",") + " %")21kdol = FuncFormatter(lambda x, p: f"{x/1000:.0f} k$")22dol = FuncFormatter(lambda x, p: fmt_milliers(x) + " $")232425def fig_schl(R):26    t = R["schl"]["taille"]; a = R["schl"]["annee"]27    t = t[t["categorie"] != "Tous les immeubles"].copy(); a = a[a["categorie"] != "Tous les immeubles"].copy()28    t["etiquette"] = t["categorie"].str.replace(" unités", "").str.replace(" et +", " et plus")29    a["etiquette"] = a["categorie"].map({"Avant 1940": "< 1940", "1940 à 1959": "1940-59", "1960 à 1974": "1960-74", "1975 à 1989": "1975-89",30                                         "1990 à 2004": "1990-2004", "2005 à 2014": "2005-14", "2015 ou après": "2015 +"})31    fig, axes = plt.subplots(2, 2, figsize=(LARGEUR, 5.4), gridspec_kw={"hspace": 0.75, "wspace": 0.35})32    for (ax_l, ax_i), df, titre in ((axes[0], t, "taille d'immeuble"), (axes[1], a, "année de construction")):33        x = np.arange(len(df)); w = 0.3834        ax_l.bar(x - w / 2, df["loyer_tous"], w, color=palette.cat[0], label="tous les logements")35        ax_l.bar(x + w / 2, df["loyer_2ch"], w, color=palette.cat[1], label="2 chambres")36        for xi, v in zip(x - w / 2, df["loyer_tous"]): ax_l.text(xi, v + 25, fmt_milliers(v), ha="center", va="bottom", fontsize=6.5, color=palette.ink)37        ax_l.set_xticks(x); ax_l.set_xticklabels(df["etiquette"], fontsize=7, rotation=30, ha="right")38        ax_l.yaxis.set_major_formatter(dol); ax_l.set_ylim(0, 2450); ax_l.set_title(f"Loyer moyen selon la {titre}", loc="left", fontsize=8.5)39        ax_i.bar(x, df["inoccupation_tous"], 0.6, color=palette.cat[2])40        for xi, v in zip(x, df["inoccupation_tous"]):41            if not np.isnan(v): ax_i.text(xi, v + 0.08, f"{v:.1f}".replace(".", ","), ha="center", va="bottom", fontsize=7, color=palette.ink)42            else: ax_i.text(xi, 0.15, "n. d.", ha="center", va="bottom", fontsize=7, color=palette.ink2)43        ax_i.set_xticks(x); ax_i.set_xticklabels(df["etiquette"], fontsize=7, rotation=30, ha="right")44        ax_i.yaxis.set_major_formatter(pct1); ax_i.set_ylim(0, 6.6); ax_i.set_title(f"Inoccupation selon la {titre}", loc="left", fontsize=8.5)45    axes[0][0].legend(loc="upper left", fontsize=6.5, ncol=1)46    fig.text(0.99, 0.005, "Source : SCHL, Enquête sur les logements locatifs, octobre 2025, île de Montréal (zones 1-18) ; « n. d. » : donnée supprimée par la SCHL (fiabilité insuffisante)",47             ha="right", va="bottom", fontsize=6.5, color=palette.ink2)48    return savefig(fig, "ch33/ch33-fig-schl-typologie.pdf")495051def fig_rmr(R):52    g = R["sommaire"]["par_rmr"]; pf = R["sommaire"]["portefeuille"]53    rmrs = list(g.index); x = np.arange(len(rmrs))54    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(LARGEUR, 3.3), gridspec_kw={"wspace": 0.38})55    ax1.bar(x, g["valeur_par_logement"], 0.6, color=palette.cat[0], alpha=0.85, label="valeur agrégée / logements (RMR)")56    rng = np.random.default_rng(20261031)57    for i, r in enumerate(rmrs):58        s = pf[pf["rmr"] == r]59        ax1.scatter(i + rng.uniform(-0.18, 0.18, len(s)), s["valeur_par_logement"], s=np.sqrt(s["nb_logements"]) * 4, color=palette.ink, alpha=0.55, zorder=3,60                    label="immeuble (aire proportionnelle au nombre de logements)" if i == 0 else None)61        ax1.text(i, 9000, fmt_milliers(round(g.loc[r, "valeur_par_logement"], -3)), ha="center", va="bottom", fontsize=6.8, color="white", rotation=90)62    ax1.set_xticks(x); ax1.set_xticklabels(rmrs, rotation=25, ha="right", fontsize=7.5); ax1.yaxis.set_major_formatter(kdol); ax1.set_ylim(0, 320_000)63    ax1.set_title("Valeur 2026 par logement", loc="left")64    ax2.errorbar(x, g["taux_median"], yerr=[g["taux_median"] - g["taux_min"], g["taux_max"] - g["taux_median"]], fmt="o", color=palette.cat[1], ecolor=palette.ink2, capsize=3, label="médiane (étendue min–max)")65    ax2.scatter(x, g["taux_pondere"], marker="_", s=260, color=palette.cat[0], linewidths=2.2, label="taux pondéré (RNE / valeur)", zorder=4)66    ax2.axhline(R["effet_taille"]["cas3_mediane"], color=palette.ink, linestyle=":", linewidth=1, label="cas 3 : médiane des 40 ventes (Montréal)")67    for i, r in enumerate(rmrs): ax2.text(i + 0.12, g.loc[r, "taux_pondere"], f"{g.loc[r, 'taux_pondere']*100:.2f}".replace(".", ","), fontsize=7, va="center")68    ax2.set_xticks(x); ax2.set_xticklabels(rmrs, rotation=25, ha="right", fontsize=7.5); ax2.yaxis.set_major_formatter(pct2); ax2.set_ylim(0.035, 0.075)69    ax2.set_title("Taux global par RMR", loc="left")70    h1, l1 = ax1.get_legend_handles_labels(); h2, l2 = ax2.get_legend_handles_labels()71    fig.legend(h1 + h2, l1 + l2, loc="lower center", bbox_to_anchor=(0.5, -0.16), ncol=2, fontsize=7)72    fig.text(0.99, -0.22, "DONNÉES SYNTHÉTIQUES — cas 4 (48 immeubles, 6 RMR)", ha="right", va="bottom", fontsize=7, color=palette.ink2)73    return savefig(fig, "ch33/ch33-fig-rmr.pdf")747576def fig_taux_taille(R):77    et = R["effet_taille"]; c3 = et["cas3_comparables"]; pf = R["sommaire"]["portefeuille"]; p = R["p45"]; reg = p["regression"]78    fig, ax = plt.subplots(figsize=(LARGEUR, 3.6))79    m = pf["rmr"] == "Montréal"80    ax.scatter(c3["nb_logements"], c3["taux_global"], s=22, color=palette.cat[0], marker="o", alpha=0.8, label=f"cas 3 : {len(c3)} ventes, Montréal (2024-09 à 2026-08)", zorder=3)81    ax.scatter(pf.loc[m, "nb_logements"], pf.loc[m, "taux_global"], s=30, color=palette.cat[1], marker="s", alpha=0.85, label=f"cas 4 : Montréal ({int(m.sum())} immeubles)", zorder=3)82    ax.scatter(pf.loc[~m, "nb_logements"], pf.loc[~m, "taux_global"], s=30, color=palette.cat[2], marker="^", alpha=0.85, label=f"cas 4 : cinq autres RMR ({int((~m).sum())} immeubles)", zorder=3)83    xs = np.linspace(6, 30, 50)84    ax.plot(xs, et["cas3_constante"] + (et["cas3_pente_pb_par_logement"] / 1e4) * (xs - 12), color=palette.cat[0], linewidth=1.2, label="cas 3 : régression linéaire (état 3)")85    xs4 = np.linspace(6, 72, 80)86    ax.plot(xs4, [ch.taux_predit(reg, "Montréal", 3, k, 50) for k in xs4], color=palette.cat[1], linewidth=1.2, linestyle="--", label="cas 4 : régression semi-log, Montréal (état 3, 50 ans)")87    ax.plot(xs4, [ch.taux_predit(reg, "Gatineau", 3, k, 50) for k in xs4], color=palette.cat[2], linewidth=1.2, linestyle="--", label="cas 4 : idem, Gatineau")88    ax.scatter([12], [et["cas3_mediane"]], marker="*", s=140, color=palette.ink, zorder=5)89    ax.annotate("sujet du cas 3 (12 log., médiane 4,415 %)", (12, et["cas3_mediane"]), xytext=(14, 0.0385), fontsize=7, arrowprops=dict(arrowstyle="-", color=palette.ink2, lw=0.7))90    ax.scatter([p["n"]], [p["taux"]["fichier"]], marker="D", s=60, facecolors="none", edgecolors=palette.ink, linewidths=1.3, zorder=5)91    ax.annotate(f"{p['id']} : {p['n']} log., Gatineau, état {p['etat']}", (p["n"], p["taux"]["fichier"]), xytext=(40, 0.0455), fontsize=7, arrowprops=dict(arrowstyle="-", color=palette.ink2, lw=0.7))92    ax.set_xscale("log"); ax.set_xticks([6, 8, 12, 16, 24, 36, 48, 72]); ax.get_xaxis().set_major_formatter(FuncFormatter(lambda x, p: f"{x:.0f}"))93    ax.xaxis.set_minor_formatter(FuncFormatter(lambda x, p: ""))94    ax.yaxis.set_major_formatter(pct2); ax.set_xlabel("Nombre de logements (échelle logarithmique)"); ax.set_ylabel("Taux global (RNE / prix ou valeur)")95    ax.set_title("Taux global selon la taille de l'immeuble : effet de taille modeste, effet de marché dominant", loc="left")96    ax.legend(loc="upper center", bbox_to_anchor=(0.5, -0.2), ncol=2, fontsize=7)97    ax.text(0.99, -0.46, "DONNÉES SYNTHÉTIQUES (cas 3 et cas 4)", transform=ax.transAxes, ha="right", va="bottom", fontsize=7, color=palette.ink2)98    return savefig(fig, "ch33/ch33-fig-taux-taille.pdf")99100101def fig_sensibilite(R):102    s = R["sensibilite"]; p = R["p45"]103    G = s["grille_valeur_par_logement"]; T = s["grille_taux"]104    fig, ax = plt.subplots(figsize=(LARGEUR, 3.4))105    im = ax.imshow(G.values / 1000, cmap=palette.cmap_seq(), aspect="auto", origin="lower")106    ax.set_xticks(range(G.shape[1])); ax.set_xticklabels(G.columns, fontsize=7.5, rotation=20, ha="right")107    ax.set_yticks(range(G.shape[0])); ax.set_yticklabels(G.index, fontsize=7.5)108    seuil = np.percentile(G.values / 1000, 60)109    for i in range(G.shape[0]):110        for j in range(G.shape[1]):111            v = G.values[i, j] / 1000; coul = "white" if v > seuil else palette.ink112            ax.text(j, i, f"{v:.0f} k$\n{T.values[i, j]*100:.2f} %".replace(".", ","), ha="center", va="center", fontsize=6.8, color=coul)113    j0 = list(G.columns).index(p["rmr"]); i0 = p["etat"] - 1114    ax.add_patch(plt.Rectangle((j0 - 0.5, i0 - 0.5), 1, 1, fill=False, edgecolor=palette.cat[1], linewidth=1.8))115    ax.grid(False); ax.set_xlabel("Région métropolitaine (RMR)"); ax.set_ylabel("État de l'immeuble (1 = mauvais, 5 = excellent)")116    ax.set_title(f"P45 (36 logements, 72 ans) : valeur par logement et taux prédits selon la RMR et l'état ; cadre = sujet", loc="left")117    cb = fig.colorbar(im, ax=ax, pad=0.02); cb.set_label("valeur par logement (k$)")118    ax.text(0.99, -0.30, "DONNÉES SYNTHÉTIQUES — régression semi-log du cas 4 (48 immeubles), RNE de P45 constant", transform=ax.transAxes, ha="right", va="bottom", fontsize=7, color=palette.ink2)119    return savefig(fig, "ch33/ch33-fig-p45-sensibilite.pdf")120121122def main():123    setup(); R = ch.calculer()124    for f in (fig_schl, fig_rmr, fig_taux_taille, fig_sensibilite):125        print("OK", f(R).relative_to(ROOT))126127128if __name__ == "__main__":129    main()130
Files · 4958fcec3502ce9b6da151998f7eaebf95b47bcc
Menu

Theme