"""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 ; MAMH RFU 2026 et répertoire du rôle ; SCHL ELL octobre 2025), identités et valeurs citées dans le texte et les solutions. Tolérances : valeurs d'indices exactes (lecture du CSV) ; pourcentages cités à 0,05 point ; montants des solutions à 1 $ ; identités à 1e-9.""" import sys, pathlib, math import numpy as np, pytest sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "cases")) import ch13_sources as cas @pytest.fixture(scope="module") def ipcb(): return cas.charger_ipcb() @pytest.fixture(scope="module") def schl(): t = cas.schl_rmr() return t, cas.tableau_schl(t) class TestIPCB: def test_structure(self, ipcb): st = cas.structure_ipcb(ipcb) assert st["lignes"] == 318954 and st["colonnes"] == 16 assert st["n_geo"] == 25 and st["n_types"] == 16 and st["n_divisions"] == 24 assert st["n_vecteurs"] == 8750 and st["produit_theorique"] == 9600 assert st["periode_min"] == "1981-01" and st["periode_max"] == "2026-07" and st["n_periodes"] == 183 assert st["unite"] == "Indice, 2023=100" and st["valeurs_manquantes"] == 0 assert st["debuts_montreal"]["Bâtiments résidentiels [621]"] == "2017-01" assert st["debuts_montreal"]["Tour d'habitations (cinq étages ou plus) [621111]"] == "1988-01" assert st["debuts_montreal"]["Bâtiments non résidentiels [622]"] == "1981-01" def test_series_citees(self, ipcb): r = cas.resume_ipcb(ipcb) m, q = r["Montréal"], r["Québec"] assert m["debut"] == "2017T1" and m["fin"] == "2026T3" and m["n"] == 39 assert m["2017T1"] == 65.5 and m["2025T3"] == 108.5 and m["2026T3"] == 117.8 and m["2023T1"] == 99.9 assert m["var_annuelle_pct"] == pytest.approx(8.57, abs=0.05) assert m["facteur_2017T1_2026T3"] == pytest.approx(1.798, abs=5e-4) assert m["facteur_2023T1_2026T3"] == pytest.approx(1.179, abs=5e-4) assert q["debut"] == "2023T1" and q["n"] == 15 and q["2023T1"] == 98.8 and q["2026T3"] == 124.8 and q["2017T1"] is None assert q["var_annuelle_pct"] == pytest.approx(9.19, abs=0.05) assert q["facteur_2023T1_2026T3"] == pytest.approx(1.263, abs=5e-4) def test_vecteur_montreal(self, ipcb): s = cas.serie_ipcb(ipcb, cas.GEO_MTL) assert s["VECTEUR"].iloc[0] == "v1617911460" assert cas.serie_ipcb(ipcb, cas.GEO_QC)["VECTEUR"].iloc[0] == "v1617911076" class TestIndices: def test_rebasage(self, ipcb): t = cas.indices_compares(ipcb, ecrire=False); r = cas.resume_indices(t) assert r["n_trimestres"] == 39 and t["trimestre"].iloc[0] == "2017T1" and t["trimestre"].iloc[-1] == "2026T3" for c in ("ipcb_mtl_resid", "ipc_quebec", "ipln_mtl"): assert t.loc[0, c + "_base2017"] == pytest.approx(100.0, abs=1e-9) assert r["ipcb_2026T3_base2017"] == pytest.approx(179.8, abs=0.05) assert r["ipln_2026T3_base2017"] == pytest.approx(159.0, abs=0.05) assert r["ipc_2026T3_base2017"] == pytest.approx(132.2, abs=0.05) # invariance du ratio d'indexation au rebasage (eq. 13.11) 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) 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) def test_fichier_processed(self, ipcb): t = cas.indices_compares(ipcb, ecrire=True) assert (cas.PROCESSED / "ch13_indices.csv").exists() and len(t) == 39 class TestDictionnaireEtRoles: def test_dictionnaire(self): d = cas.dictionnaire_role() assert d["n_pages"] == 2 and d["n_codes_uniques"] == 62 and d["n_repetitifs"] == 24 assert d["par_section"] == {1: 21, 2: 2, 3: 17, 4: 5, 5: 17} assert all(d["champs_cles_presents"].values()) assert d["date_document"] == "24 mars 2022" def test_index_roles(self): ix = cas.index_roles() assert ix["n_municipalites"] == 1134 and ix["colonnes"] == ["code géographique", "nom du territoire", "lien"] assert ix["gatineau"][-1].endswith("RL81017_2026.xml") and ix["montreal"][-1].endswith("RL66023_2026.xml") and ix["quebec"][-1].endswith("RL23027_2026.xml") class TestRFU: def test_structure_et_villes(self): rf = cas.rfu_2026() assert rf["n_lignes"] == 1132 and rf["n_colonnes"] == 37 and rf["types_org"] == {"Municipalité locale": 1132} and rf["n_postes_decrits"] == 24 v = rf["villes"] assert v["Montréal"]["imposable_uniformise"] == pytest.approx(430.75e9, abs=0.005e9) assert v["Québec"]["imposable_uniformise"] == pytest.approx(105.22e9, abs=0.005e9) assert v["Gatineau"]["imposable_uniformise"] == pytest.approx(60.01e9, abs=0.005e9) assert v["Montréal"]["facteur_redressement"] == 1.00 and v["Québec"]["facteur_redressement"] == 1.08 and v["Gatineau"]["facteur_redressement"] == 1.02 for d in v.values(): # identité : somme des six postes = évaluation imposable uniformisée assert abs(d["ecart_somme_vs_imposable"]) < 1.0 assert sum(d["parts_pct"].values()) == pytest.approx(100.0, abs=1e-6) assert v["Gatineau"]["parts_pct"]["Résidences d'un logement"] == pytest.approx(56.5, abs=0.05) assert v["Montréal"]["parts_pct"]["Immeubles à logements et condominiums"] == pytest.approx(60.6, abs=0.05) assert v["Québec"]["parts_pct"]["Résidences d'un logement"] == pytest.approx(40.2, abs=0.05) assert v["Québec"]["parts_pct"]["Immeubles à logements et condominiums"] == pytest.approx(38.8, abs=0.05) 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) class TestSCHL: def test_tableau(self, schl): t, ts = schl assert t["n_onglets"] == 20 attendu = { # inocc 2024, inocc 2025, loyer 2 ch. 2024, 2025, univers 2025, var. estimative 2 ch. "Montréal": (2.1, 2.9, 1176, 1346, 667266, 7.2), "Québec": (0.9, 2.4, 1159, 1277, 118693, 6.1), "Gatineau": (1.9, 3.8, 1353, 1460, 33553, 4.7), "Sherbrooke": (1.4, 2.7, 1002, 1062, 41949, 9.8), "Trois-Rivières": (0.9, 2.7, 885, 1016, 22505, 6.6), "Saguenay": (1.6, 1.3, 882, 985, 16241, 10.9)} for rmr, (i24, i25, l24, l25, u, ve) in attendu.items(): r = ts.loc[rmr] assert r["inocc_2024"] == i24 and r["inocc_2025"] == i25 and r["loyer_2ch_2024"] == l24 and r["loyer_2ch_2025"] == l25 assert r["univers_2025"] == u and r["var_estim_2ch_pct"] == ve assert r["var_brute_2ch_pct"] == pytest.approx((l25 / l24 - 1) * 100, rel=1e-12) assert r["logements_vacants_implicites"] == pytest.approx(u * i25 / 100, rel=1e-12) assert ts.loc["Montréal", "var_brute_2ch_pct"] == pytest.approx(14.5, abs=0.05) assert ts.loc["Montréal", "logements_vacants_implicites"] == pytest.approx(19351, abs=0.5) assert ts.loc["Saguenay", "logements_vacants_implicites"] == pytest.approx(211, abs=0.5) assert ts.loc["Sherbrooke", "var_brute_2ch_pct"] < ts.loc["Sherbrooke", "var_estim_2ch_pct"] # seule RMR où l'effet de composition est négatif assert ts.loc["Montréal", "inocc_q"] == "a" and ts.loc["Gatineau", "inocc_q"] == "c" and ts.loc["Trois-Rivières", "inocc_q"] == "b" assert ts.loc["Montréal", "univers_2ch_2025"] == 362373 class TestDatation: def test_biais(self): bd = cas.biais_datation().set_index(["croissance_annuelle_pct", "delai_mois"])["biais_pct"] assert bd[(6.0, 3)] == pytest.approx(1.47, abs=0.005) and bd[(10.0, 3)] == pytest.approx(2.41, abs=0.005) assert bd[(10.0, 6)] == pytest.approx(4.88, abs=0.005) and bd[(-3.0, 6)] == pytest.approx(-1.51, abs=0.005) assert (bd.xs(0.0, level=0) == 0).all() # approximation du premier ordre : b ≈ g·δ/12 assert bd[(3.0, 1)] == pytest.approx(0.25, abs=0.01) def test_proposition_signe(self): for g in (-0.03, 0.03, 0.10): b = (1 + g) ** (-3 / 12) - 1 assert math.copysign(1, b) == -math.copysign(1, g) class TestExercices: def test_ex1_registre(self): r = cas.exercice_registre() assert r["consultation"] == 50 and r["etats"] == 96 and r["total"] == 146 def test_ex2_prix_corrige(self): ex = cas.prix_corrige(612000, 12000, 0.06, 75 / 30.4375) assert ex["prix_immeuble"] == 600000 and ex["facteur_datation"] == pytest.approx(1.0120, abs=5e-5) assert ex["ecart_dollars"] == pytest.approx(7222, abs=1) def test_ex4_indexation(self, schl): _, ts = schl brute = ts.loc["Montréal", "loyer_2ch_2025"] / ts.loc["Montréal", "loyer_2ch_2024"] assert 1300 * brute == pytest.approx(1487.93, abs=0.01) and 1300 * 1.072 == pytest.approx(1393.60, abs=0.01) ecart = 1300 * (brute - 1.072) assert ecart == pytest.approx(94.33, abs=0.01) and ecart * 12 == pytest.approx(1132, abs=0.5) assert ecart * 12 * 12 / 0.045 == pytest.approx(301840, abs=5) def test_ex5_ipcb_quebec(self, ipcb): e = cas.exercice_ipcb_quebec(ipcb) 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) 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) 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) 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) assert e["Québec"]["ratio_2019T3_2026T3"] is None and e["Composite"]["var_annuelle_pct"] == pytest.approx(2.67, abs=0.005) assert 350000 * e["Québec"]["ratio_2023T1_2026T3"] == pytest.approx(442105, abs=1) assert 350000 * e["Montréal"]["ratio_2023T1_2026T3"] == pytest.approx(412713, abs=1) assert 350000 * e["Composite"]["ratio_2023T1_2026T3"] == pytest.approx(398671, abs=1) assert e["Montréal"]["ratio_2023T1_2026T3"] / e["Québec"]["ratio_2023T1_2026T3"] - 1 == pytest.approx(-0.066, abs=5e-4) def test_ex6_couverture(self): c = cas.exercice_couverture() assert c["couverture"] == pytest.approx(0.696, abs=5e-4) and c["appariement"] == pytest.approx(0.881, abs=5e-4) assert c["moyenne_marche"] == pytest.approx(493280, abs=0.01) and c["biais_courtier_dollars"] == pytest.approx(16720, abs=0.01) assert c["biais_courtier_pct"] == pytest.approx(3.39, abs=0.005) # identité de composition (eq. 13.6) assert c["poids_courtier"] * 510000 + (1 - c["poids_courtier"]) * 455000 == pytest.approx(c["moyenne_marche"], rel=1e-12)