spb/traite-evaluation-qc
Public
1"""Tests du chapitre 33 (immeubles de six logements et plus) : identités, cohérence avec traite.cas3/cas4 et le ch. 47,2lecture des tableaux SCHL (données réelles), valeurs citées dans le texte. Tolérances déclarées : montants ±1 $,3taux ±1e-5 (±0,1 pb), pourcentages ±1e-4."""4import sys, pathlib5import numpy as np, pytest6sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "cases"))7from traite import cas3, cas4, finance as fin8import ch33_six_logements as ch910@pytest.fixture(scope="module")11def R():12 return ch.calculer()1314# ----------------------------------------------------------------------------- SCHL (réel)1516def test_schl_lecture_coherente_avec_notes(R):17 t = R["schl"]["taille"].set_index("categorie"); a = R["schl"]["annee"].set_index("categorie")18 # valeurs consignées dans sources/notes-E.md (ch. 27 et 47) : 6-19 unités île : 2,2 % / 1 177 $ (2 ch.) / 1 162 $ (tous)19 assert t.loc["6 à 19 unités", "inoccupation_tous"] == pytest.approx(2.2) and t.loc["6 à 19 unités", "loyer_2ch"] == 1177 and t.loc["6 à 19 unités", "loyer_tous"] == 116220 assert t.loc["20 à 49 unités", "inoccupation_tous"] == pytest.approx(3.9) and t.loc["50 à 99 unités", "inoccupation_tous"] == pytest.approx(3.8)21 assert t.loc["100 unités et +", "inoccupation_tous"] == pytest.approx(5.1) and t.loc["100 unités et +", "loyer_2ch"] == 196622 assert t.loc["3 à 5 unités", "loyer_2ch"] == 1350 and t.loc["Tous les immeubles", "inoccupation_tous"] == pytest.approx(3.1)23 assert a.loc["Avant 1940", "loyer_tous"] == 1238 and a.loc["1960 à 1974", "loyer_tous"] == 1186 and a.loc["2015 ou après", "loyer_tous"] == 188224 assert a.loc["2015 ou après", "inoccupation_tous"] == pytest.approx(5.6)25 assert a.loc["Avant 1940", "rotation_tous"] == pytest.approx(9.3) and a.loc["2015 ou après", "rotation_tous"] == pytest.approx(13.7) and np.isnan(a.loc["1990 à 2004", "rotation_tous"])26 assert t.loc["6 à 19 unités", "rotation_tous"] == pytest.approx(8.5) and t.loc["100 unités et +", "rotation_tous"] == pytest.approx(12.4)27 assert list(t.index) == ["3 à 5 unités", "6 à 19 unités", "20 à 49 unités", "50 à 99 unités", "100 unités et +", "Tous les immeubles"]2829# ----------------------------------------------------------------------------- cas 4 : sommaire et identités3031def test_sommaire_cas4_identites(R):32 g = R["sommaire"]["par_rmr"]; t = R["sommaire"]["total"]; pf = R["sommaire"]["portefeuille"]33 assert t["n"] == 48 and t["logements"] == 888 and g["n"].sum() == 48 and g["logements"].sum() == 88834 assert g["valeur"].sum() == pytest.approx(t["valeur"]) and t["valeur"] == pytest.approx(143_363_000)35 for r in g.index:36 assert g.loc[r, "taux_pondere"] == pytest.approx(g.loc[r, "rne"] / g.loc[r, "valeur"])37 assert g.loc[r, "valeur_par_logement"] == pytest.approx(g.loc[r, "valeur"] / g.loc[r, "logements"])38 assert t["taux_pondere"] == pytest.approx(0.051945, abs=1e-5) and t["valeur"] / t["logements"] == pytest.approx(161_445, abs=1)39 assert g.loc["Montréal", "valeur_par_logement"] == pytest.approx(206_237, abs=1) and g.loc["Gatineau", "valeur_par_logement"] == pytest.approx(184_173, abs=1)40 assert g.loc["Québec", "valeur_par_logement"] == pytest.approx(125_455, abs=1) and g.loc["Saguenay", "valeur_par_logement"] == pytest.approx(101_969, abs=1)41 assert g.loc["Montréal", "taux_pondere"] == pytest.approx(0.044948, abs=1e-5) and g.loc["Gatineau", "taux_pondere"] == pytest.approx(0.054359, abs=1e-5)42 assert g.loc["Trois-Rivières", "taux_pondere"] == pytest.approx(0.062883, abs=1e-5) and g.loc["Saguenay", "taux_median"] == pytest.approx(0.0646, abs=1e-5)43 # cohérence avec le module partagé44 s = cas4.sommaire(pf)45 assert s["total"]["taux_pondere"] == pytest.approx(t["taux_pondere"]) and (s["par_rmr"]["valeur_par_logement"].sort_index() == g["valeur_par_logement"].sort_index()).all()4647# ----------------------------------------------------------------------------- effet de taille4849def test_effet_taille(R):50 et = R["effet_taille"]51 assert et["cas3_n"] == 40 and et["cas3_mediane"] == pytest.approx(0.04415, abs=1e-6)52 assert et["cas3_pente_pb_par_logement"] == pytest.approx(-2.27, abs=0.01) and et["d_pb_12_36_cas3"] == pytest.approx(-54.6, abs=0.1)53 assert et["cas4_coef_ln"] == pytest.approx(-0.03153, abs=1e-4) and et["cas4_se_ln"] == pytest.approx(0.01222, abs=1e-4)54 assert et["cas4_effet_doublement_pct"] == pytest.approx(np.exp(et["cas4_coef_ln"] * np.log(2)) - 1) and et["cas4_effet_doublement_pct"] == pytest.approx(-0.0216, abs=1e-4)55 assert et["d_pb_12_36_cas4"] == pytest.approx(-15.7, abs=0.1)56 # mêmes coefficients que traite.cas4.effets_regionaux57 e = cas4.effets_regionaux(R["sommaire"]["portefeuille"])58 assert e.loc["ln_logements", "coef"] == pytest.approx(et["cas4_coef_ln"]) and e.loc["rmr_Gatineau", "effet_pct"] == pytest.approx(0.1962, abs=1e-4)59 assert e.loc["rmr_Trois-Rivières", "effet_pct"] == pytest.approx(0.4017, abs=1e-4) and e.loc["etat", "effet_pct"] == pytest.approx(-0.0396, abs=1e-4)6061# ----------------------------------------------------------------------------- unités (cas 3)6263def test_unites_cas3(R):64 u = R["unites"]; b = cas3.tout()["base"]65 assert u["n"] == 12 and u["m2"] == 825 and u["chambres"] == 2266 assert u["par_logement"] == pytest.approx(2_060_000 / 12) and u["par_m2"] == pytest.approx(2_060_000 / 825) and u["par_chambre"] == pytest.approx(2_060_000 / 22)67 assert u["par_logement"] == pytest.approx(171_667, abs=1) and u["par_m2"] == pytest.approx(2_497, abs=1) and u["par_chambre"] == pytest.approx(93_636, abs=1)68 assert u["mrb"] == pytest.approx(2_060_000 / b["rbp_contrat"]) and u["mrb"] == pytest.approx(12.44, abs=0.01) and u["mrb_marche"] == pytest.approx(11.07, abs=0.01)69 assert u["taux_implicite"] == pytest.approx(b["rne"] / 2_060_000) and u["taux_implicite"] == pytest.approx(0.04367, abs=1e-5)70 assert u["valeur_cap"] == pytest.approx(2_037_502.89, abs=0.01) and u["rne_par_logement"] == pytest.approx(7_496.31, abs=0.01)71 assert u["loyer_moyen_m2"] == pytest.approx(16.72, abs=0.01)7273def test_valeur_finale_ch47_coherente():74 """La valeur finale citée (2 060 000 $) est celle de la réconciliation du chapitre 47."""75 import ch47_dossier as c4776 D = c47.dossier(n_mc=200, graine=c47.GRAINE)77 assert D["reconciliation"]["valeur_finale"] == ch.V_FINALE_CH477879# ----------------------------------------------------------------------------- P458081def test_p45_identites_et_valeurs(R):82 p = R["p45"]83 assert p["id"] == "P45" and p["n"] == 36 and p["rmr"] == "Gatineau" and p["etat"] == 2 and p["annee"] == 195484 assert p["rne"] == pytest.approx(366_800) and p["rne_par_logement"] == pytest.approx(10_188.89, abs=0.01)85 for k, R_ in p["taux"].items():86 assert p["valeurs"][k] == pytest.approx(fin.capitalisation_directe(p["rne"], R_)) and p["par_logement"][k] == pytest.approx(p["valeurs"][k] / 36)87 assert p["taux"]["montreal_cas3_mediane"] == pytest.approx(0.04415, abs=1e-6) and p["valeurs"]["montreal_cas3_mediane"] == pytest.approx(8_308_041, abs=1)88 assert p["taux"]["gatineau_pondere"] == pytest.approx(0.054359, abs=1e-5) and p["valeurs"]["gatineau_pondere"] == pytest.approx(6_747_739, abs=1)89 assert p["taux"]["gatineau_median"] == pytest.approx(0.0571, abs=1e-6) and p["valeurs"]["gatineau_median"] == pytest.approx(6_423_818, abs=1)90 assert p["taux"]["regression_sujet"] == pytest.approx(0.054611, abs=1e-5) and p["valeurs"]["regression_sujet"] == pytest.approx(6_716_534, abs=1)91 assert p["taux"]["regression_si_montreal"] == pytest.approx(0.045654, abs=1e-5) and p["valeurs"]["regression_si_montreal"] == pytest.approx(8_034_343, abs=1)92 assert p["taux"]["regression_si_12_logements"] == pytest.approx(0.056536, abs=1e-5) and p["valeurs"]["regression_si_12_logements"] == pytest.approx(6_487_875, abs=1)93 assert p["taux"]["regression_si_etat_3"] == pytest.approx(0.052452, abs=1e-5) and p["valeurs"]["regression_si_etat_3"] == pytest.approx(6_993_820, abs=1)94 assert p["valeur_fichier"] == 7_186_000 and p["valeur_fichier_par_logement"] == pytest.approx(199_611, abs=1)95 assert p["ecart_montreal_vs_sujet"] == pytest.approx(0.237, abs=1e-3)96 d = p["decomposition"]97 assert d["effet_taille_pb"] == pytest.approx(-16.1, abs=0.1) and d["effet_rmr_pb"] == pytest.approx(89.6, abs=0.1)98 # décomposition multiplicative exacte : R_sujet = R_mtl12 × (1 + effet taille) × (1 + effet RMR)99 r12 = d["regression_montreal_12_log_etat2_age72"]100 assert r12 + d["effet_taille_pb"] / 1e4 + d["effet_rmr_pb"] / 1e4 == pytest.approx(p["taux"]["regression_sujet"], abs=1e-9)101 reg = p["regression"]; assert reg["n"] == 48 and reg["r2"] == pytest.approx(0.883, abs=1e-3) and reg["duan"] == pytest.approx(1.0011, abs=1e-4)102 f = p["financement"]103 assert f["service_dette"] == pytest.approx(fin.Hypotheque(5_416_000, 0.0517, 480).service_dette_annuel) and f["rcd"] == pytest.approx(366_800 / f["service_dette"])104 assert f["rcd"] == pytest.approx(1.15, abs=0.01) and f["rpv_fichier"] == pytest.approx(0.7537, abs=1e-4)105106# ----------------------------------------------------------------------------- sensibilité107108def test_sensibilite(R):109 s = R["sensibilite"]; p = R["p45"]110 G, T = s["grille_valeur_par_logement"], s["grille_taux"]111 assert G.shape == (5, 6) and (G.values > 0).all()112 assert G.loc["état 2", "Gatineau"] == pytest.approx(p["par_logement"]["regression_sujet"]) and T.loc["état 2", "Gatineau"] == pytest.approx(p["taux"]["regression_sujet"])113 assert (np.diff(T.values, axis=0) < 0).all() # un meilleur état abaisse le taux114 assert (T["Montréal"] < T[["Québec", "Gatineau", "Sherbrooke", "Trois-Rivières", "Saguenay"]].min(axis=1)).all() # Montréal toujours le plus bas115 c = s["courbe_taille"]; assert (np.diff(c["taux_gatineau"]) < 0).all() and c.loc[c["n"] == 36, "taux_gatineau"].iloc[0] == pytest.approx(p["taux"]["regression_sujet"])116 pb = s["taux_pb"].set_index("delta_pb")117 assert pb.loc[0, "valeur"] == pytest.approx(p["valeurs"]["regression_sujet"]) and pb.loc[25, "variation_pct"] == pytest.approx(s["variation_25pb_pct"])118 assert s["variation_25pb_pct"] == pytest.approx(-0.0438, abs=1e-4) and pb.loc[-25, "variation_pct"] == pytest.approx(0.0480, abs=1e-4)119 assert s["valeur_un_pb"] == pytest.approx(12_299, abs=1) and s["pb_par_million_p45"] == pytest.approx(1_831, abs=1) and s["pb_par_million_cas3"] == pytest.approx(2_265, abs=1)120 # élasticité dV/V = -dR/R en petite variation121 R0 = p["taux"]["regression_sujet"]; assert (pb.loc[25, "valeur"] - pb.loc[-25, "valeur"]) / (50e-4) / pb.loc[0, "valeur"] == pytest.approx(-1 / R0, rel=0.01)122123# ----------------------------------------------------------------------------- exercices124125def test_exercices(R):126 ex = R["exercices"]127 assert ex["ex1"]["par_logement"] == pytest.approx(2_746_667, abs=1) and ex["ex1"]["par_m2"] == pytest.approx(2_596_848, abs=1) and ex["ex1"]["par_chambre"] == pytest.approx(2_434_545, abs=1)128 assert ex["ex1"]["ecart_max_pct"] == pytest.approx(0.1282, abs=1e-4)129 assert ex["ex2"]["taxe_5"] == pytest.approx(16_340.20, abs=0.01) and ex["ex2"]["taxe_6"] == pytest.approx(17_340.60, abs=0.01)130 assert ex["ex2"]["ecart_taux_seul"] == pytest.approx(419.40, abs=0.01) and ex["ex2"]["ecart_total"] == pytest.approx(1_000.40, abs=0.01) and ex["ex2"]["effet_valeur_a_5pct"] == pytest.approx(7_625.45, abs=0.01)131 assert ex["ex3"]["R36"] == pytest.approx(ex["ex3"]["R12"] - 54.592e-4, abs=1e-7) and ex["ex3"]["prime_pct"] == pytest.approx(0.1358, abs=1e-4)132 assert ex["ex3"]["valeur_par_logement_12"] == pytest.approx(164_141, abs=1) and ex["ex3"]["valeur_par_logement_36"] == pytest.approx(186_426, abs=1)133 assert ex["ex4"]["ecart"] == pytest.approx(1_591_507, abs=1) and ex["ex4"]["ecart_pct"] == pytest.approx(0.237, abs=1e-3)134 assert ex["ex5"]["delta_rbp"] == 5_760 and ex["ex5"]["delta_rne"] == pytest.approx(5_319.48, abs=0.01) and ex["ex5"]["delta_valeur"] == pytest.approx(97_406, abs=1) and ex["ex5"]["delta_valeur_pct"] == pytest.approx(0.0145, abs=1e-4)135 e6 = ex["ex6"]["par_rmr"]136 assert e6["Montréal"]["taux"] == pytest.approx(0.0452, abs=1e-4) and e6["Trois-Rivières"]["taux"] == pytest.approx(0.0634, abs=1e-4)137 assert e6["Gatineau"]["ic_lo"] < e6["Gatineau"]["effet_pct"] < e6["Gatineau"]["ic_hi"] and e6["Gatineau"]["effet_pct"] == pytest.approx(0.1962, abs=1e-4)138 e7 = ex["ex7"]139 assert e7["cout_2026"] == pytest.approx(6_988_800) and e7["cout_total"] == pytest.approx(8_088_800) and e7["rne"] == pytest.approx(365_124.10, abs=0.01)140 assert e7["V_450"] == pytest.approx(8_113_869, abs=1) and e7["V_475"] == pytest.approx(7_686_823, abs=1) and e7["taux_implicite_cout"] == pytest.approx(0.0451, abs=1e-4)141