SPB Git
14.5 KB · 261 lines python
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[2]["dV_pct"] == pytest.approx(-0.057, abs=5e-4) and t.iloc[3]["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
Files · 2fa255cfbe38460cb046bb32c81cf4f586081ba2
Menu

Theme