SPB Git
16.9 KB · 242 lines python
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
Files · 3f09fc5fc3793c822f5612b9fe13f99a59fa3187
Menu

Theme