spb/traite-evaluation-qc
Public
1#!/usr/bin/env python32"""Chapitre 13 — Transactions, inscriptions, loyers et coûts : lecture et description des fichiers RÉELS de data/raw.34Imprime tous les chiffres cités dans le chapitre 13 :5 * structure du tableau 18-10-0289-01 de Statistique Canada (IPCB) lue dans le CSV brut : dimensions, membres, périodes,6 vecteurs ; séries de Montréal et de la RMR de Québec (bâtiments résidentiels) ;7 * trois indices souvent confondus (IPC Québec 18-10-0004-01, IPLN Montréal 18-10-0205-01, IPCB Montréal résidentiel8 18-10-0289-01) rebasés au T1 2017 = 100 ;9 * dictionnaire du rôle d'évaluation en format ouvert (MAMH, répertoire des renseignements prescrits v2.5, PDF) :10 sections et nombre de renseignements ;11 * richesse foncière uniformisée 2026 (MAMH, CC BY 4.0) : Montréal, Québec, Gatineau, ensemble des municipalités locales ;12 * Enquête sur les logements locatifs de la SCHL (octobre 2025) : six RMR, tableaux 1.1.1, 1.1.2, 1.1.3, 1.1.5 ;13 variation brute du loyer moyen vs variation estimative « même échantillon » ;14 * sensibilité : biais de datation (date de l'acte vs date de la promesse) selon la croissance du marché.1516Usage (racine du dépôt) : PYTHONPATH=analysis/python python3 analysis/python/cases/ch13_sources.py17Aucune donnée synthétique n'est utilisée dans ce script ; toutes les valeurs proviennent de data/raw (voir data/raw/SOURCES.md)."""18from __future__ import annotations19import re, sys, pathlib20import numpy as np, pandas as pd2122ROOT = pathlib.Path(__file__).resolve().parents[3]23RAW = ROOT / "data/raw"24PROCESSED = ROOT / "data/processed"25F_IPCB = RAW / "statcan_18100289/18100289.csv"26F_IPCB_META = RAW / "statcan_18100289/18100289_MetaData.csv"27F_IPLN = RAW / "statcan_18100205/18100205.csv"28F_IPC = RAW / "statcan_18100004/18100004.csv"29F_RFU = RAW / "mamh_rfu_2026/RFU-2026.csv"30F_RFU_DESC = RAW / "mamh_rfu_2026/RFU-2026-DescriptionPoste.csv"31F_DICO = RAW / "mamh_roles_evaluation/repertoire-des-renseignements-prescrits-du-role-devaluation-fonciere-version-2.5.pdf"32F_INDEX_ROLES = RAW / "mamh_roles_evaluation/indexRole2026.csv"33D_SCHL = RAW / "schl_rml_2025"3435V_IPC_QUEBEC = "v41691783" # IPC, ensemble, Québec (2002 = 100)36V_IPLN_MONTREAL = "v111955484" # IPLN, total maison et terrain, Montréal (déc. 2016 = 100)37TYPE_RESID = "Bâtiments résidentiels [621]"38GEO_MTL, GEO_QC = "Montréal, Québec", "Québec, Québec"3940RMR_SCHL = { # libellés du fichier provincial rmr-quebec-2025-fr.xlsx (centres de 10 000 habitants et plus)41 "Montréal": "Montréal (RMR)", "Québec": "Québec (RMR)", "Gatineau": "Ottawa-Gatineau (RMR) (partie QC)",42 "Sherbrooke": "Sherbrooke (RMR)", "Trois-Rivières": "Trois-Rivières (RMR)", "Saguenay": "Saguenay (RMR)",43}44CHAMBRES = ["Studio", "1 chambre", "2 chambres", "3 chambres +", "Tous"]454647# ----------------------------------------------------------------------------------------------------------------------48# 1. Statistique Canada — IPCB 18-10-0289-01 : structure et séries49# ----------------------------------------------------------------------------------------------------------------------50def charger_ipcb() -> pd.DataFrame:51 return pd.read_csv(F_IPCB, sep=";", encoding="utf-8-sig")525354def structure_ipcb(df: pd.DataFrame | None = None) -> dict:55 """Décrit le tableau brut : lignes, colonnes, membres de chaque dimension, périodes, vecteurs, valeurs manquantes."""56 df = charger_ipcb() if df is None else df57 per = sorted(df["PÉRIODE DE RÉFÉRENCE"].unique())58 s = {59 "lignes": int(len(df)), "colonnes": int(df.shape[1]), "noms_colonnes": df.columns.tolist(),60 "n_geo": int(df["GÉO"].nunique()), "n_types": int(df["Type d'immeuble"].nunique()),61 "n_divisions": int(df["Division"].nunique()), "n_vecteurs": int(df["VECTEUR"].nunique()),62 "periode_min": per[0], "periode_max": per[-1], "n_periodes": len(per),63 "unite": df["UNITÉ DE MESURE"].iloc[0], "valeurs_manquantes": int(df["VALEUR"].isna().sum()),64 "produit_theorique": int(df["GÉO"].nunique() * df["Type d'immeuble"].nunique() * df["Division"].nunique()),65 }66 # débuts de série par type d'immeuble à Montréal (agrégat des divisions)67 mtl = df[(df["GÉO"] == GEO_MTL) & (df["Division"] == "Agrégat des divisions")]68 s["debuts_montreal"] = mtl.groupby("Type d'immeuble")["PÉRIODE DE RÉFÉRENCE"].min().to_dict()69 return s707172def serie_ipcb(df: pd.DataFrame, geo: str, type_imm: str = TYPE_RESID) -> pd.DataFrame:73 s = df[(df["GÉO"] == geo) & (df["Division"] == "Agrégat des divisions") & (df["Type d'immeuble"] == type_imm)]74 s = s[["PÉRIODE DE RÉFÉRENCE", "VECTEUR", "VALEUR"]].rename(columns={"PÉRIODE DE RÉFÉRENCE": "periode", "VALEUR": "indice"})75 s = s.sort_values("periode").reset_index(drop=True)76 s["trimestre"] = s["periode"].map(_periode_vers_trimestre)77 return s787980def _periode_vers_trimestre(p: str) -> str:81 a, m = p.split("-")82 return f"{a}T{(int(m) - 1) // 3 + 1}"838485def resume_ipcb(df: pd.DataFrame | None = None) -> dict:86 """Valeurs citées : Montréal et Québec, bâtiments résidentiels, T1 2017, T3 2025, T3 2026, variations."""87 df = charger_ipcb() if df is None else df88 out = {}89 for nom, geo in (("Montréal", GEO_MTL), ("Québec", GEO_QC)):90 s = serie_ipcb(df, geo)91 v = dict(zip(s["trimestre"], s["indice"]))92 out[nom] = {93 "vecteur": s["vecteur"].iloc[0] if "vecteur" in s else s["VECTEUR"].iloc[0], "debut": s["trimestre"].iloc[0],94 "fin": s["trimestre"].iloc[-1], "n": int(len(s)), "2017T1": v.get("2017T1"), "2023T1": v.get("2023T1"),95 "2025T3": v["2025T3"], "2026T3": v["2026T3"],96 "var_annuelle_pct": (v["2026T3"] / v["2025T3"] - 1) * 100,97 "facteur_2017T1_2026T3": (v["2026T3"] / v["2017T1"]) if "2017T1" in v else None,98 "facteur_2023T1_2026T3": v["2026T3"] / v["2023T1"],99 }100 return out101102103# ----------------------------------------------------------------------------------------------------------------------104# 2. Trois indices rebasés : IPC Québec, IPLN Montréal, IPCB Montréal résidentiel (T1 2017 = 100)105# ----------------------------------------------------------------------------------------------------------------------106def _mensuel_vers_trimestriel(s: pd.DataFrame) -> pd.DataFrame:107 """Moyenne trimestrielle simple d'une série mensuelle (colonnes periode AAAA-MM, valeur)."""108 s = s.copy(); s["trimestre"] = s["periode"].map(_periode_vers_trimestre)109 g = s.groupby("trimestre", as_index=False).agg(valeur=("valeur", "mean"), n=("valeur", "size"))110 return g[g["n"] == 3].drop(columns="n") # trimestres complets seulement111112113def indices_compares(ipcb: pd.DataFrame | None = None, ecrire: bool = True) -> pd.DataFrame:114 """Tableau trimestriel T1 2017 → T3 2026 des trois indices rebasés (T1 2017 = 100) ; écrit data/processed/ch13_indices.csv."""115 ipcb = charger_ipcb() if ipcb is None else ipcb116 ipc = pd.read_csv(F_IPC, sep=";", encoding="utf-8-sig", usecols=["PÉRIODE DE RÉFÉRENCE", "VECTEUR", "VALEUR"])117 ipc = ipc[ipc["VECTEUR"] == V_IPC_QUEBEC].rename(columns={"PÉRIODE DE RÉFÉRENCE": "periode", "VALEUR": "valeur"})118 ipln = pd.read_csv(F_IPLN, sep=";", encoding="utf-8-sig", usecols=["PÉRIODE DE RÉFÉRENCE", "VECTEUR", "VALEUR"])119 ipln = ipln[ipln["VECTEUR"] == V_IPLN_MONTREAL].rename(columns={"PÉRIODE DE RÉFÉRENCE": "periode", "VALEUR": "valeur"})120 b = serie_ipcb(ipcb, GEO_MTL)[["trimestre", "indice"]].rename(columns={"indice": "ipcb_mtl_resid"})121 q_ipc = _mensuel_vers_trimestriel(ipc).rename(columns={"valeur": "ipc_quebec"})122 q_ipln = _mensuel_vers_trimestriel(ipln).rename(columns={"valeur": "ipln_mtl"})123 t = b.merge(q_ipc, on="trimestre").merge(q_ipln, on="trimestre")124 t = t[(t["trimestre"] >= "2017T1") & (t["trimestre"] <= "2026T3")].reset_index(drop=True)125 for c in ("ipcb_mtl_resid", "ipc_quebec", "ipln_mtl"):126 t[c + "_base2017"] = t[c] / t.loc[t["trimestre"] == "2017T1", c].iloc[0] * 100127 if ecrire:128 PROCESSED.mkdir(parents=True, exist_ok=True)129 t.to_csv(PROCESSED / "ch13_indices.csv", index=False)130 return t131132133def resume_indices(t: pd.DataFrame) -> dict:134 fin = t[t["trimestre"] == "2026T3"].iloc[0]135 return {136 "ipcb_2026T3_base2017": fin["ipcb_mtl_resid_base2017"], "ipln_2026T3_base2017": fin["ipln_mtl_base2017"],137 "ipc_2026T3_base2017": fin["ipc_quebec_base2017"],138 "ipc_quebec_2017T1_moy": t.loc[0, "ipc_quebec"], "ipc_quebec_2026T3_moy": fin["ipc_quebec"],139 "ipln_mtl_2017T1_moy": t.loc[0, "ipln_mtl"], "ipln_mtl_2026T3_moy": fin["ipln_mtl"],140 "n_trimestres": int(len(t)),141 }142143144# ----------------------------------------------------------------------------------------------------------------------145# 3. Dictionnaire du rôle (MAMH, répertoire v2.5) : sections et renseignements146# ----------------------------------------------------------------------------------------------------------------------147def dictionnaire_role() -> dict:148 """Extrait le texte du PDF (pypdf) et dénombre les codes de renseignements par section."""149 from pypdf import PdfReader150 texte = "\n".join((p.extract_text() or "") for p in PdfReader(str(F_DICO)).pages)151 codes = re.findall(r"RL(\d{2})(\d{2})([A-Z])(x?)(?![A-Za-z0-9])", texte)152 uniques = sorted({f"RL{a}{b}{c}{d}" for a, b, c, d in codes})153 par_section = {}154 for a, b, c, d in {(a, b, c, d) for a, b, c, d in codes}:155 par_section[int(a)] = par_section.get(int(a), 0) + 1156 repet = sorted({f"RL{a}{b}{c}x" for a, b, c, d in codes if d == "x"})157 noms_sections = {1: "Identification de l'unité d'évaluation", 2: "Propriétaire", 3: "Caractéristiques de l'unité d'évaluation",158 4: "Valeurs au rôle d'évaluation", 5: "Répartition fiscale"}159 champs_cles = {160 "RL0101Gx": "Nom de la voie publique", "RL0104A": "Division (matricule)", "RL0105A": "Code de l'utilisation prédominante",161 "RL0107A": "Numéro d'unité de voisinage", "RL0302A": "Superficie du terrain", "RL0307A": "Année de construction originelle",162 "RL0307B": "Année réelle ou estimée", "RL0308A": "Aire d'étages du bâtiment principal", "RL0311A": "Nombre total de logements",163 "RL0401A": "Date des conditions du marché", "RL0402A": "Valeur du terrain", "RL0403A": "Valeur du ou des bâtiments",164 "RL0404A": "Valeur de l'immeuble", "RL0405A": "Valeur au rôle antérieur", "RL0501A": "Terrain vague desservi",165 }166 presents = {k: (k in uniques) for k in champs_cles}167 return {"n_pages": len(PdfReader(str(F_DICO)).pages), "n_codes_uniques": len(uniques), "par_section": dict(sorted(par_section.items())),168 "noms_sections": noms_sections, "codes_repetitifs": repet, "n_repetitifs": len(repet), "champs_cles_presents": presents,169 "date_document": "24 mars 2022" if "24 mars 2022" in texte else None}170171172def index_roles() -> dict:173 idx = pd.read_csv(F_INDEX_ROLES, dtype=str)174 return {"n_municipalites": int(len(idx)), "colonnes": idx.columns.tolist(),175 "gatineau": idx[idx.iloc[:, 0].str.lstrip("0") == "81017"].iloc[0].tolist() if (idx.iloc[:, 0].str.lstrip("0") == "81017").any() else None,176 "montreal": idx[idx.iloc[:, 0].str.lstrip("0") == "66023"].iloc[0].tolist() if (idx.iloc[:, 0].str.lstrip("0") == "66023").any() else None,177 "quebec": idx[idx.iloc[:, 0].str.lstrip("0") == "23027"].iloc[0].tolist() if (idx.iloc[:, 0].str.lstrip("0") == "23027").any() else None}178179180# ----------------------------------------------------------------------------------------------------------------------181# 4. Richesse foncière uniformisée 2026 (MAMH)182# ----------------------------------------------------------------------------------------------------------------------183POSTES_RFU = {"CIALX02141": "Résidences d'un logement", "CIALX02142": "Immeubles à logements et condominiums",184 "CIALX02143": "Autres immeubles résidentiels", "CIALX02144": "Industriels et commerciaux",185 "CIALX02145": "Agricoles", "CIALX02146": "Autres immeubles"}186VILLES = {"66023": "Montréal", "23027": "Québec", "81017": "Gatineau"}187188189def rfu_2026() -> dict:190 df = pd.read_csv(F_RFU, dtype={"cod_geo": str})191 desc = pd.read_csv(F_RFU_DESC)192 out = {"n_lignes": int(len(df)), "n_colonnes": int(df.shape[1]), "types_org": df["type_org"].value_counts().to_dict(),193 "n_postes_decrits": int(desc["Code de postes"].nunique()), "villes": {}, "postes": POSTES_RFU}194 for code, nom in VILLES.items():195 r = df[df["cod_geo"] == code].iloc[0]196 imposable = float(r["CIALX02147"]); rfu = float(r["CIALX02140"])197 parts = {POSTES_RFU[k]: float(r[k]) for k in POSTES_RFU}198 out["villes"][nom] = {"population": int(r["population"]), "rfu": rfu, "imposable_uniformise": imposable,199 "facteur_redressement": float(r["CSALX02163"]), "postes": parts,200 "parts_pct": {k: v / imposable * 100 for k, v in parts.items()},201 "somme_postes": sum(parts.values()), "ecart_somme_vs_imposable": sum(parts.values()) - imposable}202 out["total_rfu"] = float(df["CIALX02140"].sum()); out["total_imposable"] = float(df["CIALX02147"].sum())203 out["part_3_villes_rfu_pct"] = sum(v["rfu"] for v in out["villes"].values()) / out["total_rfu"] * 100204 return out205206207# ----------------------------------------------------------------------------------------------------------------------208# 5. SCHL — Enquête sur les logements locatifs, octobre 2025 (fichier provincial, centres de 10 000 habitants et plus)209# ----------------------------------------------------------------------------------------------------------------------210def _num(x):211 if x is None: return np.nan212 s = str(x).replace(" ", "").replace(" ", "").replace(",", ".")213 if s in ("**", "++", "-", "–", ""): return np.nan214 try: return float(s)215 except ValueError: return np.nan216217218def _lire_tableau_schl(ws, libelles: dict, avec_qualite: bool) -> pd.DataFrame:219 """Lit un tableau SCHL : repère la ligne d'en-tête (Centre/Zone + « Oct. »), puis extrait pour chaque RMR220 les 10 valeurs (5 types de logement × 2 années) et, s'il y a lieu, les lettres de qualité (colonne suivante)."""221 rows = [list(r) for r in ws.iter_rows(values_only=True)]222 entete = next(r for r in rows if r and isinstance(r[0], str) and r[0] in ("Centre", "Zone"))223 cols = [i for i, c in enumerate(entete) if isinstance(c, str) and c.startswith("Oct.")]224 assert len(cols) == 10, f"en-tête inattendu : {entete}"225 recs = []226 for r in rows:227 if r and isinstance(r[0], str) and r[0] in libelles.values():228 rmr = next(k for k, v in libelles.items() if v == r[0])229 if any(x["rmr"] == rmr for x in recs): continue # la première ligne de chaque fichier est répétée230 vals = [_num(r[i]) for i in cols]231 quals = [(r[i + 1] if avec_qualite and i + 1 < len(r) and isinstance(r[i + 1], str) and r[i + 1] in "abcd" else None) for i in cols]232 rec = {"rmr": rmr}233 for j, ch in enumerate(CHAMBRES):234 rec[f"{ch}|2024"] = vals[2 * j]; rec[f"{ch}|2025"] = vals[2 * j + 1]235 if avec_qualite: rec[f"{ch}|2025|q"] = quals[2 * j + 1]236 recs.append(rec)237 return pd.DataFrame(recs).set_index("rmr")238239240def schl_rmr(fichier: pathlib.Path = D_SCHL / "rmr-quebec-2025-fr.xlsx") -> dict:241 import openpyxl242 wb = openpyxl.load_workbook(fichier, read_only=True)243 t = {"inoccupation": _lire_tableau_schl(wb["Tableau 1.1.1"], RMR_SCHL, True),244 "loyer": _lire_tableau_schl(wb["Tableau 1.1.2"], RMR_SCHL, True),245 "univers": _lire_tableau_schl(wb["Tableau 1.1.3"], RMR_SCHL, False),246 "variation_estimative": _lire_tableau_schl(wb["Tableau 1.1.5"], RMR_SCHL, True)}247 t["n_onglets"] = len(wb.sheetnames); t["onglets"] = wb.sheetnames248 return t249250251def tableau_schl(t: dict) -> pd.DataFrame:252 """Tableau du chapitre : inoccupation 2024/2025, loyer 2 ch. et tous 2025, univers 2025, variation brute vs estimative."""253 rows = []254 for rmr in RMR_SCHL:255 inoc, loy, uni, var = t["inoccupation"].loc[rmr], t["loyer"].loc[rmr], t["univers"].loc[rmr], t["variation_estimative"].loc[rmr]256 brut_2ch = (loy["2 chambres|2025"] / loy["2 chambres|2024"] - 1) * 100257 brut_tous = (loy["Tous|2025"] / loy["Tous|2024"] - 1) * 100258 rows.append({"rmr": rmr, "inocc_2024": inoc["Tous|2024"], "inocc_2025": inoc["Tous|2025"], "inocc_q": inoc["Tous|2025|q"],259 "loyer_2ch_2024": loy["2 chambres|2024"], "loyer_2ch_2025": loy["2 chambres|2025"], "loyer_2ch_q": loy["2 chambres|2025|q"],260 "loyer_tous_2025": loy["Tous|2025"], "univers_2025": uni["Tous|2025"], "univers_2ch_2025": uni["2 chambres|2025"],261 "var_brute_2ch_pct": brut_2ch, "var_estim_2ch_pct": var["2 chambres|2025"],262 "var_brute_tous_pct": brut_tous, "var_estim_tous_pct": var["Tous|2025"],263 "logements_vacants_implicites": uni["Tous|2025"] * inoc["Tous|2025"] / 100})264 return pd.DataFrame(rows).set_index("rmr")265266267# ----------------------------------------------------------------------------------------------------------------------268# 6. Sensibilité : biais de datation (date de l'acte vs date de la promesse d'achat)269# ----------------------------------------------------------------------------------------------------------------------270def biais_datation(croissances_pct=(-3.0, 0.0, 3.0, 6.0, 10.0), delais_mois=(1, 2, 3, 6)) -> pd.DataFrame:271 """Biais relatif (en %) sur l'ajustement de conditions du marché si l'on date une vente à l'acte plutôt qu'à la promesse,272 pour une croissance annuelle composée g et un délai δ (mois) : b = (1+g)^(δ/12) − 1."""273 rows = []274 for g in croissances_pct:275 for d in delais_mois:276 rows.append({"croissance_annuelle_pct": g, "delai_mois": d, "biais_pct": ((1 + g / 100) ** (d / 12) - 1) * 100})277 return pd.DataFrame(rows)278279280def prix_corrige(prix_acte: float, meubles: float, g_annuel: float, delai_mois: float) -> dict:281 """Exemple : prix déclaré à l'acte incluant des meubles, daté à l'acte ; on retranche les meubles et on recule la date.282 Le prix « au marché de la promesse » est P_imm ; le prix qu'on attribuerait à tort à la date de l'acte serait P_imm aussi,283 mais l'ajustement temporel vers la date d'évaluation serait sous-estimé du facteur (1+g)^(δ/12)."""284 p_imm = prix_acte - meubles285 facteur = (1 + g_annuel) ** (delai_mois / 12)286 return {"prix_immeuble": p_imm, "facteur_datation": facteur, "ecart_dollars": p_imm * (facteur - 1)}287288289# ----------------------------------------------------------------------------------------------------------------------290# 7. Exercices (chiffres des solutions)291# ----------------------------------------------------------------------------------------------------------------------292def exercice_registre(n_actes=25, n_index=25, n_etats=3, tarif_consultation=1.0, etat_base=16.0, etat_requisition=16.0) -> dict:293 """Coût de consultation au Registre foncier (grille en vigueur à compter du 1er avril 2025) : 1 $ par document consulté ;294 état certifié 16 $ + 16 $ par réquisition (une réquisition par état ici)."""295 consult = (n_actes + n_index) * tarif_consultation296 etats = n_etats * (etat_base + etat_requisition)297 return {"consultation": consult, "etats": etats, "total": consult + etats}298299300def exercice_couverture(n_registre=1500, n_centris=1044, n_apparies=920, mu_courtier=510000.0, mu_autres=455000.0) -> dict:301 """Chiffres FICTIFS d'exercice : couverture du courtage, taux d'appariement, biais de composition."""302 couverture = n_centris / n_registre303 appariement = n_apparies / n_centris304 w = n_centris / n_registre305 mu_marche = w * mu_courtier + (1 - w) * mu_autres306 return {"couverture": couverture, "appariement": appariement, "poids_courtier": w, "moyenne_marche": mu_marche,307 "biais_courtier_dollars": mu_courtier - mu_marche, "biais_courtier_pct": (mu_courtier / mu_marche - 1) * 100}308309310def exercice_ipcb_quebec(df: pd.DataFrame | None = None) -> dict:311 """Exercice 5 : RMR de Québec vs Montréal et composite, bâtiments résidentiels : la série de Québec ne commence qu'au T1 2023,312 d'où la comparaison des facteurs T1 2023 → T3 2026 (et, pour Montréal et le composite, T3 2019 → T3 2026)."""313 df = charger_ipcb() if df is None else df314 out = {}315 for nom, geo in (("Montréal", GEO_MTL), ("Québec", GEO_QC), ("Composite", "Composite de quinze régions métropolitaines de recensement")):316 s = serie_ipcb(df, geo); v = dict(zip(s["trimestre"], s["indice"]))317 out[nom] = {"debut": s["trimestre"].iloc[0], "n": int(len(s)), "2019T3": v.get("2019T3"), "2023T1": v.get("2023T1"), "2025T3": v.get("2025T3"),318 "2026T3": v.get("2026T3"), "ratio_2019T3_2026T3": (v["2026T3"] / v["2019T3"]) if "2019T3" in v else None,319 "ratio_2023T1_2026T3": v["2026T3"] / v["2023T1"], "var_annuelle_pct": (v["2026T3"] / v["2025T3"] - 1) * 100,320 "tcac_2023T1_2026T3_pct": ((v["2026T3"] / v["2023T1"]) ** (4 / 14) - 1) * 100}321 return out322323324# ----------------------------------------------------------------------------------------------------------------------325def main() -> None:326 pd.set_option("display.width", 160); pd.set_option("display.max_columns", 30)327 print("=== 1. Statistique Canada, tableau 18-10-0289-01 (IPCB) — structure du CSV brut ===")328 ipcb = charger_ipcb(); st = structure_ipcb(ipcb)329 for k in ("lignes", "colonnes", "n_geo", "n_types", "n_divisions", "n_vecteurs", "periode_min", "periode_max", "n_periodes", "unite", "valeurs_manquantes", "produit_theorique"):330 print(f" {k:22s}: {st[k]}")331 print(" colonnes :", st["noms_colonnes"])332 print(" débuts de série à Montréal (agrégat des divisions) :")333 for k, v in st["debuts_montreal"].items(): print(f" {v} {k}")334 r = resume_ipcb(ipcb)335 for nom, d in r.items():336 print(f" {nom} résidentiel : {d['debut']} → {d['fin']} ({d['n']} obs.) ; T1 2017 = {d['2017T1']} ; T1 2023 = {d['2023T1']} ; T3 2025 = {d['2025T3']:.1f} ; "337 f"T3 2026 = {d['2026T3']:.1f} ; var. annuelle {d['var_annuelle_pct']:.2f} % ; facteur 2017T1→2026T3 {d['facteur_2017T1_2026T3']} ; facteur 2023T1→2026T3 {d['facteur_2023T1_2026T3']:.3f}")338339 print("\n=== 2. Trois indices rebasés T1 2017 = 100 (IPC Québec, IPLN Montréal, IPCB Montréal résidentiel) ===")340 t = indices_compares(ipcb); ri = resume_indices(t)341 print(t[["trimestre", "ipcb_mtl_resid", "ipc_quebec", "ipln_mtl", "ipcb_mtl_resid_base2017", "ipc_quebec_base2017", "ipln_mtl_base2017"]]342 .iloc[[0, 12, 20, 24, 28, 32, 36, 38]].to_string(index=False, float_format=lambda x: f"{x:.1f}"))343 print(f" T3 2026 (T1 2017 = 100) : IPCB {ri['ipcb_2026T3_base2017']:.1f} ; IPLN {ri['ipln_2026T3_base2017']:.1f} ; IPC {ri['ipc_2026T3_base2017']:.1f} ; "344 f"IPC Québec moy. T1 2017 {ri['ipc_quebec_2017T1_moy']:.2f} → T3 2026 {ri['ipc_quebec_2026T3_moy']:.2f} ; IPLN Mtl {ri['ipln_mtl_2017T1_moy']:.2f} → {ri['ipln_mtl_2026T3_moy']:.2f} ; {ri['n_trimestres']} trimestres")345346 print("\n=== 3. Dictionnaire du rôle en format ouvert (MAMH, répertoire v2.5) ===")347 dico = dictionnaire_role()348 print(f" {dico['n_pages']} pages ; {dico['n_codes_uniques']} codes de renseignement distincts ; {dico['n_repetitifs']} renseignements répétitifs (suffixe x) ; daté du {dico['date_document']}")349 for s, n in dico["par_section"].items(): print(f" section {s} — {dico['noms_sections'][s]} : {n} renseignements")350 print(" champs clés présents :", all(dico["champs_cles_presents"].values()), {k: v for k, v in dico["champs_cles_presents"].items() if not v} or "")351 ix = index_roles(); print(f" index des rôles 2026 : {ix['n_municipalites']} municipalités ; colonnes {ix['colonnes']} ; Gatineau → {ix['gatineau'][-1] if ix['gatineau'] else None}")352353 print("\n=== 4. Richesse foncière uniformisée 2026 (MAMH, CC BY 4.0) ===")354 rf = rfu_2026()355 print(f" {rf['n_lignes']} lignes × {rf['n_colonnes']} colonnes ; {rf['types_org']} ; {rf['n_postes_decrits']} postes décrits ; RFU totale {rf['total_rfu']/1e9:.1f} G$ ; imposable uniformisée {rf['total_imposable']/1e9:.1f} G$ ; 3 villes = {rf['part_3_villes_rfu_pct']:.1f} % de la RFU")356 for v, d in rf["villes"].items():357 print(f" {v:9s} pop. {d['population']:>9,d} ; RFU {d['rfu']/1e9:7.2f} G$ ; imposable {d['imposable_uniformise']/1e9:7.2f} G$ ; facteur {d['facteur_redressement']:.2f} ; écart somme postes vs imposable {d['ecart_somme_vs_imposable']/1e6:.1f} M$")358 for p, pct in d["parts_pct"].items(): print(f" {p:40s} {d['postes'][p]/1e9:8.2f} G$ {pct:5.1f} %")359360 print("\n=== 5. SCHL, Enquête sur les logements locatifs, octobre 2025 (six RMR, fichier provincial) ===")361 sc = schl_rmr(); ts = tableau_schl(sc)362 print(f" {sc['n_onglets']} onglets dans le classeur")363 print(ts.to_string(float_format=lambda x: f"{x:.1f}"))364365 print("\n=== 6. Sensibilité : biais de datation (acte vs promesse) ===")366 bd = biais_datation()367 print(bd.pivot(index="croissance_annuelle_pct", columns="delai_mois", values="biais_pct").to_string(float_format=lambda x: f"{x:.2f}"))368 ex = prix_corrige(612000, 12000, 0.06, 75 / 30.4375)369 print(f" exemple : prix d'acte 612 000 $ dont 12 000 $ de meubles → immeuble {ex['prix_immeuble']:.0f} $ ; facteur de datation {ex['facteur_datation']:.4f} ; écart {ex['ecart_dollars']:.0f} $")370371 print("\n=== 7. Exercices ===")372 print(" registre :", exercice_registre())373 print(" couverture (fictif) :", {k: round(v, 4) for k, v in exercice_couverture().items()})374 for nom, d in exercice_ipcb_quebec(ipcb).items():375 print(f" {nom:9s} début {d['debut']} ({d['n']} obs.) ; T3 2019 {d['2019T3']} ; T1 2023 {d['2023T1']} ; T3 2026 {d['2026T3']} ; ratio 2019T3→2026T3 {d['ratio_2019T3_2026T3']} ; "376 f"ratio 2023T1→2026T3 {d['ratio_2023T1_2026T3']:.4f} (TCAC {d['tcac_2023T1_2026T3_pct']:.2f} %) ; var. annuelle {d['var_annuelle_pct']:.2f} %")377378379if __name__ == "__main__":380 main()381