SPB Git
9.5 KB · 118 lines python
1#!/usr/bin/env python32"""Chapitre 47 — figures du dossier « 12 logements à Montréal » (DONNÉES SYNTHÉTIQUES, graine 20261031).3Produit assets/figures/ch47/ch47-fig-{etat-locatif,depenses,taux-extraits,flux-dcf,montecarlo,tornade,rcd-scenarios}.pdf4Usage (racine) : PYTHONPATH=analysis/python python3 analysis/python/figures/ch47_dossier_figures.py"""5from __future__ import annotations6import sys, pathlib7import numpy as np, pandas as pd8import matplotlib.pyplot as plt9from matplotlib.ticker import FuncFormatter, PercentFormatter10sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1]))11sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "cases"))12from traite.style import setup, palette, savefig, LARGEUR, fmt_dollars13import ch47_dossier as ch1415ETIQ = {"inoccupation_pertes": "Inoccupation et pertes", "croissance_loyers_marche": "Croissance des loyers de marché", "rotation_annuelle": "Rotation annuelle",16        "croissance_depenses": "Croissance des dépenses", "taux_actualisation": "Taux d'actualisation", "ecart_terminal": "Écart terminal (r − R_T)", "facteur_capex": "Facteur CAPEX"}1718def fig_etat_locatif(D):19    e = D["etat"]; x = np.arange(len(e)); w = 0.3820    fig, ax = plt.subplots(figsize=(LARGEUR, 3.1))21    ax.bar(x - w / 2, e["loyer_mensuel_contrat"], w, color=palette.cat[0], label="Loyer contractuel (bail 2026-2027)")22    ax.bar(x + w / 2, e["loyer_mensuel_marche"], w, color=palette.cat[1], label="Loyer de marché estimé")23    for i, r in e.iterrows():24        ecart = (r["loyer_mensuel_marche"] - r["loyer_mensuel_contrat"]) / r["loyer_mensuel_marche"]25        ax.text(x[i], r["loyer_mensuel_marche"] + 25, f"−{ecart*100:.0f} %".replace(".", ","), ha="center", va="bottom", fontsize=7, color=palette.ink)26    ax.set_xticks(x); ax.set_xticklabels([f"{l}\n{p}" for l, p in zip(e["logement"], e["pieces"])], fontsize=7.5)27    ax.set_ylabel("Loyer mensuel ($)"); ax.yaxis.set_major_formatter(FuncFormatter(fmt_dollars)); ax.set_ylim(0, 1900)28    ax.legend(loc="upper left", ncol=2); ax.grid(axis="x", visible=False)29    savefig(fig, "ch47/ch47-fig-etat-locatif.pdf")3031def fig_depenses(D):32    d = D["base"]["detail"].copy(); d = d[d["normalise"] > 0].sort_values("normalise")33    coul = {"fixe": palette.cat[0], "variable": palette.cat[1], "réserve": palette.cat[2]}34    fig, ax = plt.subplots(figsize=(LARGEUR, 3.4))35    noms = [p.replace(" (par logement)", "").replace(" (aires communes)", " (communs)").replace(" (aires communes, gaz)", " (communs, gaz)") for p in d["poste"]]36    ax.barh(noms, d["normalise"], color=[coul[t] for t in d["type"]])37    ax.scatter(d["historique_2025"], noms, marker="|", s=90, color=palette.ink, zorder=3, label="Historique 2025 (non normalisé)")38    for n, v, h in zip(noms, d["normalise"], d["historique_2025"].fillna(0)): ax.text(max(v, h) + 180, n, fmt_dollars(v), va="center", fontsize=7.5, color=palette.ink)39    from matplotlib.patches import Patch40    h = [Patch(color=coul["fixe"], label="Fixes (taxes, assurances)"), Patch(color=coul["variable"], label="Variables et gestion imputée"), Patch(color=coul["réserve"], label="Réserve de remplacement")]41    h.append(ax.collections[0]); ax.legend(handles=h, loc="lower right", fontsize=7.5)42    ax.set_xlabel("Dépense normalisée, première année ($)"); ax.xaxis.set_major_formatter(FuncFormatter(fmt_dollars)); ax.set_xlim(0, 14500); ax.grid(axis="y", visible=False)43    savefig(fig, "ch47/ch47-fig-depenses.pdf")4445def fig_taux_extraits(D):46    c = D["taux"]["comparables"]; tx = D["taux"]47    fig, ax = plt.subplots(figsize=(LARGEUR, 3.2))48    ax.axhspan(tx["q1"] * 100, tx["q3"] * 100, color=palette.grid, alpha=0.6, lw=0, label="Intervalle interquartile (Q1–Q3)")49    for i, et in enumerate([2, 3, 4]):50        s = c[c["etat_1a5"] == et]51        ax.scatter(s["nb_logements"], s["taux_global"] * 100, color=palette.cat[i], s=28, label=f"État {et}/5 (n = {len(s)})", edgecolor="white", linewidth=0.5)52    ax.axhline(tx["mediane"] * 100, color=palette.ink, lw=1.2, ls="--")53    ax.text(30.3, tx["mediane"] * 100, f"médiane {tx['mediane']*100:.3f} %".replace(".", ","), va="center", fontsize=7.5)54    ax.scatter([12], [tx["mediane"] * 100], marker="D", s=70, facecolor="white", edgecolor=palette.ink, linewidth=1.4, zorder=5)55    ax.annotate("sujet (12 log., taux retenu)", (12, tx["mediane"] * 100), xytext=(13.2, 3.78), fontsize=7.5, arrowprops=dict(arrowstyle="-", color=palette.ink2, lw=0.6))56    ax.set_xlabel("Nombre de logements du comparable"); ax.set_ylabel("Taux global extrait (%)"); ax.set_xlim(4, 34); ax.set_ylim(3.5, 5.7)57    ax.yaxis.set_major_formatter(FuncFormatter(lambda v, p: f"{v:.1f}".replace(".", ",")))58    ax.legend(loc="upper right", fontsize=7.5, ncol=2)59    savefig(fig, "ch47/ch47-fig-taux-extraits.pdf")6061def fig_flux_dcf(D):62    p = D["projection"]; p10 = p[p["annee"] <= 10]; sd = D["financement"]["service_dette"]63    fig, ax = plt.subplots(figsize=(LARGEUR, 3.2)); x = p10["annee"].values64    ax.bar(x, p10["rne"], 0.6, color=palette.cat[0], label="RNE (après réserve)")65    ax.bar(x, -p10["capex"], 0.6, color=palette.cat[1], label="CAPEX (dépenses en capital ponctuelles)")66    ax.plot(x, p10["flux_avant_dette"], color=palette.ink, marker="o", ms=4, lw=1.3, label="Flux avant dette (RNE − CAPEX)")67    ax.axhline(sd, color=palette.cat[3], ls="--", lw=1.2); ax.text(10.45, sd, "service de la dette\n(prêt RCD 1,20)", va="center", fontsize=7.2, color=palette.ink)68    ax.axhline(0, color=palette.ink2, lw=0.6)69    ax.set_xticks(x); ax.set_xticklabels([f"{a}\n{e}" for a, e in zip(p10["annee"], p10["exercice"])], fontsize=7.5)70    ax.set_xlabel("Année de projection (exercice)"); ax.set_ylabel("Montant annuel ($)"); ax.yaxis.set_major_formatter(FuncFormatter(fmt_dollars))71    ax.set_ylim(-135000, 175000); ax.set_xlim(0.3, 12.2); ax.legend(loc="lower left", fontsize=7.5, ncol=1); ax.grid(axis="x", visible=False)72    savefig(fig, "ch47/ch47-fig-flux-dcf.pdf")7374def fig_montecarlo(D):75    mc = D["mc"]; V = mc["valeur"] / 1e6; st = mc["stats"]76    fig, ax = plt.subplots(figsize=(LARGEUR, 3.2))77    ax.hist(V, bins=80, range=(1.2, 3.8), color=palette.cat[0], alpha=0.85, edgecolor="white", linewidth=0.3)78    ymax = ax.get_ylim()[1]79    for q, lab in [(st["q05"], "q05"), (st["mediane"], "médiane"), (st["q95"], "q95")]:80        ax.axvline(q / 1e6, color=palette.ink, lw=1.0, ls="--"); ax.text(q / 1e6, ymax * 0.97, f"{lab}\n{q/1e6:.2f} M$".replace(".", ","), ha="center", va="top", fontsize=7.2)81    ax.axvline(D["dcf"]["valeur"] / 1e6, color=palette.cat[1], lw=1.6); ax.text(D["dcf"]["valeur"] / 1e6 + 0.03, ymax * 0.36, "DCF déterministe\n2,21 M$", fontsize=7.2, color=palette.ink)82    ax.axvline(D["valeur_capitalisation"] / 1e6, color=palette.cat[2], lw=1.6); ax.text(D["valeur_capitalisation"] / 1e6 - 0.03, ymax * 0.52, "capitalisation\ndirecte 2,04 M$", fontsize=7.2, color=palette.ink, ha="right")83    ax.set_xlabel("Valeur simulée (millions de dollars)"); ax.set_ylabel("Nombre de tirages (sur 10 000)")84    ax.xaxis.set_major_formatter(FuncFormatter(lambda v, p: f"{v:.1f}".replace(".", ","))); ax.set_xlim(1.2, 3.8)85    savefig(fig, "ch47/ch47-fig-montecarlo.pdf")8687def fig_tornade(D):88    t = D["tornade"].iloc[::-1]; V0 = t["V_base"].iloc[0] / 1e689    fig, ax = plt.subplots(figsize=(LARGEUR, 3.0))90    y = np.arange(len(t))91    lo = np.minimum(t["V_bas"], t["V_haut"]) / 1e6; hi = np.maximum(t["V_bas"], t["V_haut"]) / 1e692    ax.barh(y, t["V_bas"] / 1e6 - V0, left=V0, color=palette.cat[0], height=0.6, label="Intrant au quantile 10 %")93    ax.barh(y, t["V_haut"] / 1e6 - V0, left=V0, color=palette.cat[1], height=0.6, label="Intrant au quantile 90 %")94    ax.axvline(V0, color=palette.ink, lw=1.0)95    for yi, l, h in zip(y, lo, hi):96        ax.text(l - 0.02, yi, f"{l:.2f}".replace(".", ","), va="center", ha="right", fontsize=7.2); ax.text(h + 0.02, yi, f"{h:.2f}".replace(".", ","), va="center", ha="left", fontsize=7.2)97    ax.set_yticks(y); ax.set_yticklabels([ETIQ[i] for i in t["intrant"]], fontsize=8)98    ax.set_xlabel("Valeur DCF (millions de dollars), autres intrants à leur valeur de base"); ax.set_xlim(1.75, 2.75)99    ax.xaxis.set_major_formatter(FuncFormatter(lambda v, p: f"{v:.2f}".replace(".", ","))); ax.legend(loc="lower right", fontsize=7.5); ax.grid(axis="y", visible=False)100    savefig(fig, "ch47/ch47-fig-tornade.pdf")101102def fig_rcd_scenarios(D):103    r = D["rcd_par_annee"]; cols = [c for c in r.columns if c != "annee"]104    fig, ax = plt.subplots(figsize=(LARGEUR, 3.0))105    for i, c in enumerate(cols):106        ax.plot(r["annee"], r[c], marker="o", ms=3.5, color=palette.cat[i], label=f"Renouvellement à l'an 6 : {c}")107    ax.axhline(1.20, color=palette.ink, ls="--", lw=1.0); ax.text(10.15, 1.20, "RCD 1,20 (prêteur)", va="center", fontsize=7.5)108    ax.axhline(1.00, color=palette.cat[5], ls=":", lw=1.2); ax.text(10.15, 1.00, "RCD 1,00 (défaut technique)", va="center", fontsize=7.5)109    ax.axvspan(5.5, 10.5, color=palette.grid, alpha=0.5, lw=0); ax.text(8, 2.12, "second terme (taux renouvelé)", ha="center", fontsize=7.5, color=palette.ink2)110    ax.set_xticks(r["annee"]); ax.set_xlim(0.7, 12.4); ax.set_ylim(0.9, 2.2); ax.set_xlabel("Année de projection"); ax.set_ylabel("Ratio de couverture de la dette (RNE / SD)")111    ax.yaxis.set_major_formatter(FuncFormatter(lambda v, p: f"{v:.1f}".replace(".", ","))); ax.legend(loc="upper left", fontsize=7.5)112    savefig(fig, "ch47/ch47-fig-rcd-scenarios.pdf")113114if __name__ == "__main__":115    setup(); D = ch.dossier()116    for f in (fig_etat_locatif, fig_depenses, fig_taux_extraits, fig_flux_dcf, fig_montecarlo, fig_tornade, fig_rcd_scenarios):117        f(D); print("OK", f.__name__)118
Files · 739a7ae283cf11e4f280ccc5836e47f331cb784a
Menu

Theme