#!/usr/bin/env python3 """Chapitre 26 — figures (4) : loyer contractuel vs loyer de marché par logement (cas 3), distribution de l'ancienneté et survie sous rotation, valeur actuelle de l'écart par logement selon la rotation, profil mensuel du coût d'électricité au tarif D (normalisation). Données synthétiques (cas 3) et hypothèses de travail déclarées (profil de consommation). Idempotent ; écrit dans assets/figures/ch26/ch26-fig-.pdf. Usage (racine) : PYTHONPATH=analysis/python python3 analysis/python/figures/ch26_figures.py""" from __future__ import annotations import sys, pathlib import numpy as np, pandas as pd import matplotlib.pyplot as plt from matplotlib.ticker import FuncFormatter sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1])) from traite.style import setup, palette, savefig, LARGEUR, DEMI, fmt_dollars sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "cases")) from ch26_etat_locatif import calculer, survie dollars = FuncFormatter(fmt_dollars) SYNTH = "DONNÉES SYNTHÉTIQUES" def fig_contrat_marche(res): t = res["ind"]["tableau"].sort_values(["anciennete", "logement"]).reset_index(drop=True) fig, ax = plt.subplots(figsize=(LARGEUR, 3.6)) y = np.arange(len(t)); h = 0.38 ax.barh(y + h / 2, t["loyer_mensuel_marche"], height=h, color=palette.cat[1], label="loyer de marché estimé", zorder=3) ax.barh(y - h / 2, t["loyer_mensuel_contrat"], height=h, color=palette.cat[0], label="loyer contractuel", zorder=3) for i, row in t.iterrows(): ax.text(row["loyer_mensuel_marche"] + 15, y[i] + h / 2, f"−{row['ecart_pct']*100:.1f} %".replace(".", ","), va="center", fontsize=7.5, color=palette.ink) ax.set_yticks(y); ax.set_yticklabels([f"{l} ({p}) — {a} an{'s' if a > 1 else ''}" for l, p, a in zip(t["logement"], t["pieces"], t["anciennete"])], fontsize=7.5) ax.invert_yaxis(); ax.xaxis.set_major_formatter(dollars); ax.set_xlim(0, 1950) ax.set_xlabel("Loyer mensuel ($)"); ax.set_ylabel("Logement (type) — ancienneté du locataire") ax.set_title("Cas 3 : loyer contractuel et loyer de marché par logement, du bail le plus récent au plus ancien", loc="left") ax.legend(loc="lower right"); ax.grid(axis="y", visible=False) ax.text(0.99, -0.16, SYNTH, transform=ax.transAxes, ha="right", va="bottom", fontsize=7, color=palette.ink2) return savefig(fig, "ch26/ch26-fig-contrat-marche.pdf") def fig_anciennete(res): t = res["ind"]["tableau"] fig, (a1, a2) = plt.subplots(1, 2, figsize=(LARGEUR, 3.1), gridspec_kw={"width_ratios": [1.15, 1]}) # (a) distribution de l'ancienneté (part des logements) et courbes de survie (1 − ρ)^t — même échelle de proportions, pas de double axe anc = t["anciennete"].values; bins = np.arange(0, 21, 2) a1.hist(anc, bins=bins, weights=np.full(len(anc), 1 / len(anc)), color=palette.cat[0], alpha=0.85, edgecolor=palette.surface, zorder=3, label="part des 12 logements (ancienneté observée)") tt = np.linspace(0, 20, 200) for rho, c, ls, lab in ((0.085, palette.cat[2], "-", "survie $(1-\\rho)^t$, ρ = 8,5 % (SCHL, 6–19 log., Montréal 2025)"), (0.094, palette.cat[3], "--", "survie, ρ = 9,4 % (SCHL, RMR Montréal 2025)"), (0.15, palette.cat[1], ":", "survie, ρ = 15 % (hypothèse du cas 3)")): a1.plot(tt, survie(rho, tt), color=c, linestyle=ls, linewidth=1.4, label=lab) a1.set_xlabel("Ancienneté du locataire (années)"); a1.set_ylabel("Proportion"); a1.set_ylim(0, 1.05); a1.set_xlim(0, 20) a1.set_title("(a) Ancienneté observée et survie théorique", loc="left") a1.legend(loc="upper center", bbox_to_anchor=(0.5, -0.24), fontsize=6.6, ncol=1) # (b) écart mensuel selon l'ancienneté, par type marq = {"3½": "o", "4½": "s", "5½": "^"} for i, (p, m) in enumerate(marq.items()): s = t[t["pieces"] == p] a2.scatter(s["anciennete"], s["ecart_mensuel"], marker=m, s=34, color=palette.cat[i], label=p, zorder=3) for _, row in t.iterrows(): a2.annotate(row["logement"], (row["anciennete"], row["ecart_mensuel"]), xytext=(4, 3), textcoords="offset points", fontsize=6.5, color=palette.ink2) a2.yaxis.set_major_formatter(dollars); a2.set_xlabel("Ancienneté du locataire (années)"); a2.set_ylabel("Écart marché − contrat ($/mois)") a2.set_title("(b) Écart mensuel selon l'ancienneté", loc="left"); a2.legend(title="Type", loc="upper left", fontsize=7) a2.set_xlim(0, 20); a2.set_ylim(0, 360) a2.legend(title="Type", loc="upper left", fontsize=7, bbox_to_anchor=(0.0, -0.24), ncol=3) fig.text(0.99, -0.30, SYNTH, ha="right", va="bottom", fontsize=7, color=palette.ink2) fig.tight_layout() return savefig(fig, "ch26/ch26-fig-anciennete.pdf") def fig_va_ecart(res): va = res["va"].sort_values("anciennete").reset_index(drop=True) fig, ax = plt.subplots(figsize=(LARGEUR, 3.3)) x = np.arange(len(va)); w = 0.27 series = [("va_rho_0.085", "ρ = 8,5 %", palette.cat[2]), ("va_rho_0.094", "ρ = 9,4 %", palette.cat[3]), ("va_rho_0.150", "ρ = 15 %", palette.cat[1])] for k, (col, lab, c) in enumerate(series): tot = va[col].sum() ax.bar(x + (k - 1) * w, va[col], width=w, color=c, label=f"{lab} — total {fmt_dollars(tot)}", zorder=3) ax.set_xticks(x); ax.set_xticklabels([f"{l}\n{a} an{'s' if a > 1 else ''}" for l, a in zip(va["logement"], va["anciennete"])], fontsize=7.5) ax.yaxis.set_major_formatter(dollars); ax.set_ylabel("VA espérée de l'écart ($)"); ax.set_xlabel("Logement — ancienneté du locataire") ax.set_title("Cas 3 : valeur actuelle de l'écart contractuel/marché de chaque logement selon la rotation ρ (r = 7,25 %, g = 3,5 %)", loc="left") ax.legend(loc="upper left", fontsize=7.5) ax.text(0.99, -0.26, SYNTH, transform=ax.transAxes, ha="right", va="bottom", fontsize=7, color=palette.ink2) return savefig(fig, "ch26/ch26-fig-va-ecart.pdf") def fig_normalisation(res): n = res["norm"]; tab = n["tableau"] fig, ax = plt.subplots(figsize=(LARGEUR, 3.0)) x = np.arange(12) ax.bar(x, tab["redevance"], color=palette.cat[2], label="redevance d'abonnement (46,154 ¢/jour)", zorder=3) ax.bar(x, tab["cout_tranche1"], bottom=tab["redevance"], color=palette.cat[0], label="1re tranche (≤ 40 kWh/jour, 7,065 ¢/kWh)", zorder=3) ax.bar(x, tab["cout_tranche2"], bottom=tab["redevance"] + tab["cout_tranche1"], color=palette.cat[1], label="2e tranche (> 40 kWh/jour, 11,142 ¢/kWh)", zorder=3) ax.axhline(n["cout_mensuel_moyen"], color=palette.ink, linestyle="--", linewidth=1.0) ax.text(11.45, n["cout_mensuel_moyen"] + 3, f"moyenne {fmt_dollars(n['cout_mensuel_moyen'])}/mois", ha="right", fontsize=7.5) for i, k in enumerate(tab["kwh"]): ax.text(i, tab["total"].iloc[i] + 3, f"{int(k)} kWh", ha="center", fontsize=6.5, color=palette.ink2) ax.set_xticks(x); ax.set_xticklabels(tab["mois"]); ax.yaxis.set_major_formatter(dollars); ax.set_ylim(0, 205) ax.set_ylabel("Coût mensuel hors taxes ($)") ax.set_title("Coût de l'électricité d'un 4½ chauffé à l'électricité au tarif D (1er avril 2026), profil de 12 000 kWh/an", loc="left") ax.legend(loc="upper right", fontsize=7.2, ncol=1) ax.text(0.99, -0.20, "HYPOTHÈSE DE TRAVAIL (profil de consommation) — structure tarifaire : Hydro-Québec", transform=ax.transAxes, ha="right", va="bottom", fontsize=7, color=palette.ink2) return savefig(fig, "ch26/ch26-fig-normalisation.pdf") if __name__ == "__main__": setup(); res = calculer() for f in (fig_contrat_marche, fig_anciennete, fig_va_ecart, fig_normalisation): print(f(res))