SPB Git
25.8 KB · 381 lines python
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
Files · 9ef9ac068989c8a15aa67882d01b89f16419100e
Menu

Theme