#!/usr/bin/env python3 """Chapitre 13 — Transactions, inscriptions, loyers et coûts : lecture et description des fichiers RÉELS de data/raw. Imprime tous les chiffres cités dans le chapitre 13 : * structure du tableau 18-10-0289-01 de Statistique Canada (IPCB) lue dans le CSV brut : dimensions, membres, périodes, vecteurs ; séries de Montréal et de la RMR de Québec (bâtiments résidentiels) ; * trois indices souvent confondus (IPC Québec 18-10-0004-01, IPLN Montréal 18-10-0205-01, IPCB Montréal résidentiel 18-10-0289-01) rebasés au T1 2017 = 100 ; * dictionnaire du rôle d'évaluation en format ouvert (MAMH, répertoire des renseignements prescrits v2.5, PDF) : sections et nombre de renseignements ; * richesse foncière uniformisée 2026 (MAMH, CC BY 4.0) : Montréal, Québec, Gatineau, ensemble des municipalités locales ; * 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 ; variation brute du loyer moyen vs variation estimative « même échantillon » ; * sensibilité : biais de datation (date de l'acte vs date de la promesse) selon la croissance du marché. Usage (racine du dépôt) : PYTHONPATH=analysis/python python3 analysis/python/cases/ch13_sources.py Aucune donnée synthétique n'est utilisée dans ce script ; toutes les valeurs proviennent de data/raw (voir data/raw/SOURCES.md).""" from __future__ import annotations import re, sys, pathlib import numpy as np, pandas as pd ROOT = pathlib.Path(__file__).resolve().parents[3] RAW = ROOT / "data/raw" PROCESSED = ROOT / "data/processed" F_IPCB = RAW / "statcan_18100289/18100289.csv" F_IPCB_META = RAW / "statcan_18100289/18100289_MetaData.csv" F_IPLN = RAW / "statcan_18100205/18100205.csv" F_IPC = RAW / "statcan_18100004/18100004.csv" F_RFU = RAW / "mamh_rfu_2026/RFU-2026.csv" F_RFU_DESC = RAW / "mamh_rfu_2026/RFU-2026-DescriptionPoste.csv" F_DICO = RAW / "mamh_roles_evaluation/repertoire-des-renseignements-prescrits-du-role-devaluation-fonciere-version-2.5.pdf" F_INDEX_ROLES = RAW / "mamh_roles_evaluation/indexRole2026.csv" D_SCHL = RAW / "schl_rml_2025" V_IPC_QUEBEC = "v41691783" # IPC, ensemble, Québec (2002 = 100) V_IPLN_MONTREAL = "v111955484" # IPLN, total maison et terrain, Montréal (déc. 2016 = 100) TYPE_RESID = "Bâtiments résidentiels [621]" GEO_MTL, GEO_QC = "Montréal, Québec", "Québec, Québec" RMR_SCHL = { # libellés du fichier provincial rmr-quebec-2025-fr.xlsx (centres de 10 000 habitants et plus) "Montréal": "Montréal (RMR)", "Québec": "Québec (RMR)", "Gatineau": "Ottawa-Gatineau (RMR) (partie QC)", "Sherbrooke": "Sherbrooke (RMR)", "Trois-Rivières": "Trois-Rivières (RMR)", "Saguenay": "Saguenay (RMR)", } CHAMBRES = ["Studio", "1 chambre", "2 chambres", "3 chambres +", "Tous"] # ---------------------------------------------------------------------------------------------------------------------- # 1. Statistique Canada — IPCB 18-10-0289-01 : structure et séries # ---------------------------------------------------------------------------------------------------------------------- def charger_ipcb() -> pd.DataFrame: return pd.read_csv(F_IPCB, sep=";", encoding="utf-8-sig") def structure_ipcb(df: pd.DataFrame | None = None) -> dict: """Décrit le tableau brut : lignes, colonnes, membres de chaque dimension, périodes, vecteurs, valeurs manquantes.""" df = charger_ipcb() if df is None else df per = sorted(df["PÉRIODE DE RÉFÉRENCE"].unique()) s = { "lignes": int(len(df)), "colonnes": int(df.shape[1]), "noms_colonnes": df.columns.tolist(), "n_geo": int(df["GÉO"].nunique()), "n_types": int(df["Type d'immeuble"].nunique()), "n_divisions": int(df["Division"].nunique()), "n_vecteurs": int(df["VECTEUR"].nunique()), "periode_min": per[0], "periode_max": per[-1], "n_periodes": len(per), "unite": df["UNITÉ DE MESURE"].iloc[0], "valeurs_manquantes": int(df["VALEUR"].isna().sum()), "produit_theorique": int(df["GÉO"].nunique() * df["Type d'immeuble"].nunique() * df["Division"].nunique()), } # débuts de série par type d'immeuble à Montréal (agrégat des divisions) mtl = df[(df["GÉO"] == GEO_MTL) & (df["Division"] == "Agrégat des divisions")] s["debuts_montreal"] = mtl.groupby("Type d'immeuble")["PÉRIODE DE RÉFÉRENCE"].min().to_dict() return s def serie_ipcb(df: pd.DataFrame, geo: str, type_imm: str = TYPE_RESID) -> pd.DataFrame: s = df[(df["GÉO"] == geo) & (df["Division"] == "Agrégat des divisions") & (df["Type d'immeuble"] == type_imm)] s = s[["PÉRIODE DE RÉFÉRENCE", "VECTEUR", "VALEUR"]].rename(columns={"PÉRIODE DE RÉFÉRENCE": "periode", "VALEUR": "indice"}) s = s.sort_values("periode").reset_index(drop=True) s["trimestre"] = s["periode"].map(_periode_vers_trimestre) return s def _periode_vers_trimestre(p: str) -> str: a, m = p.split("-") return f"{a}T{(int(m) - 1) // 3 + 1}" def resume_ipcb(df: pd.DataFrame | None = None) -> dict: """Valeurs citées : Montréal et Québec, bâtiments résidentiels, T1 2017, T3 2025, T3 2026, variations.""" df = charger_ipcb() if df is None else df out = {} for nom, geo in (("Montréal", GEO_MTL), ("Québec", GEO_QC)): s = serie_ipcb(df, geo) v = dict(zip(s["trimestre"], s["indice"])) out[nom] = { "vecteur": s["vecteur"].iloc[0] if "vecteur" in s else s["VECTEUR"].iloc[0], "debut": s["trimestre"].iloc[0], "fin": s["trimestre"].iloc[-1], "n": int(len(s)), "2017T1": v.get("2017T1"), "2023T1": v.get("2023T1"), "2025T3": v["2025T3"], "2026T3": v["2026T3"], "var_annuelle_pct": (v["2026T3"] / v["2025T3"] - 1) * 100, "facteur_2017T1_2026T3": (v["2026T3"] / v["2017T1"]) if "2017T1" in v else None, "facteur_2023T1_2026T3": v["2026T3"] / v["2023T1"], } return out # ---------------------------------------------------------------------------------------------------------------------- # 2. Trois indices rebasés : IPC Québec, IPLN Montréal, IPCB Montréal résidentiel (T1 2017 = 100) # ---------------------------------------------------------------------------------------------------------------------- def _mensuel_vers_trimestriel(s: pd.DataFrame) -> pd.DataFrame: """Moyenne trimestrielle simple d'une série mensuelle (colonnes periode AAAA-MM, valeur).""" s = s.copy(); s["trimestre"] = s["periode"].map(_periode_vers_trimestre) g = s.groupby("trimestre", as_index=False).agg(valeur=("valeur", "mean"), n=("valeur", "size")) return g[g["n"] == 3].drop(columns="n") # trimestres complets seulement def indices_compares(ipcb: pd.DataFrame | None = None, ecrire: bool = True) -> pd.DataFrame: """Tableau trimestriel T1 2017 → T3 2026 des trois indices rebasés (T1 2017 = 100) ; écrit data/processed/ch13_indices.csv.""" ipcb = charger_ipcb() if ipcb is None else ipcb ipc = pd.read_csv(F_IPC, sep=";", encoding="utf-8-sig", usecols=["PÉRIODE DE RÉFÉRENCE", "VECTEUR", "VALEUR"]) ipc = ipc[ipc["VECTEUR"] == V_IPC_QUEBEC].rename(columns={"PÉRIODE DE RÉFÉRENCE": "periode", "VALEUR": "valeur"}) ipln = pd.read_csv(F_IPLN, sep=";", encoding="utf-8-sig", usecols=["PÉRIODE DE RÉFÉRENCE", "VECTEUR", "VALEUR"]) ipln = ipln[ipln["VECTEUR"] == V_IPLN_MONTREAL].rename(columns={"PÉRIODE DE RÉFÉRENCE": "periode", "VALEUR": "valeur"}) b = serie_ipcb(ipcb, GEO_MTL)[["trimestre", "indice"]].rename(columns={"indice": "ipcb_mtl_resid"}) q_ipc = _mensuel_vers_trimestriel(ipc).rename(columns={"valeur": "ipc_quebec"}) q_ipln = _mensuel_vers_trimestriel(ipln).rename(columns={"valeur": "ipln_mtl"}) t = b.merge(q_ipc, on="trimestre").merge(q_ipln, on="trimestre") t = t[(t["trimestre"] >= "2017T1") & (t["trimestre"] <= "2026T3")].reset_index(drop=True) for c in ("ipcb_mtl_resid", "ipc_quebec", "ipln_mtl"): t[c + "_base2017"] = t[c] / t.loc[t["trimestre"] == "2017T1", c].iloc[0] * 100 if ecrire: PROCESSED.mkdir(parents=True, exist_ok=True) t.to_csv(PROCESSED / "ch13_indices.csv", index=False) return t def resume_indices(t: pd.DataFrame) -> dict: fin = t[t["trimestre"] == "2026T3"].iloc[0] return { "ipcb_2026T3_base2017": fin["ipcb_mtl_resid_base2017"], "ipln_2026T3_base2017": fin["ipln_mtl_base2017"], "ipc_2026T3_base2017": fin["ipc_quebec_base2017"], "ipc_quebec_2017T1_moy": t.loc[0, "ipc_quebec"], "ipc_quebec_2026T3_moy": fin["ipc_quebec"], "ipln_mtl_2017T1_moy": t.loc[0, "ipln_mtl"], "ipln_mtl_2026T3_moy": fin["ipln_mtl"], "n_trimestres": int(len(t)), } # ---------------------------------------------------------------------------------------------------------------------- # 3. Dictionnaire du rôle (MAMH, répertoire v2.5) : sections et renseignements # ---------------------------------------------------------------------------------------------------------------------- def dictionnaire_role() -> dict: """Extrait le texte du PDF (pypdf) et dénombre les codes de renseignements par section.""" from pypdf import PdfReader texte = "\n".join((p.extract_text() or "") for p in PdfReader(str(F_DICO)).pages) codes = re.findall(r"RL(\d{2})(\d{2})([A-Z])(x?)(?![A-Za-z0-9])", texte) uniques = sorted({f"RL{a}{b}{c}{d}" for a, b, c, d in codes}) par_section = {} for a, b, c, d in {(a, b, c, d) for a, b, c, d in codes}: par_section[int(a)] = par_section.get(int(a), 0) + 1 repet = sorted({f"RL{a}{b}{c}x" for a, b, c, d in codes if d == "x"}) noms_sections = {1: "Identification de l'unité d'évaluation", 2: "Propriétaire", 3: "Caractéristiques de l'unité d'évaluation", 4: "Valeurs au rôle d'évaluation", 5: "Répartition fiscale"} champs_cles = { "RL0101Gx": "Nom de la voie publique", "RL0104A": "Division (matricule)", "RL0105A": "Code de l'utilisation prédominante", "RL0107A": "Numéro d'unité de voisinage", "RL0302A": "Superficie du terrain", "RL0307A": "Année de construction originelle", "RL0307B": "Année réelle ou estimée", "RL0308A": "Aire d'étages du bâtiment principal", "RL0311A": "Nombre total de logements", "RL0401A": "Date des conditions du marché", "RL0402A": "Valeur du terrain", "RL0403A": "Valeur du ou des bâtiments", "RL0404A": "Valeur de l'immeuble", "RL0405A": "Valeur au rôle antérieur", "RL0501A": "Terrain vague desservi", } presents = {k: (k in uniques) for k in champs_cles} return {"n_pages": len(PdfReader(str(F_DICO)).pages), "n_codes_uniques": len(uniques), "par_section": dict(sorted(par_section.items())), "noms_sections": noms_sections, "codes_repetitifs": repet, "n_repetitifs": len(repet), "champs_cles_presents": presents, "date_document": "24 mars 2022" if "24 mars 2022" in texte else None} def index_roles() -> dict: idx = pd.read_csv(F_INDEX_ROLES, dtype=str) return {"n_municipalites": int(len(idx)), "colonnes": idx.columns.tolist(), "gatineau": idx[idx.iloc[:, 0].str.lstrip("0") == "81017"].iloc[0].tolist() if (idx.iloc[:, 0].str.lstrip("0") == "81017").any() else None, "montreal": idx[idx.iloc[:, 0].str.lstrip("0") == "66023"].iloc[0].tolist() if (idx.iloc[:, 0].str.lstrip("0") == "66023").any() else None, "quebec": idx[idx.iloc[:, 0].str.lstrip("0") == "23027"].iloc[0].tolist() if (idx.iloc[:, 0].str.lstrip("0") == "23027").any() else None} # ---------------------------------------------------------------------------------------------------------------------- # 4. Richesse foncière uniformisée 2026 (MAMH) # ---------------------------------------------------------------------------------------------------------------------- POSTES_RFU = {"CIALX02141": "Résidences d'un logement", "CIALX02142": "Immeubles à logements et condominiums", "CIALX02143": "Autres immeubles résidentiels", "CIALX02144": "Industriels et commerciaux", "CIALX02145": "Agricoles", "CIALX02146": "Autres immeubles"} VILLES = {"66023": "Montréal", "23027": "Québec", "81017": "Gatineau"} def rfu_2026() -> dict: df = pd.read_csv(F_RFU, dtype={"cod_geo": str}) desc = pd.read_csv(F_RFU_DESC) out = {"n_lignes": int(len(df)), "n_colonnes": int(df.shape[1]), "types_org": df["type_org"].value_counts().to_dict(), "n_postes_decrits": int(desc["Code de postes"].nunique()), "villes": {}, "postes": POSTES_RFU} for code, nom in VILLES.items(): r = df[df["cod_geo"] == code].iloc[0] imposable = float(r["CIALX02147"]); rfu = float(r["CIALX02140"]) parts = {POSTES_RFU[k]: float(r[k]) for k in POSTES_RFU} out["villes"][nom] = {"population": int(r["population"]), "rfu": rfu, "imposable_uniformise": imposable, "facteur_redressement": float(r["CSALX02163"]), "postes": parts, "parts_pct": {k: v / imposable * 100 for k, v in parts.items()}, "somme_postes": sum(parts.values()), "ecart_somme_vs_imposable": sum(parts.values()) - imposable} out["total_rfu"] = float(df["CIALX02140"].sum()); out["total_imposable"] = float(df["CIALX02147"].sum()) out["part_3_villes_rfu_pct"] = sum(v["rfu"] for v in out["villes"].values()) / out["total_rfu"] * 100 return out # ---------------------------------------------------------------------------------------------------------------------- # 5. SCHL — Enquête sur les logements locatifs, octobre 2025 (fichier provincial, centres de 10 000 habitants et plus) # ---------------------------------------------------------------------------------------------------------------------- def _num(x): if x is None: return np.nan s = str(x).replace(" ", "").replace(" ", "").replace(",", ".") if s in ("**", "++", "-", "–", ""): return np.nan try: return float(s) except ValueError: return np.nan def _lire_tableau_schl(ws, libelles: dict, avec_qualite: bool) -> pd.DataFrame: """Lit un tableau SCHL : repère la ligne d'en-tête (Centre/Zone + « Oct. »), puis extrait pour chaque RMR les 10 valeurs (5 types de logement × 2 années) et, s'il y a lieu, les lettres de qualité (colonne suivante).""" rows = [list(r) for r in ws.iter_rows(values_only=True)] entete = next(r for r in rows if r and isinstance(r[0], str) and r[0] in ("Centre", "Zone")) cols = [i for i, c in enumerate(entete) if isinstance(c, str) and c.startswith("Oct.")] assert len(cols) == 10, f"en-tête inattendu : {entete}" recs = [] for r in rows: if r and isinstance(r[0], str) and r[0] in libelles.values(): rmr = next(k for k, v in libelles.items() if v == r[0]) if any(x["rmr"] == rmr for x in recs): continue # la première ligne de chaque fichier est répétée vals = [_num(r[i]) for i in cols] 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] rec = {"rmr": rmr} for j, ch in enumerate(CHAMBRES): rec[f"{ch}|2024"] = vals[2 * j]; rec[f"{ch}|2025"] = vals[2 * j + 1] if avec_qualite: rec[f"{ch}|2025|q"] = quals[2 * j + 1] recs.append(rec) return pd.DataFrame(recs).set_index("rmr") def schl_rmr(fichier: pathlib.Path = D_SCHL / "rmr-quebec-2025-fr.xlsx") -> dict: import openpyxl wb = openpyxl.load_workbook(fichier, read_only=True) t = {"inoccupation": _lire_tableau_schl(wb["Tableau 1.1.1"], RMR_SCHL, True), "loyer": _lire_tableau_schl(wb["Tableau 1.1.2"], RMR_SCHL, True), "univers": _lire_tableau_schl(wb["Tableau 1.1.3"], RMR_SCHL, False), "variation_estimative": _lire_tableau_schl(wb["Tableau 1.1.5"], RMR_SCHL, True)} t["n_onglets"] = len(wb.sheetnames); t["onglets"] = wb.sheetnames return t def tableau_schl(t: dict) -> pd.DataFrame: """Tableau du chapitre : inoccupation 2024/2025, loyer 2 ch. et tous 2025, univers 2025, variation brute vs estimative.""" rows = [] for rmr in RMR_SCHL: inoc, loy, uni, var = t["inoccupation"].loc[rmr], t["loyer"].loc[rmr], t["univers"].loc[rmr], t["variation_estimative"].loc[rmr] brut_2ch = (loy["2 chambres|2025"] / loy["2 chambres|2024"] - 1) * 100 brut_tous = (loy["Tous|2025"] / loy["Tous|2024"] - 1) * 100 rows.append({"rmr": rmr, "inocc_2024": inoc["Tous|2024"], "inocc_2025": inoc["Tous|2025"], "inocc_q": inoc["Tous|2025|q"], "loyer_2ch_2024": loy["2 chambres|2024"], "loyer_2ch_2025": loy["2 chambres|2025"], "loyer_2ch_q": loy["2 chambres|2025|q"], "loyer_tous_2025": loy["Tous|2025"], "univers_2025": uni["Tous|2025"], "univers_2ch_2025": uni["2 chambres|2025"], "var_brute_2ch_pct": brut_2ch, "var_estim_2ch_pct": var["2 chambres|2025"], "var_brute_tous_pct": brut_tous, "var_estim_tous_pct": var["Tous|2025"], "logements_vacants_implicites": uni["Tous|2025"] * inoc["Tous|2025"] / 100}) return pd.DataFrame(rows).set_index("rmr") # ---------------------------------------------------------------------------------------------------------------------- # 6. Sensibilité : biais de datation (date de l'acte vs date de la promesse d'achat) # ---------------------------------------------------------------------------------------------------------------------- def biais_datation(croissances_pct=(-3.0, 0.0, 3.0, 6.0, 10.0), delais_mois=(1, 2, 3, 6)) -> pd.DataFrame: """Biais relatif (en %) sur l'ajustement de conditions du marché si l'on date une vente à l'acte plutôt qu'à la promesse, pour une croissance annuelle composée g et un délai δ (mois) : b = (1+g)^(δ/12) − 1.""" rows = [] for g in croissances_pct: for d in delais_mois: rows.append({"croissance_annuelle_pct": g, "delai_mois": d, "biais_pct": ((1 + g / 100) ** (d / 12) - 1) * 100}) return pd.DataFrame(rows) def prix_corrige(prix_acte: float, meubles: float, g_annuel: float, delai_mois: float) -> dict: """Exemple : prix déclaré à l'acte incluant des meubles, daté à l'acte ; on retranche les meubles et on recule la date. 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, mais l'ajustement temporel vers la date d'évaluation serait sous-estimé du facteur (1+g)^(δ/12).""" p_imm = prix_acte - meubles facteur = (1 + g_annuel) ** (delai_mois / 12) return {"prix_immeuble": p_imm, "facteur_datation": facteur, "ecart_dollars": p_imm * (facteur - 1)} # ---------------------------------------------------------------------------------------------------------------------- # 7. Exercices (chiffres des solutions) # ---------------------------------------------------------------------------------------------------------------------- def exercice_registre(n_actes=25, n_index=25, n_etats=3, tarif_consultation=1.0, etat_base=16.0, etat_requisition=16.0) -> dict: """Coût de consultation au Registre foncier (grille en vigueur à compter du 1er avril 2025) : 1 $ par document consulté ; état certifié 16 $ + 16 $ par réquisition (une réquisition par état ici).""" consult = (n_actes + n_index) * tarif_consultation etats = n_etats * (etat_base + etat_requisition) return {"consultation": consult, "etats": etats, "total": consult + etats} def exercice_couverture(n_registre=1500, n_centris=1044, n_apparies=920, mu_courtier=510000.0, mu_autres=455000.0) -> dict: """Chiffres FICTIFS d'exercice : couverture du courtage, taux d'appariement, biais de composition.""" couverture = n_centris / n_registre appariement = n_apparies / n_centris w = n_centris / n_registre mu_marche = w * mu_courtier + (1 - w) * mu_autres return {"couverture": couverture, "appariement": appariement, "poids_courtier": w, "moyenne_marche": mu_marche, "biais_courtier_dollars": mu_courtier - mu_marche, "biais_courtier_pct": (mu_courtier / mu_marche - 1) * 100} def exercice_ipcb_quebec(df: pd.DataFrame | None = None) -> dict: """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, d'où la comparaison des facteurs T1 2023 → T3 2026 (et, pour Montréal et le composite, T3 2019 → T3 2026).""" df = charger_ipcb() if df is None else df out = {} for nom, geo in (("Montréal", GEO_MTL), ("Québec", GEO_QC), ("Composite", "Composite de quinze régions métropolitaines de recensement")): s = serie_ipcb(df, geo); v = dict(zip(s["trimestre"], s["indice"])) out[nom] = {"debut": s["trimestre"].iloc[0], "n": int(len(s)), "2019T3": v.get("2019T3"), "2023T1": v.get("2023T1"), "2025T3": v.get("2025T3"), "2026T3": v.get("2026T3"), "ratio_2019T3_2026T3": (v["2026T3"] / v["2019T3"]) if "2019T3" in v else None, "ratio_2023T1_2026T3": v["2026T3"] / v["2023T1"], "var_annuelle_pct": (v["2026T3"] / v["2025T3"] - 1) * 100, "tcac_2023T1_2026T3_pct": ((v["2026T3"] / v["2023T1"]) ** (4 / 14) - 1) * 100} return out # ---------------------------------------------------------------------------------------------------------------------- def main() -> None: pd.set_option("display.width", 160); pd.set_option("display.max_columns", 30) print("=== 1. Statistique Canada, tableau 18-10-0289-01 (IPCB) — structure du CSV brut ===") ipcb = charger_ipcb(); st = structure_ipcb(ipcb) for k in ("lignes", "colonnes", "n_geo", "n_types", "n_divisions", "n_vecteurs", "periode_min", "periode_max", "n_periodes", "unite", "valeurs_manquantes", "produit_theorique"): print(f" {k:22s}: {st[k]}") print(" colonnes :", st["noms_colonnes"]) print(" débuts de série à Montréal (agrégat des divisions) :") for k, v in st["debuts_montreal"].items(): print(f" {v} {k}") r = resume_ipcb(ipcb) for nom, d in r.items(): 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} ; " 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}") print("\n=== 2. Trois indices rebasés T1 2017 = 100 (IPC Québec, IPLN Montréal, IPCB Montréal résidentiel) ===") t = indices_compares(ipcb); ri = resume_indices(t) print(t[["trimestre", "ipcb_mtl_resid", "ipc_quebec", "ipln_mtl", "ipcb_mtl_resid_base2017", "ipc_quebec_base2017", "ipln_mtl_base2017"]] .iloc[[0, 12, 20, 24, 28, 32, 36, 38]].to_string(index=False, float_format=lambda x: f"{x:.1f}")) 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} ; " 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") print("\n=== 3. Dictionnaire du rôle en format ouvert (MAMH, répertoire v2.5) ===") dico = dictionnaire_role() 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']}") for s, n in dico["par_section"].items(): print(f" section {s} — {dico['noms_sections'][s]} : {n} renseignements") 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 "") 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}") print("\n=== 4. Richesse foncière uniformisée 2026 (MAMH, CC BY 4.0) ===") rf = rfu_2026() 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") for v, d in rf["villes"].items(): 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$") for p, pct in d["parts_pct"].items(): print(f" {p:40s} {d['postes'][p]/1e9:8.2f} G$ {pct:5.1f} %") print("\n=== 5. SCHL, Enquête sur les logements locatifs, octobre 2025 (six RMR, fichier provincial) ===") sc = schl_rmr(); ts = tableau_schl(sc) print(f" {sc['n_onglets']} onglets dans le classeur") print(ts.to_string(float_format=lambda x: f"{x:.1f}")) print("\n=== 6. Sensibilité : biais de datation (acte vs promesse) ===") bd = biais_datation() print(bd.pivot(index="croissance_annuelle_pct", columns="delai_mois", values="biais_pct").to_string(float_format=lambda x: f"{x:.2f}")) ex = prix_corrige(612000, 12000, 0.06, 75 / 30.4375) 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} $") print("\n=== 7. Exercices ===") print(" registre :", exercice_registre()) print(" couverture (fictif) :", {k: round(v, 4) for k, v in exercice_couverture().items()}) for nom, d in exercice_ipcb_quebec(ipcb).items(): 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']} ; " f"ratio 2023T1→2026T3 {d['ratio_2023T1_2026T3']:.4f} (TCAC {d['tcac_2023T1_2026T3_pct']:.2f} %) ; var. annuelle {d['var_annuelle_pct']:.2f} %") if __name__ == "__main__": main()