spb/traite-evaluation-qc
Public
1"""Tests du chapitre 13 — lecture des fichiers RÉELS de data/raw (StatCan 18-10-0289-01, 18-10-0205-01, 18-10-0004-01 ;2MAMH RFU 2026 et répertoire du rôle ; SCHL ELL octobre 2025), identités et valeurs citées dans le texte et les solutions.3Tolérances : valeurs d'indices exactes (lecture du CSV) ; pourcentages cités à 0,05 point ; montants des solutions à 1 $ ;4identités à 1e-9."""5import sys, pathlib, math6import numpy as np, pytest7sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "cases"))8import ch13_sources as cas91011@pytest.fixture(scope="module")12def ipcb():13 return cas.charger_ipcb()141516@pytest.fixture(scope="module")17def schl():18 t = cas.schl_rmr()19 return t, cas.tableau_schl(t)202122class TestIPCB:23 def test_structure(self, ipcb):24 st = cas.structure_ipcb(ipcb)25 assert st["lignes"] == 318954 and st["colonnes"] == 1626 assert st["n_geo"] == 25 and st["n_types"] == 16 and st["n_divisions"] == 2427 assert st["n_vecteurs"] == 8750 and st["produit_theorique"] == 960028 assert st["periode_min"] == "1981-01" and st["periode_max"] == "2026-07" and st["n_periodes"] == 18329 assert st["unite"] == "Indice, 2023=100" and st["valeurs_manquantes"] == 030 assert st["debuts_montreal"]["Bâtiments résidentiels [621]"] == "2017-01"31 assert st["debuts_montreal"]["Tour d'habitations (cinq étages ou plus) [621111]"] == "1988-01"32 assert st["debuts_montreal"]["Bâtiments non résidentiels [622]"] == "1981-01"3334 def test_series_citees(self, ipcb):35 r = cas.resume_ipcb(ipcb)36 m, q = r["Montréal"], r["Québec"]37 assert m["debut"] == "2017T1" and m["fin"] == "2026T3" and m["n"] == 3938 assert m["2017T1"] == 65.5 and m["2025T3"] == 108.5 and m["2026T3"] == 117.8 and m["2023T1"] == 99.939 assert m["var_annuelle_pct"] == pytest.approx(8.57, abs=0.05)40 assert m["facteur_2017T1_2026T3"] == pytest.approx(1.798, abs=5e-4)41 assert m["facteur_2023T1_2026T3"] == pytest.approx(1.179, abs=5e-4)42 assert q["debut"] == "2023T1" and q["n"] == 15 and q["2023T1"] == 98.8 and q["2026T3"] == 124.8 and q["2017T1"] is None43 assert q["var_annuelle_pct"] == pytest.approx(9.19, abs=0.05)44 assert q["facteur_2023T1_2026T3"] == pytest.approx(1.263, abs=5e-4)4546 def test_vecteur_montreal(self, ipcb):47 s = cas.serie_ipcb(ipcb, cas.GEO_MTL)48 assert s["VECTEUR"].iloc[0] == "v1617911460"49 assert cas.serie_ipcb(ipcb, cas.GEO_QC)["VECTEUR"].iloc[0] == "v1617911076"505152class TestIndices:53 def test_rebasage(self, ipcb):54 t = cas.indices_compares(ipcb, ecrire=False); r = cas.resume_indices(t)55 assert r["n_trimestres"] == 39 and t["trimestre"].iloc[0] == "2017T1" and t["trimestre"].iloc[-1] == "2026T3"56 for c in ("ipcb_mtl_resid", "ipc_quebec", "ipln_mtl"):57 assert t.loc[0, c + "_base2017"] == pytest.approx(100.0, abs=1e-9)58 assert r["ipcb_2026T3_base2017"] == pytest.approx(179.8, abs=0.05)59 assert r["ipln_2026T3_base2017"] == pytest.approx(159.0, abs=0.05)60 assert r["ipc_2026T3_base2017"] == pytest.approx(132.2, abs=0.05)61 # invariance du ratio d'indexation au rebasage (eq. 13.11)62 assert t["ipcb_mtl_resid_base2017"].iloc[-1] / t["ipcb_mtl_resid_base2017"].iloc[0] == pytest.approx(t["ipcb_mtl_resid"].iloc[-1] / t["ipcb_mtl_resid"].iloc[0], rel=1e-12)63 assert r["ipc_quebec_2026T3_moy"] == pytest.approx(167.2, abs=0.05) and r["ipln_mtl_2026T3_moy"] == pytest.approx(159.5, abs=0.05)6465 def test_fichier_processed(self, ipcb):66 t = cas.indices_compares(ipcb, ecrire=True)67 assert (cas.PROCESSED / "ch13_indices.csv").exists() and len(t) == 39686970class TestDictionnaireEtRoles:71 def test_dictionnaire(self):72 d = cas.dictionnaire_role()73 assert d["n_pages"] == 2 and d["n_codes_uniques"] == 62 and d["n_repetitifs"] == 2474 assert d["par_section"] == {1: 21, 2: 2, 3: 17, 4: 5, 5: 17}75 assert all(d["champs_cles_presents"].values())76 assert d["date_document"] == "24 mars 2022"7778 def test_index_roles(self):79 ix = cas.index_roles()80 assert ix["n_municipalites"] == 1134 and ix["colonnes"] == ["code géographique", "nom du territoire", "lien"]81 assert ix["gatineau"][-1].endswith("RL81017_2026.xml") and ix["montreal"][-1].endswith("RL66023_2026.xml") and ix["quebec"][-1].endswith("RL23027_2026.xml")828384class TestRFU:85 def test_structure_et_villes(self):86 rf = cas.rfu_2026()87 assert rf["n_lignes"] == 1132 and rf["n_colonnes"] == 37 and rf["types_org"] == {"Municipalité locale": 1132} and rf["n_postes_decrits"] == 2488 v = rf["villes"]89 assert v["Montréal"]["imposable_uniformise"] == pytest.approx(430.75e9, abs=0.005e9)90 assert v["Québec"]["imposable_uniformise"] == pytest.approx(105.22e9, abs=0.005e9)91 assert v["Gatineau"]["imposable_uniformise"] == pytest.approx(60.01e9, abs=0.005e9)92 assert v["Montréal"]["facteur_redressement"] == 1.00 and v["Québec"]["facteur_redressement"] == 1.08 and v["Gatineau"]["facteur_redressement"] == 1.0293 for d in v.values(): # identité : somme des six postes = évaluation imposable uniformisée94 assert abs(d["ecart_somme_vs_imposable"]) < 1.095 assert sum(d["parts_pct"].values()) == pytest.approx(100.0, abs=1e-6)96 assert v["Gatineau"]["parts_pct"]["Résidences d'un logement"] == pytest.approx(56.5, abs=0.05)97 assert v["Montréal"]["parts_pct"]["Immeubles à logements et condominiums"] == pytest.approx(60.6, abs=0.05)98 assert v["Québec"]["parts_pct"]["Résidences d'un logement"] == pytest.approx(40.2, abs=0.05)99 assert v["Québec"]["parts_pct"]["Immeubles à logements et condominiums"] == pytest.approx(38.8, abs=0.05)100 assert rf["total_rfu"] == pytest.approx(2077.9e9, abs=0.05e9) and rf["part_3_villes_rfu_pct"] == pytest.approx(31.1, abs=0.05)101102103class TestSCHL:104 def test_tableau(self, schl):105 t, ts = schl106 assert t["n_onglets"] == 20107 attendu = { # inocc 2024, inocc 2025, loyer 2 ch. 2024, 2025, univers 2025, var. estimative 2 ch.108 "Montréal": (2.1, 2.9, 1176, 1346, 667266, 7.2), "Québec": (0.9, 2.4, 1159, 1277, 118693, 6.1),109 "Gatineau": (1.9, 3.8, 1353, 1460, 33553, 4.7), "Sherbrooke": (1.4, 2.7, 1002, 1062, 41949, 9.8),110 "Trois-Rivières": (0.9, 2.7, 885, 1016, 22505, 6.6), "Saguenay": (1.6, 1.3, 882, 985, 16241, 10.9)}111 for rmr, (i24, i25, l24, l25, u, ve) in attendu.items():112 r = ts.loc[rmr]113 assert r["inocc_2024"] == i24 and r["inocc_2025"] == i25 and r["loyer_2ch_2024"] == l24 and r["loyer_2ch_2025"] == l25114 assert r["univers_2025"] == u and r["var_estim_2ch_pct"] == ve115 assert r["var_brute_2ch_pct"] == pytest.approx((l25 / l24 - 1) * 100, rel=1e-12)116 assert r["logements_vacants_implicites"] == pytest.approx(u * i25 / 100, rel=1e-12)117 assert ts.loc["Montréal", "var_brute_2ch_pct"] == pytest.approx(14.5, abs=0.05)118 assert ts.loc["Montréal", "logements_vacants_implicites"] == pytest.approx(19351, abs=0.5)119 assert ts.loc["Saguenay", "logements_vacants_implicites"] == pytest.approx(211, abs=0.5)120 assert ts.loc["Sherbrooke", "var_brute_2ch_pct"] < ts.loc["Sherbrooke", "var_estim_2ch_pct"] # seule RMR où l'effet de composition est négatif121 assert ts.loc["Montréal", "inocc_q"] == "a" and ts.loc["Gatineau", "inocc_q"] == "c" and ts.loc["Trois-Rivières", "inocc_q"] == "b"122 assert ts.loc["Montréal", "univers_2ch_2025"] == 362373123124125class TestDatation:126 def test_biais(self):127 bd = cas.biais_datation().set_index(["croissance_annuelle_pct", "delai_mois"])["biais_pct"]128 assert bd[(6.0, 3)] == pytest.approx(1.47, abs=0.005) and bd[(10.0, 3)] == pytest.approx(2.41, abs=0.005)129 assert bd[(10.0, 6)] == pytest.approx(4.88, abs=0.005) and bd[(-3.0, 6)] == pytest.approx(-1.51, abs=0.005)130 assert (bd.xs(0.0, level=0) == 0).all()131 # approximation du premier ordre : b ≈ g·δ/12132 assert bd[(3.0, 1)] == pytest.approx(0.25, abs=0.01)133134 def test_proposition_signe(self):135 for g in (-0.03, 0.03, 0.10):136 b = (1 + g) ** (-3 / 12) - 1137 assert math.copysign(1, b) == -math.copysign(1, g)138139140class TestExercices:141 def test_ex1_registre(self):142 r = cas.exercice_registre()143 assert r["consultation"] == 50 and r["etats"] == 96 and r["total"] == 146144145 def test_ex2_prix_corrige(self):146 ex = cas.prix_corrige(612000, 12000, 0.06, 75 / 30.4375)147 assert ex["prix_immeuble"] == 600000 and ex["facteur_datation"] == pytest.approx(1.0120, abs=5e-5)148 assert ex["ecart_dollars"] == pytest.approx(7222, abs=1)149150 def test_ex4_indexation(self, schl):151 _, ts = schl152 brute = ts.loc["Montréal", "loyer_2ch_2025"] / ts.loc["Montréal", "loyer_2ch_2024"]153 assert 1300 * brute == pytest.approx(1487.93, abs=0.01) and 1300 * 1.072 == pytest.approx(1393.60, abs=0.01)154 ecart = 1300 * (brute - 1.072)155 assert ecart == pytest.approx(94.33, abs=0.01) and ecart * 12 == pytest.approx(1132, abs=0.5)156 assert ecart * 12 * 12 / 0.045 == pytest.approx(301840, abs=5)157158 def test_ex5_ipcb_quebec(self, ipcb):159 e = cas.exercice_ipcb_quebec(ipcb)160 assert e["Montréal"]["ratio_2023T1_2026T3"] == pytest.approx(1.1792, abs=5e-5) and e["Montréal"]["tcac_2023T1_2026T3_pct"] == pytest.approx(4.82, abs=0.005)161 assert e["Québec"]["ratio_2023T1_2026T3"] == pytest.approx(1.2632, abs=5e-5) and e["Québec"]["tcac_2023T1_2026T3_pct"] == pytest.approx(6.90, abs=0.005)162 assert e["Composite"]["ratio_2023T1_2026T3"] == pytest.approx(1.1391, abs=5e-5) and e["Composite"]["tcac_2023T1_2026T3_pct"] == pytest.approx(3.79, abs=0.005)163 assert e["Montréal"]["ratio_2019T3_2026T3"] == pytest.approx(1.6453, abs=5e-5) and e["Composite"]["ratio_2019T3_2026T3"] == pytest.approx(1.7406, abs=5e-5)164 assert e["Québec"]["ratio_2019T3_2026T3"] is None and e["Composite"]["var_annuelle_pct"] == pytest.approx(2.67, abs=0.005)165 assert 350000 * e["Québec"]["ratio_2023T1_2026T3"] == pytest.approx(442105, abs=1)166 assert 350000 * e["Montréal"]["ratio_2023T1_2026T3"] == pytest.approx(412713, abs=1)167 assert 350000 * e["Composite"]["ratio_2023T1_2026T3"] == pytest.approx(398671, abs=1)168 assert e["Montréal"]["ratio_2023T1_2026T3"] / e["Québec"]["ratio_2023T1_2026T3"] - 1 == pytest.approx(-0.066, abs=5e-4)169170 def test_ex6_couverture(self):171 c = cas.exercice_couverture()172 assert c["couverture"] == pytest.approx(0.696, abs=5e-4) and c["appariement"] == pytest.approx(0.881, abs=5e-4)173 assert c["moyenne_marche"] == pytest.approx(493280, abs=0.01) and c["biais_courtier_dollars"] == pytest.approx(16720, abs=0.01)174 assert c["biais_courtier_pct"] == pytest.approx(3.39, abs=0.005)175 # identité de composition (eq. 13.6)176 assert c["poids_courtier"] * 510000 + (1 - c["poids_courtier"]) * 455000 == pytest.approx(c["moyenne_marche"], rel=1e-12)177