SPB Git
7.5 KB · 105 lines python
1#!/usr/bin/env python32"""Chapitre 26 — figures (4) : loyer contractuel vs loyer de marché par logement (cas 3), distribution de l'ancienneté et3survie sous rotation, valeur actuelle de l'écart par logement selon la rotation, profil mensuel du coût d'électricité au4tarif D (normalisation). Données synthétiques (cas 3) et hypothèses de travail déclarées (profil de consommation).5Idempotent ; écrit dans assets/figures/ch26/ch26-fig-<slug>.pdf.6Usage (racine) : PYTHONPATH=analysis/python python3 analysis/python/figures/ch26_figures.py"""7from __future__ import annotations8import sys, pathlib9import numpy as np, pandas as pd10import matplotlib.pyplot as plt11from matplotlib.ticker import FuncFormatter12sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1]))13from traite.style import setup, palette, savefig, LARGEUR, DEMI, fmt_dollars14sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "cases"))15from ch26_etat_locatif import calculer, survie1617dollars = FuncFormatter(fmt_dollars)18SYNTH = "DONNÉES SYNTHÉTIQUES"192021def fig_contrat_marche(res):22    t = res["ind"]["tableau"].sort_values(["anciennete", "logement"]).reset_index(drop=True)23    fig, ax = plt.subplots(figsize=(LARGEUR, 3.6))24    y = np.arange(len(t)); h = 0.3825    ax.barh(y + h / 2, t["loyer_mensuel_marche"], height=h, color=palette.cat[1], label="loyer de marché estimé", zorder=3)26    ax.barh(y - h / 2, t["loyer_mensuel_contrat"], height=h, color=palette.cat[0], label="loyer contractuel", zorder=3)27    for i, row in t.iterrows():28        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)29    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)30    ax.invert_yaxis(); ax.xaxis.set_major_formatter(dollars); ax.set_xlim(0, 1950)31    ax.set_xlabel("Loyer mensuel ($)"); ax.set_ylabel("Logement (type) — ancienneté du locataire")32    ax.set_title("Cas 3 : loyer contractuel et loyer de marché par logement, du bail le plus récent au plus ancien", loc="left")33    ax.legend(loc="lower right"); ax.grid(axis="y", visible=False)34    ax.text(0.99, -0.16, SYNTH, transform=ax.transAxes, ha="right", va="bottom", fontsize=7, color=palette.ink2)35    return savefig(fig, "ch26/ch26-fig-contrat-marche.pdf")363738def fig_anciennete(res):39    t = res["ind"]["tableau"]40    fig, (a1, a2) = plt.subplots(1, 2, figsize=(LARGEUR, 3.1), gridspec_kw={"width_ratios": [1.15, 1]})41    # (a) distribution de l'ancienneté (part des logements) et courbes de survie (1 − ρ)^t — même échelle de proportions, pas de double axe42    anc = t["anciennete"].values; bins = np.arange(0, 21, 2)43    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)")44    tt = np.linspace(0, 20, 200)45    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)")):46        a1.plot(tt, survie(rho, tt), color=c, linestyle=ls, linewidth=1.4, label=lab)47    a1.set_xlabel("Ancienneté du locataire (années)"); a1.set_ylabel("Proportion"); a1.set_ylim(0, 1.05); a1.set_xlim(0, 20)48    a1.set_title("(a) Ancienneté observée et survie théorique", loc="left")49    a1.legend(loc="upper center", bbox_to_anchor=(0.5, -0.24), fontsize=6.6, ncol=1)50    # (b) écart mensuel selon l'ancienneté, par type51    marq = {"3½": "o", "4½": "s", "5½": "^"}52    for i, (p, m) in enumerate(marq.items()):53        s = t[t["pieces"] == p]54        a2.scatter(s["anciennete"], s["ecart_mensuel"], marker=m, s=34, color=palette.cat[i], label=p, zorder=3)55    for _, row in t.iterrows():56        a2.annotate(row["logement"], (row["anciennete"], row["ecart_mensuel"]), xytext=(4, 3), textcoords="offset points", fontsize=6.5, color=palette.ink2)57    a2.yaxis.set_major_formatter(dollars); a2.set_xlabel("Ancienneté du locataire (années)"); a2.set_ylabel("Écart marché − contrat ($/mois)")58    a2.set_title("(b) Écart mensuel selon l'ancienneté", loc="left"); a2.legend(title="Type", loc="upper left", fontsize=7)59    a2.set_xlim(0, 20); a2.set_ylim(0, 360)60    a2.legend(title="Type", loc="upper left", fontsize=7, bbox_to_anchor=(0.0, -0.24), ncol=3)61    fig.text(0.99, -0.30, SYNTH, ha="right", va="bottom", fontsize=7, color=palette.ink2)62    fig.tight_layout()63    return savefig(fig, "ch26/ch26-fig-anciennete.pdf")646566def fig_va_ecart(res):67    va = res["va"].sort_values("anciennete").reset_index(drop=True)68    fig, ax = plt.subplots(figsize=(LARGEUR, 3.3))69    x = np.arange(len(va)); w = 0.2770    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])]71    for k, (col, lab, c) in enumerate(series):72        tot = va[col].sum()73        ax.bar(x + (k - 1) * w, va[col], width=w, color=c, label=f"{lab} — total {fmt_dollars(tot)}", zorder=3)74    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)75    ax.yaxis.set_major_formatter(dollars); ax.set_ylabel("VA espérée de l'écart ($)"); ax.set_xlabel("Logement — ancienneté du locataire")76    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")77    ax.legend(loc="upper left", fontsize=7.5)78    ax.text(0.99, -0.26, SYNTH, transform=ax.transAxes, ha="right", va="bottom", fontsize=7, color=palette.ink2)79    return savefig(fig, "ch26/ch26-fig-va-ecart.pdf")808182def fig_normalisation(res):83    n = res["norm"]; tab = n["tableau"]84    fig, ax = plt.subplots(figsize=(LARGEUR, 3.0))85    x = np.arange(12)86    ax.bar(x, tab["redevance"], color=palette.cat[2], label="redevance d'abonnement (46,154 ¢/jour)", zorder=3)87    ax.bar(x, tab["cout_tranche1"], bottom=tab["redevance"], color=palette.cat[0], label="1re tranche (≤ 40 kWh/jour, 7,065 ¢/kWh)", zorder=3)88    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)89    ax.axhline(n["cout_mensuel_moyen"], color=palette.ink, linestyle="--", linewidth=1.0)90    ax.text(11.45, n["cout_mensuel_moyen"] + 3, f"moyenne {fmt_dollars(n['cout_mensuel_moyen'])}/mois", ha="right", fontsize=7.5)91    for i, k in enumerate(tab["kwh"]):92        ax.text(i, tab["total"].iloc[i] + 3, f"{int(k)} kWh", ha="center", fontsize=6.5, color=palette.ink2)93    ax.set_xticks(x); ax.set_xticklabels(tab["mois"]); ax.yaxis.set_major_formatter(dollars); ax.set_ylim(0, 205)94    ax.set_ylabel("Coût mensuel hors taxes ($)")95    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")96    ax.legend(loc="upper right", fontsize=7.2, ncol=1)97    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)98    return savefig(fig, "ch26/ch26-fig-normalisation.pdf")99100101if __name__ == "__main__":102    setup(); res = calculer()103    for f in (fig_contrat_marche, fig_anciennete, fig_va_ecart, fig_normalisation):104        print(f(res))105
Files · a58cc978dd49eee20fac414bfb0e69af32f1a29e
Menu

Theme