spb/traite-evaluation-qc
Public
1"""Tests du chapitre 28 — dépenses, réserves, CAPEX et RNE (cas 3 et cas 2, données synthétiques).2Identités + valeurs citées dans manuscript/chapters/ch28.tex et solutions/ch28.tex (tolérances déclarées : 0,01 $ sur les3montants au cent, 1 $ sur les valeurs capitalisées, 1e-9 sur les identités)."""4import sys, pathlib5sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1]))6sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "cases"))7import numpy as np8import pytest9from traite import cas3, cas2, finance as fin10from ch28_depenses import calculer, exercices, reserve_par_composantes, demonstration_reserve, ratio_iso_depenses, indexation_ipc1112ROOT = pathlib.Path(__file__).resolve().parents[3]131415@pytest.fixture(scope="module")16def res():17 return calculer()181920@pytest.fixture(scope="module")21def ex(res):22 return exercices(res)232425# ----------------------------------------------------------------------------- identités26def test_identite_rne(res):27 b = res["base"]28 assert b["rne"] == pytest.approx(b["rbe"] - b["depenses_hors_reserve"] - b["reserve"], abs=1e-9)29 assert b["ratio_depenses"] == pytest.approx(b["depenses_totales"] / b["rbe"], abs=1e-12)30 assert b["rne"] / b["rbe"] + b["ratio_depenses"] == pytest.approx(1.0, abs=1e-12)313233def test_coherence_avec_cas3_partage(res):34 """Les chiffres du chapitre 28 coïncident avec ceux que les chapitres 29, 30 et 47 tirent de traite.cas3."""35 t = cas3.tout()36 assert res["base"]["rne"] == pytest.approx(t["base"]["rne"], rel=1e-12)37 assert res["base"]["depenses_totales"] == pytest.approx(t["base"]["depenses_totales"], rel=1e-12)38 assert res["R"] == pytest.approx(t["taux"]["mediane"], rel=1e-12)39 assert res["V"] == pytest.approx(t["valeur_capitalisation"], rel=1e-12)404142def test_classement_somme(res):43 cl = res["classement"]; b = res["base"]44 assert cl["fixes"] + cl["variables"] + cl["reserve"] == pytest.approx(b["depenses_totales"], abs=1e-9)45 assert cl["variables_hors_gestion"] + cl["gestion"] == pytest.approx(cl["variables"], abs=1e-9)46 assert float(cl["groupes"].sum()) == pytest.approx(b["depenses_totales"], abs=1e-9)47 assert cl["part_fixes"] + cl["part_variables"] + cl["part_reserve"] == pytest.approx(1.0, abs=1e-12)484950def test_detail_somme_et_regles(res):51 d = res["detail"]; hyp = res["hyp"]52 assert float(d["normalise"].sum()) == pytest.approx(res["base"]["depenses_totales"], abs=1e-9)53 taxes = float(d.loc[d["poste"] == "Taxes municipales", "normalise"].iloc[0])54 assert taxes == pytest.approx(hyp["valeur_role_2026"] * hyp["taux_taxe_fonciere"], abs=1e-9)55 ent = d.loc[d["poste"] == "Entretien et réparations"].iloc[0]56 assert ent["normalise"] == pytest.approx(ent["historique_2025"] * 1.03, abs=1e-9)57 gestion = float(d.loc[d["poste"].str.startswith("Gestion"), "normalise"].iloc[0])58 assert gestion == pytest.approx(0.04 * res["base"]["rbe"], abs=1e-9)596061def test_historique(res):62 h = res["historique"]63 assert h.loc["Total", 2023] == 66992 and h.loc["Total", 2024] == 70438 and h.loc["Total", 2025] == 7069364 assert h.loc["Total", "var_2025_2023"] == pytest.approx(0.0552, abs=5e-4)65 assert h.loc["Total", "tcac"] == pytest.approx(0.0273, abs=5e-4)66 assert res["depenses_2025_declarees"] == 7069367 assert res["ecart_normalisation"] == pytest.approx(7795.19, abs=0.01)68 assert res["rne_historique_2025"] == pytest.approx(97750.94, abs=0.01)69 assert res["V_historique"] - res["V"] == pytest.approx(176561, abs=1)707172def test_normalisation_alternative(res):73 alt = res["alternative"]74 a = alt.loc["Assurances"]75 attendu = np.mean([11474 * 1.03 ** 3, 12614 * 1.03 ** 2, 11060 * 1.03])76 assert a["moyenne_indexee"] == pytest.approx(attendu, abs=1e-9)77 assert a["derniere_indexee"] == pytest.approx(11391.80, abs=0.01)78 assert a["ecart"] == pytest.approx(1045.51, abs=0.01)79 assert alt.loc["Entretien et réparations", "ecart"] == pytest.approx(-130.75, abs=0.01)808182# ----------------------------------------------------------------------------- réserve / taux83def test_demonstration_reserve(res):84 d = res["demo_reserve"]; b = res["base"]85 # R' = R + RR/V ; V − V' = RR/R' ; erreur relative = −RR/(RNE + RR)86 assert d["R_avant"] == pytest.approx(d["R_apres"] + b["reserve"] / d["V"], abs=1e-12)87 assert d["delta_V"] == pytest.approx(b["reserve"] / d["R_avant"], abs=1e-6)88 assert d["erreur_relative"] == pytest.approx(-b["reserve"] / (b["rne"] + b["reserve"]), abs=1e-12)89 assert d["V_err_inverse"] - d["V"] == pytest.approx(b["reserve"] / d["R_apres"], abs=1e-6)90 # valeurs citées91 assert d["R_avant"] == pytest.approx(0.048127, abs=5e-7)92 assert d["ecart_taux_pb"] == pytest.approx(39.77, abs=0.01)93 assert d["V_err"] == pytest.approx(1869136, abs=1)94 assert d["delta_V"] == pytest.approx(168367, abs=1)95 assert d["erreur_relative"] == pytest.approx(-0.0826, abs=5e-5)96 assert d["erreur_relative_inverse"] == pytest.approx(0.0901, abs=5e-5)979899def test_reserve_composantes():100 rc = reserve_par_composantes(taux=0.03)101 assert rc["lineaire_par_logement"] == pytest.approx(687.62, abs=0.01)102 assert rc["fonds_par_logement"] == pytest.approx(580.71, abs=0.01)103 assert rc["lineaire_immeuble"] == pytest.approx(8251.43, abs=0.01)104 assert rc["fonds_immeuble"] == pytest.approx(6968.47, abs=0.01)105 # fonds d'amortissement → linéaire quand le taux → 0106 rc0 = reserve_par_composantes(taux=1e-9)107 assert rc0["fonds_par_logement"] == pytest.approx(rc0["lineaire_par_logement"], rel=1e-6)108 assert reserve_par_composantes(taux=0.05)["fonds_par_logement"] == pytest.approx(518.77, abs=0.01)109110111def test_equivalence_reserve_capex():112 """VA des remplacements perpétuels tous les N ans = VA d'une réserve perpétuelle au fonds d'amortissement."""113 C, N, i = 2200.0 * 12, 12, 0.03114 va_capex = C / ((1 + i) ** N - 1)115 rr = C * fin.facteur_fonds_amortissement(i, N)116 assert rr / i == pytest.approx(va_capex, rel=1e-12)117 # vérification par sommation tronquée (2 000 cycles)118 somme = sum(C / (1 + i) ** (j * N) for j in range(1, 2001))119 assert somme == pytest.approx(va_capex, rel=1e-9)120 assert va_capex == pytest.approx(62006.64, abs=0.01)121 assert rr == pytest.approx(1860.20, abs=0.01)122123124def test_equivalence_capex_dossier(res):125 eq = res["equivalence_capex"]126 assert eq["total"] == 245000127 va = 85000 / 1.0725 ** 2 + 40000 / 1.0725 ** 4 + 120000 / 1.0725 ** 7128 assert eq["va"] == pytest.approx(va, abs=1e-6)129 assert eq["annuite_equivalente"] == pytest.approx(va * fin.facteur_recouvrement(0.0725, 10), abs=1e-6)130 assert eq["va"] == pytest.approx(177647.94, abs=0.01)131 assert eq["annuite_equivalente"] == pytest.approx(25586.15, abs=0.01)132133134# ----------------------------------------------------------------------------- valeurs citées135def test_valeurs_citees_cas3(res):136 b = res["base"]; cl = res["classement"]137 assert b["rbe"] == pytest.approx(168443.94, abs=0.01)138 assert b["depenses_hors_reserve"] == pytest.approx(70385.19, abs=0.01)139 assert b["depenses_totales"] == pytest.approx(78488.19, abs=0.01)140 assert b["rne"] == pytest.approx(89955.75, abs=0.01)141 assert b["ratio_depenses"] == pytest.approx(0.466, abs=5e-4)142 assert res["rne_par_logement"] == pytest.approx(7496.31, abs=0.01)143 assert res["rne_par_m2"] == pytest.approx(109.04, abs=0.01)144 assert cl["fixes"] == pytest.approx(23317.78, abs=0.01)145 assert cl["variables"] == pytest.approx(47067.41, abs=0.01)146 assert cl["part_fixes"] == pytest.approx(0.297, abs=5e-4)147 assert cl["part_variables"] == pytest.approx(0.600, abs=5e-4)148 assert cl["part_reserve"] == pytest.approx(0.103, abs=5e-4)149 assert cl["fixes_pct_rbe"] == pytest.approx(0.138, abs=5e-4)150 assert cl["total_par_logement"] == pytest.approx(6540.68, abs=0.01)151 assert cl["total_par_m2"] == pytest.approx(95.14, abs=0.01)152 g = cl["groupes_pct_rbe"]153 assert g["Conciergerie et services"] == pytest.approx(0.1011, abs=5e-4)154 assert g["Taxes foncières"] == pytest.approx(0.0708, abs=5e-4)155 assert g["Réserve de remplacement"] == pytest.approx(0.0481, abs=5e-4)156157158def test_valeurs_citees_marche_et_comparables(res):159 m = res["marche"]; cs = res["comps_stats"]160 assert m["rbe"] == pytest.approx(188140.60, abs=0.01)161 assert m["rne"] == pytest.approx(108864.54, abs=0.01)162 assert m["ratio"] == pytest.approx(0.421, abs=5e-4)163 assert m["loyer_moyen_contrat"] == pytest.approx(1149.58, abs=0.01)164 assert m["loyer_moyen"] == pytest.approx(1291.92, abs=0.01)165 assert cs["n"] == 40 and cs["ratio_median"] == pytest.approx(0.3985, abs=5e-4)166 assert cs["ratio_q1"] == pytest.approx(0.379, abs=1e-3) and cs["ratio_q3"] == pytest.approx(0.4185, abs=1e-3)167 # courbe iso-dépenses : passe par les deux points du sujet168 assert float(ratio_iso_depenses(m["loyer_moyen_contrat"], res["base"], res["hyp"])) == pytest.approx(res["base"]["ratio_depenses"], abs=1e-9)169 assert float(ratio_iso_depenses(m["loyer_moyen"], res["base"], res["hyp"])) == pytest.approx(m["ratio"], abs=1e-9)170171172def test_sensibilites(res):173 s = res["sensibilites"]; R = res["R"]; noi0 = res["base"]["rne"]174 r = s["reserve"].set_index("reserve_par_logement")175 assert r.loc[0, "rne"] == pytest.approx(98058.75, abs=0.01) and r.loc[0, "dV_pct"] == pytest.approx(0.090, abs=5e-4)176 assert r.loc[100, "dV_pct"] == pytest.approx(0.077, abs=5e-4) and r.loc[1200, "dV_pct"] == pytest.approx(-0.070, abs=5e-4)177 assert (np.diff(r["rne"].values) < 0).all()178 t = s["taxe"].set_index("scenario")179 assert t.iloc[1]["taxe"] == pytest.approx(11194.44, abs=0.01) and t.iloc[1]["dV_pct"] == pytest.approx(-0.011, abs=5e-4)180 assert t.iloc[3]["dV_pct"] == pytest.approx(-0.057, abs=5e-4) and t.iloc[4]["dV_pct"] == pytest.approx(-0.075, abs=5e-4)181 g = s["gestion"].set_index("gestion_pct")182 assert g.loc[0.0, "dV_pct"] == pytest.approx(0.075, abs=5e-4) and g.loc[0.10, "dV_pct"] == pytest.approx(-0.112, abs=5e-4)183 assert s["dV_dDE"] == pytest.approx(-1 / R, rel=1e-12) and abs(s["dV_dDE"]) == pytest.approx(22.65, abs=0.01)184 assert s["elasticite_rne_depenses"] == pytest.approx(-0.873, abs=5e-4)185 assert s["elasticite_rne_rbe"] == pytest.approx(1.80, abs=5e-3)186 assert s["depenses_equiv_25pb"] * R == pytest.approx(4820.78, abs=0.01)187 # pentes citées dans la légende de la figure : 12/R par dollar de réserve/logement ; 2 200/R par 10 pb ; RBE/100/R par point de gestion188 assert 12 / R == pytest.approx(272, abs=1)189 assert 2200 / R == pytest.approx(49830, abs=10)190 assert res["base"]["rbe"] / 100 / R == pytest.approx(38153, abs=1)191192193def test_indexation_ipc(res):194 ipc = res["ipc"]195 assert ipc["facteur"] == pytest.approx(167.7 / 124.7, rel=1e-12)196 assert ipc["variation"] == pytest.approx(0.345, abs=5e-4)197 assert ipc["entretien_500"] == pytest.approx(672.4, abs=0.1)198 assert ipc["salaires_300"] == pytest.approx(403.4, abs=0.1)199 assert ipc["reserve_100"] == pytest.approx(134.5, abs=0.1)200201202def test_cas2(res):203 c = res["cas2"]; b = c["base"]204 t2 = cas2.tout()205 assert b["rne"] == pytest.approx(t2["base"]["rne"], rel=1e-12)206 assert b["rbe"] == pytest.approx(40577.04, abs=0.01)207 assert c["gestion_imputee"] == pytest.approx(1623.08, abs=0.01)208 assert b["depenses_hors_reserve"] == pytest.approx(16743.08, abs=0.01)209 assert b["depenses_totales"] == pytest.approx(18543.08, abs=0.01)210 assert b["rne"] == pytest.approx(22033.96, abs=0.01)211 assert b["ratio_depenses"] == pytest.approx(0.457, abs=5e-4)212 assert c["rne_par_logement"] == pytest.approx(7344.65, abs=0.01)213 assert c["rne_par_m2"] == pytest.approx(84.10, abs=0.01)214 assert c["classement"]["part_fixes"] == pytest.approx(0.527, abs=5e-4)215 assert c["classement"]["total_par_logement"] == pytest.approx(6181.03, abs=0.01)216 assert c["classement"]["total_par_m2"] == pytest.approx(70.78, abs=0.01)217 tq = c["taxes_quebec_recalc"]218 assert tq["taxe_generale"] == pytest.approx(495000 * 0.007464, abs=1e-9)219 assert tq["total"] == pytest.approx(5437.68, abs=0.01) and tq["ecart"] == pytest.approx(462.32, abs=0.01)220 assert tq["scolaire_recalc"] == pytest.approx(371.25, abs=0.01)221222223# ----------------------------------------------------------------------------- exercices224def test_exercices(ex, res):225 e1 = ex["ex1"]226 assert e1["rbe"] == pytest.approx(118800, abs=1e-9)227 assert e1["fixes"] == pytest.approx(15260.31, abs=0.01) and e1["variables"] == pytest.approx(18740, abs=1e-9)228 assert e1["rne"] == pytest.approx(79999.70, abs=0.01) and e1["ratio"] == pytest.approx(0.3266, abs=5e-5)229 assert e1["part_fixes"] == pytest.approx(0.393, abs=5e-4)230 e2 = ex["ex2"]231 assert e2["ecart_total"] == pytest.approx(1046.00, abs=0.01) and e2["dV"] == pytest.approx(23692, abs=1)232 e3 = ex["ex3"]233 assert e3["ecart"] == pytest.approx(1006.24, abs=0.01) and e3["dV"] == pytest.approx(-22791, abs=1)234 assert e3["ecart_exemption"] == pytest.approx(19.75, abs=0.01)235 e4 = ex["ex4"]236 assert e4["lineaire"] == pytest.approx(687.62, abs=0.01) and e4["fonds_5"] == pytest.approx(518.77, abs=0.01)237 e5 = ex["ex5"]238 assert e5["R_apres"] == pytest.approx(182000 / 4100000, rel=1e-12)239 assert e5["ecart_pb"] == pytest.approx(32.93, abs=0.01)240 assert e5["V_err"] == pytest.approx(1886540, abs=1) and e5["V_coherent"] == pytest.approx(2026476, abs=1)241 assert e5["erreur_rel"] == pytest.approx(-0.0691, abs=5e-5)242 assert e5["delta_V"] == pytest.approx(139936, abs=1) and e5["delta_formule_sujet"] == pytest.approx(169935, abs=1)243 # l'écart exact n'égale RR/R' que si les parts de réserve coïncident244 assert e5["ratio_reserve_comparable"] == pytest.approx(0.0691, abs=5e-5) and e5["ratio_reserve_sujet"] == pytest.approx(0.0826, abs=5e-5)245 e6 = ex["ex6"]246 assert e6["rne_sans"] == pytest.approx(23657.04, abs=0.01) and e6["ratio_sans"] == pytest.approx(0.417, abs=5e-4)247 assert e6["rne_5"] == pytest.approx(21628.19, abs=0.01)248 assert e6["V_avec"] == pytest.approx(695078, abs=1) and e6["V_sans"] == pytest.approx(746279, abs=1)249 assert e6["surevaluation"] == pytest.approx(0.0737, abs=5e-5)250 e7 = ex["ex7"]251 assert e7["rne_double"] == pytest.approx(64369.60, abs=0.01) and e7["V_double"] == pytest.approx(1457975, abs=1)252 assert e7["erreur_rel_double"] == pytest.approx(-0.284, abs=5e-4)253 assert e7["va_capex_perpetuel"] == pytest.approx(e7["va_reserve_perpetuelle"], rel=1e-12)254255256# ----------------------------------------------------------------------------- figures257def test_figures_presentes():258 for slug in ["cascade", "structure", "evolution", "ratio-loyer", "sensibilite"]:259 p = ROOT / "assets/figures/ch28" / f"ch28-fig-{slug}.pdf"260 assert p.exists() and p.stat().st_size > 1000, p261