SPB Git
11.4 KB · 183 lines python
1"""Tests du chapitre 34 — TAL, encadrement locatif et scénarios (cas 3, données synthétiques ; IPC Québec réel).2Identités : la projection locale reproduit traite.cas3.projection_dcf avec les paramètres du dossier ; valeur de base 2 208 566 $ ;3scénario « encadrement serré » du ch. 30 (1 912 046 $) ; formule de l'art. 3.1 (3,1 % en 2026, 4,5 % en 2025) ; règles des art. 44(taux net), 5 (part) et 15.1 (arrondi) ; forme fermée E[q_t] = récursion = Monte-Carlo, limite q_∞ ; monotonie de la valeur en5rotation et en pourcentage de base ; valeurs citées dans manuscript/chapters/ch34.tex (tolérances déclarées)."""6import math, numpy as np, pytest7from traite import cas38from cases import ch34_tal as c910@pytest.fixture(scope="module")11def D():12    return cas3.tout()1314# ------------------------------------------------------------------ 1. pourcentage de base (art. 3.1), IPC réel1516def test_pourcentage_base_reproduit_tal():17    p26 = c.pourcentage_base(2026); p25 = c.pourcentage_base(2025)18    assert p26["pct_base"] == pytest.approx(0.031, abs=0.0005)     # 3,095 % calculé vs 3,1 % publié (arrondi à la décimale)19    assert p25["pct_base"] == pytest.approx(0.045, abs=0.0005)     # 4,496 % vs 4,5 %20    assert p26["B"] == p25["A"] and p26["C"] == p25["B"] and p26["D"] == p25["C"]21    assert p26["A"] == pytest.approx(161.033, abs=0.001) and p26["D"] == pytest.approx(146.983, abs=0.001)22    assert p26["pct_base"] == pytest.approx((p26["var_A_B"] + p26["var_B_C"] + p26["var_C_D"]) / 3)2324def test_pourcentage_base_plancher_zero():25    import pandas as pd26    moy = pd.Series({2021: 100.0, 2022: 98.0, 2023: 97.0, 2024: 96.0, 2025: 95.0})27    assert c.pourcentage_base(2026, moy)["pct_base"] == 0.0 and c.pourcentage_base(2026, moy)["pct_brut"] < 02829def test_historique_coherent():30    h = c.historique_pourcentage_base(2006, 2026)31    assert list(h["annee"]) == list(range(2006, 2027))32    assert (h["pct_formule"] >= 0).all()33    # la moyenne mobile lisse : la formule est moins dispersée que l'inflation annuelle34    assert h["pct_formule"].std() < h["inflation_ref"].std()35    assert h.loc[h["annee"] == 2023, "inflation_ref"].iloc[0] == pytest.approx(0.0667, abs=0.0005)3637# ------------------------------------------------------------------ 2. fixation : art. 4, 5, 15.13839def test_arrondi_art_15_1():40    assert c.arrondi_art_15_1(750.49) == 750 and c.arrondi_art_15_1(750.50) == 751 and c.arrondi_art_15_1(751.0) == 7514142def test_taux_net_art_4():43    assert c.taux_net(-0.042, 0.031) == 0.0            # variation négative : rien44    assert c.taux_net(0.019, 0.031) == 0.0             # variation positive inférieure à la base : rien45    assert c.taux_net(0.080, 0.031) == pytest.approx(0.049)46    assert c.taux_net(0.080, 0.0) == pytest.approx(0.080)4748def test_part_attribuable():49    assert c.part_attribuable(728, 174_900) == pytest.approx(728 * 12 / 174_900)5051def test_fixation_nouvelle_composantes():52    f = c.fixation_nouvelle(1000.0, 0.03, 0.10, (24_000, 26_400), (3_000, 3_060), (12_000, 13_200), immobilisations=60_000, nouveaux_services=1_200)53    assert f["base"] == pytest.approx(30.0)54    assert f["m_taxes_mun"] == pytest.approx((0.10 - 0.03) * 24_000 / 12 * 0.10)      # 14,00 $ : taux net × mensuel de la période de référence × part55    assert f["m_taxes_scol"] == 0.0                                                     # +2 % < 3 %56    assert f["m_assurances"] == pytest.approx((0.10 - 0.03) * 12_000 / 12 * 0.10)      # 7,00 $ : mensuel de la période précédente57    assert f["m_immobilisations"] == pytest.approx(0.05 * 60_000 * 0.10 / 12)           # 25,00 $58    assert f["m_services"] == pytest.approx(1_200 * 0.10 / 12)                          # 10,00 $59    assert f["total_brut"] == pytest.approx(30 + 14 + 7 + 25 + 10)60    assert f["nouveau_loyer"] == 1086 and f["augmentation"] == 866162def test_exemple_logement_103_valeurs_citees():63    ex = c.exemple_fixation(103)64    assert ex["loyer"] == 728 and ex["marche"] == 1047 and ex["ecart_marche"] == 31965    assert ex["revenus"] == pytest.approx(174_900) and ex["part"] == pytest.approx(0.04995, abs=1e-4)66    A, B = ex["A"], ex["B"]67    assert A["base"] == pytest.approx(22.568, abs=0.001)68    assert A["m_taxes_mun"] == 0 and A["m_taxes_scol"] == 0 and A["m_assurances"] == 0 and A["m_immobilisations"] == 069    assert A["nouveau_loyer"] == 751 and A["augmentation"] == 2370    assert B["net_taxes_mun"] == pytest.approx(0.049) and B["m_taxes_mun"] == pytest.approx(2.17, abs=0.01)71    assert B["net_assurances"] == pytest.approx(0.069) and B["m_assurances"] == pytest.approx(3.62, abs=0.01)72    assert B["m_immobilisations"] == pytest.approx(17.69, abs=0.01)73    assert B["total_brut"] == pytest.approx(46.05, abs=0.01) and B["nouveau_loyer"] == 77474    assert ex["ancienne_A"]["nouveau_loyer"] == 755 and ex["ancienne_B"]["nouveau_loyer"] == 78675    assert ex["annees_rattrapage_base"] == pytest.approx(11.9, abs=0.05)7677# ------------------------------------------------------------------ 3. trajectoire espérée7879def test_forme_fermee_egale_recursion_et_simulation():80    q0, rho, gc, gm, T = 0.695, 0.15, 0.031, 0.035, 1081    delta = (1 + gc) / (1 + gm)82    q, rec = q0, [q0]83    for _ in range(T):84        q = (1 - rho) * q * delta + rho * 1.0; rec.append(q)85    ff = c.ratio_espere(q0, rho, gc, gm, np.arange(T + 1))86    assert np.allclose(ff, rec, atol=1e-12)87    sim = c.simulation_ratio(q0, rho, gc, gm, T, n=20_000)88    assert np.abs(sim - ff).max() < 0.00689    assert ff[-1] == pytest.approx(0.9249, abs=0.0005)9091def test_limite_et_monotonie():92    gc, gm = 0.031, 0.03593    for rho in (0.05, 0.15, 0.25):94        qi = c.ratio_limite(rho, gc, gm)95        assert c.ratio_espere(0.5, rho, gc, gm, 400) == pytest.approx(qi, abs=1e-9)96        assert c.ratio_espere(1.0, rho, gc, gm, 400) == pytest.approx(qi, abs=1e-9)97    assert c.ratio_limite(0.05, gc, gm) < c.ratio_limite(0.15, gc, gm) < c.ratio_limite(0.25, gc, gm) < 198    assert c.ratio_limite(0.15, gc, gm) == pytest.approx(0.9786, abs=0.0001)99    assert c.ratio_limite(0.05, gc, gm) == pytest.approx(0.9316, abs=0.0001)100    assert c.ratio_limite(0.25, gc, gm) == pytest.approx(0.9885, abs=0.0001)101    assert c.ratio_limite(0.15, gm, gm) == pytest.approx(1.0)      # g_c = g_m : aucun écart limite102103def test_trajectoire_cas3(D):104    tr = c.trajectoire_cas3_esperee(D)105    assert tr["rbp_marche"].iloc[0] == pytest.approx(186_036) and tr["rbp_esperance"].iloc[0] == pytest.approx(165_540)106    assert tr["ratio_deterministe"].iloc[-1] == pytest.approx(0.9904, abs=0.0005) and tr["ratio_esperance"].iloc[-1] == pytest.approx(0.9618, abs=0.0005)107    assert (tr["rbp_sans_relocation"] <= tr["rbp_esperance"] + 1e-9).all() and (tr["rbp_esperance"] <= tr["rbp_deterministe"] + 1e-6).all()108109# ------------------------------------------------------------------ 4. scénarios110111def test_projection_locale_reproduit_module_commun(D):112    ref = cas3.projection_dcf(D["etat"], D["dep"], D["hyp"])113    loc = c.projection_scenario(D["etat"], D["dep"], D["hyp"])114    for col in ("rbp", "rbp_marche", "rbe", "depenses", "reserve", "rne", "capex", "flux_avant_dette"):115        assert np.allclose(ref[col].values, loc[col].values, atol=1e-6), col116    assert (loc["logements_reloues"].iloc[1:] == 2).all()117118def test_valeur_base_et_scenario_ch30(D):119    assert c.valeur_scenario(D)["valeur"] == pytest.approx(2_208_566, abs=1)120    assert c.valeur_scenario(D, g_c=0.02, rotation=0.08, g_m=0.03)["valeur"] == pytest.approx(1_912_046, abs=1)   # tab. 30.x « encadrement serré »121122def test_grille_monotone_et_valeurs_citees(D):123    g = c.grille_scenarios(D)124    assert len(g) == 9125    for b in (0.02, 0.031, 0.04):126        v = g[g["base"] == b].sort_values("rotation")["valeur"].values127        assert v[0] < v[1] < v[2]128    for r in (0.05, 0.15, 0.25):129        v = g[g["rotation"] == r].sort_values("base")["valeur"].values130        assert v[0] < v[1] < v[2]131    cell = lambda r, b: g[(g["rotation"] == r) & (g["base"] == b)]["valeur"].iloc[0]132    assert cell(0.15, 0.031) == pytest.approx(2_208_566, abs=1)133    assert cell(0.05, 0.02) == pytest.approx(1_974_822, abs=1)134    assert cell(0.25, 0.04) == pytest.approx(2_340_208, abs=1)135    assert cell(0.05, 0.031) == pytest.approx(2_140_916, abs=1)136    assert cell(0.25, 0.031) == pytest.approx(2_231_655, abs=1)137    assert cell(0.15, 0.02) == pytest.approx(2_118_624, abs=1)138    assert cell(0.15, 0.04) == pytest.approx(2_326_053, abs=1)139140def test_scenarios_complementaires(D):141    s = c.scenarios_complementaires(D).set_index("scenario")142    V0 = D["dcf"]["valeur"]143    assert s.loc["Indexation seule (aucune relocation)", "valeur"] == pytest.approx(1_756_414, abs=1)144    assert s.loc["Rattrapage partiel à la relocation (φ = 0,5)", "valeur"] == pytest.approx(2_056_076, abs=1)145    assert s.loc["Encadrement renforcé (base 2 %, rotation 8 %, φ = 0,5, marché 3 %)", "valeur"] == pytest.approx(1_701_507, abs=1)146    assert s.loc["Récupération de 5 % des dépenses d'immobilisation", "valeur"] == pytest.approx(2_289_503, abs=1)147    assert s.loc["Base, mode espérance (rotation fractionnaire)", "valeur"] == pytest.approx(2_091_629, abs=1)148    assert s.loc["Tout au marché dès l'année 1 (borne supérieure théorique)", "valeur"] == pytest.approx(2_267_398, abs=1)149    # ordre attendu150    assert s.loc["Encadrement renforcé (base 2 %, rotation 8 %, φ = 0,5, marché 3 %)", "valeur"] < s.loc["Indexation seule (aucune relocation)", "valeur"] < \151           s.loc["Encadrement serré (ch. 30 : base 2 %, rotation 8 %, marché 3 %)", "valeur"] < V0152    # VA de l'écart au marché : positive et décroissante avec la rotation153    assert s.loc["Indexation seule (aucune relocation)", "va_ecart_marche"] > s.loc["Base (dossier)", "va_ecart_marche"] > 0154155def test_rattrapage_partiel_et_recuperation(D):156    p1 = c.projection_scenario(D["etat"], D["dep"], D["hyp"], fraction_rattrapage=0.5)157    p0 = c.projection_scenario(D["etat"], D["dep"], D["hyp"])158    assert (p1["rbp"].iloc[1:] < p0["rbp"].iloc[1:]).all()159    pr = c.projection_scenario(D["etat"], D["dep"], D["hyp"], recuperation_capex=True)160    # la récupération n'agit qu'à partir de l'année suivant le premier CAPEX (2027 → 2028, année 3)161    assert np.allclose(pr["rbp"].iloc[:2], p0["rbp"].iloc[:2]) and (pr["rbp"].iloc[2:] > p0["rbp"].iloc[2:]).all()162    # hausse de l'année 3 ≈ 5 % × 85 000 $ = 4 250 $ (avant indexation ultérieure)163    assert pr["rbp"].iloc[2] - p0["rbp"].iloc[2] == pytest.approx(0.05 * 85_000, rel=0.02)164165def test_elasticites(D):166    e = c.elasticites(D)167    assert e["dV_par_point_rotation"] > 0 and e["dV_par_point_base"] > 0168    assert e["dV_par_point_rotation"] == pytest.approx(11_877, rel=0.01)169    assert e["dV_par_point_base"] == pytest.approx(131_236, rel=0.01)170171# ------------------------------------------------------------------ 5. exercices172173def test_exercices():174    e = c.exercices()175    assert e["ex1"]["pct"] == pytest.approx(0.03084, abs=1e-5)176    assert e["ex2"]["montant"] == pytest.approx(10.12, abs=0.01) and e["ex2"]["montant_si_2pct"] == 0.0177    assert e["ex3"]["fixation"]["nouveau_loyer"] == 1194178    assert e["ex4"]["q_inf"] == pytest.approx(0.9664, abs=0.0001) and e["ex4"]["survie_10"] == pytest.approx(0.3487, abs=0.0001)179    assert e["ex5"]["rot10_base25"] == pytest.approx(2_049_073, abs=1) and e["ex5"]["rot10_base25_phi05"] == pytest.approx(1_829_294, abs=1)180    assert e["ex5"]["rot10_base25_recup"] == pytest.approx(2_148_793, abs=1)181    assert e["ex6"]["indemnite"] == 13_104 and e["ex6"]["recuperation_ans"] == pytest.approx(3.8, abs=0.05)182    assert e["ex7"]["dq_drho_5"] > e["ex7"]["dq_drho_15"] > 0183
Files · 98fa933b6a1cb092e58d4abad01e8e47ddf90324
Menu

Theme