spb/traite-evaluation-qc
Public
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