SPB Git
10.5 KB · 140 lines python
1#!/usr/bin/env python32"""Chapitre 27 — figures (4) :3  ch27-fig-inoccupation-rmr.pdf  : (a) inoccupation SCHL par RMR, oct. 2024 et 2025 ; (b) inoccupation selon la taille de l'immeuble, RMR et île de Montréal,4                                   oct. 2025 — DONNÉES RÉELLES (SCHL, Enquête sur les logements locatifs, tableaux 1.1.1 et 1.3.1), citées avec attribution.5  ch27-fig-cascade.pdf           : cascade RBP au marché → RBP contractuel → loyers effectifs → RBE, cas 3 (SYNTHÉTIQUE).6  ch27-fig-trajectoires.pdf      : trajectoires du RBP du cas 3 : TAL seul, rotation 15 % (base), rotation 9,4 % (SCHL), marché (SYNTHÉTIQUE).7  ch27-fig-sensibilite.pdf       : (a) inoccupation frictionnelle v_f = ρ·d/12 ; (b) RBE et valeur du cas 3 selon v (SYNTHÉTIQUE).8Idempotent ; écrit dans assets/figures/ch27/. Usage (racine) : PYTHONPATH=analysis/python python3 analysis/python/figures/ch27_figures.py"""9from __future__ import annotations10import sys, pathlib11import numpy as np12import matplotlib.pyplot as plt13from matplotlib.ticker import FuncFormatter14sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1]))15sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "cases"))16from traite.style import setup, palette, savefig, LARGEUR, fmt_milliers17from traite import finance as fin18import ch27_revenus as c1920pct1 = FuncFormatter(lambda v, p: f"{v:.1f}".replace(".", ",") + " %")21pct0 = FuncFormatter(lambda v, p: f"{v:.0f} %")22def _f1(v): return f"{v:.1f}".replace(".", ",")232425def fig_inoccupation_rmr(res):26    s = res["schl_rmr"]; t = res["schl_taille"]27    noms = {"Montréal": "Montréal", "Québec": "Québec", "Gatineau (partie QC)": "Gatineau\n(partie QC)", "Sherbrooke": "Sher-\nbrooke", "Trois-Rivières": "Trois-\nRivières", "Saguenay": "Sague-\nnay"}28    fig, (a, b) = plt.subplots(1, 2, figsize=(LARGEUR, 3.3), gridspec_kw={"width_ratios": [1.15, 1]})29    x = np.arange(len(s)); w = 0.3830    a.bar(x - w / 2, s["inocc_2024"], w, color=palette.cat[1], label="Octobre 2024")31    a.bar(x + w / 2, s["inocc_2025"], w, color=palette.cat[0], label="Octobre 2025")32    for xi, v in zip(x + w / 2, s["inocc_2025"]): a.text(xi, v + 0.06, _f1(v), ha="center", va="bottom", fontsize=7)33    for xi, v in zip(x - w / 2, s["inocc_2024"]): a.text(xi, v + 0.06, _f1(v), ha="center", va="bottom", fontsize=6.5, color=palette.ink2)34    a.set_xticks(x); a.set_xticklabels([noms[i] for i in s.index], fontsize=7)35    a.set_ylabel("Taux d'inoccupation, tous les appartements (%)"); a.set_ylim(0, 4.6); a.yaxis.set_major_formatter(pct0)36    a.legend(loc="upper right", fontsize=7); a.set_title("(a) Six RMR du Québec", loc="left", fontsize=8.5)37    tailles = ["3 à 5", "6 à 19", "20 à 49", "50 à 99", "100 et +", "Tous"]38    ile = t[t["zone"] == "Île de Montréal"]["inocc_2025"].values; rmr = t[t["zone"] == "RMR de Montréal"]["inocc_2025"].values39    x2 = np.arange(len(tailles))40    b.bar(x2 - w / 2, rmr, w, color=palette.cat[0], label="RMR de Montréal")41    b.bar(x2 + w / 2, ile, w, color=palette.cat[2], label="Île de Montréal")42    for xi, v in zip(x2 - w / 2, rmr): b.text(xi, v + 0.06, _f1(v), ha="center", va="bottom", fontsize=6.5)43    for xi, v in zip(x2 + w / 2, ile): b.text(xi, v + 0.06, _f1(v), ha="center", va="bottom", fontsize=6.5, color=palette.ink2)44    b.set_xticks(x2); b.set_xticklabels(tailles, fontsize=7); b.set_xlabel("Logements dans l'immeuble")45    b.set_ylim(0, 6.6); b.yaxis.set_major_formatter(pct0); b.legend(loc="upper left", fontsize=7)46    b.set_title("(b) Selon la taille de l'immeuble, octobre 2025", loc="left", fontsize=8.5)47    fig.tight_layout(w_pad=1.5)48    return savefig(fig, "ch27/ch27-fig-inoccupation-rmr.pdf")495051def fig_cascade(res):52    r = res["cas3"]53    etapes = [("RBP au\nmarché", r["rbp_marche"], "total"), ("Déficit\nde loyer", -r["deficit_loyer"], "delta"),54              ("RBP\ncontractuel", r["rbp_contrat"], "total"), ("Inoccupation\net créances", -r["vacance_pertes"], "delta"),55              ("Loyers\neffectifs", r["loyers_effectifs"], "total"), ("Autres\nrevenus", r["autres_revenus"], "delta"), ("RBE", r["rbe"], "total")]56    fig, ax = plt.subplots(figsize=(LARGEUR, 3.5))57    cum = 0.058    for i, (lab, val, kind) in enumerate(etapes):59        if kind == "total":60            ax.bar(i, val / 1000, 0.62, color=palette.cat[0] if i in (0, 2) else palette.brand2 if i == 6 else palette.cat[2], zorder=3)61            ax.text(i, val / 1000 + 2.5, fmt_milliers(val) + " $", ha="center", va="bottom", fontsize=7.5)62            cum = val63        else:64            bas = cum + min(val, 0.0)65            ax.bar(i, abs(val) / 1000, 0.62, bottom=bas / 1000, color=palette.cat[3] if val < 0 else palette.cat[4], zorder=3)66            ax.text(i, (cum + val) / 1000 - 2.5 if val < 0 else (cum + val) / 1000 + 2.5, ("−" if val < 0 else "+") + fmt_milliers(abs(val)) + " $",67                    ha="center", va="top" if val < 0 else "bottom", fontsize=7.5)68            cum += val69        if i < len(etapes) - 1:70            ax.plot([i + 0.31, i + 1 - 0.31], [cum / 1000, cum / 1000], color=palette.ink2, lw=0.6, ls=":", zorder=2)71    ax.set_xticks(range(len(etapes))); ax.set_xticklabels([e[0] for e in etapes], fontsize=6.8); ax.set_xlim(-0.55, len(etapes) - 0.45)72    ax.set_ylim(140, 195); ax.set_ylabel("Milliers de dollars par année")73    ax.yaxis.set_major_formatter(FuncFormatter(lambda v, p: fmt_milliers(v)))74    ax.set_title("Cas 3 : du revenu brut potentiel au revenu brut effectif (première année, 2026-2027)", loc="left")75    ax.text(0.99, -0.26, "DONNÉES SYNTHÉTIQUES", transform=ax.transAxes, ha="right", va="bottom", fontsize=7, color=palette.ink2)76    return savefig(fig, "ch27/ch27-fig-cascade.pdf")777879def fig_trajectoires(res):80    t = res["trajectoires"]["table"]; h = res["trajectoires"]["hyp"]; T = res["trajectoires"]["T"]81    t = t[t["annee"] <= T]82    fig, ax = plt.subplots(figsize=(LARGEUR, 3.9))83    x = t["exercice"].values84    ax.fill_between(x, t["tal_seul"] / 1000, t["marche"] / 1000, color=palette.grid, alpha=0.8, label="Écart entre l'indexation seule et le marché")85    ax.plot(x, t["marche"] / 1000, color=palette.cat[1], marker="s", ms=3.2, label=f"RBP au loyer de marché (indice : +{h['croissance_loyers_marche']*100:.1f} %/an)".replace(".", ","))86    ax.plot(x, t["base_rot15"] / 1000, color=palette.cat[0], marker="o", ms=3.2, label=f"Baux en place : TAL {h['croissance_loyers_contrat']*100:.1f} % + rotation {h['rotation_annuelle']*100:.0f} % (base)".replace(".", ","))87    ax.plot(x, t["rot_schl_094"] / 1000, color=palette.cat[2], marker="^", ms=3.2, ls="--", label="Baux en place : TAL 3,1 % + rotation 9,4 % (SCHL, RMR 2025)")88    ax.plot(x, t["tal_seul"] / 1000, color=palette.cat[3], ls=":", lw=1.4, label="Baux en place : indexation TAL 3,1 % seule (rotation nulle)")89    for k, col, dy in (("tal_seul", palette.cat[3], -6), ("marche", palette.cat[1], 4)):90        ax.text(x[-1] + 0.15, t[k].iloc[-1] / 1000 + dy / 10, fmt_milliers(t[k].iloc[-1]) + " $", fontsize=7, color=palette.ink, va="center")91    ax.set_xticks(x); ax.set_xlabel("Exercice (1er septembre → 31 août)"); ax.set_ylabel("Revenu brut potentiel (milliers de $)")92    ax.yaxis.set_major_formatter(FuncFormatter(lambda v, p: fmt_milliers(v)))93    ax.set_xlim(x[0] - 0.3, x[-1] + 1.4); ax.legend(loc="upper center", bbox_to_anchor=(0.5, -0.17), ncol=2, fontsize=6.8)94    ax.set_title("Cas 3 : quatre trajectoires du revenu brut potentiel sur dix ans", loc="left")95    ax.text(0.99, -0.40, "DONNÉES SYNTHÉTIQUES", transform=ax.transAxes, ha="right", va="bottom", fontsize=7, color=palette.ink2)96    return savefig(fig, "ch27/ch27-fig-trajectoires.pdf")979899def fig_sensibilite(res):100    s = res["schl_rmr"]; base = res["cas3"]101    fig, (a, b) = plt.subplots(1, 2, figsize=(LARGEUR, 3.2))102    d = np.linspace(0, 3, 61)103    rots = [(0.05, "ρ = 5 %"), (0.094, "ρ = 9,4 % (Montréal 2025)"), (0.13, "ρ = 13 % (Québec 2025)"), (0.162, "ρ = 16,2 % (Gatineau 2025)"), (0.20, "ρ = 20 %")]104    for i, (rho, lab) in enumerate(rots):105        a.plot(d, [fin.taux_vacance_frictionnelle(rho, x) * 100 for x in d], color=palette.cat[i], label=lab)106    a.axhline(s.loc["Montréal", "inocc_2025"], color=palette.ink2, lw=0.7, ls=":")107    a.text(0.05, s.loc["Montréal", "inocc_2025"] - 0.12, "SCHL, RMR de Montréal, oct. 2025 : 2,9 %", fontsize=6.3, color=palette.ink2, va="top")108    a.scatter([1.0], [base["v_frictionnel_dossier"] * 100], color=palette.ink, marker="*", s=70, zorder=4)109    a.annotate("dossier du cas 3 :\nρ = 15 %, d = 1 mois\n→ 1,25 %", (1.0, base["v_frictionnel_dossier"] * 100), xytext=(1.25, 0.35), fontsize=6.8,110               arrowprops=dict(arrowstyle="-", color=palette.ink2, lw=0.6))111    a.set_xlabel("Durée moyenne de vacance entre deux baux, d (mois)"); a.set_ylabel("Inoccupation frictionnelle v_f = ρ·d/12 (%)")112    a.set_ylim(0, 5.2); a.yaxis.set_major_formatter(pct1); a.legend(fontsize=6.5, loc="upper left")113    a.set_title("(a) Rotation, durée de vacance et inoccupation", loc="left", fontsize=8.5)114    sv = c.sensibilite_v(base, taux=tuple(sorted(set(np.round(np.arange(0.01, 0.0801, 0.0025), 4)) | {0.029, 0.039, 0.059})))115    b.plot(sv["v"] * 100, sv["valeur"] / 1000, color=palette.cat[0])116    b.scatter([base["taux_vacance_pertes"] * 100], [sv.loc[np.isclose(sv["v"], base["taux_vacance_pertes"]), "valeur"].iloc[0] / 1000], color=palette.ink, marker="*", s=70, zorder=4)117    for v0, lab, yt in ((0.029, "SCHL RMR\n2,9 %", 1856), (0.039, "dossier\n3,9 %", 1886), (0.059, "+2 points\n5,9 %", 1856)):118        y = sv.loc[np.isclose(sv["v"], v0), "valeur"].iloc[0] / 1000119        b.plot([v0 * 100, v0 * 100], [1850, y], color=palette.ink2, lw=0.6, ls=":")120        b.text(v0 * 100, yt, lab, fontsize=6.5, ha="center", va="bottom", color=palette.ink2)121    b.set_xlabel("Provision pour inoccupation et pertes, v (% du RBP)"); b.set_ylabel("Valeur par capitalisation directe (milliers de $)")122    b.xaxis.set_major_formatter(pct0); b.yaxis.set_major_formatter(FuncFormatter(lambda v, p: fmt_milliers(v)))123    b.set_ylim(1850, 2160)124    b.text(0.97, 0.95, f"pente : {fmt_milliers(-sv.attrs['dV_dv_pt'])} $ par point de v\n(R = {sv.attrs['R']*100:.3f} %)".replace(".", ","), transform=b.transAxes, ha="right", va="top", fontsize=7)125    b.set_title("(b) Cas 3 : valeur selon la provision v", loc="left", fontsize=8.5)126    fig.tight_layout(w_pad=1.6)127    fig.text(0.99, -0.02, "Panneau (b) : DONNÉES SYNTHÉTIQUES", ha="right", va="bottom", fontsize=7, color=palette.ink2)128    return savefig(fig, "ch27/ch27-fig-sensibilite.pdf")129130131def main():132    setup()133    res = c.calculer()134    for f in (fig_inoccupation_rmr, fig_cascade, fig_trajectoires, fig_sensibilite):135        print("OK", f(res))136137138if __name__ == "__main__":139    main()140
Files · fd7f44c8789c8b7d81811c1898b290bc997b5c13
Menu

Theme