spb/traite-evaluation-qc
Public
1"""Tests du chapitre 43 — risques environnementaux et sinistres (DONNÉES SYNTHÉTIQUES, graine 20261031).2Tolérances : 0,5 $ sur les montants arrondis au dollar ; 0,05 point de pourcentage sur les pourcentages cités avec deux3décimales ; 5e-5 sur les coefficients en logarithme cités avec quatre décimales ; 1e-9 sur les identités algébriques.4Les valeurs « citées » sont celles de manuscript/chapters/ch43.tex et de solutions/ch43.tex."""5import sys, pathlib, math6import numpy as np, pandas as pd, pytest7sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "cases"))8import ch43_risques as cas9from traite import cas4, comparables as cp, hedonique as h1011TOL, TOLP, TOLL = 0.5, 0.05, 5e-5121314@pytest.fixture(scope="module")15def res():16 return cas.main(ecrire=False, verbeux=False)171819class TestPrimeTaux:20 def test_structure_cas4(self, res):21 pr = res["prime"]22 assert pr["n"] == 48 and pr["n_inondable"] == 3 and pr["n_sol"] == 223 assert set(pr["immeubles_inondables"]["id_immeuble"]) == {"P36", "P41", "P43"}24 assert set(pr["immeubles_sol"]["id_immeuble"]) == {"P20", "P21"}25 assert pr["part_valeur_inondable"] * 100 == pytest.approx(6.66, abs=TOLP)2627 def test_coefficients_cites(self, res):28 e = res["prime"]["inondable"]29 assert e["coef"] == pytest.approx(0.0756, abs=TOLL) and e["se"] == pytest.approx(0.0347, abs=TOLL)30 assert e["effet_pct"] * 100 == pytest.approx(7.86, abs=TOLP)31 assert e["ic_bas_pct"] * 100 == pytest.approx(0.77, abs=TOLP) and e["ic_haut_pct"] * 100 == pytest.approx(15.44, abs=TOLP)32 assert e["effet_kennedy_pct"] * 100 == pytest.approx(7.79, abs=TOLP) and e["t_vs_declare"] == pytest.approx(0.77, abs=0.005)33 s = res["prime"]["sol"]34 assert s["coef"] == pytest.approx(0.0536, abs=TOLL) and s["se"] == pytest.approx(0.0288, abs=TOLL)35 assert s["effet_pct"] * 100 == pytest.approx(5.50, abs=TOLP)36 assert res["prime"]["decote_valeur_inondable_pct"] * 100 == pytest.approx(-7.29, abs=TOLP)37 assert res["prime"]["decote_valeur_declaree_pct"] * 100 == pytest.approx(-4.76, abs=TOLP)3839 def test_identites(self, res):40 e = res["prime"]["inondable"]41 # effet % = exp(coef) − 1 ; IC symétrique en log ; décote de valeur = exp(−coef) − 1 = −effet/(1+effet)42 assert e["effet_pct"] == pytest.approx(math.exp(e["coef"]) - 1, rel=1e-12)43 assert math.log(1 + e["ic_haut_pct"]) - e["coef"] == pytest.approx(e["coef"] - math.log(1 + e["ic_bas_pct"]), rel=1e-9)44 assert res["prime"]["decote_valeur_inondable_pct"] == pytest.approx(-e["effet_pct"] / (1 + e["effet_pct"]), rel=1e-12)45 # le coefficient provient bien de cas4.effets_regionaux46 eff = cas4.effets_regionaux(cas4.charger()["portefeuille"])47 assert e["coef"] == pytest.approx(float(eff.loc["inondable", "coef"]), rel=1e-12)484950class TestDiDCas4:51 def test_choc_injecte(self, res):52 pan = res["panel"]53 tr = pan[pan["traite"] == 1]54 assert set(tr["id_immeuble"]) == {"P36", "P41", "P43"} and len(pan) == 48 * 4255 assert tr.loc[tr["trimestre"] == "2019Q2", "choc"].unique().tolist() == [-0.25]56 assert tr.loc[tr["trimestre"] == "2021Q1", "choc"].unique().tolist() == [-0.05]57 assert (pan.loc[pan["traite"] == 0, "choc"] == 0).all() and (tr.loc[tr["trimestre"] < "2019Q2", "choc"] == 0).all()58 assert (pan["ln_rne"] - pan["ln_rne_sans_choc"] - pan["choc"]).abs().max() < 1e-125960 def test_valeurs_citees(self, res):61 d = res["did"]62 c = d["cellules"]63 assert c.loc[0, 0] == pytest.approx(10.0063, abs=TOLL) and c.loc[0, 1] == pytest.approx(10.1607, abs=TOLL)64 assert c.loc[1, 0] == pytest.approx(10.0995, abs=TOLL) and c.loc[1, 1] == pytest.approx(10.1409, abs=TOLL)65 assert d["estimateur_2x2"] == pytest.approx(-0.1129, abs=TOLL) and d["se_hc1"] == pytest.approx(0.0252, abs=TOLL)66 assert d["estimateur_strate"] == pytest.approx(-0.0794, abs=TOLL)67 assert d["n_combinaisons"] == 17296 and d["n_traites"] == 3 and d["n_temoins"] == 4568 assert d["p_ri"] == pytest.approx(0.0052, abs=5e-5) and d["p_ri_strate"] == pytest.approx(0.0240, abs=5e-5)69 tw = d["twfe"]70 assert tw.loc["traite_transitoire", "coef"] == pytest.approx(-0.1653, abs=TOLL) and tw.loc["traite_transitoire", "se_grappes"] == pytest.approx(0.0263, abs=TOLL)71 assert tw.loc["traite_permanent", "coef"] == pytest.approx(-0.0888, abs=TOLL) and tw.loc["traite_permanent", "se_grappes"] == pytest.approx(0.0466, abs=TOLL)72 es = d["event_study"]73 assert es.loc[0, "coef"] == pytest.approx(-0.2625, abs=TOLL) and es.loc[1, "coef"] == pytest.approx(-0.1278, abs=TOLL)74 assert es.loc[2, "coef"] == pytest.approx(-0.0620, abs=TOLL) and es.loc[-7, "coef"] == pytest.approx(0.0208, abs=TOLL)75 assert es.loc[12, "coef"] == pytest.approx(-0.0912, abs=TOLL) and es.loc[-1, "coef"] == 0.076 assert d["effet_valeur_estime_pct"] * 100 == pytest.approx(-15.16, abs=TOLP) and d["effet_valeur_declare_pct"] * 100 == pytest.approx(-9.41, abs=TOLP)77 assert d["verite_permanente_pct"] * 100 == pytest.approx(-4.88, abs=TOLP)7879 def test_identites(self, res):80 d = res["did"]; c = d["cellules"]81 assert d["estimateur_2x2"] == pytest.approx((c.loc[1, 1] - c.loc[1, 0]) - (c.loc[0, 1] - c.loc[0, 0]), rel=1e-12)82 # la régression des différences longues redonne exactement l'estimateur 2 × 2 (panel cylindré)83 assert d["coef_reg"] == pytest.approx(d["estimateur_2x2"], abs=1e-9)84 # l'observé est l'un des placebos ; p = part des |placebos| ≥ |observé|85 assert np.any(np.isclose(d["placebo_dist"], d["estimateur_2x2"]))86 assert d["p_ri"] == pytest.approx(np.mean(np.abs(d["placebo_dist"]) >= abs(d["estimateur_2x2"]) - 1e-12), rel=1e-12)87 # la vérité permanente est dans l'IC à 95 % en grappes du TWFE88 tw = d["twfe"].loc["traite_permanent"]89 assert tw["coef"] - 1.96 * tw["se_grappes"] <= cas.CHOC_PERMANENT <= tw["coef"] + 1.96 * tw["se_grappes"]90 # effet valeur = Δln RNE − Δln R91 assert d["effet_valeur_estime_log"] == pytest.approx(tw["coef"] - res["prime"]["inondable"]["coef"], rel=1e-12)92 assert d["effet_valeur_declare_log"] == pytest.approx(cas.CHOC_PERMANENT - math.log(1.05), rel=1e-12)939495class TestRiviereFictive:96 def test_structure(self, res):97 lots, v = res["lots"], res["ventes"]98 assert len(lots) == 625 and int(lots["expose"].sum()) == 17599 assert lots["classe"].value_counts().to_dict() == {"hors zone": 450, "élevée": 50, "modérée": 50, "faible": 50, "très élevée": 25}100 assert int(lots["rive"].sum()) == 75 + 75 - 9 # 3 rangées × 25 + 3 colonnes × 25 − 9 lots communs101 e = res["hedoniques"]["effectifs"]102 assert e == {"ventes": 1014, "exposees": 302, "rive": 241, "apres": 695}103 assert (v["prix_vente"] % 500 == 0).all()104105 def test_reproductibilite(self):106 l1, v1 = cas.generer_riverain(); l2, v2 = cas.generer_riverain()107 assert v1.equals(v2) and l1.equals(l2)108109 def test_verites(self, res):110 ver = res["hedoniques"]["verite"]111 assert ver["decote_avant_log"] == pytest.approx(-0.0499, abs=TOLL) and ver["decote_apres_log"] == pytest.approx(-0.0763, abs=TOLL)112 assert ver["stigmate_log"] == pytest.approx(-0.0263, abs=TOLL)113 assert ver["stigmate_log"] == pytest.approx(ver["decote_apres_log"] - ver["decote_avant_log"], rel=1e-12)114 assert cas.DECOTE_AVANT["très élevée"] * (1 + cas.STIGMATE) == pytest.approx(-0.15)115116 def test_paires(self, res):117 rp = res["resume_paires"]; p = res["paires"]118 assert rp["n"] == 206 and rp["moyenne"] == pytest.approx(-19113, abs=TOL)119 assert rp["moyenne_pct"] * 100 == pytest.approx(-4.83, abs=TOLP) and rp["mediane_pct"] * 100 == pytest.approx(-5.35, abs=TOLP)120 assert rp["ic95_pct"][0] * 100 == pytest.approx(-6.15, abs=TOLP) and rp["ic95_pct"][1] * 100 == pytest.approx(-3.51, abs=TOLP)121 assert rp["verite_pct"] * 100 == pytest.approx(-7.26, abs=TOLP)122 pc = rp["par_classe"]123 assert pc.loc["très élevée", "n"] == 25 and pc.loc["très élevée", "moyenne_pct"] * 100 == pytest.approx(-8.47, abs=TOLP)124 assert pc.loc["élevée", "moyenne_pct"] * 100 == pytest.approx(-5.97, abs=TOLP) and pc.loc["faible", "moyenne_pct"] * 100 == pytest.approx(-1.30, abs=TOLP)125 pr = rp["par_rive"]126 assert pr.loc[0, "moyenne_pct"] * 100 == pytest.approx(-3.62, abs=TOLP) and pr.loc[1, "moyenne_pct"] * 100 == pytest.approx(-5.91, abs=TOLP)127 # identités : différence = exposé − témoin ajusté ; témoin ajusté = (témoin + ajust. temps) × facteur physique ; témoins uniques128 assert (p["difference"] - (p["prix_expose"] - p["prix_temoin_ajuste"])).abs().max() < 1e-6129 assert (p["prix_temoin_ajuste"] - (p["prix_temoin"] + p["ajust_temps"]) * p["facteur_physique"]).abs().max() < 1e-6130 assert p["id_temoin"].is_unique and p["id_expose"].is_unique131 r = cp.ventes_appariees(p["prix_expose"].tolist(), p["prix_temoin_ajuste"].tolist())132 assert r["moyenne"] == pytest.approx(rp["moyenne"], rel=1e-12)133134 def test_hedoniques(self, res):135 he = res["hedoniques"]136 assert he["a_sans_rive"]["coef"] == pytest.approx(-0.0447, abs=TOLL) and he["a_sans_rive"]["se"] == pytest.approx(0.0052, abs=TOLL)137 assert he["b_avec_rive"]["coef"] == pytest.approx(-0.0501, abs=TOLL) and he["b_avec_rive"]["se"] == pytest.approx(0.0059, abs=TOLL)138 assert he["b_avec_rive"]["coef_rive"] == pytest.approx(0.0132, abs=TOLL) and he["b_avec_rive"]["sigma"] == pytest.approx(0.0713, abs=TOLL)139 assert he["b_avec_rive"]["r2"] == pytest.approx(0.874, abs=0.0005)140 c = he["c_classes"]141 attendu = {"très élevée": (-0.1035, 0.0183), "élevée": (-0.0564, 0.0149), "modérée": (-0.0426, 0.0158), "faible": (-0.0043, 0.0122),142 "très élevée × après": (-0.0218, 0.0223), "élevée × après": (-0.0430, 0.0154), "modérée × après": (-0.0193, 0.0184), "faible × après": (-0.0095, 0.0163),143 "rive": (0.0417, 0.0074)}144 for k, (b, s) in attendu.items():145 assert c.loc[k, "coef"] == pytest.approx(b, abs=TOLL) and c.loc[k, "se"] == pytest.approx(s, abs=TOLL)146 assert c["r2"].iloc[0] == pytest.approx(0.882, abs=0.0005)147 d = he["d_did_reg"]148 assert d["coef_expose"] == pytest.approx(-0.0316, abs=TOLL) and d["se_expose"] == pytest.approx(0.0092, abs=TOLL)149 assert d["coef_did"] == pytest.approx(-0.0269, abs=TOLL) and d["se_did"] == pytest.approx(0.0107, abs=TOLL)150 assert he["d_did_2x2"]["estimateur"] == pytest.approx(-0.0237, abs=TOLL)151 assert he["d_did_2x2"]["effectifs"] == {(0, 0): 223, (0, 1): 489, (1, 0): 96, (1, 1): 206}152153 def test_identites_hedoniques(self, res):154 he = res["hedoniques"]; m = he["d_did_2x2"]["moyennes"]155 assert he["d_did_2x2"]["estimateur"] == pytest.approx((m[(1, 1)] - m[(1, 0)]) - (m[(0, 1)] - m[(0, 0)]), rel=1e-12)156 c = he["c_classes"]157 # la vérité de chaque classe est dans l'IC à 95 % (sauf mention au texte : « très élevée × après » et « faible » sont à la limite)158 for k in ["très élevée", "élevée", "modérée", "élevée × après", "modérée × après", "faible × après", "rive"]:159 assert abs(c.loc[k, "coef"] - c.loc[k, "verite"]) <= 1.96 * c.loc[k, "se"] + 1e-12, k160 t = res["effets_methodes"]161 assert len(t) == 7 and (t["effet_pct"] - (np.exp(t["coef"]) - 1)).abs().max() < 1e-12162 assert t.loc[5, "coef"] == pytest.approx(t.loc[3, "coef"] + t.loc[4, "coef"], rel=1e-12)163164165class TestModelesValeur:166 def test_contamination(self, res):167 ct = res["contamination"]; b = ct["base"]168 assert ct["cout_total"] == pytest.approx(206500) and b["valeur"] == pytest.approx(1595816, abs=TOL)169 assert b["va_propre_stigmatisee"] == pytest.approx(1843979, abs=TOL) and b["va_cout"] == pytest.approx(248163, abs=TOL)170 assert b["cout_delai"] == pytest.approx(151021, abs=TOL) and b["cout_stigmate"] == pytest.approx(105000, abs=TOL)171 assert b["ecart_pct"] * 100 == pytest.approx(-24.01, abs=TOLP)172 # identité : V_c = VA propre stigmatisée − VA coût − VA pertes ; et écart = V_p − V_c173 assert b["valeur"] == pytest.approx(b["va_propre_stigmatisee"] - b["va_cout"] - b["va_perte_revenu"], rel=1e-12)174 assert b["ecart_total"] == pytest.approx(ct["hyp"]["v_propre"] - b["valeur"], rel=1e-12)175 s = ct["sensibilite"]176 assert s[(0.0, 6)] == pytest.approx(1780814, abs=TOL) and s[(0.15, 24)] == pytest.approx(1331391, abs=TOL) and s[(0.05, 15)] == pytest.approx(b["valeur"], rel=1e-12)177 assert ct["sensibilite_cout"][2.0] == pytest.approx(1347653, abs=TOL)178 # sans délai, sans stigmate, sans contingence : V_c = V_p − C179 z = cas.valeur_terrain_contamine(2_100_000, 206_500, 0, 0.065, 0.0)180 assert z["valeur"] == pytest.approx(2_100_000 - 206_500, rel=1e-12)181182 def test_sinistre(self, res):183 sn = res["sinistre"]; b = sn["base"]184 assert sn["hyp"]["v_avant"] == 1_714_000 and sn["hyp"]["nb_logements"] == 8185 assert sn["loyer_perdu_mensuel"] == pytest.approx(5608) and sn["indemnite"] == pytest.approx(194000)186 assert b["va_perte_usage"] == pytest.approx(27603, abs=TOL) and b["stigmate_dollars"] == pytest.approx(85700, abs=TOL)187 assert b["valeur"] == pytest.approx(1360697, abs=TOL) and b["perte_pct"] * 100 == pytest.approx(-20.61, abs=TOLP)188 assert b["position_assure"] == pytest.approx(1554697, abs=TOL) and b["perte_nette_assure"] == pytest.approx(159303, abs=TOL)189 assert b["part_non_assuree"] * 100 == pytest.approx(45.1, abs=0.05)190 assert b["valeur"] == pytest.approx(sn["hyp"]["v_avant"] - sn["hyp"]["cout_remise"] - b["va_perte_usage"] - b["stigmate_dollars"], rel=1e-12)191 assert sn["sensibilite_stigmate"][0.0] == pytest.approx(1446397, abs=TOL) and sn["sensibilite_stigmate"][0.15] == pytest.approx(1189297, abs=TOL)192 # le stigmate n'est jamais indemnisé : perte nette assuré − stigmate = part non couverte des coûts + perte d'usage193 assert b["perte_nette_assure"] - b["stigmate_dollars"] == pytest.approx(sn["hyp"]["cout_remise"] - sn["indemnite"] + b["va_perte_usage"], rel=1e-9)194195 def test_pyrite(self, res):196 py = res["pyrite"]197 assert py["curable"]["retenu"] == pytest.approx(27000) and py["incurable"]["retenu"] == pytest.approx(1800 / 0.055, rel=1e-12)198 assert py["incurable"]["retenu"] == pytest.approx(32727, abs=TOL)199200201class TestExercices:202 """Chiffres des solutions (solutions/ch43.tex)."""203204 def test_ex1_classes(self):205 # exercice 1 : lecture du coefficient d'une indicatrice : exp(−0,083) − 1 et Kennedy206 assert (math.exp(-0.083) - 1) * 100 == pytest.approx(-7.96, abs=0.005)207 assert (math.exp(-0.083 - 0.5 * 0.021 ** 2) - 1) * 100 == pytest.approx(-7.99, abs=0.005)208209 def test_ex2_prime_taux(self):210 # exercice 2 : RNE 120 000 $, R hors zone 5,25 %, prime +7,86 % du taux211 v0 = 120_000 / 0.0525; v1 = 120_000 / (0.0525 * 1.0786)212 assert v0 == pytest.approx(2_285_714, abs=TOL) and v1 == pytest.approx(2_119_149, abs=TOL)213 assert (v1 / v0 - 1) * 100 == pytest.approx(-7.29, abs=0.005)214215 def test_ex3_contamination(self):216 r = cas.valeur_terrain_contamine(850_000, 95_000, 12, 0.07, 0.08, 0.20)217 assert r["va_propre_stigmatisee"] == pytest.approx(730_841, abs=TOL) and r["va_cout"] == pytest.approx(110_208, abs=TOL)218 assert r["valeur"] == pytest.approx(620_633, abs=TOL) and r["ecart_pct"] * 100 == pytest.approx(-26.98, abs=0.005)219220 def test_ex4_sinistre(self):221 r = cas.valeur_apres_sinistre(980_000, 140_000, 3_600, 4, 0.06, 0.04, 0.80 * 140_000 - 5_000)222 assert r["va_perte_usage"] == pytest.approx(14_226, abs=TOL) and r["stigmate_dollars"] == pytest.approx(39_200)223 assert r["valeur"] == pytest.approx(786_574, abs=TOL) and r["position_assure"] == pytest.approx(893_574, abs=TOL)224 assert r["perte_nette_assure"] == pytest.approx(86_426, abs=TOL)225226 def test_ex5_did(self):227 m = {(0, 0): 12.90, (0, 1): 12.98, (1, 0): 12.84, (1, 1): 12.86}228 delta = (m[(1, 1)] - m[(1, 0)]) - (m[(0, 1)] - m[(0, 0)])229 assert delta == pytest.approx(-0.06, abs=1e-9) and (math.exp(delta) - 1) * 100 == pytest.approx(-5.82, abs=0.005)230231 def test_ex6_randomisation(self, res):232 # exercice 6 : p exact = part des |placebos| ≥ |observé| ; nombre de combinaisons C(48,3)233 d = res["did"]234 assert math.comb(48, 3) == 17296 == d["n_combinaisons"]235 assert d["p_ri"] == pytest.approx(np.mean(np.abs(d["placebo_dist"]) >= abs(d["estimateur_2x2"]) - 1e-12))236237 def test_ex7_pyrite_vei(self):238 # exercice 7 : seuil p* = t/C, et désuétude curable vs incurable239 assert 1200 / 25000 == pytest.approx(0.048)240 assert cas.desuetude_vs_correction(25_000, 2_000, 0, 0.055, True)["retenu"] == 27_000241 assert cas.desuetude_vs_correction(0, 0, 1_800, 0.055, False)["retenu"] == pytest.approx(32_727.27, abs=0.01)242