SPB Git
10.4 KB · 147 lines python
1#!/usr/bin/env python32"""Chapitre 29 — figures (5) : taux extraits (histogramme), taux vs état/taille (nuage), surface V(RNE, R) (carte de3chaleur), décomposition du taux (barres), rendements des obligations du GdC et taux hypothécaire affiché (série réelle,4Banque du Canada). Idempotent ; écrit dans assets/figures/ch29/ch29-fig-<slug>.pdf.5Usage (racine) : PYTHONPATH=analysis/python python3 analysis/python/figures/ch29_figures.py"""6from __future__ import annotations7import sys, pathlib8import numpy as np, pandas as pd9import matplotlib.pyplot as plt10from matplotlib.ticker import FuncFormatter, PercentFormatter11sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1]))12from traite.style import setup, palette, savefig, LARGEUR, DEMI, fmt_dollars13sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "cases"))14from ch29_capitalisation import calculer1516ROOT = pathlib.Path(__file__).resolve().parents[3]17RAW = ROOT / "data/raw/bdc_valet"18pct = FuncFormatter(lambda x, p: f"{x*100:.2f}".replace(".", ",") + " %")19pct1 = FuncFormatter(lambda x, p: f"{x*100:.1f}".replace(".", ",") + " %")202122def fig_taux_extraits(res):23    c = res["taux"]["comparables"]; R = res["R"]24    fig, ax = plt.subplots(figsize=(LARGEUR, 3.0))25    bins = np.arange(0.0365, 0.0566, 0.0025)26    ax.hist(c["taux_global"], bins=bins, color=palette.cat[0], alpha=0.85, edgecolor=palette.surface, linewidth=0.8)27    ymax = ax.get_ylim()[1] * 1.12; ax.set_ylim(0, ymax)28    for x, lab, ls, h, ha in ((res["taux"]["q1"], "Q1", ":", 0.97, "right"), (R, "médiane", "-", 0.88, "right"), (res["taux"]["q3"], "Q3", ":", 0.97, "left")):29        ax.axvline(x, color=palette.ink, linestyle=ls, linewidth=1.1)30        ax.text(x + (0.0002 if ha == "left" else -0.0002), ymax * h, f"{lab} {x*100:.2f} %".replace(".", ","), ha=ha, va="top", fontsize=8)31    xr = res["regression"]["prevision_sujet"]32    ax.axvline(xr, color=palette.cat[1], linestyle="--", linewidth=1.1)33    ax.text(xr + 0.0002, ymax * 0.70, f"régression (état 3, 12 log.)\n{xr*100:.2f} %".replace(".", ","), color=palette.ink, fontsize=7.5, ha="left", va="top")34    ax.xaxis.set_major_formatter(pct)35    ax.set_xlabel("Taux global extrait (RNE après réserve / prix)"); ax.set_ylabel("Nombre de ventes")36    ax.set_title(f"Distribution des taux extraits — {len(c)} ventes synthétiques, 6 à 30 logements, 2024-09 à 2026-08", loc="left")37    ax.text(0.99, -0.30, "DONNÉES SYNTHÉTIQUES", transform=ax.transAxes, ha="right", va="bottom", fontsize=7, color=palette.ink2)38    return savefig(fig, "ch29/ch29-fig-taux-extraits.pdf")394041def fig_taux_etat_taille(res):42    c = res["taux"]["comparables"]; reg = res["regression"]43    fig, ax = plt.subplots(figsize=(LARGEUR, 3.2))44    marqueurs = {2: "s", 3: "o", 4: "^"}45    for i, et in enumerate([2, 3, 4]):46        s = c[c["etat_1a5"] == et]47        ax.scatter(s["nb_logements"], s["taux_global"], color=palette.cat[i], marker=marqueurs[et], s=28, label=f"état {et} (n = {len(s)})", zorder=3)48        xs = np.array([6, 30]); ax.plot(xs, reg["constante"] + reg["etat"] * (et - 3) + reg["taille"] * (xs - 12), color=palette.cat[i], linewidth=1.0, alpha=0.8)49    ax.scatter([12], [reg["prevision_sujet"]], marker="*", s=120, color=palette.ink, zorder=4)50    ax.annotate("sujet (12 log., état 3)\nprévision de la régression", (12, reg["prevision_sujet"]), xytext=(16.5, 0.0535), fontsize=7.5,51                arrowprops=dict(arrowstyle="-", color=palette.ink2, lw=0.7))52    ax.yaxis.set_major_formatter(pct); ax.set_xlabel("Nombre de logements"); ax.set_ylabel("Taux global extrait")53    ax.set_title("Taux extraits selon la taille et l'état : droites de la régression $R = a + b_1(\\mathrm{état}-3) + b_2(n-12)$", loc="left")54    ax.legend(loc="upper center", bbox_to_anchor=(0.5, -0.22), ncol=3)55    ax.text(0.99, -0.42, "DONNÉES SYNTHÉTIQUES", transform=ax.transAxes, ha="right", va="bottom", fontsize=7, color=palette.ink2)56    return savefig(fig, "ch29/ch29-fig-taux-etat-taille.pdf")575859def fig_surface(res):60    rne0 = res["base"]["rne"]; R0 = res["R"]61    Rs = np.array(sorted(set(np.round(np.arange(0.0375, 0.0526, 0.0025), 4)) | {round(R0, 5)}))62    mult = np.array([0.90, 0.95, 1.00, 1.05, 1.10])63    V = np.array([[rne0 * m / r for r in Rs] for m in mult])64    fig, ax = plt.subplots(figsize=(LARGEUR, 3.4))65    im = ax.imshow(V / 1e6, cmap=palette.cmap_seq(), aspect="auto", origin="lower")66    ax.set_xticks(range(len(Rs))); ax.set_xticklabels([f"{r*100:.2f}".replace(".", ",") for r in Rs])67    ax.set_yticks(range(len(mult))); ax.set_yticklabels([f"{m*100-100:+.0f} %\n{rne0*m/1000:,.1f} k$".replace(",", " ").replace(".", ",") for m in mult])68    for i in range(len(mult)):69        for j in range(len(Rs)):70            v = V[i, j]; coul = "white" if v / 1e6 > np.percentile(V / 1e6, 55) else palette.ink71            ax.text(j, i, f"{v/1e6:.2f}".replace(".", ","), ha="center", va="center", fontsize=7.5, color=coul)72    j0 = int(np.argmin(abs(Rs - R0))); ax.add_patch(plt.Rectangle((j0 - 0.5, 2 - 0.5), 1, 1, fill=False, edgecolor=palette.cat[1], linewidth=1.6))73    ax.set_xlabel("Taux global $R$ (%)"); ax.set_ylabel("RNE normalisé (écart au cas de base ; k$)")74    rne_txt = f"{rne0:,.0f}".replace(",", " "); r_txt = f"{R0*100:.3f}".replace(".", ",")75    ax.set_title(f"Valeur indiquée $V = RNE/R$ (M\\$) ; cadre : cas de base (RNE {rne_txt} \\$, R = {r_txt} %)", loc="left")76    ax.grid(False)77    cb = fig.colorbar(im, ax=ax, pad=0.02); cb.set_label("V (M$)")78    ax.text(0.99, -0.28, "DONNÉES SYNTHÉTIQUES", transform=ax.transAxes, ha="right", va="bottom", fontsize=7, color=palette.ink2)79    return savefig(fig, "ch29/ch29-fig-surface-v.pdf")808182def fig_bande(res):83    e = res["ellwood"]; M, f, R = res["M"], res["f"], res["R"]84    constructions = [85        ("Bande\nd'investissement\n(R_E impliqué)", [("M·f", M * f), ("(1−M)·R_E", (1 - M) * res["re_implicite"])]),86        ("Couverture\nde la dette\n(RCD 1,20)", [("RCD·M·f", res["R_rcd"])]),87        ("Akerson\nΔ implicite 7,7 %\n(Y = 7,25 %)", [("M·f", e["composantes_marche"]["M f"]), ("(1−M)·Y", e["composantes_marche"]["(1-M) Y"]),88                                                ("−M·P·SFF", e["composantes_marche"]["-M P SFF"]), ("−Δ·SFF", e["composantes_marche"]["-delta SFF"])]),89        ("Akerson\nΔ = 24,9 %\n(Y = 7,25 %)", [("M·f", e["composantes"]["M f"]), ("(1−M)·Y", e["composantes"]["(1-M) Y"]),90                                               ("−M·P·SFF", e["composantes"]["-M P SFF"]), ("−Δ·SFF", e["composantes"]["-delta SFF"])]),91    ]92    coul = {"M·f": palette.cat[0], "(1−M)·R_E": palette.cat[2], "RCD·M·f": palette.cat[4], "(1−M)·Y": palette.cat[2], "−M·P·SFF": palette.cat[1], "−Δ·SFF": palette.cat[3]}93    fig, ax = plt.subplots(figsize=(LARGEUR, 3.9))94    vus = set()95    for k, (nom, comps) in enumerate(constructions):96        pos, neg = 0.0, 0.097        for lab, v in comps:98            if v >= 0:99                ax.bar(k, v, bottom=pos, color=coul[lab], width=0.6, label=lab if lab not in vus else None); pos += v100            else:101                # composante négative dessinée en retrait depuis le sommet des composantes positives102                ax.bar(k + 0.0, v, bottom=pos + neg, color=coul[lab], width=0.6, alpha=0.9, hatch="//", edgecolor=palette.surface, label=lab if lab not in vus else None); neg += v103            vus.add(lab)104        total = pos + neg105        ax.plot([k - 0.38, k + 0.38], [total, total], color=palette.ink, linewidth=1.4)106        ax.text(k, max(pos, total) + 0.0012, f"R = {total*100:.2f} %".replace(".", ","), ha="center", va="bottom", fontsize=8, fontweight="bold")107    ax.axhline(R, color=palette.ink2, linestyle="--", linewidth=0.9, label=f"médiane du marché ({R*100:.2f} %)".replace(".", ","))108    ax.set_xticks(range(len(constructions))); ax.set_xticklabels([c[0] for c in constructions], fontsize=7.5)109    ax.yaxis.set_major_formatter(pct1); ax.set_ylabel("Taux global $R$"); ax.set_xlim(-0.5, 3.5); ax.set_ylim(0, 0.066)110    ax.set_title("Décomposition du taux global selon la méthode de construction (cas 3 ; M = 75 %, f = 5,08 %)", loc="left")111    ax.legend(loc="upper center", bbox_to_anchor=(0.5, -0.30), ncol=4, fontsize=7.5)112    ax.text(0.99, -0.46, "DONNÉES SYNTHÉTIQUES (paramètres du cas 3)", transform=ax.transAxes, ha="right", va="bottom", fontsize=7, color=palette.ink2)113    return savefig(fig, "ch29/ch29-fig-bande-investissement.pdf")114115116def _valet(nom: str) -> pd.Series:117    df = pd.read_csv(RAW / f"{nom}.csv", skiprows=8, header=None, names=["date", "v"])118    df["date"] = pd.to_datetime(df["date"], format="%Y-%m-%d", errors="coerce"); df["v"] = pd.to_numeric(df["v"], errors="coerce")119    return df.dropna().set_index("date")["v"] / 100120121122def fig_gdc():123    s5, s10, hyp = _valet("BD.CDN.5YR.DQ.YLD"), _valet("BD.CDN.10YR.DQ.YLD"), _valet("V80691335")124    fig, ax = plt.subplots(figsize=(LARGEUR, 3.2))125    ax.plot(s10.resample("MS").mean(), color=palette.cat[0], label="Obligation du GdC 10 ans (moyenne mensuelle)")126    ax.plot(s5.resample("MS").mean(), color=palette.cat[1], label="Obligation du GdC 5 ans (moyenne mensuelle)")127    ax.plot(hyp.resample("MS").mean(), color=palette.cat[2], label="Taux hypothécaire affiché 5 ans, banques (conventionnel)")128    for s, dy in ((s10, 5), (s5, -5), (hyp, 0)):129        ax.annotate(f"{s.iloc[-1]*100:.2f} %".replace(".", ","), (s.index[-1], s.iloc[-1]), xytext=(4, dy), textcoords="offset points", fontsize=7.5, va="center", color=palette.ink)130    ax.yaxis.set_major_formatter(pct1); ax.set_ylabel("Taux annuel"); ax.set_xlabel("")131    ax.set_title("Taux sans risque et coût de la dette au Canada, 2001-2026 : la base des primes de risque du taux global", loc="left")132    ax.legend(loc="upper right", fontsize=7.5); ax.set_xlim(pd.Timestamp("2001-01-01"), pd.Timestamp("2027-06-01"))133    return savefig(fig, "ch29/ch29-fig-taux-gdc.pdf"), {"gdc5": float(s5.iloc[-1]), "gdc10": float(s10.iloc[-1]), "hyp": float(hyp.iloc[-1]), "date5": str(s5.index[-1].date()), "datehyp": str(hyp.index[-1].date()),134                                                       "gdc10_moy_2001_2026": float(s10.mean()), "gdc10_min": float(s10.min()), "gdc10_min_date": str(s10.idxmin().date())}135136137def main():138    setup(); res = calculer()139    outs = [fig_taux_extraits(res), fig_taux_etat_taille(res), fig_surface(res), fig_bande(res)]140    o, info = fig_gdc(); outs.append(o)141    for p in outs: print("OK", p.relative_to(ROOT))142    print("Banque du Canada (dernières observations) :", info)143144145if __name__ == "__main__":146    main()147
Files · 66a30b7f12cdbf4da142dea3e146d765bc5bf70e
Menu

Theme