SPB Git
9.1 KB · 118 lines python
1#!/usr/bin/env python32"""Figures du chapitre 35 — Indices immobiliers et ventes répétées.3Produit dans assets/figures/ch35/ :4  ch35-fig-indices-compares.pdf   indices hédonique (IC 95 %), BMN, Case–Shiller, SPAR et vérité de génération ; écarts à la vérité [SYNTHÉTIQUE]5  ch35-fig-biais-mediane.pdf      indice de la médiane vs indice hédonique et composition de l'échantillon par trimestre [SYNTHÉTIQUE]6  ch35-fig-poids-case-shiller.pdf carrés des résidus BMN selon l'intervalle (reventes rapides en évidence), droites ajustées, poids [SYNTHÉTIQUE]7  ch35-fig-revision-filtre.pdf    révision des indices de ventes répétées selon le millésime ; effet du filtre des reventes rapides [SYNTHÉTIQUE]8  ch35-fig-ipln-variation.pdf     IPLN, variation sur douze mois par RMR du Québec et Canada, 2011-2026 [RÉEL : StatCan 18-10-0205-01]9Usage (racine) : PYTHONPATH=analysis/python python3 analysis/python/figures/ch35_figures.py"""10from __future__ import annotations11import sys, pathlib12ROOT = pathlib.Path(__file__).resolve().parents[3]13sys.path.insert(0, str(ROOT / "analysis/python")); sys.path.insert(0, str(ROOT / "analysis/python/cases"))14import numpy as np, pandas as pd15import matplotlib.pyplot as plt16import matplotlib.dates as mdates17from matplotlib.ticker import FuncFormatter18from traite.style import setup, palette, savefig, LARGEUR19import ch35_indices as c352021def _xticks(ax, labels, pas=2):22    ax.set_xticks(range(0, len(labels), pas)); ax.set_xticklabels([labels[i] for i in range(0, len(labels), pas)], fontsize=7.5)2324def fig_indices(res):25    tab = res["tableau"]; L = c35.LABELS; x = np.arange(len(L))26    fig, (a1, a2) = plt.subplots(2, 1, figsize=(LARGEUR, 5.2), gridspec_kw={"height_ratios": [3, 2]}, sharex=True)27    a1.fill_between(x, tab["hed_bas"], tab["hed_haut"], color=palette.cat[0], alpha=0.15, lw=0)28    a1.plot(x, tab["hedonique"], color=palette.cat[0], lw=2.0, marker="o", ms=3.5, label="Hédonique (indicatrices trimestrielles, IC 95 %)")29    a1.plot(x, tab["bmn"], color=palette.cat[1], lw=1.6, marker="s", ms=3.2, label="Ventes répétées, BMN")30    a1.plot(x, tab["cs"], color=palette.cat[2], lw=1.6, ls="--", marker="^", ms=3.4, label="Ventes répétées, Case–Shiller")31    a1.plot(x, tab["spar"], color=palette.cat[3], lw=1.6, marker="D", ms=3.0, label="SPAR (prix / valeur au rôle)")32    a1.plot(x, tab["vrai"], color=palette.ink, lw=1.2, ls=":", label="Vérité de génération (4,43 %/an)")33    a1.set_ylabel("Indice (2023T1 = 100)"); a1.legend(loc="upper left", fontsize=7.5); a1.set_ylim(95, 135)34    for k, col, m in (("hedonique", palette.cat[0], "o"), ("bmn", palette.cat[1], "s"), ("cs", palette.cat[2], "^"), ("spar", palette.cat[3], "D")):35        a2.plot(x, 100 * (tab[k] / tab["vrai"] - 1), color=col, lw=1.3, marker=m, ms=3)36    a2.axhline(0, color=palette.ink, lw=0.8, ls=":")37    a2.set_ylabel("Écart à la vérité (%)"); a2.set_ylim(-6, 9)38    _xticks(a2, L)39    return savefig(fig, "ch35/ch35-fig-indices-compares.pdf")4041def fig_biais_mediane(res):42    n = res["naifs"]; tab = res["tableau"]; L = c35.LABELS; x = np.arange(len(L))43    fig, (a1, a2) = plt.subplots(2, 1, figsize=(LARGEUR, 5.0), gridspec_kw={"height_ratios": [3, 2]}, sharex=True)44    a1.plot(x, n["indice_mediane"], color=palette.cat[1], lw=1.8, marker="s", ms=3.3, label="Indice de la médiane des prix")45    a1.plot(x, n["indice_moy_geo"], color=palette.cat[2], lw=1.4, ls="--", label="Indice de la moyenne géométrique")46    a1.plot(x, tab["hedonique"], color=palette.cat[0], lw=2.0, label="Indice hédonique (qualité constante)")47    a1.scatter(x, n["indice_moy_geo_corrigee"], s=34, facecolors="none", edgecolors=palette.cat[3], lw=1.3, zorder=5, label="Moyenne géométrique corrigée de la composition (= hédonique)")48    a1.plot(x, tab["vrai"], color=palette.ink, lw=1.1, ls=":", label="Vérité de génération")49    a1.set_ylabel("Indice (2023T1 = 100)"); a1.legend(loc="upper left", fontsize=7.3); a1.set_ylim(95, 140)50    a2.bar(x, 100 * (n["indice_composition"] / 100 - 1), color=palette.cat[4], alpha=0.85, width=0.6, label="Effet de composition $\\bar{\\mathbf{x}}_t'\\hat\\beta$ (%)")51    a2.plot(x, 100 * n["part_aylmer"], color=palette.ink2, lw=1.2, marker=".", label="Part des ventes à Aylmer (%)")52    a2.axhline(0, color=palette.ink, lw=0.7)53    a2.set_ylabel("%"); a2.legend(loc="upper left", fontsize=7.3, ncol=2); a2.set_ylim(-2, 42)54    _xticks(a2, L)55    return savefig(fig, "ch35/ch35-fig-biais-mediane.pdf")5657def fig_poids_cs(res):58    cs = res["cs"]; p = res["paires"]; gap = cs["gap_trimestres"]; e2 = cs["residus_bmn2"]; flip = p["flip"].to_numpy()59    sens = res["sensibilite"]["filtre"]; a_all = cs["coef_variance"]; a_or = sens["sans les reventes rénovées (oracle)"]["cs_coef_variance"]60    th = cs["variance_theorique"]61    fig, (a1, a2) = plt.subplots(1, 2, figsize=(LARGEUR, 3.1), gridspec_kw={"width_ratios": [3, 2]})62    rng = np.random.default_rng(1); jit = rng.uniform(-0.18, 0.18, len(gap))63    a1.scatter(gap[~flip] + jit[~flip], e2[~flip], s=9, color=palette.cat[0], alpha=0.5, lw=0, label="Reventes ordinaires")64    a1.scatter(gap[flip] + jit[flip], e2[flip], s=12, color=palette.cat[1], alpha=0.9, lw=0, marker="^", label="Reventes rapides rénovées")65    g = np.linspace(0.5, 13.5, 50)66    a1.plot(g, a_all[0] + a_all[1] * g, color=palette.cat[1], lw=1.4, ls="--", label="Ajustement, toutes les paires")67    a1.plot(g, a_or[0] + a_or[1] * g, color=palette.cat[0], lw=1.4, label="Ajustement sans les reventes rénovées")68    a1.plot(g, th[0] + th[1] * g, color=palette.ink, lw=1.0, ls=":", label="Théorie : $2\\sigma^2 + \\delta^2 \\Delta$")69    a1.set_xlabel("Intervalle entre les deux ventes (trimestres)"); a1.set_ylabel("Carré du résidu BMN"); a1.set_ylim(0, 0.16); a1.set_xlim(0, 14)70    a1.legend(loc="upper right", fontsize=6.8)71    for a, col, lab, ls in ((a_or, palette.cat[0], "poids estimés (sans reventes rénovées)", "-"), (np.array(th), palette.ink, "poids théoriques", ":")):72        var = a[0] + a[1] * g; a2.plot(g, np.sqrt((a[0] + a[1] * 1) / var), color=col, lw=1.5, ls=ls, label=lab)73    a2.set_xlabel("Intervalle (trimestres)"); a2.set_ylabel("Poids relatif (intervalle d'un trimestre = 1)"); a2.set_ylim(0.5, 1.05); a2.set_xlim(0, 14)74    a2.legend(loc="lower left", fontsize=6.8)75    return savefig(fig, "ch35/ch35-fig-poids-case-shiller.pdf")7677def fig_revision_filtre(res):78    s = res["sensibilite"]; L = c35.LABELS; vrai = res["vrai"]79    fig, (a1, a2) = plt.subplots(1, 2, figsize=(LARGEUR, 3.1))80    for i, (lab, d) in enumerate(s["millesimes"].items()):81        n = len(d["bmn"]); a1.plot(range(n), d["bmn"], color=palette.cat[i], lw=1.5, marker="o", ms=2.8, label=f"Millésime {lab} ({d['n']} paires)")82    a1.plot(range(len(vrai)), vrai, color=palette.ink, lw=1.0, ls=":", label="Vérité")83    a1.set_ylabel("Indice BMN (2023T1 = 100)"); a1.legend(loc="upper left", fontsize=7); _xticks(a1, L, 3); a1.set_ylim(95, 130)84    f = s["filtre"]85    for i, (lab, key) in enumerate((("toutes les paires", "toutes"), ("intervalle ≥ 12 mois", "intervalle ≥ 12 mois"), ("intervalle ≥ 18 mois", "intervalle ≥ 18 mois"), ("sans reventes rénovées (oracle)", "sans les reventes rénovées (oracle)"))):86        d = f[key]; a2.plot(range(len(L)), d["bmn_indice"], color=palette.cat[i], lw=1.4, marker="o", ms=2.6, ls="-" if i < 3 else "--", label=f"{lab} (n = {d['n']})")87    a2.plot(range(len(L)), vrai, color=palette.ink, lw=1.0, ls=":", label="Vérité")88    a2.set_ylabel("Indice BMN (2023T1 = 100)"); a2.legend(loc="upper left", fontsize=7); _xticks(a2, L, 3); a2.set_ylim(95, 130)89    return savefig(fig, "ch35/ch35-fig-revision-filtre.pdf")9091def fig_ipln():92    import ch04_marches as c0493    df = c04.ipln(); df = df[df["composante"] == "Total"]94    ordre = ["Montréal", "Québec", "Gatineau (partie du Québec)", "Sherbrooke", "Trois-Rivières", "Canada"]95    fig, ax = plt.subplots(figsize=(LARGEUR, 3.5))96    for i, geo in enumerate(ordre):97        s = df[df["geo"] == geo].set_index("date")["indice"].sort_index()98        var = 100 * (s / s.shift(12) - 1); var = var[var.index >= "2011-01-01"]99        ax.plot(var.index, var.values, color=palette.cat[i], lw=2.0 if geo == "Montréal" else 1.3, ls="--" if geo == "Canada" else "-",100                label=geo.replace(" (partie du Québec)", " (partie QC)"))101    ax.axhline(0, color=palette.ink, lw=0.8)102    ax.axvspan(pd.Timestamp("2020-03-01"), pd.Timestamp("2022-12-31"), color=palette.grid, alpha=0.5, lw=0)103    ax.set_ylabel("Variation sur douze mois (%)"); ax.set_xlim(pd.Timestamp("2011-01-01"), pd.Timestamp("2026-12-31"))104    ax.xaxis.set_major_locator(mdates.YearLocator(2)); ax.xaxis.set_major_formatter(mdates.DateFormatter("%Y"))105    ax.legend(loc="upper left", ncol=2, fontsize=7.5)106    out = savefig(fig, "ch35/ch35-fig-ipln-variation.pdf")107    last = {geo: float(100 * (df[df["geo"] == geo].set_index("date")["indice"].sort_index().pipe(lambda s: s.iloc[-1] / s.iloc[-13] - 1))) for geo in ordre}108    return out, last109110def main():111    setup(); res = c35.main(verbose=False)112    for f in (fig_indices, fig_biais_mediane, fig_poids_cs, fig_revision_filtre):113        print("OK", f(res))114    out, last = fig_ipln(); print("OK", out, {k: round(v, 1) for k, v in last.items()})115116if __name__ == "__main__":117    main()118
Files · da8a4ee0304be0aa9c54ed0aad08d985fc4539fc
Menu

Theme