"""Chapitre 20 — réconciliation et incertitude : identités, chiffres cités dans le texte et dans les solutions (tolérances déclarées).""" import sys, pathlib, math import numpy as np, pytest sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "cases")) import ch20_reconciliation as c20 from traite import cas1, cout as ct @pytest.fixture(scope="module") def r(): return c20.tout() # ---------------------------------------------------------------- indications héritées (ch. 16-18, 21) def test_indications_heritees(r): b = r["base"] assert list(b["adj5"]["id_vente"]) == ["G0574", "G0404", "G0468", "G0477", "G0575"] assert b["adj5"]["prix_ajuste"].values == pytest.approx([640247, 541222, 572793, 568853, 584862], abs=1) assert b["V_comp"] == pytest.approx(575565, abs=1) and b["sd_comp_pondere"] == pytest.approx(33997, abs=1) assert b["poids_comp"] == pytest.approx([0.179, 0.327, 0.202, 0.159, 0.133], abs=0.001) assert b["V_hed"] == pytest.approx(637953, abs=1) assert b["ip_hed"]["bas"] == pytest.approx(539682, abs=1) and b["ip_hed"]["haut"] == pytest.approx(749087, abs=1) assert len(b["cand"]) == 16 assert r["cout"]["CN"] == pytest.approx(397567.33, abs=0.01) def test_grille_rapide_reproduit_la_bibliotheque(r): """La cascade vectorisée du chapitre donne les mêmes prix ajustés et bruts que traite.cas1.ajustements_hedoniques (16 candidats).""" b = r["base"]; g = c20.grille_rapide(b["beta"], b["Xs"], b["Xc16"], b["P16"], b["G"]) assert g["P_aj"] == pytest.approx(b["adj16"]["prix_ajuste"].values, rel=1e-9) assert g["brut"] == pytest.approx(b["adj16"]["ajust_brut_pct"].values, rel=1e-9) assert g["valeur"] != pytest.approx(b["V_comp"], abs=1) # 16 candidats ≠ 5 retenus g5 = c20.grille_rapide(b["beta"], b["Xs"], b["Xc16"][:5], b["P16"][:5], b["G"]) assert g5["valeur"] == pytest.approx(b["V_comp"], abs=1e-6) and g5["sd_pondere"] == pytest.approx(b["sd_comp_pondere"], abs=1e-6) # identité P_aj = exp(x_s'β)·exp(e_j) pour les 16 candidats (prop. 17.2) point = math.exp(float(b["Xs"] @ b["beta"])) assert g["P_aj"] == pytest.approx(point * np.exp(b["adj16"]["residu_log"].values), rel=1e-9) def test_residus_candidats(r): a = r["base"]["adj16"] ret = a["retenu"].values assert np.mean(np.exp(a.loc[ret, "residu_log"]) - 1) == pytest.approx(-0.0853, abs=0.0005) assert np.mean(np.exp(a.loc[~ret, "residu_log"]) - 1) == pytest.approx(-0.0171, abs=0.0005) assert np.mean(np.exp(a["residu_log"]) - 1) == pytest.approx(-0.0384, abs=0.0005) # ---------------------------------------------------------------- bootstrap et énumération def test_bootstrap(r): bs = r["bootstrap"]; rs = bs["resume"] assert len(bs["VA"]) == c20.B == 2000 assert rs["candidats"]["ecart_type"] == pytest.approx(18160, abs=30) assert rs["candidats"]["q05"] == pytest.approx(552872, abs=60) and rs["candidats"]["q95"] == pytest.approx(611283, abs=60) assert rs["ajustements"]["ecart_type"] == pytest.approx(2672, abs=10) assert rs["conjoint"]["ecart_type"] == pytest.approx(18385, abs=30) assert rs["conjoint"]["q05"] == pytest.approx(552095, abs=60) and rs["conjoint"]["q95"] == pytest.approx(611779, abs=60) assert rs["parametrique"]["ecart_type"] == pytest.approx(2751, abs=10) assert rs["prediction"]["ecart_type"] == pytest.approx(6531, abs=15) and rs["prediction"]["moyenne"] == pytest.approx(637736, abs=30) assert bs["rho_conjoint_pred"] == pytest.approx(0.088, abs=0.005) and bs["rho_ajustements_pred"] == pytest.approx(0.425, abs=0.01) assert bs["part_C_sous_hed"] > 0.99 # l'incertitude des coefficients est petite devant celle de la sélection assert rs["ajustements"]["ecart_type"] < 0.2 * rs["candidats"]["ecart_type"] # la variance conjointe est à peu près la somme des deux (indépendance des deux tirages) assert rs["conjoint"]["ecart_type"] ** 2 == pytest.approx(rs["candidats"]["ecart_type"] ** 2 + rs["ajustements"]["ecart_type"] ** 2, rel=0.05) def test_exhaustif(r): ex = r["exhaustif"] assert ex["n"] == math.comb(16, 5) == 4368 assert ex["moyenne"] == pytest.approx(606317, abs=1) and ex["ecart_type"] == pytest.approx(20492, abs=1) assert ex["min"] == pytest.approx(550231, abs=1) and ex["max"] == pytest.approx(661670, abs=1) assert ex["rang_selection"] == 279 assert ex["part_sous_hed"] == pytest.approx(0.935, abs=0.002) par = ex["par_n_retenus"] assert list(par["count"]) == [462, 1650, 1650, 550, 55, 1] assert par.loc[5, "mean"] == pytest.approx(575565, abs=1) # la moyenne par nombre de retenus décroît : plus on inclut les cinq « vrais » comparables, plus la valeur baisse assert all(np.diff(par["mean"].values) < 0) def test_elargissement(r): el = r["elargissement"].set_index("k") assert el.loc[5, "valeur_ponderee"] == pytest.approx(575565, abs=1) assert el.loc[16, "valeur_ponderee"] == pytest.approx(604689, abs=1) and el.loc[16, "moyenne"] == pytest.approx(611401, abs=1) assert el.loc[16, "residu_moyen_pct"] == pytest.approx(-0.0384, abs=0.0005) assert el.loc[10, "valeur_ponderee"] == pytest.approx(592737, abs=1) # ---------------------------------------------------------------- coût (chapitre 24) def test_cout(r): co = r["cout"]; CN = co["CN"] assert CN == pytest.approx(397567.33, abs=0.01) assert co["V_L"] == 277000 and co["V_L_mediane"] == pytest.approx(276954, abs=1) and co["n_terrain"] == 13 assert co["D_ext"] == pytest.approx(87461.05, abs=0.05) and co["taux_ext"] == pytest.approx(0.2200, abs=0.0001) assert co["D_fonc"] == pytest.approx(7605.31, abs=0.01) assert co["D_tot"] == pytest.approx(95066.36, abs=0.05) and co["taux_tot"] == pytest.approx(0.2391, abs=0.0001) assert co["valeur"] == pytest.approx(co["V_L"] + CN - co["D_tot"], abs=1e-6) and co["valeur"] == pytest.approx(579500.97, abs=0.05) assert co["arrondie"] == 580000 and co["ventilee"] == pytest.approx(445872.14, abs=0.05) assert co["cout_neuf_plus_terrain"] == pytest.approx(674567, abs=1) assert co["extraction_hed"]["depreciation"] == pytest.approx(36614, abs=1) and co["extraction_hed"]["taux"] == pytest.approx(0.0921, abs=0.0001) assert co["extraction_comp"]["depreciation"] == pytest.approx(99002, abs=1) and co["extraction_comp"]["taux"] == pytest.approx(0.2490, abs=0.0001) # contre-exemple : hypothèses d'usage (allocation 30 %, âge-vie 60 ans) u = co["usage"] assert u["V_L"] == pytest.approx(172669, abs=1) and u["D"] == pytest.approx(185531, abs=1) and u["valeur"] == pytest.approx(384705, abs=1) assert u["extraction_hed"]["depreciation"] == pytest.approx(-67716, abs=1) assert co["sd_spec"] == pytest.approx(2377, abs=2) # identité de la bibliothèque : valeur_par_cout = V_L + C_N − D v = ct.valeur_par_cout(100000, 400000, 150000) assert v["valeur"] == 350000 and v["taux_depreciation"] == pytest.approx(0.375) def test_bootstrap_cout(r): bk = r["bootstrap_cout"]; rk = bk["resume"] assert len(bk["VK"]) == 2000 and not np.isnan(bk["VK"]).any() assert rk["ecart_type"] == pytest.approx(23575, abs=60) assert rk["q05"] == pytest.approx(552142, abs=150) and rk["q95"] == pytest.approx(628078, abs=150) assert bk["rho_cout_pred"] == pytest.approx(0.225, abs=0.01) and bk["rho_cout_comp"] == pytest.approx(0.018, abs=0.01) assert bk["sd_VL"] == pytest.approx(29803, abs=80) and bk["sd_T28"] == pytest.approx(0.0419, abs=0.0005) # ---------------------------------------------------------------- combinaison def test_poids_optimaux_et_mcg(r): cb = r["combinaison"]; S = cb["Sigma"] for w in cb["schemas"].values(): assert w.sum() == pytest.approx(1.0, abs=1e-12) w = c20.poids_gls(S) var = w @ S @ w rng = np.random.default_rng(0) for _ in range(200): # tout autre vecteur de poids sommant à 1 a une variance ≥ celle des MCG d = rng.normal(size=3); d -= d.mean(); w2 = w + 0.1 * d assert w2 @ S @ w2 >= var - 1e-6 # formule à deux indications = MCG sur la sous-matrice 2 × 2 s1, s2, rho = cb["s"][0], cb["s"][1], cb["rho"] S2 = np.array([[s1 ** 2, rho * s1 * s2], [rho * s1 * s2, s2 ** 2]]) assert c20.poids_optimaux_deux(s1, s2, rho) == pytest.approx(c20.poids_gls(S2)[0], rel=1e-12) assert c20.poids_optimaux_deux(s1, s2, 0.0) == pytest.approx((1 / s1 ** 2) / (1 / s1 ** 2 + 1 / s2 ** 2), rel=1e-12) assert c20.poids_optimaux_deux(s1, s2, 0.9) < 0 # poids négatif lorsque ρ > σ_h/σ_c def test_combinaison_chiffres(r): cb = r["combinaison"]; t = cb["tableau"].set_index("schema") assert cb["V"] == pytest.approx([575565, 637953, 579501], abs=1) assert cb["s"] == pytest.approx([18385, 11462, 23695], abs=60) assert cb["sd_spec_hed"] == pytest.approx(9420, abs=1) and cb["sd_pred_boot"] == pytest.approx(6531, abs=15) assert cb["R"][0, 1] == pytest.approx(0.088, abs=0.005) and cb["R"][1, 2] == pytest.approx(0.225, abs=0.01) and cb["R"][0, 2] == pytest.approx(0.018, abs=0.01) assert t.loc["égale (un tiers chacune)", "valeur"] == pytest.approx(597673, abs=1) assert cb["moyenne_simple_usage"] == pytest.approx(532741, abs=1) assert t.loc["inverse de la variance (indépendance)", "valeur"] == pytest.approx(614579, abs=200) assert t.loc["inverse de la variance (indépendance)", ["w_comp", "w_hed", "w_cout"]].values == pytest.approx([0.240, 0.616, 0.144], abs=0.005) assert t.loc["variance minimale avec corrélation (MCG)", "valeur"] == pytest.approx(616823, abs=250) assert t.loc["variance minimale avec corrélation (MCG)", ["w_comp", "w_hed", "w_cout"]].values == pytest.approx([0.248, 0.655, 0.097], abs=0.006) assert t.loc["raisonnée (retenue)", "valeur"] == pytest.approx(604230, abs=1) assert t.loc["raisonnée (retenue)", "ecart_type"] == pytest.approx(10450, abs=40) assert cb["conclusion"] == 605000 and cb["fourchette90"] == (590000, 625000) and cb["fourchette_conclue"] == (575000, 640000) assert cb["q"]["q05"] == pytest.approx(592310, abs=150) and cb["q"]["q95"] == pytest.approx(622009, abs=150) assert cb["ecart_conclusion_comp"] == pytest.approx(0.0511, abs=0.0005) and cb["ecart_conclusion_hed"] == pytest.approx(-0.0517, abs=0.0005) assert cb["w_opt_deux"][0.0] == pytest.approx(0.280, abs=0.002) and cb["w_opt_deux"][0.6] == pytest.approx(0.023, abs=0.002) def test_verite_synthetique(r): vt = r["verite"]; b = r["base"] assert vt["V_true"] == pytest.approx(641159, abs=5) assert vt["se_c"] == pytest.approx(0.0034, abs=0.0002) and vt["sigma_e"] == pytest.approx(0.0827, abs=0.001) assert abs(vt["ecart_hed"]) < 0.01 and vt["ecart_comp"] == pytest.approx(-0.1023, abs=0.001) assert r["combinaison"]["conclusion"] / vt["V_true"] - 1 == pytest.approx(-0.0564, abs=0.001) # la prédiction hédonique est dans l'intervalle de la vérité ± 2 erreurs-types de la constante assert vt["V_true_bas"] <= b["V_hed"] * 1.01 and b["V_hed"] * 0.99 <= vt["V_true_haut"] # ---------------------------------------------------------------- solutions des exercices def test_solutions_exercices(r): e = r["exercices"] assert e["ex1"]["w1"] == pytest.approx(0.1379, abs=0.0001) and e["ex1"]["V"] == pytest.approx(628552, abs=1) and e["ex1"]["sd"] == pytest.approx(11142, abs=1) assert e["ex1"]["V_egal"] == 609000 and e["ex1"]["sd_egal"] == pytest.approx(16155, abs=1) assert e["ex2"]["ic_bas"] == pytest.approx(625586, abs=5) and e["ex2"]["ic_haut"] == pytest.approx(650596, abs=5) assert e["ex2"]["ip_bas"] == pytest.approx(539682, abs=60) and e["ex2"]["ip_haut"] == pytest.approx(749087, abs=60) assert e["ex2"]["rapport_largeurs"] == pytest.approx(8.36, abs=0.02) assert e["ex6"]["V_30_55_15"] == pytest.approx(610469, abs=1) and e["ex6"]["arrondi_30_55_15"] == 610000 assert e["ex6"]["V_k16_raisonnee"] == pytest.approx(615880, abs=1) and e["ex6"]["arrondi_k16"] == 615000 assert e["ex6"]["V_55_30_15"] == pytest.approx(594872, abs=1) and e["ex6"]["arrondi_55_30_15"] == 595000 assert e["ex3"][0.3]["w1"] == pytest.approx(0.0435, abs=0.0001) and e["ex3"][0.5]["w1"] == pytest.approx(-0.0526, abs=0.0001) assert e["ex3"][0.3]["sd"] == pytest.approx(11935, abs=1) assert e["ex4"]["moyenne"] == 586150 and e["ex4"]["ecart_type"] == pytest.approx(14954, abs=1) assert e["ex4"]["q05"] == pytest.approx(567650, abs=1) and e["ex4"]["q95"] == pytest.approx(611650, abs=1) assert e["ex5"]["a"] == pytest.approx(453240, abs=1) and e["ex5"]["b"] == pytest.approx(450590, abs=1) assert e["ex5"]["extraction_hed"]["depreciation"] == pytest.approx(36614, abs=1) and e["ex5"]["extraction_comp"]["depreciation"] == pytest.approx(99002, abs=1) assert e["ex5"]["V_L_pour_hed"] == pytest.approx(335452, abs=1) assert e["ex7"]["delta"] == 14000 and e["ex7"]["esperance"] == 600800 and e["ex7"]["V_H"] == 605000