SPB Git
13.5 KB · 227 lines python
1"""Tests du chapitre 3 — Offre, demande et cycles.2Tolérances : identités algébriques 1e-9 (relatif) ; valeurs citées dans le texte : 0,5 unité sur les logements et les dollars arrondis,30,05 point de pourcentage sur les taux ; valeurs réelles lues dans data/raw : exactes (sommes d'entiers) ou 0,01."""4import sys, pathlib, math5import numpy as np, pytest6sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "cases"))7import ch03_cycles as cas8from dataclasses import replace91011@pytest.fixture(scope="module")12def p4q():13    return cas.Parametres4Q()141516@pytest.fixture(scope="module")17def pc():18    return cas.ParametresCycle()192021class TestQuatreQuadrants:22    def test_parametres_derives(self, p4q):23        assert p4q.b == pytest.approx(3.4722, abs=5e-4)24        assert p4q.a == pytest.approx(150_000, abs=1e-6)25        assert p4q.P0 == pytest.approx(288_000, abs=1e-6)26        assert p4q.C0 == pytest.approx(1_000, abs=1e-9)27        assert p4q.c1 == pytest.approx(48.0, abs=1e-9)2829    def test_equilibre_ferme_egal_iteratif(self, p4q):30        eq, it = cas.equilibre_4q(p4q), cas.equilibre_4q_iteratif(p4q)31        for k in ("S", "R", "P", "C"):32            assert eq[k] == pytest.approx(it[k], rel=1e-6)33        assert eq["S"] == pytest.approx(100_000, abs=1e-6) and eq["R"] == pytest.approx(14_400, abs=1e-6)34        assert eq["C"] == pytest.approx(p4q.delta * eq["S"], rel=1e-12)           # ΔS = 035        assert eq["P"] == pytest.approx(eq["R"] / p4q.i, rel=1e-12)                 # P = R/i36        assert eq["C"] == pytest.approx((eq["P"] - p4q.c0) / p4q.c1, rel=1e-9)     # C = g(P)3738    def test_baisse_du_taux(self, p4q):39        lt = cas.equilibre_4q(p4q, i=0.04); ct = cas.court_terme_4q(p4q, i=0.04)40        assert ct["P"] == pytest.approx(360_000, abs=0.5)41        assert lt["S"] == pytest.approx(109_375, abs=0.5)42        assert lt["R"] == pytest.approx(11_700, abs=0.5)43        assert lt["P"] == pytest.approx(292_500, abs=0.5)44        assert lt["C"] == pytest.approx(1_093.75, abs=0.01)4546    def test_statique_comparative(self, p4q):47        sc = cas.statique_comparative(p4q)48        assert sc.loc["Demande a : +10 %", "R_ct"] == pytest.approx(18_720, abs=0.5)49        assert sc.loc["Demande a : +10 %", "S_lt"] == pytest.approx(113_846.15, abs=0.5)50        assert sc.loc["Demande a : +10 %", "R_lt"] == pytest.approx(14_732.31, abs=0.5)51        assert sc.loc["Demande a : +10 %", "dR_ct_pct"] == pytest.approx(30.0, abs=0.05)52        assert sc.loc["Demande a : +10 %", "dR_lt_pct"] == pytest.approx(2.31, abs=0.05)53        assert sc.loc["Coût c0 : +10 %", "S_lt"] == pytest.approx(96_153.85, abs=0.5)54        assert sc.loc["Coût c0 : +10 %", "dP_lt_pct"] == pytest.approx(7.69, abs=0.05)55        assert sc.loc["Taux i : 5 % → 4 %", "dP_lt_pct"] == pytest.approx(1.56, abs=0.05)56        assert sc.loc["Taux i : 5 % → 4 %", "dR_lt_pct"] == pytest.approx(-18.75, abs=0.05)5758    def test_elasticites(self, p4q):59        el = cas.elasticites_4q(p4q)60        assert el["eps_C_P"] == pytest.approx(6.0, rel=1e-9)61        assert el["kappa"] == pytest.approx(0.12, rel=1e-9)62        assert el["eps_S_a"] == pytest.approx(1.385, abs=5e-3)636465class TestDynamique:66    def test_etat_initial_stationnaire(self, pc):67        df = cas.simuler_cycle(replace(pc, choc_demande=0.0))68        assert np.allclose(df["S"], pc.p4q.S0, rtol=1e-9) and np.allclose(df["R"], pc.p4q.R0, rtol=1e-9)69        assert np.allclose(df["v"], pc.v_nat, atol=1e-12)70        assert pc.c0 == pytest.approx(276_000, abs=1e-6) and pc.kappa == pytest.approx(0.48, rel=1e-9)7172    def test_stationnaire_apres_choc(self, pc):73        ss = cas.etat_stationnaire_cycle(pc)74        assert ss["S"] == pytest.approx(108_902, abs=1.0)75        assert ss["R"] == pytest.approx(14_453, abs=1.0)76        assert ss["C"] == pytest.approx(pc.p4q.delta * ss["S"], rel=1e-12)77        df = cas.simuler_cycle(replace(pc, T=600))78        assert df["S"].iloc[-1] == pytest.approx(ss["S"], rel=2e-3)79        assert df["v"].iloc[-1] == pytest.approx(pc.v_nat, abs=2e-3)8081    def test_mesures_du_cycle_cite(self, pc):82        r = cas.resume_cycle(cas.simuler_cycle(pc), pc)83        assert r["R_pointe_pct"] == pytest.approx(15.0, abs=0.1)84        assert r["t_pointe"] == 1385        assert r["R_creux_pct"] == pytest.approx(-7.0, abs=0.1)86        assert r["t_creux"] == 3587        assert r["v_max_pct"] == pytest.approx(5.0, abs=0.05)88        assert r["v_min_pct"] == pytest.approx(1.17, abs=0.05)89        assert r["C_max_annuel"] == pytest.approx(4_711, abs=1.0)90        assert r["periode_trim"] == 4791        assert r["amortissement"] == pytest.approx(0.17, abs=0.01)9293    def test_sensibilite(self, pc):94        s = cas.sensibilite_cycle(pc, delais=(2, 6, 12), mult_c1=(0.25, 1.0), lams=(0.75,)).set_index(["L", "mult_c1"])95        assert s.loc[(12, 0.25), "R_creux_pct"] == pytest.approx(-18.06, abs=0.1)96        assert s.loc[(2, 0.25), "R_creux_pct"] == pytest.approx(-1.21, abs=0.1)97        assert s.loc[(6, 1.0), "R_creux_pct"] > 0            # offre peu réactive : pas de sous-ajustement98        assert math.isnan(s.loc[(6, 1.0), "periode_trim"])    # pas de second cycle99        # le délai allonge la période et creuse le creux100        assert s.loc[(12, 0.25), "periode_trim"] > s.loc[(6, 0.25), "periode_trim"] > s.loc[(2, 0.25), "periode_trim"]101102    def test_condition_oscillation(self, pc):103        d = pc.p4q.delta104        for k in (0.05, 0.12, 0.2449, 0.2451, 0.3, 0.48, 0.6, 1.2):105            a = cas.condition_oscillation_L1(k, d); b = cas.racines_caracteristiques(k, d, L=1)106            assert a["oscille"] == b["oscille"]107            assert a["module"] == pytest.approx(b["module"], rel=1e-9)108            if a["oscille"]:109                assert a["periode"] == pytest.approx(b["periode"], rel=1e-9)110                assert a["module"] == pytest.approx(math.sqrt(k), rel=1e-12)111        assert cas.condition_oscillation_L1(0.12, d)["seuil"] == pytest.approx(0.2450, abs=1e-4)112        assert not cas.condition_oscillation_L1(0.12, d)["oscille"]113        c = cas.condition_oscillation_L1(0.48, d)114        assert c["oscille"] and c["stable"] and c["periode"] == pytest.approx(8.1, abs=0.05) and c["module"] == pytest.approx(0.693, abs=5e-4)115        assert not cas.condition_oscillation_L1(1.2, d)["stable"]116        assert cas.racines_caracteristiques(0.48, d, L=3)["module"] > 1.0   # délai de 3 ans : instable117118119class TestDonneesReelles:120    def test_mises_en_chantier_quebec(self):121        ann = cas.mises_en_chantier_annuelles()122        assert (ann.loc[1990:2025, "trimestres"] == 4).all() and ann.loc[2026, "trimestres"] == 3123        attendu = {1990: 48_070, 2000: 24_695, 2005: 50_910, 2015: 37_926, 2020: 54_066, 2021: 67_810, 2022: 57_107,124                   2023: 38_912, 2024: 48_713, 2025: 59_864, 2026: 42_159}125        for y, v in attendu.items():126            assert ann.loc[y, "total"] == v127        assert ann.loc[2005, "individuelles"] == 23_930 and ann.loc[2025, "individuelles"] == 8_477128        assert ann.loc[2005, "collectifs"] == 26_980 and ann.loc[2025, "collectifs"] == 51_387129        assert ann.loc[2025, "part_individuelles"] == pytest.approx(0.1416, abs=5e-4)130        assert ann.loc[2005, "part_individuelles"] == pytest.approx(0.470, abs=5e-4)131        # cohérence interne : les cinq types somment au total132        tot = ann[["individuelles", "jumeles", "bande", "appartements"]].sum(axis=1)133        assert np.allclose(tot.loc[1990:2026], ann.loc[1990:2026, "total"])134        a9 = ann.loc[1990:2026]135        assert np.allclose(a9["jumeles"] + a9["bande"] + a9["appartements"], a9["collectifs"])136        # tableau 3.4 du chapitre (total, individuelles, jumelés + en bande, appartements, part des individuelles)137        tab = {1990: (48_070, 24_942, 3_623, 19_505, 51.9), 2005: (50_910, 23_930, 3_752, 23_228, 47.0), 2015: (37_926, 9_698, 3_895, 24_333, 25.6),138               2021: (67_810, 14_826, 6_265, 46_719, 21.9), 2023: (38_912, 8_601, 2_342, 27_969, 22.1), 2025: (59_864, 8_477, 3_716, 47_671, 14.2)}139        for y, (tt, ind, jb, ap, part) in tab.items():140            r = ann.loc[y]141            assert (r["total"], r["individuelles"], r["jumeles"] + r["bande"], r["appartements"]) == (tt, ind, jb, ap)142            assert 100 * r["part_individuelles"] == pytest.approx(part, abs=0.05)143        # variation 2021 → 2023 et 2023 → 2025144        assert (ann.loc[2023, "total"] / ann.loc[2021, "total"] - 1) == pytest.approx(-0.426, abs=5e-4)145        assert (ann.loc[2025, "total"] / ann.loc[2023, "total"] - 1) == pytest.approx(0.538, abs=5e-4)146147    def test_montreal_incomplet(self):148        m = cas.mises_en_chantier_montreal_annuelles()149        assert m.loc[2024, "mois"] == 12 and m.loc[2024, "total"] == 17_570150        assert m.loc[2025, "mois"] == 9 and m.loc[2025, "total"] == 21_138     # juillet-septembre 2025 absents du fichier151        q = cas.mises_en_chantier_montreal_annuelles("Québec")152        assert q.loc[2025, "mois"] == 12 and q.loc[2025, "total"] == 54_220153154    def test_taux_directeur(self):155        ch = cas.changements_taux_directeur()156        assert len(ch) == 23157        assert ch.loc["2022-03-03", "apres"] == 0.50 and ch.loc["2023-07-13", "apres"] == 5.00158        assert ch.loc["2025-10-30", "apres"] == 2.25 and ch.loc["2026-10-29", "apres"] == 2.50159        assert ch.loc["2022-07-14", "variation_pb"] == 100160        assert int(ch.loc["2022-03-03":"2023-07-13", "variation_pb"].sum()) == 475161        assert int(ch.loc["2024-06-06":"2025-10-30", "variation_pb"].sum()) == -275162163    def test_taux_effectifs_a4(self):164        a4 = cas.taux_effectifs_a4()165        assert a4["serie_assure"] == "V122667780" and a4["serie_non_assure"] == "V122667786"166        assert a4["assure_fixe_5ans_plus"] == pytest.approx(4.12) and a4["non_assure_fixe_5ans_plus"] == pytest.approx(4.58)167168    def test_ipc_ipln(self):169        ipc = cas.ipc_quebec(); ipln = cas.ipln_quebec()170        assert ipc.index[-1].strftime("%Y-%m") == "2026-09" and ipc.iloc[-1] == 167.7 and ipc.iloc[-13] == 162.6171        assert (ipc.iloc[-1] / ipc.iloc[-13] - 1) * 100 == pytest.approx(3.14, abs=0.01)172        assert ipln.iloc[-1] == 150.0 and (ipln.iloc[-1] / ipln.iloc[-13] - 1) * 100 == pytest.approx(1.28, abs=0.01)173        loyer = cas.ipc_quebec("Loyer")174        assert loyer.iloc[-1] == 174.2 and (loyer.iloc[-1] / loyer.iloc[-13] - 1) * 100 == pytest.approx(3.3, abs=0.05)175176    def test_serie_cout_usage(self):177        cu = cas.serie_cout_usage()178        assert cu.index[0].strftime("%Y-%m") == "2001-01" and cu.index[-1].strftime("%Y-%m") == "2026-09"179        t = cu.loc[cas.DATES_TABLEAU_COUT_USAGE]180        assert t["hyp5"].round(2).tolist() == [7.79, 7.49, 5.29, 4.94, 7.04, 6.09]181        assert t["pi"].round(2).tolist() == [2.76, 2.02, 2.84, 0.30, 4.16, 3.14]182        assert t["pr_ipc"].round(1).tolist() == [12.0, 11.4, 17.3, 12.6, 16.1, 15.9]183        assert np.allclose(cu["u_ipc"], cu["hyp5"] + 100 * (cas.TAUX_TAXE_QUEBEC_2026 + cas.DELTA_ENTRETIEN_RISQUE) - cu["pi"])184        assert np.allclose(cu["pr_ipc"] * cu["u_ipc"], 100.0)185        # plancher du coût d'usage sous l'hypothèse IPLN en 2021-2022 (anticipations extrapolatives)186        assert cu["u_ipln"].min() < 1.5 and cu["u_ipln"].idxmin().year in (2021, 2022)187188189class TestCoutUsageExemple:190    def test_formules(self):191        assert cas.taux_cout_usage(0.06, 0.01, 0.02, 0.03) == pytest.approx(0.06)192        assert cas.cout_usage(400_000, 0.06, 0.01, 0.02, 0.03) == pytest.approx(24_000)193        assert cas.ratio_prix_loyer(0.06, 0.01, 0.02, 0.03) == pytest.approx(1 / 0.06)194        assert cas.ratio_prix_loyer(0.02, 0.0, 0.0, 0.03) == math.inf195196    def test_exemple_cite(self):197        ex = cas.exemple_cout_usage()198        assert cas.TAUX_TAXE_QUEBEC_2026 == pytest.approx(0.0082539, abs=1e-7)199        assert ex["u_affiche_ipc"] == pytest.approx(0.06415, abs=5e-5)200        assert ex["cout_annuel_affiche_ipc"] == pytest.approx(29_735, abs=0.5)201        assert ex["cout_mensuel_affiche_ipc"] == pytest.approx(2_478, abs=0.5)202        assert ex["pr_affiche_ipc"] == pytest.approx(15.59, abs=0.005)203        assert ex["u_effectif_ipc"] == pytest.approx(0.04805, abs=5e-5)204        assert ex["cout_annuel_effectif_ipc"] == pytest.approx(22_273, abs=0.5)205        assert ex["cout_mensuel_effectif_ipc"] == pytest.approx(1_856, abs=0.5)206        assert ex["pr_effectif_ipc"] == pytest.approx(20.81, abs=0.005)207        assert ex["u_effectif_ipln"] == pytest.approx(0.06605, abs=5e-5)208        assert ex["cout_mensuel_effectif_ipln"] == pytest.approx(2_551, abs=0.5)209        assert ex["pr_effectif_ipln"] == pytest.approx(15.14, abs=0.005)210        assert ex["var_pr_plus100pb_pct"] == pytest.approx(-17.2, abs=0.05)211        assert ex["semi_elasticite"] == pytest.approx(-20.8, abs=0.05)212        assert ex["prix_implicite_loyer_3ch_effectif_ipc"] == pytest.approx(368_336, abs=0.5)213        # identité : coût annuel / 12 = coût mensuel ; prix implicite × u = loyer annuel214        assert ex["cout_annuel_effectif_ipc"] / 12 == pytest.approx(ex["cout_mensuel_effectif_ipc"], rel=1e-12)215        assert ex["prix_implicite_loyer_3ch_effectif_ipc"] * ex["u_effectif_ipc"] == pytest.approx(1475 * 12, rel=1e-12)216217218class TestIndicateurs:219    def test_mois_inventaire(self):220        assert cas.mois_inventaire(2_204, 2_161) == pytest.approx(3.06, abs=0.005)221        ind = cas.indicateurs_apciq()222        attendu = {"Province": 6.08, "RMR Montréal": 6.59, "RMR Québec": 3.06, "RMR Gatineau": 6.48,223                   "RMR Sherbrooke": 4.35, "RMR Trois-Rivières": 3.60, "RMR Saguenay": 4.03}224        for k, v in attendu.items():225            assert ind.loc[k, "mois_inventaire"] == pytest.approx(v, abs=0.005)226        assert ind.loc["RMR Montréal", "ventes_mensuelles"] == pytest.approx(3_088.33, abs=0.01)227
Files · bc6e342882c88717d8bf4f61abdc43c1df67af90
Menu

Theme