#!/usr/bin/env python3 """Figures du chapitre 35 — Indices immobiliers et ventes répétées. Produit dans assets/figures/ch35/ : 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] ch35-fig-biais-mediane.pdf indice de la médiane vs indice hédonique et composition de l'échantillon par trimestre [SYNTHÉTIQUE] 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] 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] 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] Usage (racine) : PYTHONPATH=analysis/python python3 analysis/python/figures/ch35_figures.py""" from __future__ import annotations import sys, pathlib ROOT = pathlib.Path(__file__).resolve().parents[3] sys.path.insert(0, str(ROOT / "analysis/python")); sys.path.insert(0, str(ROOT / "analysis/python/cases")) import numpy as np, pandas as pd import matplotlib.pyplot as plt import matplotlib.dates as mdates from matplotlib.ticker import FuncFormatter from traite.style import setup, palette, savefig, LARGEUR import ch35_indices as c35 def _xticks(ax, labels, pas=2): ax.set_xticks(range(0, len(labels), pas)); ax.set_xticklabels([labels[i] for i in range(0, len(labels), pas)], fontsize=7.5) def fig_indices(res): tab = res["tableau"]; L = c35.LABELS; x = np.arange(len(L)) fig, (a1, a2) = plt.subplots(2, 1, figsize=(LARGEUR, 5.2), gridspec_kw={"height_ratios": [3, 2]}, sharex=True) a1.fill_between(x, tab["hed_bas"], tab["hed_haut"], color=palette.cat[0], alpha=0.15, lw=0) a1.plot(x, tab["hedonique"], color=palette.cat[0], lw=2.0, marker="o", ms=3.5, label="Hédonique (indicatrices trimestrielles, IC 95 %)") a1.plot(x, tab["bmn"], color=palette.cat[1], lw=1.6, marker="s", ms=3.2, label="Ventes répétées, BMN") 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") a1.plot(x, tab["spar"], color=palette.cat[3], lw=1.6, marker="D", ms=3.0, label="SPAR (prix / valeur au rôle)") a1.plot(x, tab["vrai"], color=palette.ink, lw=1.2, ls=":", label="Vérité de génération (4,43 %/an)") a1.set_ylabel("Indice (2023T1 = 100)"); a1.legend(loc="upper left", fontsize=7.5); a1.set_ylim(95, 135) for k, col, m in (("hedonique", palette.cat[0], "o"), ("bmn", palette.cat[1], "s"), ("cs", palette.cat[2], "^"), ("spar", palette.cat[3], "D")): a2.plot(x, 100 * (tab[k] / tab["vrai"] - 1), color=col, lw=1.3, marker=m, ms=3) a2.axhline(0, color=palette.ink, lw=0.8, ls=":") a2.set_ylabel("Écart à la vérité (%)"); a2.set_ylim(-6, 9) _xticks(a2, L) return savefig(fig, "ch35/ch35-fig-indices-compares.pdf") def fig_biais_mediane(res): n = res["naifs"]; tab = res["tableau"]; L = c35.LABELS; x = np.arange(len(L)) fig, (a1, a2) = plt.subplots(2, 1, figsize=(LARGEUR, 5.0), gridspec_kw={"height_ratios": [3, 2]}, sharex=True) 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") a1.plot(x, n["indice_moy_geo"], color=palette.cat[2], lw=1.4, ls="--", label="Indice de la moyenne géométrique") a1.plot(x, n["indice_moy_geo_corrigee"], color=palette.cat[3], lw=1.4, ls="-.", label="Moyenne géométrique corrigée de la composition") a1.plot(x, tab["hedonique"], color=palette.cat[0], lw=2.0, marker="o", ms=3.3, label="Indice hédonique (qualité constante)") a1.plot(x, tab["vrai"], color=palette.ink, lw=1.1, ls=":", label="Vérité de génération") a1.set_ylabel("Indice (2023T1 = 100)"); a1.legend(loc="upper left", fontsize=7.3); a1.set_ylim(95, 140) 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$ (%)") a2.plot(x, 100 * n["part_aylmer"], color=palette.ink2, lw=1.2, marker=".", label="Part des ventes à Aylmer (%)") a2.axhline(0, color=palette.ink, lw=0.7) a2.set_ylabel("%"); a2.legend(loc="upper left", fontsize=7.3, ncol=2); a2.set_ylim(-2, 42) _xticks(a2, L) return savefig(fig, "ch35/ch35-fig-biais-mediane.pdf") def fig_poids_cs(res): cs = res["cs"]; p = res["paires"]; gap = cs["gap_trimestres"]; e2 = cs["residus_bmn2"]; flip = p["flip"].to_numpy() sens = res["sensibilite"]["filtre"]; a_all = cs["coef_variance"]; a_or = sens["sans les reventes rénovées (oracle)"]["cs_coef_variance"] th = cs["variance_theorique"] fig, (a1, a2) = plt.subplots(1, 2, figsize=(LARGEUR, 3.1), gridspec_kw={"width_ratios": [3, 2]}) rng = np.random.default_rng(1); jit = rng.uniform(-0.18, 0.18, len(gap)) a1.scatter(gap[~flip] + jit[~flip], e2[~flip], s=9, color=palette.cat[0], alpha=0.5, lw=0, label="Reventes ordinaires") 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") g = np.linspace(0.5, 13.5, 50) a1.plot(g, a_all[0] + a_all[1] * g, color=palette.cat[1], lw=1.4, ls="--", label="Ajustement, toutes les paires") 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") a1.plot(g, th[0] + th[1] * g, color=palette.ink, lw=1.0, ls=":", label="Théorie : $2\\sigma^2 + \\delta^2 \\Delta$") 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) a1.legend(loc="upper right", fontsize=6.8) 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", ":")): 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) 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) a2.legend(loc="lower left", fontsize=6.8) return savefig(fig, "ch35/ch35-fig-poids-case-shiller.pdf") def fig_revision_filtre(res): s = res["sensibilite"]; L = c35.LABELS; vrai = res["vrai"] fig, (a1, a2) = plt.subplots(1, 2, figsize=(LARGEUR, 3.1)) for i, (lab, d) in enumerate(s["millesimes"].items()): 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)") a1.plot(range(len(vrai)), vrai, color=palette.ink, lw=1.0, ls=":", label="Vérité") a1.set_ylabel("Indice BMN (2023T1 = 100)"); a1.legend(loc="upper left", fontsize=7); _xticks(a1, L, 3); a1.set_ylim(95, 130) f = s["filtre"] 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)"))): 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']})") a2.plot(range(len(L)), vrai, color=palette.ink, lw=1.0, ls=":", label="Vérité") a2.set_ylabel("Indice BMN (2023T1 = 100)"); a2.legend(loc="upper left", fontsize=7); _xticks(a2, L, 3); a2.set_ylim(95, 130) return savefig(fig, "ch35/ch35-fig-revision-filtre.pdf") def fig_ipln(): import ch04_marches as c04 df = c04.ipln(); df = df[df["composante"] == "Total"] ordre = ["Montréal", "Québec", "Gatineau (partie du Québec)", "Sherbrooke", "Trois-Rivières", "Canada"] fig, ax = plt.subplots(figsize=(LARGEUR, 3.5)) for i, geo in enumerate(ordre): s = df[df["geo"] == geo].set_index("date")["indice"].sort_index() var = 100 * (s / s.shift(12) - 1); var = var[var.index >= "2011-01-01"] 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 "-", label=geo.replace(" (partie du Québec)", " (partie QC)")) ax.axhline(0, color=palette.ink, lw=0.8) ax.axvspan(pd.Timestamp("2020-03-01"), pd.Timestamp("2022-12-31"), color=palette.grid, alpha=0.5, lw=0) ax.set_ylabel("Variation sur douze mois (%)"); ax.set_xlim(pd.Timestamp("2011-01-01"), pd.Timestamp("2026-12-31")) ax.xaxis.set_major_locator(mdates.YearLocator(2)); ax.xaxis.set_major_formatter(mdates.DateFormatter("%Y")) ax.legend(loc="upper left", ncol=2, fontsize=7.5) out = savefig(fig, "ch35/ch35-fig-ipln-variation.pdf") 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} return out, last def main(): setup(); res = c35.main(verbose=False) for f in (fig_indices, fig_biais_mediane, fig_poids_cs, fig_revision_filtre): print("OK", f(res)) out, last = fig_ipln(); print("OK", out, {k: round(v, 1) for k, v in last.items()}) if __name__ == "__main__": main()