spb/traite-evaluation-qc
Public
1"""Tests du chapitre 8 — zonage et urbanisme (DONNÉES SYNTHÉTIQUES, graine 20261031).2Tolérances : 0,5 $ sur les montants cités arrondis au dollar ; 0,05 point de pourcentage sur les pourcentages cités avec deux3décimales ; 1e-9 sur les identités algébriques. Les valeurs « citées » sont celles de manuscript/chapters/ch08.tex et de4solutions/ch08.tex."""5import sys, pathlib, math6import numpy as np, pytest7sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "cases"))8import ch08_zonage as cas9from traite import comparables as cp1011TOL = 0.5121314@pytest.fixture(scope="module")15def res():16 return cas.main(ecrire=False)171819class TestDonneesSynthetiques:20 def test_structure(self, res):21 lots, ventes = res["lots"], res["ventes"]22 assert len(lots) == 400 and lots["zone_initiale"].value_counts().to_dict() == {"H1": 200, "C": 60, "H2": 60, "H3": 60, "P": 20}23 assert int(lots["rezone"].sum()) == 36 and set(lots.loc[lots["rezone"], "zone_apres_T0"]) == {"H2"}24 assert len(ventes) == 482 and int((ventes["zone_initiale"] == "H1").sum()) == 305 and int(ventes["traite_post"].sum()) == 2925 assert (ventes["prix_vente"] % 500 == 0).all()2627 def test_reproductibilite(self):28 l1, v1 = cas.charger_ou_generer(ecrire=False); l2, v2 = cas.charger_ou_generer(ecrire=False)29 assert v1.equals(v2) and l1.equals(l2)3031 def test_prime_injectee(self):32 assert cas.prime_rezonage(20) == pytest.approx(0.03) and cas.prime_rezonage(60) == pytest.approx(0.09) and cas.prime_rezonage(10) == pytest.approx(0.03)333435class TestPaires:36 def test_resume_cite(self, res):37 rp = res["resume_paires"]38 assert rp["n"] == 2939 assert rp["moyenne"] == pytest.approx(32285, abs=TOL) and rp["mediane"] == pytest.approx(19944, abs=TOL)40 assert rp["ecart_type"] == pytest.approx(51335, abs=TOL) and rp["erreur_type_moyenne"] == pytest.approx(9533, abs=TOL)41 assert rp["moyenne_pct"] * 100 == pytest.approx(9.29, abs=0.05) and rp["mediane_pct"] * 100 == pytest.approx(6.28, abs=0.05)42 assert rp["ic95_pct"][0] * 100 == pytest.approx(4.32, abs=0.05) and rp["ic95_pct"][1] * 100 == pytest.approx(14.26, abs=0.05)43 assert rp["n_jeunes"] == 16 and rp["n_vieux"] == 1344 assert rp["moyenne_pct_jeunes"] * 100 == pytest.approx(4.85, abs=0.05) and rp["moyenne_pct_vieux"] * 100 == pytest.approx(14.75, abs=0.05)45 assert res["prime_theorique_moyenne"] * 100 == pytest.approx(6.15, abs=0.05)4647 def test_identites_paires(self, res):48 p = res["paires"]49 assert (p["difference"] - (p["prix_traite"] - p["prix_temoin_ajuste"])).abs().max() < 1e-650 assert (p["prix_temoin_ajuste"] - (p["prix_temoin"] + p["ajust_temps"]) * p["facteur_physique"]).abs().max() < 1e-651 assert p["id_temoin"].is_unique and p["id_traite"].is_unique52 r = cp.ventes_appariees(p["prix_traite"].tolist(), p["prix_temoin_ajuste"].tolist())53 assert r["moyenne"] == pytest.approx(p["difference"].mean(), rel=1e-12)54 assert r["ecart_type"] / math.sqrt(r["n"]) == pytest.approx(res["resume_paires"]["erreur_type_moyenne"], rel=1e-12)5556 def test_ajustement_temps_coherent(self, res):57 p = res["paires"].iloc[0]58 from datetime import date59 mois = (date.fromisoformat(p["date_traite"]) - date.fromisoformat(p["date_temoin"])).days / 30.437560 assert p["ajust_temps"] == pytest.approx(cp.ajustement_temps_taux(p["prix_temoin"], cas.TAUX_MARCHE, mois), rel=1e-12)616263class TestDiD:64 def test_valeurs_citees(self, res):65 d = res["did"]66 assert d["n_obs"] == 305 and d["effectifs"] == {(False, False): 134, (False, True): 112, (True, False): 30, (True, True): 29}67 assert d["delta_2x2"] == pytest.approx(0.1061, abs=5e-5) and d["delta_2x2_pct"] * 100 == pytest.approx(11.19, abs=0.05)68 assert d["delta_reg"] == pytest.approx(0.0657, abs=5e-5) and d["se_delta"] == pytest.approx(0.0208, abs=5e-5)69 assert d["delta_reg_pct"] * 100 == pytest.approx(6.79, abs=0.05) and d["sigma_resid"] == pytest.approx(0.0709, abs=5e-5)70 assert d["coef"]["ln_hab"] == pytest.approx(0.4765, abs=5e-5) and d["coef"]["tendance"] == pytest.approx(0.0308, abs=5e-5)7172 def test_identite_2x2(self, res):73 d = res["did"]; m = d["moyennes_lnP"]74 assert d["delta_2x2"] == pytest.approx((m[(True, True)] - m[(True, False)]) - (m[(False, True)] - m[(False, False)]), rel=1e-12)75 # la régression retrouve, à l'erreur-type près, la prime moyenne injectée76 assert abs(d["delta_reg"] - math.log(1 + res["prime_theorique_moyenne"])) < 2 * d["se_delta"]777879class TestOutils:80 def test_grille(self, res):81 g = res["grille"]82 assert g["lot_conforme"] and g["logements_par_densite"] == 4 and g["logements_admissibles"] == 4 and g["aire_plancher_max_m2"] == pytest.approx(360.0)83 g2 = cas.densite_admissible(430.0, 150.0, 4, 0.6, 450.0)84 assert not g2["lot_conforme"] and g2["logements_admissibles"] == 28586 def test_redeveloppement(self, res):87 rd = res["redev"]88 assert rd["gdv"] == pytest.approx(2088000) and rd["couts_directs"] == pytest.approx(1044000) and rd["couts_indirects"] == pytest.approx(125280)89 assert rd["marge"] == pytest.approx(313200) and rd["financement"] == pytest.approx(62640) and rd["reste_avant_parcs"] == pytest.approx(512880)90 assert rd["valeur_residuelle"] == pytest.approx(466254.55, abs=0.01) and rd["contribution_parcs"] == pytest.approx(46625.45, abs=0.01)91 assert rd["valeur_residuelle"] + rd["contribution_parcs"] == pytest.approx(rd["reste_avant_parcs"], rel=1e-12)92 assert res["prime_option"] == pytest.approx(86254.55, abs=0.01) and res["prime_option_pct"] * 100 == pytest.approx(22.7, abs=0.05)9394 def test_grille_attendue(self, res):95 G = res["grille_attendue"]96 assert G.shape == (4, 4)97 assert G[2, 2] == pytest.approx(55462, abs=TOL) and G[3, 0] == pytest.approx(82998, abs=TOL) and G[0, 3] == pytest.approx(17118, abs=TOL)98 assert G[3, 0] == pytest.approx(res["prime_option"] * 1.08 ** -0.5, rel=1e-9)99 assert res["exemple_p75_T2"] == pytest.approx(427462, abs=TOL)100 assert res["exemple_p75_T2"] == pytest.approx(380000 + G[2, 2] - 8000, abs=TOL)101102 def test_droits_acquis(self, res):103 assert res["noi_duplex"] == pytest.approx(22620) and res["noi_maison"] == pytest.approx(19320) and res["delta_noi"] == pytest.approx(3300)104 attendu = {0.0: 60000.0, 0.01: 50769.23, 0.02: 44000.0, 0.05: 31428.57}105 for lam, v in attendu.items():106 assert res["v_delta"][lam] == pytest.approx(v, abs=0.01)107 assert res["v_delta_T"][0.0] == pytest.approx(44830, abs=TOL) and res["v_delta_T"][0.02] == pytest.approx(37252, abs=TOL)108 assert res["v_droits_acquis_lam02"] == pytest.approx(474000)109 # horizon infini = limite de l'horizon fini110 assert cas.valeur_surplus_horizon(3300, 0.055, 0.02, 1e6) == pytest.approx(cas.valeur_surplus_derogatoire(3300, 0.055, 0.02), rel=1e-9)111112 def test_parcs_et_agricole(self, res):113 assert res["parcs"]["terrain_max_m2"] == pytest.approx(240) and res["parcs"]["somme_max"] == pytest.approx(96000)114 assert res["v_agricole"] == pytest.approx(75000) and res["v_si_autorise"] == pytest.approx(157500) and res["v_attendue_agricole"] == pytest.approx(90796, abs=TOL)115116117class TestExercices:118 """Chiffres des solutions (solutions/ch08.tex)."""119120 def test_ex1_grille(self):121 g = cas.densite_admissible(540.0, 150.0, 4, 0.60, 450.0)122 assert g["lot_conforme"] and g["logements_admissibles"] == 3 and g["aire_plancher_max_m2"] == pytest.approx(324.0)123124 def test_ex2_parcs(self):125 c = cas.contribution_parcs(1250000.0, 3100.0)126 assert c["terrain_max_m2"] == pytest.approx(310) and c["somme_max"] == pytest.approx(125000)127128 def test_ex3_droits_acquis(self):129 d_noi = 3 * 12 * 1250 * (1 - 0.38) - 12 * 2600 * (1 - 0.30)130 assert d_noi == pytest.approx(6060.0)131 assert cas.valeur_surplus_derogatoire(d_noi, 0.06, 0.0) == pytest.approx(101000, abs=TOL)132 assert cas.valeur_surplus_derogatoire(d_noi, 0.06, 0.03) == pytest.approx(67333.33, abs=0.01)133 assert cas.valeur_surplus_horizon(d_noi, 0.06, 0.03, 20) == pytest.approx(56203, abs=TOL)134135 def test_ex4_did(self):136 m = {(False, False): 12.70, (False, True): 12.76, (True, False): 12.72, (True, True): 12.83}137 delta = (m[(True, True)] - m[(True, False)]) - (m[(False, True)] - m[(False, False)])138 assert delta == pytest.approx(0.05, abs=1e-9) and (math.exp(delta) - 1) * 100 == pytest.approx(5.13, abs=0.005)139140 def test_ex5_agricole(self):141 v_agr = 8 * 14000.0; v_aut = 110000.0 + 7.5 * 14000.0142 assert v_agr == pytest.approx(112000) and v_aut == pytest.approx(215000)143 assert cas.valeur_attendue_zone_agricole(v_agr, v_aut, 0.25, 2.0, 0.06, 6000.0) == pytest.approx(125837, abs=TOL)144 assert cas.valeur_attendue_zone_agricole(v_agr, v_aut, 0.50, 2.0, 0.06, 6000.0) == pytest.approx(145675, abs=TOL)145146 def test_ex6_option(self):147 rd = cas.valeur_residuelle_redeveloppement(3, 95.0, 5600.0, 2950.0, 0.12, 28000.0, 0.15, 0.03, 0.10, 0.0)148 assert rd["gdv"] == pytest.approx(1596000) and rd["valeur_residuelle"] == pytest.approx(308254.55, abs=0.01)149 rd2 = cas.valeur_residuelle_redeveloppement(3, 95.0, 5600.0, 2950.0, 0.12, 28000.0, 0.15, 0.03, 0.10, 45000.0)150 assert rd2["valeur_residuelle"] == pytest.approx(267345.45, abs=0.01)151 assert rd["valeur_residuelle"] - rd2["valeur_residuelle"] == pytest.approx(45000 / 1.10, rel=1e-9)152 assert cas.prime_option(350000.0, rd["valeur_residuelle"]) == 0.0153 assert cas.prime_option(280000.0, rd["valeur_residuelle"]) == pytest.approx(28254.55, abs=0.01)154155 def test_ex7_derivation(self):156 # V_Δ(T) → ΔRNE/(R+λ) ; et sensibilité : dV/dλ = −V/(R+λ)157 dn, R, lam = 3300.0, 0.055, 0.02158 v = cas.valeur_surplus_derogatoire(dn, R, lam)159 h = 1e-6160 deriv = (cas.valeur_surplus_derogatoire(dn, R, lam + h) - cas.valeur_surplus_derogatoire(dn, R, lam - h)) / (2 * h)161 assert deriv == pytest.approx(-v / (R + lam), rel=1e-6)162 assert cas.valeur_surplus_derogatoire(dn, R, lam + 0.002) == pytest.approx(42857.14, abs=0.01)163