"""Chapitre 17 — vérification des identités et des chiffres cités dans le texte et 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 ch17_ajustements as c17 from traite import comparables as cp @pytest.fixture(scope="module") def r(): return c17.tout() def test_memes_comparables_que_ch16(r): a = r["base"]["adj"] assert list(a["id_vente"]) == ["G0574", "G0404", "G0468", "G0477", "G0575"] assert list(a["prix_vente"]) == [598000, 506500, 624500, 538000, 513500] def test_prix_ajustes_et_statistiques(r): a = r["base"]["adj"] assert a["prix_ajuste"].values == pytest.approx([640247, 541222, 572793, 568853, 584862], abs=1) assert a["prix_ajuste"].mean() == pytest.approx(581595, abs=1) and a["prix_ajuste"].median() == pytest.approx(572793, abs=1) assert a["prix_ajuste"].std() < a["prix_vente"].std() # ajustement de localisation nul : même secteur, tous à moins de 3 km du centre (variable tronquée) assert (a["localisation"].abs() < 1e-6).all() # brut et net recalculés avec la bibliothèque for _, row in a.iterrows(): bn = cp.ajustement_brut_net({g: row[g] for g in c17.GROUPES}, row["prix_vente"]) assert bn["brut_pct"] == pytest.approx(row["ajust_brut_pct"], abs=1e-9) and bn["net_pct"] == pytest.approx(row["ajust_net_pct"], abs=1e-9) assert a["ajust_brut_pct"].values == pytest.approx([0.198, 0.109, 0.176, 0.224, 0.268], abs=0.001) def test_identite_somme_des_ajustements(r): a = r["base"]["adj"] for _, row in a.iterrows(): assert row["prix_vente"] + sum(row[g] for g in c17.GROUPES) == pytest.approx(row["prix_ajuste"], abs=1e-6) def test_identite_prix_ajuste_egal_prediction_fois_residu(r): """P_aj_j = exp(x_s'β) × exp(e_j) : les prix ajustés par la grille hédonique sont la prédiction ponctuelle multipliée par le facteur résiduel du comparable.""" i = r["identite_residus"] assert i["prix_ajuste"].values == pytest.approx(i["point_x_exp_res"].values, rel=1e-9) b = r["base"] assert b["point_sans_duan"] == pytest.approx(635821, abs=1) and b["prediction_hedonique"] == pytest.approx(637953, abs=1) assert np.mean(np.exp(b["adj"]["residu_log"])) - 1 == pytest.approx(-0.0853, abs=0.0005) assert b["adj"]["residu_log"].mean() == pytest.approx(-0.0907, abs=0.0005) assert b["adj"]["prix_ajuste"].mean() / b["point_sans_duan"] - 1 == pytest.approx(-0.0853, abs=0.0005) def test_cascade_g0404(r): c = r["cascade_G0404"] etapes = dict(zip(c["etape"], c["ajustement"])) assert etapes["Conditions du marché"] == pytest.approx(28733, abs=1) assert etapes["Superficie habitable"] == pytest.approx(8953, abs=1) assert etapes["Terrain"] == pytest.approx(7279, abs=1) assert etapes["Âge"] == pytest.approx(-10242, abs=1) assert c["base_apres"].iloc[-1] == pytest.approx(541222, abs=1) # commutativité du total, non de la répartition ci = r["cascade_G0404_ordre_inverse"] assert ci["base_apres"].iloc[-1] == pytest.approx(541222, abs=1) ei = dict(zip(ci["etape"], ci["ajustement"])) assert ei["Conditions du marché"] == pytest.approx(29054, abs=1) and ei["Âge"] == pytest.approx(-9407, abs=1) # Δln total = somme des Δln (exactitude de la forme multiplicative) assert c["dlog"].iloc[1:-1].sum() == pytest.approx(c["dlog"].iloc[-1], abs=1e-12) def test_formes_additive_multiplicative_mixte(r): f = r["formes"] assert (f["ecart_additif_pct"].abs() < 0.004).all() and (f["ecart_mixte_pct"].abs() < 0.005).all() g0574 = f[f["id_vente"] == "G0574"].iloc[0] assert g0574["additif"] == pytest.approx(641341, abs=1) and g0574["multiplicatif"] == pytest.approx(640247, abs=1) for _, row in f.iterrows(): assert row["multiplicatif"] == pytest.approx(row["prix_vente"] * math.exp(row["somme_dlog"]), rel=1e-12) def test_ajustement_de_temps(r): t = r["temps"]; b = r["base"] assert t["mois"] == pytest.approx(15.2, abs=0.05) assert 100 * b["g_mensuel"] == pytest.approx(0.364, abs=0.001) and 100 * b["g_annuel"] == pytest.approx(4.46, abs=0.01) assert 100 * c17.TAUX_GENERATION == pytest.approx(4.43, abs=0.01) assert t["coefficient"] == pytest.approx(28733, abs=1) and t["taux_compose_estime"] == pytest.approx(t["coefficient"], abs=1) assert t["indices"] == pytest.approx(t["coefficient"], abs=1) assert t["taux_compose_generation"] == pytest.approx(28558, abs=1) and t["lineaire_simple"] == pytest.approx(27998, abs=1) def test_financement(r): fi = r["financement"]; fa = r["financement_amorti"] assert fi["paiement_mensuel"] == pytest.approx(248.96, abs=0.01) and fi["valeur_actualisee"] == pytest.approx(130389, abs=1) assert fi["avantage"] == pytest.approx(19611, abs=1) and 100 * fi["ajustement_pct"] == pytest.approx(-3.16, abs=0.01) and fi["prix_comptant"] == pytest.approx(600389, abs=1) assert fa["avantage"] == pytest.approx(18138, abs=1) assert 100 * fi["i_m_contrat"] == pytest.approx(0.1660, abs=0.0001) and 100 * fi["i_m_marche"] == pytest.approx(0.4124, abs=0.0001) def test_paires(r): g = r["resume_garage"]; s = r["resume_sous_sol"] assert g["n"] == 15 and s["n"] == 14 assert g["moyenne"] == pytest.approx(7991, abs=1) and g["mediane"] == pytest.approx(6034, abs=1) and g["erreur_type"] == pytest.approx(16594, abs=1) assert s["moyenne"] == pytest.approx(23623, abs=1) and s["mediane"] == pytest.approx(30789, abs=1) and s["erreur_type"] == pytest.approx(6255, abs=1) ig = r["implicite_garage"]; iss = r["implicite_sous_sol"] assert 100 * ig["effet_pct"] == pytest.approx(3.30, abs=0.01) and ig["au_prix_median_paires"] == pytest.approx(17155, abs=1) assert 100 * iss["effet_pct"] == pytest.approx(2.22, abs=0.01) and iss["au_prix_median_paires"] == pytest.approx(10394, abs=1) # le prix implicite hédonique est à l'intérieur de ± 1,96 erreur-type de la moyenne des paires (garage) ; à l'extérieur pour le sous-sol assert abs(ig["au_prix_median_paires"] - g["moyenne"]) < 1.96 * g["erreur_type"] assert abs(iss["au_prix_median_paires"] - s["moyenne"]) > 1.96 * s["erreur_type"] # critères déclarés : mêmes secteur/type, superficie ±5 %, âge ±5 ans, dates à moins de 12 mois p = r["paires_garage"] assert (abs(p["superficie_avec"] / p["superficie_sans"] - 1) <= 0.05 + 1e-12).all() and (p["delta_mois"].abs() <= 12).all() vg = r["variantes_garage"]; assert vg["n"].min() >= 9 and vg["n"].max() == 45 def test_halvorsen_palmquist_kennedy(r): h = r["hp_kennedy"].set_index("variable") assert 100 * h.loc["garage", "hp_pct"] == pytest.approx(3.297, abs=0.002) and 100 * h.loc["garage", "kennedy_pct"] == pytest.approx(3.295, abs=0.002) assert 100 * h.loc["sect_Buckingham", "naif_pct"] == pytest.approx(-25.04, abs=0.01) assert 100 * h.loc["sect_Buckingham", "hp_pct"] == pytest.approx(-22.15, abs=0.01) and 100 * h.loc["sect_Buckingham", "kennedy_pct"] == pytest.approx(-22.35, abs=0.01) assert cp.ajustement_depuis_coefficient_log(0.03243) == pytest.approx(math.exp(0.03243) - 1) assert cp.ajustement_depuis_coefficient_log_sans_biais(0.03243, 0.00501 ** 2) < cp.ajustement_depuis_coefficient_log(0.03243) def test_ponderation_et_reconciliation(r): rec = r["reconciliation"] assert rec["poids_inverse_brut"] == pytest.approx([0.179, 0.327, 0.202, 0.159, 0.133], abs=0.001) assert sum(rec["poids_inverse_brut"]) == pytest.approx(1.0) assert rec["egal"]["valeur_ponderee"] == pytest.approx(581595, abs=1) assert rec["inverse_brut"]["valeur_ponderee"] == pytest.approx(575565, abs=1) assert rec["inverse_brut"]["min"] == pytest.approx(541222, abs=1) and rec["inverse_brut"]["max"] == pytest.approx(640247, abs=1) assert rec["inverse_brut"]["etendue_pct"] == pytest.approx(0.172, abs=0.001) def test_analyse_qualitative(r): q = r["qualitatif"].set_index("id_vente"); e = r["encadrement"] assert q.loc["G0468", "position"] == "supérieur" and (q.drop("G0468")["position"] == "inférieur").all() assert e == {"borne_basse": 598000.0, "borne_haute": 624500.0, "n_inferieurs": 4, "n_superieurs": 1} def test_sensibilite(r): s = r["sensibilite"].set_index("scenario") assert s.loc["Omission de l'ajustement « conditions du marché »", "valeur"] == pytest.approx(545208, abs=1) assert s.loc["Omission de l'ajustement « qualité »", "valeur"] == pytest.approx(597252, abs=1) assert s.loc["Conditions du marché : taux de 3 % à 6 %/an", "etendue"] == pytest.approx(16734, abs=2) assert s.loc["Superficie habitable : coefficient ±1,96 é.-t.", "etendue"] < 1000 assert s["etendue"].max() == pytest.approx(30357, abs=2) def test_solutions_exercices(): # ex. 17.1 : additif, signe du point de vue du sujet aj = {"temps": 18000, "superficie": -12500, "garage": 15000, "sous_sol": -9000, "qualite": 0} assert cp.prix_ajuste_additif(540000, aj) == pytest.approx(551500) bn = cp.ajustement_brut_net(aj, 540000) assert bn["brut_pct"] == pytest.approx(54500 / 540000) and bn["net_pct"] == pytest.approx(11500 / 540000) # ex. 17.2 : temps par taux composé et par indices assert cp.ajustement_temps_taux(506500, 0.0443, 15.2) == pytest.approx(28587, abs=1) assert 506500 * 0.003642 * 15.2 == pytest.approx(28040, abs=5) assert cp.ajustement_temps(506500, 131.2, 138.7) == pytest.approx(28954, abs=1) # ex. 17.3 : paires p = cp.ventes_appariees([612000, 548000, 589500, 571000, 603000, 557500], [590000, 541000, 562000, 570500, 578000, 549000]) assert p["moyenne"] == pytest.approx(15083, abs=1) and p["mediane"] == pytest.approx(15250) and p["ecart_type"] == pytest.approx(11151, abs=1) assert p["ecart_type"] / math.sqrt(6) == pytest.approx(4552, abs=1) # ex. 17.4 : Halvorsen–Palmquist et Kennedy assert 100 * cp.ajustement_depuis_coefficient_log(0.045) == pytest.approx(4.603, abs=0.001) assert 100 * cp.ajustement_depuis_coefficient_log_sans_biais(0.045, 0.020 ** 2) == pytest.approx(4.582, abs=0.001) assert 100 * cp.ajustement_depuis_coefficient_log(-0.25) == pytest.approx(-22.12, abs=0.01) assert 100 * cp.ajustement_depuis_coefficient_log_sans_biais(-0.25, 0.08 ** 2) == pytest.approx(-22.37, abs=0.01) # ex. 17.5 : multiplicatif vs additif assert cp.prix_ajuste_multiplicatif(500000, [0.06, 0.04, -0.05]) == pytest.approx(523640) assert cp.prix_ajuste_additif(500000, {"a": 30000, "b": 20000, "c": -25000}) == pytest.approx(525000) assert cp.prix_ajuste_mixte(500000, [0.06], {"b": 530000 * 0.04, "c": -530000 * 0.05})["prix_ajuste"] == pytest.approx(524700) # ex. 17.6 : pondération inverse du brut cs = [cp.ComparableAjuste("A", 600000, {"x": 30000, "y": -60000}), cp.ComparableAjuste("B", 520000, {"x": 26000, "y": 13000}), cp.ComparableAjuste("C", 575000, {"x": -23000, "y": 46000, "z": 23000})] w = cp.poids_inverse_brut(cs) assert [c.prix_ajuste for c in cs] == [570000, 559000, 621000] and [c.brut_pct for c in cs] == pytest.approx([0.15, 0.075, 0.16]) assert w == pytest.approx([0.254, 0.508, 0.238], abs=0.001) rec = cp.reconciliation(cs, w) assert rec["valeur_ponderee"] == pytest.approx(576556, abs=1) and cp.reconciliation(cs)["valeur_ponderee"] == pytest.approx(583333, abs=1) # ex. 17.7 : grille avec les ajustements tirés des paires (garage 7 991 $, sous-sol 23 623 $) au lieu des prix implicites hédoniques r7 = c17.tout(); a = r7["base"]["adj"].copy() g, ss = r7["resume_garage"]["moyenne"], r7["resume_sous_sol"]["moyenne"] a.loc[a["id_vente"] == "G0574", "garage"] = g; a.loc[a["id_vente"] == "G0575", "garage"] = g; a.loc[a["id_vente"] == "G0574", "sous_sol"] = ss a["prix_ajuste"] = a["prix_vente"] + a[c17.GROUPES].sum(axis=1) rec7 = c17.reconcilier(r7["base"], a) assert a.loc[a["id_vente"] == "G0574", "prix_ajuste"].iloc[0] == pytest.approx(637948, abs=2) assert a.loc[a["id_vente"] == "G0575", "prix_ajuste"].iloc[0] == pytest.approx(574188, abs=2) assert a["prix_ajuste"].mean() == pytest.approx(579001, abs=2) and rec7["inverse_brut"]["valeur_ponderee"] == pytest.approx(573963, abs=3)