SPB Git
18.2 KB · 248 lines python
1"""Chapitre 19 — tests des fonctions spatiales ajoutées à traite.hedonique et des chiffres cités dans le texte et les solutions.2Tolérances : coefficients ±0,001 (ou relatives 1 %), log-vraisemblances ±0,1, indications en dollars ±1 $ (arrondi au dollar)."""3import sys, pathlib4import numpy as np, pytest5sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "cases"))6from traite import hedonique as h7import ch19_spatial as c1989# ----------------------------------------------------------------------------- fonctions génériques (données simulées)1011def _simul(n=400, seed=5):12    rng = np.random.default_rng(seed); coords = rng.uniform(0, 10, (n, 2)); W = h.matrice_poids_knn(coords, 6)13    X = np.column_stack([np.ones(n), rng.normal(size=n), rng.normal(size=n)])14    return rng, coords, W, X1516def test_sem_recupere_lambda_et_beta():17    rng, coords, W, X = _simul(seed=21); n = len(X); beta = np.array([1.0, 0.8, -0.5]); lam = 0.618    u = np.linalg.solve(np.eye(n) - lam * W, rng.normal(0, 0.3, n)); y = X @ beta + u19    r = h.sem_mv(y, X, W)20    assert abs(r["lambda"] - lam) < 0.12 and np.allclose(r["beta"], beta, atol=0.1)21    assert r["loglik"] > h.sar_mv(y, X, W)["loglik"] - 50      # cohérence des échelles de vraisemblance2223def test_sdm_recupere_theta_et_sar_coherent_avec_sar_lag_mv():24    rng, coords, W, X = _simul(seed=8); n = len(X); beta = np.array([1.0, 0.8, -0.5]); theta = np.array([0.4, 0.0]); rho = 0.425    y = np.linalg.solve(np.eye(n) - rho * W, X @ beta + (W @ X[:, 1:]) @ theta + rng.normal(0, 0.3, n))26    r = h.sar_mv(y, X, W, durbin=True)27    assert abs(r["rho"] - rho) < 0.12 and np.allclose(r["beta"][:3], beta, atol=0.12) and abs(r["beta"][3] - 0.4) < 0.1528    # le SAR par raffinement coïncide avec la recherche sur grille de sar_lag_mv (à la résolution de la grille)29    s1 = h.sar_mv(y, X, W); s2 = h.sar_lag_mv(y, X, W)30    assert abs(s1["rho"] - s2["rho"]) < 0.013132def test_tests_lm_detectent_la_bonne_forme():33    rng, coords, W, X = _simul(seed=3); n = len(X); beta = np.array([1.0, 0.8, -0.5])34    y0 = X @ beta + rng.normal(0, 0.3, n)35    t0 = h.tests_lm_spatial(y0, X, W)36    assert t0["LM_erreur"][1] > 0.01 and t0["LM_decalage"][1] > 0.01           # pas de dépendance : tests non significatifs37    u = np.linalg.solve(np.eye(n) - 0.6 * W, rng.normal(0, 0.3, n)); y1 = X @ beta + u38    t1 = h.tests_lm_spatial(y1, X, W)39    assert t1["LM_erreur"][1] < 1e-6 and t1["RLM_erreur"][0] > t1["RLM_decalage"][0]40    assert t1["SARMA"][0] == pytest.approx(t1["LM_erreur"][0] + t1["RLM_decalage"][0])4142def test_effets_sar_identites():43    _, coords, W, X = _simul(); rho = 0.5; beta = np.array([1.0, 0.8, -0.5])44    e = h.effets_sar(rho, beta, W)45    assert np.allclose(e["total"], e["direct"] + e["indirect"])46    # W standardisée en ligne : effet total moyen = β/(1 − ρ) exactement47    assert np.allclose(e["total"], beta / (1 - rho), atol=1e-8)48    assert (np.abs(e["direct"] - beta) < np.abs(e["indirect"]) + 1e-9).all() and e["direct"][1] > beta[1]4950def test_gwr_grande_bande_egale_mco_et_cv_minimal_raisonnable():51    rng, coords, W, X = _simul(seed=2); n = len(X)52    y = X @ np.array([1.0, 0.8, -0.5]) + rng.normal(0, 0.3, n)53    g = h.gwr(y, X, coords, bande=1e4); r = h.mco(X, y)54    assert np.allclose(g["beta"], r["beta"][None, :], atol=1e-4) and abs(g["trace_S"] - 3) < 1e-355    # coefficient variant dans l'espace : la bande choisie par CV est finie et la GWR améliore la CV56    y2 = 1.0 + (0.3 + 0.1 * coords[:, 0]) * X[:, 1] - 0.5 * X[:, 2] + rng.normal(0, 0.3, n)57    sel = h.gwr_bande_cv(y2, X, coords, bornes=(0.5, 50.0), n_grille=12)58    assert sel["bande"] < 20 and sel["cv"] < h.gwr(y2, X, coords, 1e4)["cv"]5960def test_variogramme_bruit_blanc_plat_et_krigeage_exact():61    rng = np.random.default_rng(9); coords = rng.uniform(0, 20, (500, 2)); z = rng.normal(0, 0.1, 500)62    vg = h.variogramme_empirique(coords, z, np.arange(0, 10.1, 1.0))63    assert np.allclose(vg["gamma"], 0.01, atol=0.0025)                              # γ(h) ≈ σ² pour un bruit blanc64    p = {"pepite": 0.0, "palier_partiel": 0.01, "portee": 3.0}65    k = h.krigeage_ordinaire(coords[:5], z[:5], coords[:5], p, voisins=None)66    assert np.allclose(k["prediction"], z[:5], atol=1e-8) and np.allclose(k["variance"], 0.0, atol=1e-8)   # interpolateur exact sans pépite67    assert np.allclose(h.variogramme_exponentiel(np.array([0.0, 1e9]), 0.002, 0.008, 2.0), [0.0, 0.010])6869def test_matrice_spatio_temporelle_unidirectionnelle_et_nilpotente():70    rng = np.random.default_rng(4); coords = rng.uniform(0, 5, (120, 2)); t = np.sort(rng.uniform(0, 36, 120))71    W = h.matrice_poids_spatio_temporelle(coords, t, portee=2.0, fenetre=12)72    assert np.all(np.triu(W) == 0)                                                  # seules les ventes antérieures (indices inférieurs) pèsent73    assert np.abs(np.linalg.eigvals(W)).max() < 1e-10                             # nilpotente : log|I − ρW| = 074    s = W.sum(1); assert np.allclose(s[s > 0], 1.0)75    Wb = h.matrice_poids_spatio_temporelle(coords, t, 2.0, 12, unidirectionnel=False)76    assert (Wb > 0).sum() >= (W > 0).sum()7778def test_coordonnees_km_et_moran_par_distance():79    xy = h.coordonnees_km([45.0, 45.0 + 1 / 110.57], [-75.0, -75.0], 45.0, -75.0)80    assert xy[1, 1] == pytest.approx(1.0, abs=1e-6) and xy[1, 0] == pytest.approx(0.0, abs=1e-9)81    rng = np.random.default_rng(6); coords = rng.uniform(0, 10, (300, 2)); x = coords[:, 0] + rng.normal(0, 0.3, 300)82    cg = h.moran_par_distance(x, coords, [0, 1, 2, 4, 8])83    assert cg[0]["I"] > cg[-1]["I"] and cg[0]["z"] > 38485# ----------------------------------------------------------------------------- chiffres du chapitre (cas 1)8687@pytest.fixture(scope="module")88def res():89    return c19.main(verbose=False)9091def test_moran_residus_avec_et_sans_secteur(res):92    m = res["moran"].set_index("serie")93    assert m.loc["Résidus, modèle de référence", "I_knn"] == pytest.approx(-0.0136, abs=0.001)94    assert abs(m.loc["Résidus, modèle de référence", "z_knn"]) < 1.9695    assert m.loc["Résidus, sans indicatrices de secteur", "I_knn"] == pytest.approx(0.3237, abs=0.001)96    assert m.loc["Résidus, sans indicatrices de secteur", "z_knn"] == pytest.approx(17.11, abs=0.05)97    assert m.loc["Résidus, sans secteur ni distance", "I_knn"] == pytest.approx(0.3595, abs=0.001)98    assert m.loc["ln P (sans modèle)", "I_knn"] == pytest.approx(0.2013, abs=0.001)99    assert m.loc["Résidus, sans indicatrices de secteur", "I_st"] == pytest.approx(0.305, abs=0.001)100    assert res["base"]["modele"]["r2"] == pytest.approx(0.853, abs=0.001) and res["base"]["mco_r"]["r2"] == pytest.approx(0.766, abs=0.001)101102def test_correlogramme(res):103    cg = res["correlogramme"]["sans_secteur"]104    assert cg[0]["I"] == pytest.approx(0.225, abs=0.002) and cg[4]["I"] == pytest.approx(0.261, abs=0.002)105    assert cg[-1]["I"] < -0.25 and all(abs(c["z"]) < 2 for c in res["correlogramme"]["reference"])106107def test_tests_lm(res):108    r = res["lm"]["sans_secteur"]109    assert r["LM_erreur"][0] == pytest.approx(285.18, abs=0.05) and r["LM_decalage"][0] == pytest.approx(116.21, abs=0.05)110    assert r["RLM_erreur"][0] == pytest.approx(170.06, abs=0.05) and r["RLM_decalage"][0] == pytest.approx(1.09, abs=0.01)111    assert r["RLM_decalage"][1] == pytest.approx(0.2975, abs=0.001)112    c = res["lm"]["reference"]113    assert all(p > 0.1 for _, p in c.values())114115def test_sar_sem_sdm(res):116    m = res["modeles"]117    assert m["sar_r"]["rho"] == pytest.approx(0.4385, abs=0.0005) and m["sar_r"]["se_rho"] == pytest.approx(0.0412, abs=0.0005)118    assert m["sem_r"]["lambda"] == pytest.approx(0.6486, abs=0.0005) and m["sem_r"]["se_lambda"] == pytest.approx(0.0447, abs=0.0005)119    assert m["sdm_r"]["rho"] == pytest.approx(0.5089, abs=0.0005)120    assert m["mco_r"]["loglik"] == pytest.approx(510.71, abs=0.05) and m["sar_r"]["loglik"] == pytest.approx(554.23, abs=0.05)121    assert m["sem_r"]["loglik"] == pytest.approx(590.58, abs=0.05) and m["sdm_r"]["loglik"] == pytest.approx(606.72, abs=0.05)122    assert m["mco_c"]["loglik"] == pytest.approx(650.00, abs=0.05)123    assert m["mco_r"]["aic"] == pytest.approx(-995.4, abs=0.1) and m["sar_r"]["aic"] == pytest.approx(-1080.5, abs=0.1)124    assert m["sem_r"]["aic"] == pytest.approx(-1153.2, abs=0.1) and m["sdm_r"]["aic"] == pytest.approx(-1163.4, abs=0.1) and m["mco_c"]["aic"] == pytest.approx(-1266.0, abs=0.1)125    assert m["sar_r"]["moran_residus"] == pytest.approx(0.121, abs=0.002) and m["sem_r"]["moran_residus"] == pytest.approx(-0.041, abs=0.002)126    # modèle de référence : aucun paramètre spatial significatif127    assert abs(m["sar_c"]["rho"] / m["sar_c"]["se_rho"]) < 1.96 and abs(m["sem_c"]["lambda"] / m["sem_c"]["se_lambda"]) < 1.96128    assert m["sar_c"]["rho"] == pytest.approx(-0.080, abs=0.001) and m["sem_c"]["lambda"] == pytest.approx(-0.066, abs=0.001)129    # coefficients cités130    cols = list(res["base"]["Xr"].columns)131    assert m["sar_r"]["beta"][cols.index("ln_S")] == pytest.approx(0.5571, abs=0.0005)132    assert m["sem_r"]["beta"][cols.index("ln_S")] == pytest.approx(0.5416, abs=0.0005)133    assert m["mco_r"]["beta"][cols.index("ln_T")] == pytest.approx(0.0419, abs=0.0005) and m["sem_r"]["beta"][cols.index("ln_T")] == pytest.approx(0.0886, abs=0.0005)134    assert m["mco_r"]["beta"][cols.index("dist_centre")] == pytest.approx(-0.0117, abs=0.0005)135    assert m["sdm_r"]["beta"][len(cols) + cols.index("ln_S") - 1] == pytest.approx(-0.2439, abs=0.0005)   # θ de W·ln_S136137def test_effets_directs_indirects(res):138    e = res["effets"]139    assert e.loc["ln_S", "sar_direct"] == pytest.approx(0.5715, abs=0.0005) and e.loc["ln_S", "sar_indirect"] == pytest.approx(0.4206, abs=0.0005)140    assert e.loc["ln_S", "sar_total"] == pytest.approx(0.9921, abs=0.0005)141    assert e.loc["ln_S", "sar_total"] == pytest.approx(res["modeles"]["sar_r"]["beta"][1] / (1 - res["modeles"]["sar_r"]["rho"]), abs=1e-6)142    assert e.loc["qualite", "sar_total"] == pytest.approx(0.1076, abs=0.0005)143    assert e.loc["ln_S", "sdm_direct"] == pytest.approx(0.5484, abs=0.0005) and e.loc["ln_S", "sdm_indirect"] == pytest.approx(0.0666, abs=0.0005)144    assert e.loc["ln_S", "sdm_total"] == pytest.approx(0.6150, abs=0.0005)145146def test_gwr(res):147    g = res["gwr"]148    assert g["selection"]["bande"] == pytest.approx(4.99, abs=0.03)149    assert g["gwr"]["trace_S"] == pytest.approx(44.8, abs=0.1) and g["gwr"]["aicc"] == pytest.approx(-1233.2, abs=0.2)150    assert g["aicc_mco_sans_secteur"] == pytest.approx(-994.8, abs=0.2) and g["aicc_mco_reference"] == pytest.approx(-1264.9, abs=0.2)151    assert g["gwr"]["r2"] == pytest.approx(0.861, abs=0.001) and g["gwr"]["sigma"] == pytest.approx(0.0829, abs=0.0002)152    q = g["quartiles"]153    assert q.loc["ln_S", "50%"] == pytest.approx(0.5418, abs=0.0005) and q.loc["ln_S", "min"] == pytest.approx(0.5083, abs=0.0005) and q.loc["ln_S", "max"] == pytest.approx(0.5522, abs=0.0005)154    c = g["comparaison_secteurs"]155    assert c.loc["Buckingham", "gwr_localisation_rel_hull"] == pytest.approx(-0.3131, abs=0.001) and c.loc["Buckingham", "reference_localisation"] == pytest.approx(-0.3440, abs=0.001)156    assert c.loc["Buckingham", "generateur_localisation"] == pytest.approx(-0.3418, abs=0.001)157    assert c.loc["Aylmer", "gwr_localisation_rel_hull"] == pytest.approx(0.0034, abs=0.001) and c.loc["Aylmer", "generateur_localisation"] == pytest.approx(0.0622, abs=0.001)158    assert c.loc["Masson-Angers", "gwr_localisation_rel_hull"] == pytest.approx(-0.3190, abs=0.001)159160def test_effets_fixes(res):161    ef = res["effets_fixes"].set_index("specification")162    assert ef.iloc[1]["aic"] == pytest.approx(-1266.0, abs=0.1)                     # référence : meilleur AIC163    assert ef["aic"].idxmin() == ef.index[1]164    g2 = [i for i in ef.index if i.startswith("Grille de 2")][0]; g5 = [i for i in ef.index if i.startswith("Grille de 5")][0]165    assert ef.loc[g2, "r2"] == pytest.approx(0.8721, abs=0.0005) and ef.loc[g2, "aic"] == pytest.approx(-1225.4, abs=0.1) and ef.loc[g2, "parametres"] == 78166    assert ef.loc[g5, "aic"] == pytest.approx(-1252.1, abs=0.1) and ef.loc[g5, "parametres"] == 30167    tri = [i for i in ef.index if "trimestrielles" in i][0]168    assert ef.loc[tri, "aic"] == pytest.approx(-1253.8, abs=0.1)169    idx = res["effets_fixes"].attrs["indice_trimestriel"]170    assert idx["2026Q3"] == pytest.approx(0.199, abs=0.001) and res["effets_fixes"].attrs["tendance_mensuelle"] == pytest.approx(0.0036, abs=0.0001)171172def test_variogramme_krigeage(res):173    vk = res["variogramme"]; p = vk["parametres"]174    assert vk["variogramme"]["gamma"][0] == pytest.approx(0.00699, abs=0.00005) and vk["variogramme"]["gamma"].max() == pytest.approx(0.01702, abs=0.00005)175    assert p["pepite"] == pytest.approx(0.00455, abs=0.00005) and p["palier_partiel"] == pytest.approx(0.00897, abs=0.00005) and p["portee"] == pytest.approx(5.0, abs=1e-6)176    assert vk["variance_residus"] == pytest.approx(0.01067, abs=0.00005) and vk["sigma2_reference"] == pytest.approx(0.00689, abs=0.00005)177    assert vk["sujet"]["prediction"][0] == pytest.approx(-0.0597, abs=0.0005) and vk["sujet"]["variance"][0] == pytest.approx(0.00567, abs=0.00005)178    assert np.all(np.abs(vk["variogramme_reference"]["gamma"] - 0.0068) < 0.0006)   # variogramme plat pour le modèle de référence179180def test_spatio_temporel(res):181    st = res["spatio_temporel"]182    assert st["n_retenues"] == 582 and st["n_sans_voisin"] == 18 and st["max_abs_valeur_propre"] < 1e-10183    assert st["moran_e_st"]["I"] == pytest.approx(0.305, abs=0.001)184    assert st["rho_st"] == pytest.approx(0.3696, abs=0.0005) and st["se_rho_st"] == pytest.approx(0.0532, abs=0.0005)185    assert st["moran_e_apres_lag"] == pytest.approx(0.128, abs=0.002)186    assert st["sem_st"]["lambda"] == pytest.approx(0.7485, abs=0.0005) and st["sem_st"]["aic"] == pytest.approx(-1154.8, abs=0.1)187188def test_validation_croisee(res):189    v = res["validation"].set_index("modele")["rmse_log"]190    attendu = {"MCO, modèle de référence (secteurs)": 0.0841, "MCO, sans indicatrices de secteur": 0.1053, "MCO sans secteur + krigeage des résidus": 0.0878,191               "SEM sans secteur (espérance conditionnelle)": 0.0882, "SAR sans secteur (prédiction conditionnelle)": 0.0950, "GWR sans secteur (validation croisée)": 0.0860}192    for k, x in attendu.items():193        assert v[k] == pytest.approx(x, abs=0.0001), k194    e = res["validation"].set_index("modele")["erreur_abs_moy_pct"]195    assert e["MCO, modèle de référence (secteurs)"] == pytest.approx(0.0681, abs=0.0002) and e["MCO, sans indicatrices de secteur"] == pytest.approx(0.0838, abs=0.0002)196197def test_indications_sujet(res):198    s = res["sujet"]["indication"].round(0).values199    assert list(s) == [637953, 664342, 624799, 633352, 654429, 627519]200    assert res["sujet"]["ln_correction"].iloc[3] == pytest.approx(-0.0520, abs=0.0005) and res["sujet"]["ln_correction"].iloc[5] == pytest.approx(-0.0311, abs=0.0005)201    assert all(sct == "Hull" for sct in res["sujet"].attrs["secteurs_voisins"]) and len(res["sujet"].attrs["voisins_sujet"]) == 8202    assert "G0404" in res["sujet"].attrs["voisins_sujet"] and "G0574" in res["sujet"].attrs["voisins_sujet"]203204def test_sensibilite(res):205    s = res["sensibilite"]; k = s[s["famille"].str.startswith("k")].set_index("parametre")206    assert list(k.index) == [4, 6, 8, 12, 16] and (k["lignes_vides"] == 0).all()207    assert k.loc[4, "rho"] == pytest.approx(0.2359, abs=0.0005) and k.loc[16, "rho"] == pytest.approx(0.6537, abs=0.0005)208    assert k.loc[4, "lambda"] == pytest.approx(0.5066, abs=0.0005) and k.loc[16, "lambda"] == pytest.approx(0.7855, abs=0.0005)209    assert k["rho"].is_monotonic_increasing and k["lambda"].is_monotonic_increasing210    assert (k["I"].between(0.31, 0.34)).all()211    d = s[s["famille"] == "distance inverse"].set_index("parametre")212    assert d.loc[2.0, "rho"] == pytest.approx(0.6360, abs=0.0005) and d.loc[4.0, "lambda"] == pytest.approx(0.8876, abs=0.0005)213214# ----------------------------------------------------------------------------- solutions des exercices215216def test_solutions_exercices():217    from scipy.stats import chi2218    # ex. 19.1 : cinq maisons alignées, contiguïté standardisée219    e = np.array([0.10, 0.06, -0.02, -0.08, -0.06]); W = np.zeros((5, 5))220    for i in range(5):221        for j in (i - 1, i + 1):222            if 0 <= j < 5: W[i, j] = 1223    W = W / W.sum(1, keepdims=True); r = h.moran_i(e, W)224    assert r["I"] == pytest.approx(0.6917, abs=0.0005) and r["E_I"] == pytest.approx(-0.25) and r["z"] == pytest.approx(2.37, abs=0.01)225    # ex. 19.3 : règle de décision226    assert chi2.sf(1.09, 1) == pytest.approx(0.296, abs=0.001) and chi2.ppf(0.95, 1) == pytest.approx(3.841, abs=0.001)227    # ex. 19.4 : effet total = β/(1 − ρ)228    assert 0.5571 / (1 - 0.4385) == pytest.approx(0.992, abs=0.001) and 0.0604 / (1 - 0.4385) == pytest.approx(0.1076, abs=0.0005)229    # ex. 19.5 : poids gaussiens230    d = np.array([0.5, 1, 2, 5])231    assert np.allclose(np.exp(-0.5 * (d / 2) ** 2), [0.969, 0.882, 0.607, 0.044], atol=0.001)232    assert np.allclose(np.exp(-0.5 * (d / 5) ** 2), [0.995, 0.980, 0.923, 0.607], atol=0.001)233    # ex. 19.6 : krigeage ordinaire à trois points234    co = np.array([[1, 0], [0, 2], [-3, 0.0]]); z = np.array([0.08, 0.05, -0.01]); p = {"pepite": 0.0045, "palier_partiel": 0.009, "portee": 5.0}235    k = h.krigeage_ordinaire(co, z, np.array([[0, 0.0]]), p, None)236    assert k["prediction"][0] == pytest.approx(0.0489, abs=0.0001) and k["variance"][0] == pytest.approx(0.00869, abs=0.00001)237    # ex. 19.1 (suite) : signes alternés → I = −1 exactement sur cette configuration238    e2 = np.array([0.10, -0.10, 0.10, -0.10, 0.10]); assert h.moran_i(e2, W)["I"] == pytest.approx(-1.0, abs=1e-9)239    # ex. 19.6 : poids du krigeage vs inverse de la distance240    idw = (1 / np.array([1, 2, 3.0])); idw /= idw.sum(); assert idw[2] == pytest.approx(0.182, abs=0.001) and (idw * z).sum() == pytest.approx(0.0555, abs=0.0001)241242def test_solution_exercice_7(res):243    r = c19.exercice_7(res["base"]); s = r["sem_par_k"].set_index("k")244    assert s.loc[4, "lambda"] == pytest.approx(0.5066, abs=0.0005) and s.loc[16, "lambda"] == pytest.approx(0.7855, abs=0.0005)245    assert s.loc[4, "u_sujet"] == pytest.approx(-0.0189, abs=0.0005) and s.loc[16, "u_sujet"] == pytest.approx(-0.0554, abs=0.0005)246    assert list(s["indication"].round(0)) == [656724, 633352, 628672]247    assert r["st_24"]["sans_voisin"] == 16 and r["st_24"]["rho_st"] == pytest.approx(0.4149, abs=0.0005) and r["st_12"]["rho_st"] == pytest.approx(0.3696, abs=0.0005)248
Files · 11f6c54e39ac2d8ce41b1b464d12e942e4170c06
Menu

Theme