#!/usr/bin/env python3 """Chapitre 10 — statistiques descriptives de la matrice des dispositions normatives du domaine C (sources/normes-C.csv : 100 dispositions vérifiées le 2026-10-31) et chronologie documentaire des cadres normatifs (dates tirées de sources/registry-C.csv). Imprime les chiffres cités au chapitre 10. Usage (racine du dépôt) : PYTHONPATH=analysis/python python3 analysis/python/cases/ch10_matrice_normes.py""" from __future__ import annotations import sys, pathlib, datetime as dt import pandas as pd sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1])) ROOT = pathlib.Path(__file__).resolve().parents[3] MATRICE = ROOT / "sources/normes-C.csv" # Regroupement des 17 domaines de la matrice en six familles (choix de présentation de l'auteur) FAMILLES = { "OEAQ-NPP": "OEAQ — normes de pratique", "OEAQ-Déontologie": "OEAQ — déontologie", "OEAQ-Règlements": "OEAQ — règlements", "Code des professions": "OEAQ — règlements", "IVS": "Cadres hors Québec", "CUSPAP": "Cadres hors Québec", "USPAP": "Cadres hors Québec", "RICS": "Cadres hors Québec", "IAAO": "Cadres hors Québec", "Banque du Canada": "Finance et prudentiel", "BSIF": "Finance et prudentiel", "Finances Canada": "Finance et prudentiel", "SCHL": "Finance et prudentiel", "Statistique Canada": "Données de marché", "APCIQ": "Données de marché", "JLR": "Données de marché", "Teranet–BNC": "Données de marché", } # Chronologie documentaire (source_id du registre C ; dates vérifiées le 2026-10-31). # publication : date de publication connue (None si non documentée) ; effet : date d'entrée en vigueur ; # fin : fin de validité connue (None = en vigueur au 2026-10-31) ; statut : en_vigueur | remplace | projet | reference CADRES = [ dict(cadre="IAAO — Standard on AVMs (rév. 2018)", source="SRC-C-026", publication=None, effet=dt.date(2018, 7, 1), fin=None, statut="reference", precision="mois"), dict(cadre="IAAO — Standard on Ratio Studies (2013)", source="SRC-C-024", publication=None, effet=dt.date(2013, 4, 1), fin=None, statut="reference", precision="mois"), dict(cadre="IAAO — Standard on Mass Appraisal (rév. 2025)", source="SRC-C-025", publication=None, effet=dt.date(2025, 1, 1), fin=None, statut="reference", precision="annee"), dict(cadre="USPAP 2024 (États-Unis)", source="SRC-C-021", publication=None, effet=dt.date(2024, 1, 1), fin=None, statut="en_vigueur", precision="jour", verification="partielle"), dict(cadre="CUSPAP 2024 (Canada)", source="SRC-C-020", publication=None, effet=dt.date(2024, 1, 1), fin=dt.date(2026, 3, 31), statut="remplace", precision="jour"), dict(cadre="NPP de l'OEAQ (Québec)", source="SRC-C-001", publication=None, effet=dt.date(2024, 2, 1), fin=None, statut="en_vigueur", precision="jour"), dict(cadre="IVS 2025 (IVSC)", source="SRC-C-014", publication=dt.date(2024, 1, 31), effet=dt.date(2025, 1, 31), fin=None, statut="en_vigueur", precision="jour"), dict(cadre="RICS Red Book Global (2025)", source="SRC-C-023", publication=dt.date(2024, 12, 11), effet=dt.date(2025, 1, 31), fin=None, statut="en_vigueur", precision="jour"), dict(cadre="CUSPAP 2026 (Canada)", source="SRC-C-018", publication=None, effet=dt.date(2026, 4, 1), fin=None, statut="en_vigueur", precision="jour"), dict(cadre="IVS 2028 — projet (exposure draft)", source="SRC-C-015", publication=dt.date(2026, 1, 1), effet=dt.date(2028, 1, 31), fin=None, statut="projet", precision="mois"), ] DATE_REF = dt.date(2026, 10, 31) def charger() -> pd.DataFrame: m = pd.read_csv(MATRICE) m["famille"] = m["domaine"].map(FAMILLES) assert m["famille"].notna().all(), "domaine sans famille" m["n_chapitres"] = m["chapitres"].str.split(";").apply(len) m["ch10"] = m["chapitres"].str.contains("ch10") return m def statistiques(m: pd.DataFrame) -> dict: par_domaine = m["domaine"].value_counts() par_famille = m["famille"].value_counts() par_chapitre = m["chapitres"].str.split(";").explode().value_counts() oeaq = m["domaine"].str.startswith("OEAQ") | (m["domaine"] == "Code des professions") return { "n": len(m), "n_domaines": m["domaine"].nunique(), "n_chapitres_couverts": par_chapitre.size, "par_domaine": par_domaine, "par_famille": par_famille, "par_chapitre": par_chapitre, "n_oeaq": int(oeaq.sum()), "part_oeaq": float(oeaq.mean()), "n_ch10": int(m["ch10"].sum()), "part_ch10": float(m["ch10"].mean()), "ch10_par_domaine": m.loc[m["ch10"], "domaine"].value_counts(), "moy_chapitres_par_disposition": float(m["n_chapitres"].mean()), "max_chapitres_par_disposition": int(m["n_chapitres"].max()), "part_multi_chapitres": float((m["n_chapitres"] > 1).mean()), } def chronologie() -> pd.DataFrame: c = pd.DataFrame(CADRES) c["en_vigueur_2026_10_31"] = c.apply(lambda r: r["effet"] <= DATE_REF and (r["fin"] is None or r["fin"] >= DATE_REF) and r["statut"] != "projet", axis=1) c["delai_publication_effet_jours"] = c.apply(lambda r: (r["effet"] - r["publication"]).days if r["publication"] else None, axis=1) return c def main(verbose: bool = True) -> dict: m = charger(); st = statistiques(m); ch = chronologie() if verbose: print(f"== Matrice C : {st['n']} dispositions, {st['n_domaines']} domaines, {st['n_chapitres_couverts']} chapitres couverts") print("== Par famille :\n", st["par_famille"].to_string()) print("== Par domaine :\n", st["par_domaine"].to_string()) print(f"== Dispositions OEAQ + Code des professions : {st['n_oeaq']} ({st['part_oeaq']*100:.0f} %)") print(f"== Dispositions renvoyant au ch. 10 : {st['n_ch10']} ({st['part_ch10']*100:.0f} %) ; par domaine :\n", st["ch10_par_domaine"].to_string()) print("== Dix chapitres les plus cités :\n", st["par_chapitre"].head(10).to_string()) print(f"== Chapitres par disposition : moyenne {st['moy_chapitres_par_disposition']:.2f}, max {st['max_chapitres_par_disposition']}, " f"part multi-chapitres {st['part_multi_chapitres']*100:.0f} %") pd.set_option("display.width", 200) print("== Chronologie des cadres (registre C) :\n", ch[["cadre", "publication", "effet", "fin", "statut", "en_vigueur_2026_10_31", "delai_publication_effet_jours"]].to_string(index=False)) return {"matrice": m, "stats": st, "chronologie": ch} if __name__ == "__main__": main()