SPB Git
14.3 KB · 221 lines python
1"""Tests du chapitre 25 — Évaluation du terrain : jeu synthétique de terrains (graine 20261031), modèle hédonique des terrains,2comparaison directe (grille et réconciliation), allocation et prix implicite marginal (cas 1), extraction (cinq comparables du ch. 16),3technique résiduelle (cas 3), lotissement actualisé, rente foncière, valeur retenue et exercices.4Tolérances : 1 $ sur les montants recalculés ; 0,5 $ ou 0,05 % sur les valeurs arrondies citées ; 1e-9 sur les identités."""5import sys, pathlib, math6import numpy as np, pytest7sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "cases"))8import ch25_terrain as cas9from traite import finance as fin101112@pytest.fixture(scope="module")13def S():14    return cas.synthese()151617class TestJeuTerrains:18    def test_reproductibilite(self, S):19        df = S["comparaison"]["terrains"]20        regen = cas.generer_terrains()21        assert len(df) == 200 and len(regen) == 20022        assert int(df["desservi"].sum()) == 15623        assert df["prix_vente"].sum() == pytest.approx(52_123_500.0, abs=0.5)24        assert np.allclose(df["prix_vente"].values, regen["prix_vente"].values)25        assert df.iloc[0]["id_vente"] == "T001" and df.iloc[0]["prix_vente"] == 180_500.026        assert (df["prix_vente"] % 500 == 0).all()2728    def test_structure(self, S):29        df = S["comparaison"]["terrains"]30        assert df.groupby("secteur").size().to_dict() == {"Aylmer": 40, "Buckingham": 15, "Gatineau": 60, "Hull": 70, "Masson-Angers": 15}31        assert df["date_vente"].min() >= np.datetime64("2023-01-01") and df["date_vente"].max() <= np.datetime64("2026-08-31")32        # valeur unitaire décroissante avec la superficie, desservis plus chers au m²33        d1, d0 = df[df["desservi"] == 1], df[df["desservi"] == 0]34        assert d1["prix_m2"].median() > 2 * d0["prix_m2"].median()35        assert np.corrcoef(np.log(d1["superficie_m2"]), np.log(d1["prix_m2"]))[0, 1] < -0.5363738class TestHedoniqueTerrains:39    def test_recuperation_des_lois(self, S):40        m = S["comparaison"]["modele"]; b = m["beta"]41        assert m["n"] == 200 and 0.85 < m["r2"] < 0.9542        assert b["ln_T"] == pytest.approx(cas.ELAST_T, abs=0.07)43        assert b["non_desservi"] == pytest.approx(cas.B_NON_DESSERVI, abs=0.08)44        assert b["roc"] < b["pente"] < 0 and b["logements_sup"] > 0.0845        assert b["sect_Aylmer"] > 0 > b["sect_Buckingham"] and b["mois"] > 046        assert b["ln_T"] == pytest.approx(0.5267, abs=5e-4) and b["mois"] == pytest.approx(0.0028, abs=5e-4)4748    def test_prediction_sujet(self, S):49        assert S["comparaison"]["prediction_hedonique"] == pytest.approx(253_412, abs=1)505152class TestComparaison:53    def test_entonnoir_et_selection(self, S):54        sel = S["comparaison"]["selection"]55        assert sel["etapes"] == {"total": 200, "fenetre": 101, "rayon": 29, "desservi_unifamilial": 21, "superficie": 20}56        assert sel["comparables"]["id_vente"].tolist() == ["T059", "T004", "T030", "T038", "T049"]57        assert (sel["comparables"]["secteur"] == "Hull").all() and (sel["comparables"]["desservi"] == 1).all()5859    def test_grille_identites(self, S):60        g = S["comparaison"]["grille"]61        for _, r in g.iterrows():62            assert r["prix_ajuste"] == pytest.approx(r["prix_vente"] + sum(r[k] for k in cas.GROUPES), rel=1e-12)63            assert r["ajust_net_pct"] == pytest.approx((r["prix_ajuste"] - r["prix_vente"]) / r["prix_vente"], rel=1e-9)64            assert r["ajust_brut_pct"] >= abs(r["ajust_net_pct"]) - 1e-1265        assert (g["localisation"] == 0).all()   # tous les comparables sont dans le secteur du sujet66        assert g.set_index("id_vente").loc["T049", "forme"] == pytest.approx(9_046, abs=1)67        assert g["prix_ajuste"].round(0).tolist() == [265_383, 259_943, 274_166, 294_813, 232_086]6869    def test_reconciliation(self, S):70        r = S["comparaison"]["reconciliation"]71        assert sum(r["poids"]) == pytest.approx(1.0, abs=1e-12)72        assert np.round(r["poids"], 3).tolist() == [0.402, 0.131, 0.217, 0.146, 0.104]73        assert r["valeur_ponderee"] == pytest.approx(267_388, abs=1)74        assert r["moyenne"] == pytest.approx(265_278, abs=1) and r["mediane"] == pytest.approx(265_383, abs=1)75        assert r["min"] == pytest.approx(232_086, abs=1) and r["max"] == pytest.approx(294_813, abs=1)76        assert r["etendue_pct"] == pytest.approx(0.235, abs=1e-3)77        assert S["comparaison"]["valeur_m2"] == pytest.approx(431.3, abs=0.1)787980class TestAllocationExtraction:81    def test_allocation(self, S):82        a = S["allocation"]; e = S["extraction"]83        assert a["ratio"] == pytest.approx(0.482, abs=5e-4) and e["part_hull_q1"] == pytest.approx(0.424, abs=5e-4) and e["part_hull_q3"] == pytest.approx(0.543, abs=5e-4)84        assert a["P_grille"] == pytest.approx(581_595, abs=1) and a["P_hed"] == pytest.approx(637_953, abs=1)85        assert a["sur_grille"] == pytest.approx(280_412, abs=1) and a["sur_hedonique"] == pytest.approx(307_584, abs=1)86        assert a["sur_grille"] == pytest.approx(a["ratio"] * a["P_grille"], rel=1e-12)8788    def test_prix_implicite_marginal(self, S):89        a = S["allocation"]90        assert a["beta_ln_T"] == pytest.approx(0.0937, abs=5e-4)91        assert a["prix_marginal_m2"] == pytest.approx(96.4, abs=0.1)92        assert a["contribution_marginale"] == pytest.approx(59_750, abs=1)93        assert a["prix_marginal_m2"] * 620 == pytest.approx(a["contribution_marginale"], rel=1e-12)94        assert a["sujet_age_factor"] == pytest.approx(0.8852, abs=5e-4) and a["delta_marche_sujet"] == pytest.approx(0.208, abs=5e-4)9596    def test_extraction_comparables(self, S):97        e = S["extraction"]["comparables"].set_index("id_vente")98        # identités : V_L = P − C_N(1 − δ) sous les deux conventions99        for k, r in e.iterrows():100            assert r["V_L_agevie"] == pytest.approx(r["prix_vente"] - r["C_N"] * (1 - r["age"] / cas.VIE_ECONOMIQUE), rel=1e-12)101            assert r["V_L_marche"] == pytest.approx(r["prix_vente"] - r["C_N"] * (1 - r["delta_marche"]), rel=1e-9)102            assert r["V_L_marche"] == pytest.approx(r["P_neuf"] - r["C_N"], rel=1e-12)103        g = e.loc["G0477"]104        assert g["C_N"] == pytest.approx(391_729, abs=1) and g["delta_agevie"] == pytest.approx(0.40, abs=1e-9)105        assert g["V_L_agevie"] == pytest.approx(302_963, abs=1) and g["V_L_marche"] == pytest.approx(207_093, abs=1)106        assert g["P_neuf"] == pytest.approx(598_821, abs=1) and g["delta_marche"] == pytest.approx(0.155, abs=5e-4)107        assert e.loc["G0574", "V_L_marche"] == pytest.approx(281_458, abs=1) and e.loc["G0404", "V_L_marche"] == pytest.approx(174_883, abs=1)108        assert e.loc["G0575", "V_L_marche_m2"] == pytest.approx(618.7, abs=0.1)109        assert S["extraction"]["indication_marche"] == pytest.approx(242_545, abs=1)110        assert S["extraction"]["indication_agevie"] == pytest.approx(354_826, abs=1)111        assert S["extraction"]["V_L_hull_mediane"] == pytest.approx(243_421, abs=1)112113    def test_cout_neuf_comparable(self):114        assert cas.cout_neuf_comparable(165, 3) == pytest.approx(cas.C_N_SUJET, rel=1e-12)115        assert cas.cout_neuf_comparable(146, 4) == pytest.approx(391_729, abs=1)116117118class TestResiduelle:119    def test_cas_de_base(self, S):120        r = S["residuelle"]121        assert r["noi"] == pytest.approx(89_955.75, abs=0.01) and r["R"] == pytest.approx(0.04415, abs=1e-6)122        assert r["C_N"] == pytest.approx(2_375_000, abs=0.01) and r["C_B"] == pytest.approx(1_140_000, abs=0.01)123        assert r["R_B"] == pytest.approx(0.0625, abs=1e-12) and r["revenu_batiment"] == pytest.approx(71_250, abs=0.01)124        assert r["revenu_terrain"] == pytest.approx(18_705.75, abs=0.01)125        assert r["V_L"] == pytest.approx(498_820, abs=1) and r["V_L_m2"] == pytest.approx(674.1, abs=0.1)126        # identité de la bande d'investissement : RNE = R_L V_L + R_B C_B127        assert r["R_L"] * r["V_L"] + r["R_B"] * r["C_B"] == pytest.approx(r["noi"], rel=1e-12)128        assert r["somme"] == pytest.approx(1_638_820, abs=1) and r["ecart_vs_cap"] == pytest.approx(-0.196, abs=1e-3)129        assert r["V_cap"] == pytest.approx(2_037_503, abs=1) and r["V_cap"] - r["C_B"] == pytest.approx(897_503, abs=1)130        assert r["R_L_implicite"] == pytest.approx(0.0208, abs=5e-4) and r["part_terrain"] == pytest.approx(0.304, abs=5e-4)131132    def test_sensibilite(self, S):133        s = cas.sensibilite_residuelle(S["cas3"]).set_index(["taux_depreciation", "n_restante", "R_L"])["V_L"]134        assert s.loc[(0.44, 30, 0.0425)] == pytest.approx(-256_531, abs=1)   # valeur négative : la technique « casse »135        assert s.loc[(0.52, 40, 0.0425)] == pytest.approx(306_018, abs=1)136        assert s.loc[(0.52, 40, 0.0325)] == pytest.approx(750_946, abs=1)137        assert s.loc[(0.60, 50, 0.0325)] == pytest.approx(1_233_254, abs=1)138        assert s.loc[(0.52, 30, 0.0375)] == pytest.approx(245_487, abs=1) and s.loc[(0.52, 50, 0.0375)] == pytest.approx(650_820, abs=1)139        # effet de levier : −50 pb sur R_L (3,75 → 4,25 %) retranche 38,6 % à V_L140        assert s.loc[(0.52, 40, 0.0425)] / s.loc[(0.52, 40, 0.0375)] - 1 == pytest.approx(-0.386, abs=1e-3)141142143class TestLotissement:144    def test_cas_de_base(self, S):145        L = S["lotissement"]; p = L["params"]146        assert p["n_lots"] * p["lot_m2"] + p["rue_m2"] + p["parc_m2"] == p["site_m2"]147        assert p["parc_m2"] / p["site_m2"] == pytest.approx(0.10, abs=1e-12)   # plafond de l'art. 117.4 LAU148        assert L["prix_lot"] == pytest.approx(254_486, abs=1) and L["produit_brut"] == pytest.approx(3_053_830, abs=1)149        assert L["n_ans"] == 3 and len(L["flux"]) == 4150        assert L["valeur_actualisee"] == pytest.approx(fin.van(L["flux"], p["taux"]), rel=1e-12)151        assert L["valeur_actualisee"] == pytest.approx(1_230_878, abs=1) and L["valeur_m2"] == pytest.approx(123.1, abs=0.1)152        assert L["valeur_statique"] == pytest.approx(1_579_602, abs=1) and L["valeur_statique_m2"] == pytest.approx(158.0, abs=0.1)153        assert L["ratio_terrain_brut_sur_produit"] == pytest.approx(0.403, abs=1e-3)154        d = L["detail"]155        assert d["revenus"].sum() == pytest.approx(L["produit_brut"], rel=1e-12)156        assert d.loc[1, "infrastructure"] == pytest.approx(780_000, abs=0.01) and d.loc[1, "profit"] == pytest.approx(152_692, abs=1)157        assert d["flux"].round(0).tolist() == [-90_000, 32_534, 816_534, 820_534]158159    def test_sensibilite(self, S):160        sl = cas.sensibilite_lotissement(S["comparaison"]["modele"])161        assert sl["absorption"][2] == pytest.approx(963_545, abs=1) and sl["absorption"][6] == pytest.approx(1_331_556, abs=1)162        assert sl["taux"][0.08] == pytest.approx(1_291_537, abs=1) and sl["taux"][0.15] == pytest.approx(1_095_222, abs=1)163        assert sl["prix"][-0.10] == pytest.approx(1_025_828, abs=1) and sl["prix"][0.10] == pytest.approx(1_435_927, abs=1)164        assert sl["infrastructure"][80_000] == pytest.approx(1_067_241, abs=1) and sl["profit"][0.20] == pytest.approx(1_104_304, abs=1)165        # effet de levier du prix : ±10 % sur les lots → ±16,7 % sur le site brut166        assert sl["prix"][0.10] / sl["base"] - 1 == pytest.approx(0.1666, abs=5e-4)167168169class TestRenteEtSynthese:170    def test_rente(self, S):171        r = S["rente"]172        assert r["perpetuelle"] == pytest.approx(220_000, abs=0.01) and r["gordon"] == pytest.approx(366_667, abs=1)173        assert r["bail_n"] == pytest.approx(294_278, abs=1) and r["va_reversion"] == pytest.approx(42_614, abs=1) and r["va_rentes"] == pytest.approx(251_664, abs=1)174        assert r["gordon"] == pytest.approx(11_000 / (0.05 - 0.02), rel=1e-12)175176    def test_valeur_retenue(self, S):177        assert S["valeur_retenue"] == 265_000178        assert S["valeur_retenue"] / 620 == pytest.approx(427.4, abs=0.1)179        assert S["part_retenue_grille"] == pytest.approx(0.456, abs=5e-4) and S["part_retenue_hed"] == pytest.approx(0.415, abs=5e-4)180        # cohérence : la valeur retenue est encadrée par les indications d'extraction (marché) et d'allocation (grille)181        assert S["extraction"]["indication_marche"] < S["valeur_retenue"] < S["allocation"]["sur_grille"]182183184class TestExercices:185    def test_ex1_unites(self):186        P, T, F = [240_000, 268_000, 227_500], [560, 720, 480], [17, 20, 15]187        pm = [p / t for p, t in zip(P, T)]; pf = [p / f for p, f in zip(P, F)]188        assert [round(x, 1) for x in pm] == [428.6, 372.2, 474.0] and [round(x) for x in pf] == [14_118, 13_400, 15_167]189        assert float(np.median(pm)) * 620 == pytest.approx(265_714, abs=1) and float(np.median(pf)) * 20 == pytest.approx(282_353, abs=1)190191    def test_ex2_allocation(self):192        assert 0.45 * 560_000 == pytest.approx(252_000) and 180_000 / 480_000 == pytest.approx(0.375)193194    def test_ex3_extraction(self, S):195        g = S["extraction"]["comparables"].set_index("id_vente").loc["G0477"]196        assert g["V_L_agevie"] == pytest.approx(302_963, abs=1) and g["V_L_marche"] == pytest.approx(207_093, abs=1)197        assert g["V_L_agevie"] / 452 == pytest.approx(670.3, abs=0.1) and g["V_L_marche"] / 452 == pytest.approx(458.2, abs=0.1)198        assert g["V_L_agevie_sujet"] == pytest.approx(354_826, abs=1) and g["V_L_marche_sujet"] == pytest.approx(242_545, abs=1)199200    def test_ex4_residuelle(self, S):201        r0 = S["residuelle"]; r1 = cas.residuelle_cas3({"R_L": 0.0425}, S["cas3"])202        assert r0["V_L"] == pytest.approx(498_820, abs=1) and r1["V_L"] == pytest.approx(306_018, abs=1) and r1["R_B"] == pytest.approx(0.0675, abs=1e-12)203        assert r1["V_L"] / r0["V_L"] - 1 == pytest.approx(-0.386, abs=1e-3)204205    def test_ex5_superficie(self):206        assert 283_500 * (620 / 743.1) ** 0.5 == pytest.approx(258_956, abs=1)207        assert 283_500 / 743.1 * 620 == pytest.approx(236_536, abs=1)208209    def test_ex6_lotissement(self, S):210        m = S["comparaison"]["modele"]; L = S["lotissement"]211        L2 = cas.lotissement(m, {"lots_par_an": 2, "taux": 0.12})212        assert L2["n_ans"] == 6 and L2["valeur_actualisee"] == pytest.approx(877_093, abs=1)213        assert L2["valeur_actualisee"] / L["valeur_actualisee"] - 1 == pytest.approx(-0.287, abs=1e-3)214        perte_parc = 1.6 * L["prix_lot"] * (1 - 0.04 - 0.15) / 1.1 ** 3215        assert perte_parc == pytest.approx(247_794, abs=1)216217    def test_ex7_rente(self, S):218        r = cas.rente_fonciere(rente=11_000, R_L=0.05, g=0.02, n=40, valeur_reversion=300_000)219        assert r["bail_n"] == pytest.approx(294_278, abs=1)220        assert cas.rente_fonciere(g=0.0, n=40)["va_rentes"] == pytest.approx(11_000 * fin.facteur_va_annuite(0.05, 40), rel=1e-9)221
Files · d49e547b9b0535e034d80fed31849741adb255e2
Menu

Theme