#!/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") 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.)"), (0.094, palette.cat[3], "--", "survie, ρ = 9,4 % (SCHL, RMR Montréal)"), (0.15, palette.cat[1], ":", "survie, ρ = 15 % (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 left", bbox_to_anchor=(0.0, -0.20), fontsize=6.3, ncol=2, frameon=False) # (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.text(1.0, -0.36, SYNTH, transform=a2.transAxes, ha="right", va="bottom", fontsize=7, color=palette.ink2) fig.subplots_adjust(wspace=0.38) 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))