SPB Git
13.8 KB · 184 lines python
1"""Tests du chapitre 27 — revenus potentiels, vacance et pertes (cas 3 et cas 2, données synthétiques ; tableaux SCHL réels).2Identités (RBE, cascade, modèle frictionnel, concessions, trajectoires) + valeurs citées dans manuscript/chapters/ch27.tex et3solutions/ch27.tex (tolérances déclarées : 0,01 $ sur les montants, 1e-6 sur les taux, 0,05 point sur les lectures SCHL)."""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 np, pytest8from traite import cas2, cas3, finance as fin9import ch27_revenus as c1011ROOT = pathlib.Path(__file__).resolve().parents[3]121314@pytest.fixture(scope="module")15def res():16    return c.calculer()171819# ----------------------------------------------------------------------------- bibliothèque : fonctions ajoutées pour le chapitre20def test_vacance_frictionnelle_identites():21    assert fin.taux_vacance_frictionnelle(0.15, 1.0) == pytest.approx(0.0125)22    assert fin.taux_vacance_frictionnelle(0.094, 12.0) == pytest.approx(0.094)       # une année entière de vacance = la rotation23    assert fin.taux_vacance_frictionnelle(0.0, 3.0) == 0.0 and fin.taux_vacance_frictionnelle(0.2, 0.0) == 0.024    # linéarité : doubler la durée ou la rotation double la provision25    assert fin.taux_vacance_frictionnelle(0.1, 2.0) == pytest.approx(2 * fin.taux_vacance_frictionnelle(0.1, 1.0)) == pytest.approx(2 * fin.taux_vacance_frictionnelle(0.05, 2.0))26    with pytest.raises(ValueError):27        fin.taux_vacance_frictionnelle(-0.1, 1.0)282930def test_loyer_effectif_concession():31    assert fin.loyer_effectif_concession(1300, 1, 12) == pytest.approx(1300 * 11 / 12)32    assert fin.loyer_effectif_concession(1300, 0, 12) == 1300 and fin.loyer_effectif_concession(1300, 12, 12) == 033    # un mois sur douze ≡ deux mois sur vingt-quatre34    assert fin.loyer_effectif_concession(1300, 1, 12) == pytest.approx(fin.loyer_effectif_concession(1300, 2, 24))35    with pytest.raises(ValueError):36        fin.loyer_effectif_concession(1300, 13, 12)373839# ----------------------------------------------------------------------------- cas 3 : identités et valeurs citées40def test_cascade_cas3_identites(res):41    r = res["cas3"]42    assert r["rbp_contrat"] == pytest.approx(12 * r["etat"]["loyer_mensuel_contrat"].sum())43    assert r["rbp_marche"] == pytest.approx(12 * r["etat"]["loyer_mensuel_marche"].sum())44    assert r["rbe"] == pytest.approx(fin.revenu_brut_effectif(r["rbp_contrat"], r["taux_vacance_pertes"], r["autres_revenus"]))45    assert r["rbe"] == pytest.approx(r["loyers_effectifs"] + r["autres_revenus"])46    assert r["loyers_effectifs"] == pytest.approx(r["rbp_contrat"] - r["vacance_pertes"])47    assert r["rbp_marche"] - r["deficit_loyer"] == pytest.approx(r["rbp_contrat"])48    assert r["taux_occupation_economique"] == pytest.approx((1 - r["taux_vacance_pertes"]) * r["rbp_contrat_sur_marche"])49    # cohérence avec le module partagé (ch. 29, 30, 47)50    t = cas3.tout()51    assert r["rbe"] == pytest.approx(t["base"]["rbe"], rel=1e-12) and r["rbp_contrat"] == pytest.approx(t["base"]["rbp_contrat"], rel=1e-12)525354def test_valeurs_citees_cas3(res):55    r = res["cas3"]56    assert r["loyer_mensuel_contrat"] == 13795 and r["loyer_mensuel_marche"] == 1550357    assert r["rbp_contrat"] == pytest.approx(165_540, abs=0.01) and r["rbp_marche"] == pytest.approx(186_036, abs=0.01)58    assert r["deficit_loyer"] == pytest.approx(20_496, abs=0.01) and r["deficit_pct"] == pytest.approx(0.1102, abs=5e-5)59    assert r["vacance_pertes"] == pytest.approx(6_456.06, abs=0.01) and r["loyers_effectifs"] == pytest.approx(159_083.94, abs=0.01)60    assert r["autres_revenus"] == pytest.approx(9_360, abs=0.01) and r["autres_par_logement_mois"] == pytest.approx(65, abs=0.01)61    assert r["rbe"] == pytest.approx(168_443.94, abs=0.01) and r["rbe_sur_rbp"] == pytest.approx(1.0175, abs=5e-5)62    assert r["rbe_par_logement"] == pytest.approx(14_037.00, abs=0.01) and r["rbe_par_m2"] == pytest.approx(204.17, abs=0.01)63    assert r["taux_occupation_economique"] == pytest.approx(0.8551, abs=5e-5)64    assert r["rbe_marche"] == pytest.approx(188_140.60, abs=0.01)65    assert r["rbe_variante_v_sur_tout"] == pytest.approx(168_078.90, abs=0.01) and r["ecart_variante_v_sur_tout"] == pytest.approx(-365.04, abs=0.01)66    assert r["v_frictionnel_dossier"] == pytest.approx(0.0125, abs=1e-9) and r["v_residuel_dossier"] == pytest.approx(0.0265, abs=1e-9)676869def test_par_type_cas3(res):70    p = res["par_type"]71    assert p.loc["3½", "n"] == 4 and p.loc["4½", "n"] == 6 and p.loc["5½", "n"] == 272    assert p.loc["3½", "ecart_pct"] == pytest.approx(15.76, abs=0.01) and p.loc["4½", "ecart_pct"] == pytest.approx(11.07, abs=0.01)73    assert p.loc["5½", "ecart_pct"] == pytest.approx(4.93, abs=0.01) and p.loc["Total", "ecart_pct"] == pytest.approx(11.02, abs=0.01)74    assert p.loc["3½", "anciennete"] == pytest.approx(9.0) and p.loc["Total", "anciennete"] == pytest.approx(6.33, abs=0.01)75    assert p.loc["Total", "rbp_contrat"] == pytest.approx(165_540) and p.loc["Total", "rbp_marche"] == pytest.approx(186_036)767778# ----------------------------------------------------------------------------- cas 279def test_cascade_cas2(res):80    r = res["cas2"]81    t = cas2.tout()82    assert r["rbe"] == pytest.approx(t["base"]["rbe"], rel=1e-12)83    assert r["rbp_contrat"] == pytest.approx(41_832) and r["rbp_marche"] == pytest.approx(49_332) and r["ecart_marche"] == pytest.approx(7_500)84    assert r["deficit_pct"] == pytest.approx(0.1520, abs=5e-5) and r["vacance_pertes"] == pytest.approx(1_254.96, abs=0.01)85    assert r["rbe"] == pytest.approx(40_577.04, abs=0.01) and r["rbe_marche"] == pytest.approx(47_852.04, abs=0.01)86    assert r["taux_occupation_economique"] == pytest.approx(0.8225, abs=5e-5) and r["rbe_variante_occupant"] == pytest.approx(40_953.96, abs=0.01)87    assert r["rbe_par_logement"] == pytest.approx(13_525.68, abs=0.01)888990# ----------------------------------------------------------------------------- SCHL (réel) : lectures des tableaux91def test_schl_lectures(res):92    s = res["schl_rmr"]93    attendu = {"Montréal": (2.1, 2.9, 11.1, 9.4), "Québec": (0.9, 2.4, 13.9, 13.0), "Gatineau (partie QC)": (1.9, 3.8, 17.3, 16.2),94               "Sherbrooke": (1.4, 2.7, 10.4, 12.0), "Trois-Rivières": (0.9, 2.7, 10.2, 10.1), "Saguenay": (1.6, 1.3, 13.3, 12.6)}95    for k, (i24, i25, r24, r25) in attendu.items():96        assert (s.loc[k, "inocc_2024"], s.loc[k, "inocc_2025"], s.loc[k, "rotation_2024"], s.loc[k, "rotation_2025"]) == pytest.approx((i24, i25, r24, r25), abs=0.05)97    t = res["schl_taille"].set_index(["zone", "taille"])98    assert t.loc[("Île de Montréal", "6 à 19 unités"), "inocc_2025"] == pytest.approx(2.2) and t.loc[("Île de Montréal", "3 à 5 unités"), "inocc_2025"] == pytest.approx(2.3)99    assert t.loc[("Île de Montréal", "100 unités et +"), "inocc_2025"] == pytest.approx(5.1) and t.loc[("Île de Montréal", "Tous les immeubles"), "inocc_2025"] == pytest.approx(3.1)100    assert t.loc[("RMR de Montréal", "6 à 19 unités"), "rotation_2025"] == pytest.approx(8.5) and t.loc[("RMR de Montréal", "100 unités et +"), "rotation_2025"] == pytest.approx(14.2)101    assert t.loc[("RMR de Montréal", "Tous les immeubles"), "inocc_2025"] == pytest.approx(2.9)102    q = res["schl_quartiles"]103    assert (q.loc["Q1", "inocc_2025"], q.loc["Q4", "inocc_2025"]) == pytest.approx((1.3, 4.9))104    io = res["schl_inocc_occ"]105    assert (io.loc["Tous", "inoccupes"], io.loc["Tous", "occupes"]) == (1573, 1275) and io.loc["Tous", "ecart_pct"] == pytest.approx(23.4, abs=0.05)106    assert (io.loc["2 chambres", "inoccupes"], io.loc["2 chambres", "occupes"]) == (1815, 1349)107    a = res["schl_annee"]108    assert a.loc["2015 ou après", "inocc_2025"] == pytest.approx(5.6) and a.loc["Avant 1940", "inocc_2025"] == pytest.approx(2.2)109110111def test_ipc_loyer(res):112    i = res["ipc_loyer"]113    assert (i["sept2026"], i["sept2025"]) == pytest.approx((174.2, 168.6))114    assert i["var_12m"] == pytest.approx(0.0332, abs=5e-5) and i["cagr_10ans"] == pytest.approx(0.0402, abs=5e-5)115116117# ----------------------------------------------------------------------------- modèle frictionnel, sensibilité, trajectoires118def test_grille_et_sensibilite(res):119    g = res["grille_frictionnelle"]120    assert g.loc[9.4, 1.0] == pytest.approx(0.78, abs=0.005) and g.loc[13.0, 2.0] == pytest.approx(2.17, abs=0.005) and g.loc[15.0, 1.0] == pytest.approx(1.25)121    s = res["sensibilite_v"]; b = res["cas3"]122    assert s.attrs["R"] == pytest.approx(0.04415, abs=1e-6)123    assert s.attrs["dRBE_dv_pt"] == pytest.approx(-1_655.40, abs=0.01) and s.attrs["dRNE_dv_pt"] == pytest.approx(-1_589.18, abs=0.01)124    assert s.attrs["dV_dv_pt"] == pytest.approx(-35_995.11, abs=0.01)125    row = s.set_index("v")126    assert row.loc[0.039, "valeur"] == pytest.approx(2_037_502.89, abs=0.01)      # = ch. 29127    assert row.loc[0.029, "valeur"] == pytest.approx(2_073_497.99, abs=0.01) and row.loc[0.08, "valeur"] == pytest.approx(1_889_922.94, abs=0.01)128    assert row.loc[0.02, "rbe"] == pytest.approx(171_589.20, abs=0.01) and row.loc[0.06, "rne"] == pytest.approx(86_618.47, abs=0.01)129    # linéarité exacte en v : V(v) = V(0,039) + (v − 0,039)·100·dV/dv130    for v in (0.02, 0.05, 0.08):131        assert row.loc[v, "valeur"] == pytest.approx(row.loc[0.039, "valeur"] + (v - 0.039) * 100 * s.attrs["dV_dv_pt"], abs=0.01)132133134def test_trajectoires(res):135    tr = res["trajectoires"]; t = tr["table"].set_index("annee"); h = tr["hyp"]136    assert t.loc[1, "base_rot15"] == pytest.approx(165_540) and t.loc[1, "marche"] == pytest.approx(186_036)137    # indexation seule = RBP × 1,031^(t−1)138    for k in range(1, 12):139        assert t.loc[k, "tal_seul"] == pytest.approx(165_540 * 1.031 ** (k - 1), rel=1e-9)140    # ordre des trajectoires : TAL seul ≤ rotation 9,4 % ≤ rotation 15 % ≤ marché141    assert ((t["tal_seul"] <= t["rot_schl_094"] + 1e-6) & (t["rot_schl_094"] <= t["base_rot15"] + 1e-6) & (t["base_rot15"] <= t["marche"] + 1e-6)).all()142    # base = traite.cas3.projection_dcf (ch. 30/47)143    p = cas3.tout()["projection"]144    assert np.allclose(t["base_rot15"].values, p["rbp"].values) and np.allclose(t["marche"].values, p["rbp_marche"].values)145    assert tr["rbp_T"]["tal_seul"] == pytest.approx(217_886.81, abs=0.01) and tr["rbp_T"]["base_rot15"] == pytest.approx(251_101.39, abs=0.01)146    assert tr["rbp_T"]["rot_schl_094"] == pytest.approx(247_211.89, abs=0.01) and tr["rbp_T"]["marche"] == pytest.approx(253_547.97, abs=0.01)147    assert tr["cagr"]["base_rot15"] == pytest.approx(0.04738, abs=5e-6) and tr["cagr"]["rot_schl_094"] == pytest.approx(0.04557, abs=5e-6)148    assert t.loc[10, "ecart_base_pct"] == pytest.approx(0.96, abs=0.005) and t.loc[10, "ecart_tal_pct"] == pytest.approx(14.06, abs=0.005)149    assert t.loc[10, "ecart_rot094_pct"] == pytest.approx(2.50, abs=0.005) and t.loc[2, "base_rot15"] == pytest.approx(177_389.21, abs=0.01)150    assert tr["valeur_dcf"]["base_rot15"] == pytest.approx(2_208_565.91, abs=0.01)   # = ch. 30/47151    assert tr["valeur_dcf"]["tal_seul"] == pytest.approx(1_756_414.23, abs=0.01) and tr["valeur_dcf"]["rot_schl_094"] == pytest.approx(2_140_916.43, abs=0.01)152    assert tr["somme_rbp_1_T"]["base_rot15"] == pytest.approx(2_113_053.32, abs=0.01)153154155def test_mini_projection():156    mp = c.mini_projection([900, 950, 1000, 1100], 1200, 0.031, 0.035, 1, 4)157    assert mp.loc[0, "rbp"] == pytest.approx(47_400) and mp.loc[0, "rbp_marche"] == pytest.approx(57_600)158    assert mp.loc[1, "rbp"] == pytest.approx(52_638.60, abs=0.01)    # 900 → 1 242 ; autres × 1,031159    assert mp.loc[2, "rbp"] == pytest.approx(57_578.28, abs=0.01) and mp.loc[3, "rbp"] == pytest.approx(62_177.79, abs=0.01)160    assert (mp["rbp"] <= mp["rbp_marche"] + 1e-9).all()161162163# ----------------------------------------------------------------------------- exercices (solutions/ch27.tex)164def test_exercices(res):165    ex = res["exercices"]166    assert ex["1"]["rbp_c"] == 78_360 and ex["1"]["rbp_m"] == 87_000 and ex["1"]["deficit"] == 8_640 and ex["1"]["deficit_pct"] == pytest.approx(0.0993, abs=5e-5)167    assert ex["2"]["rbe_livre"] == pytest.approx(80_025.60, abs=0.01) and ex["2"]["rbe_v_sur_tout"] == pytest.approx(79_833.60, abs=0.01)168    assert ex["2"]["rbe_autres_demi"] == pytest.approx(79_929.60, abs=0.01) and ex["2"]["vacance"] == pytest.approx(3_134.40, abs=0.01)169    assert ex["3"]["vf_1"] == pytest.approx(0.010833, abs=1e-6) and ex["3"]["vf_15"] == pytest.approx(0.01625, abs=1e-9)170    assert ex["3"]["v_1"] == pytest.approx(0.015833, abs=1e-6) and ex["3"]["v_15"] == pytest.approx(0.02125, abs=1e-9) and ex["3"]["duree_implicite_schl"] == pytest.approx(2.215, abs=5e-4)171    assert ex["4"]["L_eff_1_12"] == pytest.approx(1_191.67, abs=0.01) and ex["4"]["rbe_correct"] == pytest.approx(14_300) and ex["4"]["double_compte"] == pytest.approx(13_108.33, abs=0.01)172    assert ex["5"]["rbe"] == pytest.approx(40_577.04, abs=0.01) and ex["5"]["rbe_variante_occupant"] == pytest.approx(40_953.96, abs=0.01)173    assert ex["6"]["rbp"] == pytest.approx([47_400, 52_638.60, 57_578.28, 62_177.79], abs=0.01)174    assert ex["6"]["tal_seul"][3] == pytest.approx(51_946.27, abs=0.01) and ex["6"]["ecart_pct"][3] == pytest.approx(0.0264, abs=5e-5)175    assert ex["7"]["V_029"] == pytest.approx(2_073_497.99, abs=0.01) and ex["7"]["V_059"] == pytest.approx(1_965_512.67, abs=0.01)176    assert ex["7"]["rne_029"] == pytest.approx(91_544.94, abs=0.01) and ex["7"]["rne_059"] == pytest.approx(86_777.38, abs=0.01)177    assert ex["7"]["approx_lineaire_2pts"] == pytest.approx(ex["7"]["V_059"] - ex["7"]["V_039"], abs=0.01)   # linéaire en v178    assert ex["8"]["ratio_tous"] == pytest.approx(1.2337, abs=5e-5) and ex["8"]["ratio_2ch"] == pytest.approx(1.3454, abs=5e-5) and ex["8"]["q4_sur_q1"] == pytest.approx(3.77, abs=0.005)179180181def test_figures_presentes():182    for slug in ("inoccupation-rmr", "cascade", "trajectoires", "sensibilite"):183        assert (ROOT / f"assets/figures/ch27/ch27-fig-{slug}.pdf").exists(), slug184
Files · 0b0a78499997700c9aa16d44b91243a1ef46c0bb
Menu

Theme