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["2017T1"], "2025T3": v["2025T3"], "2026T3": v["2026T3"],95 "var_annuelle_pct": (v["2026T3"] / v["2025T3"] - 1) * 100, "facteur_2017T1_2026T3": v["2026T3"] / v["2017T1"],96 }97 return out9899100# ----------------------------------------------------------------------------------------------------------------------101# 2. Trois indices rebasés : IPC Québec, IPLN Montréal, IPCB Montréal résidentiel (T1 2017 = 100)102# ----------------------------------------------------------------------------------------------------------------------103def _mensuel_vers_trimestriel(s: pd.DataFrame) -> pd.DataFrame:104 """Moyenne trimestrielle simple d'une série mensuelle (colonnes periode AAAA-MM, valeur)."""105 s = s.copy(); s["trimestre"] = s["periode"].map(_periode_vers_trimestre)106 g = s.groupby("trimestre", as_index=False).agg(valeur=("valeur", "mean"), n=("valeur", "size"))107 return g[g["n"] == 3].drop(columns="n") # trimestres complets seulement108109110def indices_compares(ipcb: pd.DataFrame | None = None, ecrire: bool = True) -> pd.DataFrame:111 """Tableau trimestriel T1 2017 → T3 2026 des trois indices rebasés (T1 2017 = 100) ; écrit data/processed/ch13_indices.csv."""112 ipcb = charger_ipcb() if ipcb is None else ipcb113 ipc = pd.read_csv(F_IPC, sep=";", encoding="utf-8-sig", usecols=["PÉRIODE DE RÉFÉRENCE", "VECTEUR", "VALEUR"])114 ipc = ipc[ipc["VECTEUR"] == V_IPC_QUEBEC].rename(columns={"PÉRIODE DE RÉFÉRENCE": "periode", "VALEUR": "valeur"})115 ipln = pd.read_csv(F_IPLN, sep=";", encoding="utf-8-sig", usecols=["PÉRIODE DE RÉFÉRENCE", "VECTEUR", "VALEUR"])116 ipln = ipln[ipln["VECTEUR"] == V_IPLN_MONTREAL].rename(columns={"PÉRIODE DE RÉFÉRENCE": "periode", "VALEUR": "valeur"})117 b = serie_ipcb(ipcb, GEO_MTL)[["trimestre", "indice"]].rename(columns={"indice": "ipcb_mtl_resid"})118 q_ipc = _mensuel_vers_trimestriel(ipc).rename(columns={"valeur": "ipc_quebec"})119 q_ipln = _mensuel_vers_trimestriel(ipln).rename(columns={"valeur": "ipln_mtl"})120 t = b.merge(q_ipc, on="trimestre").merge(q_ipln, on="trimestre")121 t = t[(t["trimestre"] >= "2017T1") & (t["trimestre"] <= "2026T3")].reset_index(drop=True)122 for c in ("ipcb_mtl_resid", "ipc_quebec", "ipln_mtl"):123 t[c + "_base2017"] = t[c] / t.loc[t["trimestre"] == "2017T1", c].iloc[0] * 100124 if ecrire:125 PROCESSED.mkdir(parents=True, exist_ok=True)126 t.to_csv(PROCESSED / "ch13_indices.csv", index=False)127 return t128129130def resume_indices(t: pd.DataFrame) -> dict:131 fin = t[t["trimestre"] == "2026T3"].iloc[0]132 return {133 "ipcb_2026T3_base2017": fin["ipcb_mtl_resid_base2017"], "ipln_2026T3_base2017": fin["ipln_mtl_base2017"],134 "ipc_2026T3_base2017": fin["ipc_quebec_base2017"],135 "ipc_quebec_2017T1_moy": t.loc[0, "ipc_quebec"], "ipc_quebec_2026T3_moy": fin["ipc_quebec"],136 "ipln_mtl_2017T1_moy": t.loc[0, "ipln_mtl"], "ipln_mtl_2026T3_moy": fin["ipln_mtl"],137 "n_trimestres": int(len(t)),138 }139140141# ----------------------------------------------------------------------------------------------------------------------142# 3. Dictionnaire du rôle (MAMH, répertoire v2.5) : sections et renseignements143# ----------------------------------------------------------------------------------------------------------------------144def dictionnaire_role() -> dict:145 """Extrait le texte du PDF (pypdf) et dénombre les codes de renseignements par section."""146 from pypdf import PdfReader147 texte = "\n".join((p.extract_text() or "") for p in PdfReader(str(F_DICO)).pages)148 codes = re.findall(r"\bRL(\d{2})(\d{2})([A-Z])(x?)\b", texte)149 uniques = sorted({f"RL{a}{b}{c}{d}" for a, b, c, d in codes})150 par_section = {}151 for a, b, c, d in {(a, b, c, d) for a, b, c, d in codes}:152 par_section[int(a)] = par_section.get(int(a), 0) + 1153 repet = sorted({f"RL{a}{b}{c}x" for a, b, c, d in codes if d == "x"})154 noms_sections = {1: "Identification de l'unité d'évaluation", 2: "Propriétaire", 3: "Caractéristiques de l'unité d'évaluation",155 4: "Valeurs au rôle d'évaluation", 5: "Répartition fiscale"}156 champs_cles = {157 "RL0101Gx": "Nom de la voie publique", "RL0104A": "Division (matricule)", "RL0105A": "Code de l'utilisation prédominante",158 "RL0107A": "Numéro d'unité de voisinage", "RL0302A": "Superficie du terrain", "RL0307A": "Année de construction originelle",159 "RL0307B": "Année réelle ou estimée", "RL0308A": "Aire d'étages du bâtiment principal", "RL0311A": "Nombre total de logements",160 "RL0401A": "Date des conditions du marché", "RL0402A": "Valeur du terrain", "RL0403A": "Valeur du ou des bâtiments",161 "RL0404A": "Valeur de l'immeuble", "RL0405A": "Valeur au rôle antérieur", "RL0501A": "Terrain vague desservi",162 }163 presents = {k: (k in uniques) for k in champs_cles}164 return {"n_pages": len(PdfReader(str(F_DICO)).pages), "n_codes_uniques": len(uniques), "par_section": dict(sorted(par_section.items())),165 "noms_sections": noms_sections, "codes_repetitifs": repet, "n_repetitifs": len(repet), "champs_cles_presents": presents,166 "date_document": "24 mars 2022" if "24 mars 2022" in texte else None}167168169def index_roles() -> dict:170 idx = pd.read_csv(F_INDEX_ROLES, dtype=str)171 return {"n_municipalites": int(len(idx)), "colonnes": idx.columns.tolist(),172 "gatineau": idx[idx.iloc[:, 0].str.lstrip("0") == "81017"].iloc[0].tolist() if (idx.iloc[:, 0].str.lstrip("0") == "81017").any() else None,173 "montreal": idx[idx.iloc[:, 0].str.lstrip("0") == "66023"].iloc[0].tolist() if (idx.iloc[:, 0].str.lstrip("0") == "66023").any() else None,174 "quebec": idx[idx.iloc[:, 0].str.lstrip("0") == "23027"].iloc[0].tolist() if (idx.iloc[:, 0].str.lstrip("0") == "23027").any() else None}175176177# ----------------------------------------------------------------------------------------------------------------------178# 4. Richesse foncière uniformisée 2026 (MAMH)179# ----------------------------------------------------------------------------------------------------------------------180POSTES_RFU = {"CIALX02141": "Résidences d'un logement", "CIALX02142": "Immeubles à logements et condominiums",181 "CIALX02143": "Autres immeubles résidentiels", "CIALX02144": "Industriels et commerciaux",182 "CIALX02145": "Agricoles", "CIALX02146": "Autres immeubles"}183VILLES = {"66023": "Montréal", "23027": "Québec", "81017": "Gatineau"}184185186def rfu_2026() -> dict:187 df = pd.read_csv(F_RFU, dtype={"cod_geo": str})188 desc = pd.read_csv(F_RFU_DESC)189 out = {"n_lignes": int(len(df)), "n_colonnes": int(df.shape[1]), "types_org": df["type_org"].value_counts().to_dict(),190 "n_postes_decrits": int(desc["Code de postes"].nunique()), "villes": {}, "postes": POSTES_RFU}191 for code, nom in VILLES.items():192 r = df[df["cod_geo"] == code].iloc[0]193 imposable = float(r["CIALX02147"]); rfu = float(r["CIALX02140"])194 parts = {POSTES_RFU[k]: float(r[k]) for k in POSTES_RFU}195 out["villes"][nom] = {"population": int(r["population"]), "rfu": rfu, "imposable_uniformise": imposable,196 "facteur_redressement": float(r["CSALX02163"]), "postes": parts,197 "parts_pct": {k: v / imposable * 100 for k, v in parts.items()},198 "somme_postes": sum(parts.values()), "ecart_somme_vs_imposable": sum(parts.values()) - imposable}199 out["total_rfu"] = float(df["CIALX02140"].sum()); out["total_imposable"] = float(df["CIALX02147"].sum())200 out["part_3_villes_rfu_pct"] = sum(v["rfu"] for v in out["villes"].values()) / out["total_rfu"] * 100201 return out202203204# ----------------------------------------------------------------------------------------------------------------------205# 5. SCHL — Enquête sur les logements locatifs, octobre 2025 (fichier provincial, centres de 10 000 habitants et plus)206# ----------------------------------------------------------------------------------------------------------------------207def _num(x):208 if x is None: return np.nan209 s = str(x).replace(" ", "").replace(" ", "").replace(",", ".")210 if s in ("**", "++", "-", "–", ""): return np.nan211 try: return float(s)212 except ValueError: return np.nan213214215def _lire_tableau_schl(ws, libelles: dict, avec_qualite: bool) -> pd.DataFrame:216 """Lit un tableau SCHL : repère la ligne d'en-tête (Centre/Zone + « Oct. »), puis extrait pour chaque RMR217 les 10 valeurs (5 types de logement × 2 années) et, s'il y a lieu, les lettres de qualité (colonne suivante)."""218 rows = [list(r) for r in ws.iter_rows(values_only=True)]219 entete = next(r for r in rows if r and isinstance(r[0], str) and r[0] in ("Centre", "Zone"))220 cols = [i for i, c in enumerate(entete) if isinstance(c, str) and c.startswith("Oct.")]221 assert len(cols) == 10, f"en-tête inattendu : {entete}"222 recs = []223 for r in rows:224 if r and isinstance(r[0], str) and r[0] in libelles.values():225 rmr = next(k for k, v in libelles.items() if v == r[0])226 if any(x["rmr"] == rmr for x in recs): continue # la première ligne de chaque fichier est répétée227 vals = [_num(r[i]) for i in cols]228 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]229 rec = {"rmr": rmr}230 for j, ch in enumerate(CHAMBRES):231 rec[f"{ch}|2024"] = vals[2 * j]; rec[f"{ch}|2025"] = vals[2 * j + 1]232 if avec_qualite: rec[f"{ch}|2025|q"] = quals[2 * j + 1]233 recs.append(rec)234 return pd.DataFrame(recs).set_index("rmr")235236237def schl_rmr(fichier: pathlib.Path = D_SCHL / "rmr-quebec-2025-fr.xlsx") -> dict:238 import openpyxl239 wb = openpyxl.load_workbook(fichier, read_only=True)240 t = {"inoccupation": _lire_tableau_schl(wb["Tableau 1.1.1"], RMR_SCHL, True),241 "loyer": _lire_tableau_schl(wb["Tableau 1.1.2"], RMR_SCHL, True),242 "univers": _lire_tableau_schl(wb["Tableau 1.1.3"], RMR_SCHL, False),243 "variation_estimative": _lire_tableau_schl(wb["Tableau 1.1.5"], RMR_SCHL, True)}244 t["n_onglets"] = len(wb.sheetnames); t["onglets"] = wb.sheetnames245 return t246247248def tableau_schl(t: dict) -> pd.DataFrame:249 """Tableau du chapitre : inoccupation 2024/2025, loyer 2 ch. et tous 2025, univers 2025, variation brute vs estimative."""250 rows = []251 for rmr in RMR_SCHL:252 inoc, loy, uni, var = t["inoccupation"].loc[rmr], t["loyer"].loc[rmr], t["univers"].loc[rmr], t["variation_estimative"].loc[rmr]253 brut_2ch = (loy["2 chambres|2025"] / loy["2 chambres|2024"] - 1) * 100254 brut_tous = (loy["Tous|2025"] / loy["Tous|2024"] - 1) * 100255 rows.append({"rmr": rmr, "inocc_2024": inoc["Tous|2024"], "inocc_2025": inoc["Tous|2025"], "inocc_q": inoc["Tous|2025|q"],256 "loyer_2ch_2024": loy["2 chambres|2024"], "loyer_2ch_2025": loy["2 chambres|2025"], "loyer_2ch_q": loy["2 chambres|2025|q"],257 "loyer_tous_2025": loy["Tous|2025"], "univers_2025": uni["Tous|2025"], "univers_2ch_2025": uni["2 chambres|2025"],258 "var_brute_2ch_pct": brut_2ch, "var_estim_2ch_pct": var["2 chambres|2025"],259 "var_brute_tous_pct": brut_tous, "var_estim_tous_pct": var["Tous|2025"],260 "logements_vacants_implicites": uni["Tous|2025"] * inoc["Tous|2025"] / 100})261 return pd.DataFrame(rows).set_index("rmr")262263264# ----------------------------------------------------------------------------------------------------------------------265# 6. Sensibilité : biais de datation (date de l'acte vs date de la promesse d'achat)266# ----------------------------------------------------------------------------------------------------------------------267def biais_datation(croissances_pct=(-3.0, 0.0, 3.0, 6.0, 10.0), delais_mois=(1, 2, 3, 6)) -> pd.DataFrame:268 """Biais relatif (en %) sur l'ajustement de conditions du marché si l'on date une vente à l'acte plutôt qu'à la promesse,269 pour une croissance annuelle composée g et un délai δ (mois) : b = (1+g)^(δ/12) − 1."""270 rows = []271 for g in croissances_pct:272 for d in delais_mois:273 rows.append({"croissance_annuelle_pct": g, "delai_mois": d, "biais_pct": ((1 + g / 100) ** (d / 12) - 1) * 100})274 return pd.DataFrame(rows)275276277def prix_corrige(prix_acte: float, meubles: float, g_annuel: float, delai_mois: float) -> dict:278 """Exemple : prix déclaré à l'acte incluant des meubles, daté à l'acte ; on retranche les meubles et on recule la date.279 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,280 mais l'ajustement temporel vers la date d'évaluation serait sous-estimé du facteur (1+g)^(δ/12)."""281 p_imm = prix_acte - meubles282 facteur = (1 + g_annuel) ** (delai_mois / 12)283 return {"prix_immeuble": p_imm, "facteur_datation": facteur, "ecart_dollars": p_imm * (facteur - 1)}284285286# ----------------------------------------------------------------------------------------------------------------------287# 7. Exercices (chiffres des solutions)288# ----------------------------------------------------------------------------------------------------------------------289def exercice_registre(n_actes=25, n_index=25, n_etats=3, tarif_consultation=1.0, etat_base=16.0, etat_requisition=16.0) -> dict:290 """Coût de consultation au Registre foncier (grille en vigueur à compter du 1er avril 2025) : 1 $ par document consulté ;291 état certifié 16 $ + 16 $ par réquisition (une réquisition par état ici)."""292 consult = (n_actes + n_index) * tarif_consultation293 etats = n_etats * (etat_base + etat_requisition)294 return {"consultation": consult, "etats": etats, "total": consult + etats}295296297def exercice_couverture(n_registre=1500, n_centris=1044, n_apparies=920, mu_courtier=510000.0, mu_autres=455000.0) -> dict:298 """Chiffres FICTIFS d'exercice : couverture du courtage, taux d'appariement, biais de composition."""299 couverture = n_centris / n_registre300 appariement = n_apparies / n_centris301 w = n_centris / n_registre302 mu_marche = w * mu_courtier + (1 - w) * mu_autres303 return {"couverture": couverture, "appariement": appariement, "poids_courtier": w, "moyenne_marche": mu_marche,304 "biais_courtier_dollars": mu_courtier - mu_marche, "biais_courtier_pct": (mu_courtier / mu_marche - 1) * 100}305306307def exercice_ipcb_quebec(df: pd.DataFrame | None = None) -> dict:308 """Exercice 5 : RMR de Québec vs Montréal, bâtiments résidentiels, ratio T3 2019 → T3 2026 et composite 15 RMR."""309 df = charger_ipcb() if df is None else df310 out = {}311 for nom, geo in (("Montréal", GEO_MTL), ("Québec", GEO_QC), ("Composite", "Composite de quinze régions métropolitaines de recensement")):312 s = serie_ipcb(df, geo); v = dict(zip(s["trimestre"], s["indice"]))313 out[nom] = {"debut": s["trimestre"].iloc[0], "2019T3": v.get("2019T3"), "2025T3": v.get("2025T3"), "2026T3": v.get("2026T3"),314 "ratio_2019T3_2026T3": v["2026T3"] / v["2019T3"], "var_annuelle_pct": (v["2026T3"] / v["2025T3"] - 1) * 100}315 return out316317318# ----------------------------------------------------------------------------------------------------------------------319def main() -> None:320 pd.set_option("display.width", 160); pd.set_option("display.max_columns", 30)321 print("=== 1. Statistique Canada, tableau 18-10-0289-01 (IPCB) — structure du CSV brut ===")322 ipcb = charger_ipcb(); st = structure_ipcb(ipcb)323 for k in ("lignes", "colonnes", "n_geo", "n_types", "n_divisions", "n_vecteurs", "periode_min", "periode_max", "n_periodes", "unite", "valeurs_manquantes", "produit_theorique"):324 print(f" {k:22s}: {st[k]}")325 print(" colonnes :", st["noms_colonnes"])326 print(" débuts de série à Montréal (agrégat des divisions) :")327 for k, v in st["debuts_montreal"].items(): print(f" {v} {k}")328 r = resume_ipcb(ipcb)329 for nom, d in r.items():330 print(f" {nom} résidentiel : {d['debut']} → {d['fin']} ({d['n']} obs.) ; T1 2017 = {d['2017T1']:.1f} ; T3 2025 = {d['2025T3']:.1f} ; "331 f"T3 2026 = {d['2026T3']:.1f} ; var. annuelle {d['var_annuelle_pct']:.1f} % ; facteur 2017T1→2026T3 {d['facteur_2017T1_2026T3']:.3f}")332333 print("\n=== 2. Trois indices rebasés T1 2017 = 100 (IPC Québec, IPLN Montréal, IPCB Montréal résidentiel) ===")334 t = indices_compares(ipcb); ri = resume_indices(t)335 print(t[["trimestre", "ipcb_mtl_resid", "ipc_quebec", "ipln_mtl", "ipcb_mtl_resid_base2017", "ipc_quebec_base2017", "ipln_mtl_base2017"]]336 .iloc[[0, 12, 20, 24, 28, 32, 36, 38]].to_string(index=False, float_format=lambda x: f"{x:.1f}"))337 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} ; "338 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")339340 print("\n=== 3. Dictionnaire du rôle en format ouvert (MAMH, répertoire v2.5) ===")341 dico = dictionnaire_role()342 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']}")343 for s, n in dico["par_section"].items(): print(f" section {s} — {dico['noms_sections'][s]} : {n} renseignements")344 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 "")345 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}")346347 print("\n=== 4. Richesse foncière uniformisée 2026 (MAMH, CC BY 4.0) ===")348 rf = rfu_2026()349 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")350 for v, d in rf["villes"].items():351 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$")352 for p, pct in d["parts_pct"].items(): print(f" {p:40s} {d['postes'][p]/1e9:8.2f} G$ {pct:5.1f} %")353354 print("\n=== 5. SCHL, Enquête sur les logements locatifs, octobre 2025 (six RMR, fichier provincial) ===")355 sc = schl_rmr(); ts = tableau_schl(sc)356 print(f" {sc['n_onglets']} onglets dans le classeur")357 print(ts.to_string(float_format=lambda x: f"{x:.1f}"))358359 print("\n=== 6. Sensibilité : biais de datation (acte vs promesse) ===")360 bd = biais_datation()361 print(bd.pivot(index="croissance_annuelle_pct", columns="delai_mois", values="biais_pct").to_string(float_format=lambda x: f"{x:.2f}"))362 ex = prix_corrige(612000, 12000, 0.06, 75 / 30.4375)363 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} $")364365 print("\n=== 7. Exercices ===")366 print(" registre :", exercice_registre())367 print(" couverture (fictif) :", {k: round(v, 4) for k, v in exercice_couverture().items()})368 for nom, d in exercice_ipcb_quebec(ipcb).items():369 print(f" {nom:9s} début {d['debut']} ; T3 2019 {d['2019T3']} ; T3 2026 {d['2026T3']} ; ratio {d['ratio_2019T3_2026T3']:.3f} ; var. annuelle {d['var_annuelle_pct']:.1f} %")370371372if __name__ == "__main__":373 main()374