SPB Git
10.7 KB · 128 lines python
1"""Chapitre 35 — Indices immobiliers et ventes répétées : identités et valeurs citées dans le texte, les tableaux et les solutions.2Modèle de référence : traite.cas1 ; estimateurs : traite.hedonique (indice_dummies_temps, ventes_repetees_bmn, ventes_repetees_case_shiller).3Tolérances déclarées dans chaque assertion."""4import sys, pathlib, math5import numpy as np, pytest6sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "cases"))7import ch35_indices as c358from traite import hedonique as h910@pytest.fixture(scope="module")11def res():12    return c35.main(verbose=False)1314def test_verite_et_donnees(res):15    assert len(res["ventes"]) == 600 and res["naifs"]["n"].sum() == 600 and list(res["naifs"]["n"]) == [39, 39, 42, 40, 50, 44, 51, 39, 32, 43, 50, 30, 33, 46, 22]16    assert c35.BETA_VRAI == pytest.approx(math.log(1 + ((1 + 0.0906) ** 0.5 - 1)) / 12, rel=1e-9)17    assert (math.exp(12 * c35.BETA_VRAI) - 1) * 100 == pytest.approx(4.43, abs=0.005)18    assert res["vrai"][0] == 100 and res["vrai"][-1] == pytest.approx(116.37, abs=0.005)1920def test_indice_hedonique_identites(res):21    td = res["td"]22    assert td["k"] == 29 and td["sigma"] == pytest.approx(0.0830, abs=0.0005) and td["r2_ajuste"] == pytest.approx(0.849, abs=0.001)23    assert td["indice"][0] == 100.0 and np.all(td["bas"] <= td["indice"]) and np.all(td["indice"] <= td["haut"])24    # exp(τ_t) = indice / 10025    assert np.allclose(td["indice"], 100 * np.exp(td["tau"]))26    assert td["indice"][-1] == pytest.approx(122.04, abs=0.005) and td["indice"][9] == pytest.approx(112.13, abs=0.005)27    assert res["precision"]["hedonique"]["largeur_ic_finale_pct"] == pytest.approx(7.47, abs=0.01)28    # imputation géométrique (Laspeyres = Paasche = Fisher) = indice à indicatrices quand les coefficients sont communs29    imp = res["imputation"]30    assert imp["ecart_max_vs_td"] < 1e-9 and np.allclose(imp["laspeyres"], imp["paasche"], atol=1e-9)31    # chaînage : I_t = I_{t-1} exp(δ_t)32    ch = res["chaine"]; assert np.allclose(ch["indice"], 100 * np.exp(np.cumsum(ch["delta"]))) and ch["indice"][-1] == pytest.approx(121.69, abs=0.005)3334def test_mediane_et_composition(res):35    n = res["naifs"]36    assert n["indice_mediane"].iloc[-1] == pytest.approx(128.63, abs=0.005) and n["indice_mediane"].max() == pytest.approx(133.68, abs=0.005)37    assert n["indice_moy_geo"].iloc[-1] == pytest.approx(127.78, abs=0.005)38    # décomposition exacte : ln(moyenne géométrique)_t − ln(…)_0 = composition_t + τ_t (les résidus MCO somment à zéro par trimestre)39    lhs = np.log(n["moy_geo"].to_numpy() / n["moy_geo"].iloc[0]); rhs = n["composition_log"].to_numpy() + res["td"]["tau"]40    assert np.allclose(lhs, rhs, atol=1e-9)41    assert np.allclose(n["indice_moy_geo_corrigee"].to_numpy(), res["td"]["indice"], atol=1e-6)42    assert res["corr_ecart_composition"] == pytest.approx(0.86, abs=0.01)43    assert n["composition_log"].iloc[12] == pytest.approx(0.101, abs=0.001) and n["part_aylmer"].iloc[-1] == pytest.approx(0.364, abs=0.001)44    p = res["precision"]45    assert p["mediane"]["EAM_pct"] == pytest.approx(10.97, abs=0.01) and p["mediane"]["croissance_totale_pct"] == pytest.approx(28.63, abs=0.01)46    assert p["hedonique"]["EAM_pct"] == pytest.approx(2.81, abs=0.01) and p["hedonique"]["croissance_totale_pct"] == pytest.approx(22.04, abs=0.01)4748def test_reventes_synthetiques(res):49    p = res["paires"]; r = res["resume_paires"]50    assert r["n_paires"] == 220 and r["n_flips"] == 47 and r["intervalle_median"] == 10.0 and r["intervalle_min"] == 3 and r["intervalle_max"] == 3851    assert (p["date_2"] <= c35.FIN_FENETRE).all() and (p["t_2"] > p["t_1"]).all() and r["meme_trimestre"] == 052    assert (p["prix_2"] % 500 == 0).all()53    assert r["part_moins_12"] == pytest.approx(0.559, abs=0.001) and r["sd_ln_ratio"] == pytest.approx(0.144, abs=0.001)54    # reproductibilité (graine fixée)55    p2 = c35.generer_reventes(res["ventes"]); assert len(p2) == 220 and np.allclose(p2["prix_2"], p["prix_2"])56    assert p[p["flip"]]["ln_ratio"].mean() == pytest.approx(0.0957, abs=0.0005) and p[~p["flip"]]["ln_ratio"].mean() == pytest.approx(0.0436, abs=0.0005)5758def test_bmn_identites(res):59    b = res["bmn"]60    D = b["D"]; assert D.shape == (220, 14)61    # chaque ligne de la matrice complète (période 0 comprise) somme à zéro : ici, −1 à t1 et +1 à t2, période 0 omise62    assert set(np.unique(D)) <= {-1.0, 0.0, 1.0} and np.all(np.abs(D).sum(1) <= 2)63    assert np.allclose(b["beta"], b["beta_ferme"], atol=1e-10)64    assert np.allclose(b["indice"], 100 * np.exp(np.concatenate([[0.0], b["beta"]])))65    assert b["indice"][-1] == pytest.approx(120.68, abs=0.005) and b["indice"][9] == pytest.approx(113.42, abs=0.005)66    assert b["sigma"] == pytest.approx(0.1455, abs=0.0005) and b["se"][-1] == pytest.approx(0.0483, abs=0.0005)67    assert res["precision"]["bmn"]["EAM_pct"] == pytest.approx(3.02, abs=0.01) and res["precision"]["bmn"]["largeur_ic_finale_pct"] == pytest.approx(20.83, abs=0.01)6869def test_case_shiller(res):70    cs = res["cs"]71    assert cs["indice"][-1] == pytest.approx(120.48, abs=0.005) and res["precision"]["cs"]["EAM_pct"] == pytest.approx(2.90, abs=0.01)72    a = cs["coef_variance"]; assert a[0] == pytest.approx(0.0208, abs=0.0001) and a[1] == pytest.approx(-0.00023, abs=0.00001)73    # sans les reventes rénovées, la pente redevient positive (structure 2σ² + δ²Δ de la loi de génération)74    ao = res["sensibilite"]["filtre"]["sans les reventes rénovées (oracle)"]["cs_coef_variance"]75    assert ao[0] == pytest.approx(0.0157, abs=0.0001) and ao[1] == pytest.approx(0.00053, abs=0.00001)76    assert cs["variance_theorique"][0] == pytest.approx(2 * 0.085 ** 2) and cs["variance_theorique"][1] == pytest.approx(0.0012)77    # poids = 1/sqrt(var) ; var minorée78    assert np.allclose(cs["poids"], 1 / np.sqrt(np.maximum(a[0] + a[1] * cs["gap_trimestres"], 1e-8)))79    # CS ≈ BMN quand les poids sont presque uniformes80    assert np.max(np.abs(cs["indice"] - res["bmn"]["indice"])) < 0.58182def test_spar(res):83    sp = res["spar"]; v = res["ventes"]84    assert sp["indice_geo"][0] == 100 and sp["indice_arith"][0] == 10085    assert sp["indice_geo"][-1] == pytest.approx(115.06, abs=0.005) and sp["indice_arith"][-1] == pytest.approx(114.36, abs=0.005)86    assert sp["ratio_median_global"] == pytest.approx(1.095, abs=0.001) and sp["sd_ln_ratio"] == pytest.approx(0.087, abs=0.001)87    assert res["precision"]["spar"]["EAM_pct"] == pytest.approx(1.26, abs=0.01)88    # identité arithmétique : ratio des sommes89    t0 = v[v["trim"] == 0]; assert sp["ratio_arith"][0] == pytest.approx(t0["prix_vente"].sum() / t0["valeur_role_2025"].sum())9091def test_ajustement_et_sensibilite(res):92    a = res["ajustement"]93    assert a["prix"] == 506_500 and a["trimestre_vente"] == "2025T2" and a["mois_ecoules"] == pytest.approx(15.2, abs=0.05)94    assert a["hedonique"] == pytest.approx(44_760, abs=1) and a["bmn"] == pytest.approx(32_406, abs=1) and a["cs"] == pytest.approx(32_359, abs=1)95    assert a["mediane"] == pytest.approx(44_009, abs=1) and a["spar"] == pytest.approx(26_575, abs=1) and a["tendance"] == pytest.approx(28_733, abs=1)96    assert a["vrai"] == pytest.approx(28_558, abs=1) and a["chaine"] == pytest.approx(30_987, abs=1)97    s = res["sensibilite"]98    r = s["revisions"]["2025T2"]; assert r["bmn_v2025T2"] == pytest.approx(116.39, abs=0.005) and r["bmn_v2025T4"] == pytest.approx(114.69, abs=0.005) and r["bmn_v2026T3"] == pytest.approx(113.42, abs=0.005)99    r4 = s["revisions"]["2024T4"]; assert r4["bmn_v2025T2"] == pytest.approx(114.45, abs=0.005) and r4["bmn_v2026T3"] == pytest.approx(109.99, abs=0.005)100    f = s["filtre"]101    assert f["toutes"]["n"] == 220 and f["intervalle ≥ 12 mois"]["n"] == 97 and f["intervalle ≥ 18 mois"]["n"] == 45 and f["sans les reventes rénovées (oracle)"]["n"] == 173102    assert f["toutes"]["bmn"]["croissance_totale_pct"] == pytest.approx(20.68, abs=0.01) and f["intervalle ≥ 12 mois"]["bmn"]["croissance_totale_pct"] == pytest.approx(17.98, abs=0.01)103    assert f["sans les reventes rénovées (oracle)"]["bmn"]["croissance_totale_pct"] == pytest.approx(18.39, abs=0.01) and f["sans les reventes rénovées (oracle)"]["bmn"]["EAM_pct"] == pytest.approx(2.01, abs=0.01)104    assert f["intervalle ≥ 12 mois"]["bmn"]["EAM_pct"] == pytest.approx(2.76, abs=0.01) and f["intervalle ≥ 18 mois"]["bmn"]["EAM_pct"] == pytest.approx(6.56, abs=0.01)105    t = s["taille"]; assert t[0.5]["EAM_moyen_pct"] == pytest.approx(4.26, abs=0.01) and t[0.25]["EAM_moyen_pct"] == pytest.approx(6.46, abs=0.01)106    assert t[0.5]["sd_indice_final"] == pytest.approx(4.77, abs=0.01) and t[0.25]["sd_indice_final"] == pytest.approx(8.96, abs=0.01)107    assert s["fenetre"]["n"] == 295 and s["fenetre"]["ecart_max_pct"] == pytest.approx(0.37, abs=0.01)108109def test_solutions_exercices(res):110    e = res["exercices"]111    assert e["1"]["var_mediane_pct"] == pytest.approx(6.67, abs=0.01) and e["1"]["composition_pct"] == pytest.approx(3.79, abs=0.01) and e["1"]["variation_pure_pct"] == pytest.approx(2.77, abs=0.01)112    assert np.allclose(e["2"]["indice"], [100, 103.15, 105.97, 110.74], atol=0.01) and e["2"]["variation_t3_t2_pct"] == pytest.approx(4.50, abs=0.01)113    assert e["2"]["ic_t3"][0] == pytest.approx(106.27, abs=0.01) and e["2"]["ic_t3"][1] == pytest.approx(115.39, abs=0.01)114    assert np.allclose(e["3"]["DtD"], [[3, -1], [-1, 2]]) and np.allclose(e["3"]["Dty"], [0.0288, 0.1726], atol=0.0001)115    assert np.allclose(e["3"]["beta"], [0.0461, 0.1093], atol=0.0001) and np.allclose(e["3"]["indice"], [100, 104.71, 111.55], atol=0.01)116    assert abs(e["3"]["residus"] @ np.array([[1, 0], [-1, 1], [0, 1], [1, 0]])).max() < 1e-12   # orthogonalité des résidus aux colonnes de D117    assert np.allclose(e["4"]["poids_rel"], [1, 0.902, 0.807, 0.737], atol=0.001) and np.allclose(e["4"]["sd_pct"], [12.53, 13.89, 15.52, 17.00], atol=0.01)118    assert e["5"]["ratio_arith"] == pytest.approx(1.0802, abs=0.0001) and e["5"]["ratio_geo"] == pytest.approx(1.0781, abs=0.0001)119    assert e["5"]["spar_arith"] == pytest.approx(103.36, abs=0.01) and e["5"]["spar_geo"] == pytest.approx(103.16, abs=0.01)120    assert e["6"]["ajustements"]["hedonique"] == pytest.approx(44_760, abs=1) and e["6"]["ajustements"]["bmn"] == pytest.approx(32_406, abs=1)121    assert e["6"]["ajustements"]["mediane"] == pytest.approx(44_009, abs=1) and e["6"]["tendance"] == pytest.approx(28_774, abs=1)122123def test_bibliotheque_bmn_cas_simple():124    """Deux périodes, deux paires : l'indice BMN est la moyenne géométrique des ratios de prix (forme fermée)."""125    r = h.ventes_repetees_bmn([100, 200], [110, 230], [0, 0], [1, 1], 2)126    assert r["indice"][1] == pytest.approx(100 * math.sqrt(1.10 * 1.15), rel=1e-12)127    idx = h.indice_dummies_temps([0.05, 0.10]); assert np.allclose(idx, [100, 100 * math.exp(0.05), 100 * math.exp(0.10)])128
Files · 11f6c54e39ac2d8ce41b1b464d12e942e4170c06
Menu

Theme