"""Tests du chapitre 43 — risques environnementaux et sinistres (DONNÉES SYNTHÉTIQUES, graine 20261031). Tolérances : 0,5 $ sur les montants arrondis au dollar ; 0,05 point de pourcentage sur les pourcentages cités avec deux décimales ; 5e-5 sur les coefficients en logarithme cités avec quatre décimales ; 1e-9 sur les identités algébriques. Les valeurs « citées » sont celles de manuscript/chapters/ch43.tex et de solutions/ch43.tex.""" import sys, pathlib, math import numpy as np, pandas as pd, pytest sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "cases")) import ch43_risques as cas from traite import cas4, comparables as cp, hedonique as h TOL, TOLP, TOLL = 0.5, 0.05, 5e-5 @pytest.fixture(scope="module") def res(): return cas.main(ecrire=False, verbeux=False) class TestPrimeTaux: def test_structure_cas4(self, res): pr = res["prime"] assert pr["n"] == 48 and pr["n_inondable"] == 3 and pr["n_sol"] == 2 assert set(pr["immeubles_inondables"]["id_immeuble"]) == {"P36", "P41", "P43"} assert set(pr["immeubles_sol"]["id_immeuble"]) == {"P20", "P21"} assert pr["part_valeur_inondable"] * 100 == pytest.approx(6.66, abs=TOLP) def test_coefficients_cites(self, res): e = res["prime"]["inondable"] assert e["coef"] == pytest.approx(0.0756, abs=TOLL) and e["se"] == pytest.approx(0.0347, abs=TOLL) assert e["effet_pct"] * 100 == pytest.approx(7.86, abs=TOLP) assert e["ic_bas_pct"] * 100 == pytest.approx(0.77, abs=TOLP) and e["ic_haut_pct"] * 100 == pytest.approx(15.44, abs=TOLP) assert e["effet_kennedy_pct"] * 100 == pytest.approx(7.79, abs=TOLP) and e["t_vs_declare"] == pytest.approx(0.77, abs=0.005) s = res["prime"]["sol"] assert s["coef"] == pytest.approx(0.0536, abs=TOLL) and s["se"] == pytest.approx(0.0288, abs=TOLL) assert s["effet_pct"] * 100 == pytest.approx(5.50, abs=TOLP) assert res["prime"]["decote_valeur_inondable_pct"] * 100 == pytest.approx(-7.29, abs=TOLP) assert res["prime"]["decote_valeur_declaree_pct"] * 100 == pytest.approx(-4.76, abs=TOLP) def test_identites(self, res): e = res["prime"]["inondable"] # effet % = exp(coef) − 1 ; IC symétrique en log ; décote de valeur = exp(−coef) − 1 = −effet/(1+effet) assert e["effet_pct"] == pytest.approx(math.exp(e["coef"]) - 1, rel=1e-12) assert math.log(1 + e["ic_haut_pct"]) - e["coef"] == pytest.approx(e["coef"] - math.log(1 + e["ic_bas_pct"]), rel=1e-9) assert res["prime"]["decote_valeur_inondable_pct"] == pytest.approx(-e["effet_pct"] / (1 + e["effet_pct"]), rel=1e-12) # le coefficient provient bien de cas4.effets_regionaux eff = cas4.effets_regionaux(cas4.charger()["portefeuille"]) assert e["coef"] == pytest.approx(float(eff.loc["inondable", "coef"]), rel=1e-12) class TestDiDCas4: def test_choc_injecte(self, res): pan = res["panel"] tr = pan[pan["traite"] == 1] assert set(tr["id_immeuble"]) == {"P36", "P41", "P43"} and len(pan) == 48 * 42 assert tr.loc[tr["trimestre"] == "2019Q2", "choc"].unique().tolist() == [-0.25] assert tr.loc[tr["trimestre"] == "2021Q1", "choc"].unique().tolist() == [-0.05] assert (pan.loc[pan["traite"] == 0, "choc"] == 0).all() and (tr.loc[tr["trimestre"] < "2019Q2", "choc"] == 0).all() assert (pan["ln_rne"] - pan["ln_rne_sans_choc"] - pan["choc"]).abs().max() < 1e-12 def test_valeurs_citees(self, res): d = res["did"] c = d["cellules"] assert c.loc[0, 0] == pytest.approx(10.0063, abs=TOLL) and c.loc[0, 1] == pytest.approx(10.1607, abs=TOLL) assert c.loc[1, 0] == pytest.approx(10.0995, abs=TOLL) and c.loc[1, 1] == pytest.approx(10.1409, abs=TOLL) assert d["estimateur_2x2"] == pytest.approx(-0.1129, abs=TOLL) and d["se_hc1"] == pytest.approx(0.0252, abs=TOLL) assert d["estimateur_strate"] == pytest.approx(-0.0794, abs=TOLL) assert d["n_combinaisons"] == 17296 and d["n_traites"] == 3 and d["n_temoins"] == 45 assert d["p_ri"] == pytest.approx(0.0052, abs=5e-5) and d["p_ri_strate"] == pytest.approx(0.0240, abs=5e-5) tw = d["twfe"] 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) 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) es = d["event_study"] assert es.loc[0, "coef"] == pytest.approx(-0.2625, abs=TOLL) and es.loc[1, "coef"] == pytest.approx(-0.1278, abs=TOLL) assert es.loc[2, "coef"] == pytest.approx(-0.0620, abs=TOLL) and es.loc[-7, "coef"] == pytest.approx(0.0208, abs=TOLL) assert es.loc[12, "coef"] == pytest.approx(-0.0912, abs=TOLL) and es.loc[-1, "coef"] == 0.0 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) assert d["verite_permanente_pct"] * 100 == pytest.approx(-4.88, abs=TOLP) def test_identites(self, res): d = res["did"]; c = d["cellules"] 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) # la régression des différences longues redonne exactement l'estimateur 2 × 2 (panel cylindré) assert d["coef_reg"] == pytest.approx(d["estimateur_2x2"], abs=1e-9) # l'observé est l'un des placebos ; p = part des |placebos| ≥ |observé| assert np.any(np.isclose(d["placebo_dist"], d["estimateur_2x2"])) assert d["p_ri"] == pytest.approx(np.mean(np.abs(d["placebo_dist"]) >= abs(d["estimateur_2x2"]) - 1e-12), rel=1e-12) # la vérité permanente est dans l'IC à 95 % en grappes du TWFE tw = d["twfe"].loc["traite_permanent"] assert tw["coef"] - 1.96 * tw["se_grappes"] <= cas.CHOC_PERMANENT <= tw["coef"] + 1.96 * tw["se_grappes"] # effet valeur = Δln RNE − Δln R assert d["effet_valeur_estime_log"] == pytest.approx(tw["coef"] - res["prime"]["inondable"]["coef"], rel=1e-12) assert d["effet_valeur_declare_log"] == pytest.approx(cas.CHOC_PERMANENT - math.log(1.05), rel=1e-12) class TestRiviereFictive: def test_structure(self, res): lots, v = res["lots"], res["ventes"] assert len(lots) == 625 and int(lots["expose"].sum()) == 175 assert lots["classe"].value_counts().to_dict() == {"hors zone": 450, "élevée": 50, "modérée": 50, "faible": 50, "très élevée": 25} assert int(lots["rive"].sum()) == 75 + 75 - 9 # 3 rangées × 25 + 3 colonnes × 25 − 9 lots communs e = res["hedoniques"]["effectifs"] assert e == {"ventes": 1014, "exposees": 302, "rive": 241, "apres": 695} assert (v["prix_vente"] % 500 == 0).all() def test_reproductibilite(self): l1, v1 = cas.generer_riverain(); l2, v2 = cas.generer_riverain() assert v1.equals(v2) and l1.equals(l2) def test_verites(self, res): ver = res["hedoniques"]["verite"] assert ver["decote_avant_log"] == pytest.approx(-0.0499, abs=TOLL) and ver["decote_apres_log"] == pytest.approx(-0.0763, abs=TOLL) assert ver["stigmate_log"] == pytest.approx(-0.0263, abs=TOLL) assert ver["stigmate_log"] == pytest.approx(ver["decote_apres_log"] - ver["decote_avant_log"], rel=1e-12) assert cas.DECOTE_AVANT["très élevée"] * (1 + cas.STIGMATE) == pytest.approx(-0.15) def test_paires(self, res): rp = res["resume_paires"]; p = res["paires"] assert rp["n"] == 206 and rp["moyenne"] == pytest.approx(-19113, abs=TOL) assert rp["moyenne_pct"] * 100 == pytest.approx(-4.83, abs=TOLP) and rp["mediane_pct"] * 100 == pytest.approx(-5.35, abs=TOLP) assert rp["ic95_pct"][0] * 100 == pytest.approx(-6.15, abs=TOLP) and rp["ic95_pct"][1] * 100 == pytest.approx(-3.51, abs=TOLP) assert rp["verite_pct"] * 100 == pytest.approx(-7.26, abs=TOLP) pc = rp["par_classe"] 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) 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) pr = rp["par_rive"] 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) # identités : différence = exposé − témoin ajusté ; témoin ajusté = (témoin + ajust. temps) × facteur physique ; témoins uniques assert (p["difference"] - (p["prix_expose"] - p["prix_temoin_ajuste"])).abs().max() < 1e-6 assert (p["prix_temoin_ajuste"] - (p["prix_temoin"] + p["ajust_temps"]) * p["facteur_physique"]).abs().max() < 1e-6 assert p["id_temoin"].is_unique and p["id_expose"].is_unique r = cp.ventes_appariees(p["prix_expose"].tolist(), p["prix_temoin_ajuste"].tolist()) assert r["moyenne"] == pytest.approx(rp["moyenne"], rel=1e-12) def test_hedoniques(self, res): he = res["hedoniques"] assert he["a_sans_rive"]["coef"] == pytest.approx(-0.0447, abs=TOLL) and he["a_sans_rive"]["se"] == pytest.approx(0.0052, abs=TOLL) assert he["b_avec_rive"]["coef"] == pytest.approx(-0.0501, abs=TOLL) and he["b_avec_rive"]["se"] == pytest.approx(0.0059, abs=TOLL) 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) assert he["b_avec_rive"]["r2"] == pytest.approx(0.874, abs=0.0005) c = he["c_classes"] 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), "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), "rive": (0.0417, 0.0074)} for k, (b, s) in attendu.items(): assert c.loc[k, "coef"] == pytest.approx(b, abs=TOLL) and c.loc[k, "se"] == pytest.approx(s, abs=TOLL) assert c["r2"].iloc[0] == pytest.approx(0.882, abs=0.0005) d = he["d_did_reg"] assert d["coef_expose"] == pytest.approx(-0.0316, abs=TOLL) and d["se_expose"] == pytest.approx(0.0092, abs=TOLL) assert d["coef_did"] == pytest.approx(-0.0269, abs=TOLL) and d["se_did"] == pytest.approx(0.0107, abs=TOLL) assert he["d_did_2x2"]["estimateur"] == pytest.approx(-0.0237, abs=TOLL) assert he["d_did_2x2"]["effectifs"] == {(0, 0): 223, (0, 1): 489, (1, 0): 96, (1, 1): 206} def test_identites_hedoniques(self, res): he = res["hedoniques"]; m = he["d_did_2x2"]["moyennes"] assert he["d_did_2x2"]["estimateur"] == pytest.approx((m[(1, 1)] - m[(1, 0)]) - (m[(0, 1)] - m[(0, 0)]), rel=1e-12) c = he["c_classes"] # 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) 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"]: assert abs(c.loc[k, "coef"] - c.loc[k, "verite"]) <= 1.96 * c.loc[k, "se"] + 1e-12, k t = res["effets_methodes"] assert len(t) == 7 and (t["effet_pct"] - (np.exp(t["coef"]) - 1)).abs().max() < 1e-12 assert t.loc[5, "coef"] == pytest.approx(t.loc[3, "coef"] + t.loc[4, "coef"], rel=1e-12) class TestModelesValeur: def test_contamination(self, res): ct = res["contamination"]; b = ct["base"] assert ct["cout_total"] == pytest.approx(206500) and b["valeur"] == pytest.approx(1595816, abs=TOL) assert b["va_propre_stigmatisee"] == pytest.approx(1843979, abs=TOL) and b["va_cout"] == pytest.approx(248163, abs=TOL) assert b["cout_delai"] == pytest.approx(151021, abs=TOL) and b["cout_stigmate"] == pytest.approx(105000, abs=TOL) assert b["ecart_pct"] * 100 == pytest.approx(-24.01, abs=TOLP) # identité : V_c = VA propre stigmatisée − VA coût − VA pertes ; et écart = V_p − V_c assert b["valeur"] == pytest.approx(b["va_propre_stigmatisee"] - b["va_cout"] - b["va_perte_revenu"], rel=1e-12) assert b["ecart_total"] == pytest.approx(ct["hyp"]["v_propre"] - b["valeur"], rel=1e-12) s = ct["sensibilite"] 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) assert ct["sensibilite_cout"][2.0] == pytest.approx(1347653, abs=TOL) # sans délai, sans stigmate, sans contingence : V_c = V_p − C z = cas.valeur_terrain_contamine(2_100_000, 206_500, 0, 0.065, 0.0) assert z["valeur"] == pytest.approx(2_100_000 - 206_500, rel=1e-12) def test_sinistre(self, res): sn = res["sinistre"]; b = sn["base"] assert sn["hyp"]["v_avant"] == 1_714_000 and sn["hyp"]["nb_logements"] == 8 assert sn["loyer_perdu_mensuel"] == pytest.approx(5608) and sn["indemnite"] == pytest.approx(194000) assert b["va_perte_usage"] == pytest.approx(27603, abs=TOL) and b["stigmate_dollars"] == pytest.approx(85700, abs=TOL) assert b["valeur"] == pytest.approx(1360697, abs=TOL) and b["perte_pct"] * 100 == pytest.approx(-20.61, abs=TOLP) assert b["position_assure"] == pytest.approx(1554697, abs=TOL) and b["perte_nette_assure"] == pytest.approx(159303, abs=TOL) assert b["part_non_assuree"] * 100 == pytest.approx(45.1, abs=0.05) assert b["valeur"] == pytest.approx(sn["hyp"]["v_avant"] - sn["hyp"]["cout_remise"] - b["va_perte_usage"] - b["stigmate_dollars"], rel=1e-12) assert sn["sensibilite_stigmate"][0.0] == pytest.approx(1446397, abs=TOL) and sn["sensibilite_stigmate"][0.15] == pytest.approx(1189297, abs=TOL) # le stigmate n'est jamais indemnisé : perte nette assuré − stigmate = part non couverte des coûts + perte d'usage assert b["perte_nette_assure"] - b["stigmate_dollars"] == pytest.approx(sn["hyp"]["cout_remise"] - sn["indemnite"] + b["va_perte_usage"], rel=1e-9) def test_pyrite(self, res): py = res["pyrite"] assert py["curable"]["retenu"] == pytest.approx(27000) and py["incurable"]["retenu"] == pytest.approx(1800 / 0.055, rel=1e-12) assert py["incurable"]["retenu"] == pytest.approx(32727, abs=TOL) class TestExercices: """Chiffres des solutions (solutions/ch43.tex).""" def test_ex1_classes(self): # exercice 1 : lecture du coefficient d'une indicatrice : exp(−0,083) − 1 et Kennedy assert (math.exp(-0.083) - 1) * 100 == pytest.approx(-7.96, abs=0.005) assert (math.exp(-0.083 - 0.5 * 0.021 ** 2) - 1) * 100 == pytest.approx(-7.99, abs=0.005) def test_ex2_prime_taux(self): # exercice 2 : RNE 120 000 $, R hors zone 5,25 %, prime +7,86 % du taux v0 = 120_000 / 0.0525; v1 = 120_000 / (0.0525 * 1.0786) assert v0 == pytest.approx(2_285_714, abs=TOL) and v1 == pytest.approx(2_119_149, abs=TOL) assert (v1 / v0 - 1) * 100 == pytest.approx(-7.29, abs=0.005) def test_ex3_contamination(self): r = cas.valeur_terrain_contamine(850_000, 95_000, 12, 0.07, 0.08, 0.20) assert r["va_propre_stigmatisee"] == pytest.approx(730_841, abs=TOL) and r["va_cout"] == pytest.approx(110_208, abs=TOL) assert r["valeur"] == pytest.approx(620_633, abs=TOL) and r["ecart_pct"] * 100 == pytest.approx(-26.98, abs=0.005) def test_ex4_sinistre(self): r = cas.valeur_apres_sinistre(980_000, 140_000, 3_600, 4, 0.06, 0.04, 0.80 * 140_000 - 5_000) assert r["va_perte_usage"] == pytest.approx(14_226, abs=TOL) and r["stigmate_dollars"] == pytest.approx(39_200) assert r["valeur"] == pytest.approx(786_574, abs=TOL) and r["position_assure"] == pytest.approx(893_574, abs=TOL) assert r["perte_nette_assure"] == pytest.approx(86_426, abs=TOL) def test_ex5_did(self): m = {(0, 0): 12.90, (0, 1): 12.98, (1, 0): 12.84, (1, 1): 12.86} delta = (m[(1, 1)] - m[(1, 0)]) - (m[(0, 1)] - m[(0, 0)]) assert delta == pytest.approx(-0.06, abs=1e-9) and (math.exp(delta) - 1) * 100 == pytest.approx(-5.82, abs=0.005) def test_ex6_randomisation(self, res): # exercice 6 : p exact = part des |placebos| ≥ |observé| ; nombre de combinaisons C(48,3) d = res["did"] assert math.comb(48, 3) == 17296 == d["n_combinaisons"] assert d["p_ri"] == pytest.approx(np.mean(np.abs(d["placebo_dist"]) >= abs(d["estimateur_2x2"]) - 1e-12)) def test_ex7_pyrite_vei(self): # exercice 7 : seuil p* = t/C, et désuétude curable vs incurable assert 1200 / 25000 == pytest.approx(0.048) assert cas.desuetude_vs_correction(25_000, 2_000, 0, 0.055, True)["retenu"] == 27_000 assert cas.desuetude_vs_correction(0, 0, 1_800, 0.055, False)["retenu"] == pytest.approx(32_727.27, abs=0.01)