spb/traite-evaluation-qc
Public
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, n["indice_moy_geo_corrigee"], color=palette.cat[3], lw=1.4, ls="-.", label="Moyenne géométrique corrigée de la composition")47 a1.plot(x, tab["hedonique"], color=palette.cat[0], lw=2.0, marker="o", ms=3.3, label="Indice hédonique (qualité constante)")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