SPB Git
15.0 KB · 229 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_rapprochement_ch24(self, S):177        c = S["ch24"]; rp = S["rapprochement"]178        assert c["n"] == 13 and c["retenue"] == 277_000 and c["mediane"] == pytest.approx(276_954, abs=1) and c["part"] == pytest.approx(0.505, abs=5e-4)179        assert rp["ecart_comp_pct"] == pytest.approx(-0.035, abs=1e-3) and rp["ecart_alloc_pct"] == pytest.approx(0.012, abs=1e-3)180        assert rp["ecart_extr_marche_pct"] == pytest.approx(-0.124, abs=1e-3) and rp["ecart_hed_pct"] == pytest.approx(-0.085, abs=1e-3)181        assert abs(rp["z_comp"]) < 2 and rp["z_comp"] == pytest.approx(0.60, abs=0.01) and rp["confirmee"]182        assert rp["mediane_trois"] == pytest.approx(277_000, abs=0.01)183184    def test_valeur_retenue(self, S):185        assert S["valeur_retenue"] == 277_000 and S["valeur_retenue"] == S["ch24"]["retenue"]186        assert S["valeur_retenue"] / 620 == pytest.approx(446.8, abs=0.1)187        assert S["part_retenue_grille"] == pytest.approx(0.476, abs=5e-4) and S["part_retenue_hed"] == pytest.approx(0.434, abs=5e-4)188        # cohérence : la valeur retenue est encadrée par la comparaison directe et l'allocation sur la grille189        assert S["comparaison"]["reconciliation"]["valeur_ponderee"] < S["valeur_retenue"] < S["allocation"]["sur_grille"]190191192class TestExercices:193    def test_ex1_unites(self):194        P, T, F = [240_000, 268_000, 227_500], [560, 720, 480], [17, 20, 15]195        pm = [p / t for p, t in zip(P, T)]; pf = [p / f for p, f in zip(P, F)]196        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]197        assert float(np.median(pm)) * 620 == pytest.approx(265_714, abs=1) and float(np.median(pf)) * 20 == pytest.approx(282_353, abs=1)198199    def test_ex2_allocation(self):200        assert 0.45 * 560_000 == pytest.approx(252_000) and 180_000 / 480_000 == pytest.approx(0.375)201202    def test_ex3_extraction(self, S):203        g = S["extraction"]["comparables"].set_index("id_vente").loc["G0477"]204        assert g["V_L_agevie"] == pytest.approx(302_963, abs=1) and g["V_L_marche"] == pytest.approx(207_093, abs=1)205        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)206        assert g["V_L_agevie_sujet"] == pytest.approx(354_826, abs=1) and g["V_L_marche_sujet"] == pytest.approx(242_545, abs=1)207208    def test_ex4_residuelle(self, S):209        r0 = S["residuelle"]; r1 = cas.residuelle_cas3({"R_L": 0.0425}, S["cas3"])210        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)211        assert r1["V_L"] / r0["V_L"] - 1 == pytest.approx(-0.386, abs=1e-3)212213    def test_ex5_superficie(self):214        assert 283_500 * (620 / 743.1) ** 0.5 == pytest.approx(258_956, abs=1)215        assert 283_500 / 743.1 * 620 == pytest.approx(236_536, abs=1)216217    def test_ex6_lotissement(self, S):218        m = S["comparaison"]["modele"]; L = S["lotissement"]219        L2 = cas.lotissement(m, {"lots_par_an": 2, "taux": 0.12})220        assert L2["n_ans"] == 6 and L2["valeur_actualisee"] == pytest.approx(877_093, abs=1)221        assert L2["valeur_actualisee"] / L["valeur_actualisee"] - 1 == pytest.approx(-0.287, abs=1e-3)222        perte_parc = 1.6 * L["prix_lot"] * (1 - 0.04 - 0.15) / 1.1 ** 3223        assert perte_parc == pytest.approx(247_794, abs=1)224225    def test_ex7_rente(self, S):226        r = cas.rente_fonciere(rente=11_000, R_L=0.05, g=0.02, n=40, valeur_reversion=300_000)227        assert r["bail_n"] == pytest.approx(294_278, abs=1)228        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)229
Files · fd7f44c8789c8b7d81811c1898b290bc997b5c13
Menu

Theme