SPB Git
7.4 KB · 104 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")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.)"), (0.094, palette.cat[3], "--", "survie, ρ = 9,4 % (SCHL, RMR Montréal)"), (0.15, palette.cat[1], ":", "survie, ρ = 15 % (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 left", bbox_to_anchor=(0.0, -0.20), fontsize=6.3, ncol=2, frameon=False)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.text(1.0, -0.36, SYNTH, transform=a2.transAxes, ha="right", va="bottom", fontsize=7, color=palette.ink2)61    fig.subplots_adjust(wspace=0.38)62    return savefig(fig, "ch26/ch26-fig-anciennete.pdf")636465def fig_va_ecart(res):66    va = res["va"].sort_values("anciennete").reset_index(drop=True)67    fig, ax = plt.subplots(figsize=(LARGEUR, 3.3))68    x = np.arange(len(va)); w = 0.2769    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])]70    for k, (col, lab, c) in enumerate(series):71        tot = va[col].sum()72        ax.bar(x + (k - 1) * w, va[col], width=w, color=c, label=f"{lab} — total {fmt_dollars(tot)}", zorder=3)73    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)74    ax.yaxis.set_major_formatter(dollars); ax.set_ylabel("VA espérée de l'écart ($)"); ax.set_xlabel("Logement — ancienneté du locataire")75    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")76    ax.legend(loc="upper left", fontsize=7.5)77    ax.text(0.99, -0.26, SYNTH, transform=ax.transAxes, ha="right", va="bottom", fontsize=7, color=palette.ink2)78    return savefig(fig, "ch26/ch26-fig-va-ecart.pdf")798081def fig_normalisation(res):82    n = res["norm"]; tab = n["tableau"]83    fig, ax = plt.subplots(figsize=(LARGEUR, 3.0))84    x = np.arange(12)85    ax.bar(x, tab["redevance"], color=palette.cat[2], label="redevance d'abonnement (46,154 ¢/jour)", zorder=3)86    ax.bar(x, tab["cout_tranche1"], bottom=tab["redevance"], color=palette.cat[0], label="1re tranche (≤ 40 kWh/jour, 7,065 ¢/kWh)", zorder=3)87    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)88    ax.axhline(n["cout_mensuel_moyen"], color=palette.ink, linestyle="--", linewidth=1.0)89    ax.text(11.45, n["cout_mensuel_moyen"] + 3, f"moyenne {fmt_dollars(n['cout_mensuel_moyen'])}/mois", ha="right", fontsize=7.5)90    for i, k in enumerate(tab["kwh"]):91        ax.text(i, tab["total"].iloc[i] + 3, f"{int(k)} kWh", ha="center", fontsize=6.5, color=palette.ink2)92    ax.set_xticks(x); ax.set_xticklabels(tab["mois"]); ax.yaxis.set_major_formatter(dollars); ax.set_ylim(0, 205)93    ax.set_ylabel("Coût mensuel hors taxes ($)")94    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")95    ax.legend(loc="upper right", fontsize=7.2, ncol=1)96    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)97    return savefig(fig, "ch26/ch26-fig-normalisation.pdf")9899100if __name__ == "__main__":101    setup(); res = calculer()102    for f in (fig_contrat_marche, fig_anciennete, fig_va_ecart, fig_normalisation):103        print(f(res))104
Files · fd7f44c8789c8b7d81811c1898b290bc997b5c13
Menu

Theme