SPB Git
10.9 KB · 137 lines python
1"""Chapitre 15 — Études de marché et segmentation : vérification des identités et des chiffres cités dans le texte et les solutions.2Tolérances : indicateurs APCIQ ± 0,05 ; statistiques de test ± 0,005 (F) et ± 0,05 (W) ; silhouettes ± 0,002 ; RMSE ± 0,0005 ; montants ± 1 $."""3import sys, pathlib, math4import numpy as np, pandas as pd, pytest5from scipy import stats6sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "cases"))7import ch15_segmentation as c158from traite import cas1, cas3910@pytest.fixture(scope="module")11def r():12    return c15.tout()1314# ------------------------------------------------------------------ A. indicateurs de marché15def test_mois_inventaire_et_absorption(r):16    ind = r["indicateurs"].set_index("territoire")17    attendu = {"Province de Québec": 6.1, "RMR de Montréal": 6.6, "RMR de Québec": 3.1, "RMR de Gatineau": 6.5,18               "RMR de Sherbrooke": 4.3, "RMR de Trois-Rivières": 3.6, "RMR de Saguenay": 4.0}19    for t, mi in attendu.items():20        assert ind.loc[t, "mois_inventaire"] == pytest.approx(mi, abs=0.05)21        assert ind.loc[t, "taux_absorption_mensuel"] * ind.loc[t, "mois_inventaire"] == pytest.approx(1.0, abs=1e-12)   # a = 1/MI22    assert ind.loc["RMR de Québec", "taux_absorption_mensuel"] * 100 == pytest.approx(32.7, abs=0.05)23    assert ind.loc["RMR de Montréal", "taux_absorption_mensuel"] * 100 == pytest.approx(15.2, abs=0.05)24    assert ind.loc["RMR de Montréal", "ratio_stock_flux"] == pytest.approx(5.0, abs=0.05)25    assert ind.loc["RMR de Gatineau", "ratio_stock_flux"] == pytest.approx(5.8, abs=0.05)26    assert ind.loc["RMR de Québec", "ratio_stock_flux"] == pytest.approx(3.2, abs=0.05)27    assert c15.mois_inventaire(20350, 9265) == pytest.approx(20350 / (9265 / 3))2829def test_parts_secteurs_gatineau(r):30    p = r["parts_gatineau"]31    assert p["ventes_apciq_t3_2026"].sum() == 1044 and p["ventes_synthetiques"].sum() == 60032    assert p.loc["Hull", "part_apciq"] * 100 == pytest.approx(16.7, abs=0.05) and p.loc["Hull", "part_synthetique"] * 100 == pytest.approx(31.0, abs=0.05)33    assert p.loc["Gatineau", "part_apciq"] * 100 == pytest.approx(33.1, abs=0.05)3435# ------------------------------------------------------------------ B. Chow36def test_chow_identites_et_valeurs(r):37    ch = r["chow"]["secteur"]38    assert ch["G"] == 5 and ch["k0"] == 12 and ch["n"] == 600 and ch["q"] == 44 and ch["df2"] == 540 and ch["q_classique"] == 4839    assert ch["ssr_libre"] <= ch["ssr_contraint"] <= ch["ssr_classique"]           # emboîtement des modèles40    assert ch["ssr_contraint"] == pytest.approx(4.0245, abs=5e-4) and ch["ssr_libre"] == pytest.approx(3.7360, abs=5e-4) and ch["ssr_classique"] == pytest.approx(6.4024, abs=5e-4)41    F = ((ch["ssr_contraint"] - ch["ssr_libre"]) / 44) / (ch["ssr_libre"] / 540)42    assert ch["F"] == pytest.approx(F) and ch["F"] == pytest.approx(0.948, abs=0.005) and ch["p"] == pytest.approx(0.570, abs=0.005)43    assert ch["F_classique"] == pytest.approx(8.029, abs=0.005) and ch["p_classique"] < 1e-3044    assert ch["wald_hc1"] == pytest.approx(54.48, abs=0.05) and ch["p_wald"] == pytest.approx(0.134, abs=0.002) and ch["q_wald"] == 4445    assert ch["p"] == pytest.approx(stats.f.sf(ch["F"], 44, 540))46    # la SCR du modèle contraint (constantes de secteur + pentes communes) est celle du modèle de référence traite.cas1.hedonique47    ref = r["reference"]; assert float(ref["residus"] @ ref["residus"]) == pytest.approx(ch["ssr_contraint"], rel=1e-9)48    assert ref["r2"] == pytest.approx(0.853, abs=0.0005) and ref["sigma"] == pytest.approx(0.0830, abs=5e-5)49    assert math.sqrt(ch["ssr_classique"] / (600 - 12)) == pytest.approx(0.104, abs=0.001)50    # coefficients par secteur cités au tableau51    ps = ch["par_segment"]52    assert ps["Aylmer"]["coef"]["ln_S"] == pytest.approx(0.513, abs=0.001) and ps["Masson-Angers"]["coef"]["ln_S"] == pytest.approx(0.558, abs=0.001)53    assert ps["Hull"]["coef"]["qualite"] == pytest.approx(0.064, abs=0.001) and ps["Buckingham"]["n"] == 42 and ps["Masson-Angers"]["n"] == 4754    assert ps["Aylmer"]["coef"]["mois"] == pytest.approx(0.0044, abs=5e-5) and ps["Masson-Angers"]["coef"]["mois"] == pytest.approx(0.0030, abs=5e-5)5556def test_chow_autres_partitions(r):57    c = r["chow"]58    assert c["secteur4"]["p"] == pytest.approx(0.426, abs=0.005) and c["secteur4"]["q"] == 3359    assert c["type"]["k0"] == 11 and c["type"]["regresseurs_retires"] == ["etages"] and c["type"]["q"] == 1060    assert c["type"]["p"] == pytest.approx(0.079, abs=0.005) and c["type"]["p_wald"] == pytest.approx(0.0096, abs=0.0005)61    assert c["periode"]["p"] == pytest.approx(0.034, abs=0.002) and c["periode"]["p_classique"] == pytest.approx(0.024, abs=0.002) and c["periode"]["p_wald"] == pytest.approx(0.037, abs=0.002)62    assert c["periode"]["F"] < stats.f.ppf(0.99, 11, 576)        # non rejeté au seuil de 1 %63    assert c["kmeans"]["p"] == pytest.approx(0.588, abs=0.005) and c["kmeans"]["p_classique"] < 1e-3064    assert c["kmeans5"]["p"] == pytest.approx(0.427, abs=0.005) and c["kmeans5"]["p_wald"] == pytest.approx(0.057, abs=0.002)65    assert c["ward"]["F"] == pytest.approx(c["kmeans"]["F"])    # mêmes partitions à k = 26667# ------------------------------------------------------------------ B. classification68def test_classification(r):69    t = r["classification"]["tableau"].set_index("k")70    assert r["k_opt"] == 271    assert t.loc[2, "silhouette_kmeans"] == pytest.approx(0.417, abs=0.002) and t.loc[3, "silhouette_kmeans"] == pytest.approx(0.225, abs=0.002)72    assert t.loc[5, "silhouette_kmeans"] == pytest.approx(0.188, abs=0.002) and t.loc[5, "silhouette_ward"] == pytest.approx(0.156, abs=0.002)73    assert t.loc[2, "ari_kmeans_ward"] == pytest.approx(1.0) and t.loc[5, "ari_kmeans_ward"] == pytest.approx(0.476, abs=0.002)74    assert all(np.diff(t["inertie_kmeans"].values) < 0)         # l'inertie décroît avec k75    assert r["ari_secteur"] == pytest.approx(0.213, abs=0.002) and r["ari_secteur5"] == pytest.approx(0.124, abs=0.002)76    # silhouette : implémentation directe = sklearn77    Z = r["Z"].values; lab = r["labels_kmeans"]78    s_man = c15.silhouette_manuelle(Z, lab)79    assert np.allclose(s_man, r["silhouette_i"], atol=1e-10) and -1 <= s_man.min() and s_man.max() <= 180    # segment Est = Buckingham + Masson-Angers81    cr = r["croise"]; est = cr.loc[cr.sum(axis=1).idxmin()]82    assert est["Buckingham"] == 42 and est["Masson-Angers"] == 47 and est.sum() == 8983    p5 = r["profil5"]; assert p5["n"].sum() == 600 and sorted(p5["n"].tolist()) == [89, 96, 98, 120, 197]84    assert p5.loc[p5["n"] == 89, "prix_med"].iloc[0] == 371000 and p5.loc[p5["n"] == 96, "qualite_moy"].iloc[0] == pytest.approx(1.81, abs=0.005)85    j = r["jeux"]86    assert j["coordonnees"]["ari_secteur_k5"] == pytest.approx(0.986, abs=0.002) and j["coordonnees"]["silhouette"] == pytest.approx(0.735, abs=0.002)87    assert j["attributs"]["ari_secteur"] == pytest.approx(0.001, abs=0.002) and j["attributs"]["silhouette"] == pytest.approx(0.221, abs=0.002)8889def test_validation_croisee(r):90    cv = r["cv"].set_index("specification")91    ref = cv.loc["Pentes communes + indicatrices (secteur)"]92    assert ref["rmse_log"] == pytest.approx(0.0839, abs=5e-4) and ref["mape_mediane_pct"] == pytest.approx(5.81, abs=0.01)93    assert cv.loc["Équation unique sans segment", "rmse_log"] == pytest.approx(0.1053, abs=5e-4)94    assert cv.loc["Une équation par segment (secteur)", "mape_mediane_pct"] == pytest.approx(6.26, abs=0.01)95    assert cv.loc["Une équation par segment (k-moyennes, k = 5)", "mape_mediane_pct"] == pytest.approx(6.71, abs=0.01)96    assert (cv["rmse_log"] >= ref["rmse_log"]).all()            # la référence est la meilleure spécification9798# ------------------------------------------------------------------ C. marché locatif99def test_locatif(r):100    L = r["locatif"]; rev = L["revenus"]101    assert rev["rbp_contrat"] == 165540 and rev["rbp_marche"] == 186036 and rev["ecart_marche"] == 20496102    assert L["ecart_global_pct"] == pytest.approx(11.0, abs=0.05)103    pt = L["par_type"]104    assert pt.loc["3½", "ecart_contrat_marche_pct"] == pytest.approx(15.8, abs=0.05) and pt.loc["5½", "ecart_contrat_marche_pct"] == pytest.approx(4.9, abs=0.05)105    assert pt.loc["4½", "ratio_marche_schl"] == pytest.approx(0.994, abs=0.0005) and pt.loc["5½", "ratio_marche_schl"] == pytest.approx(0.944, abs=0.0005)106    u = c15.SCHL_ZONE9["univers_2025"]; assert u["studio"] + u["1 ch."] + u["2 ch."] + u["3 ch. +"] == u["tous"] == 44554107    assert L["parts_univers"]["2 ch."] * 100 == pytest.approx(50.5, abs=0.05) and L["parts_univers"]["1 ch."] * 100 == pytest.approx(27.8, abs=0.05)108    assert L["ecart_inoccupe_occupe_1ch_zone9_pct"] == pytest.approx(22.9, abs=0.05) and L["ecart_inoccupe_occupe_rmr_pct"] == pytest.approx(27.1, abs=0.05)109    assert L["rattrapage_an1"]["hypothese_cas3"] == pytest.approx(3074, abs=1) and L["rattrapage_an1"]["schl_ile_6_19_2025"] == pytest.approx(1742, abs=1)110    assert L["horizon_renouvellement"]["schl_ile_6_19_2025"] == pytest.approx(11.8, abs=0.05)111    assert L["neuf_prime_pct"] == pytest.approx(55.2, abs=0.05) and L["neuf_part_univers_pct"] == pytest.approx(1.41, abs=0.005)112    assert 44554 / c15.SCHL_RMR["univers_2025"] * 100 == pytest.approx(6.7, abs=0.05) and 1750 / 1626 - 1 == pytest.approx(0.076, abs=0.0005)113114# ------------------------------------------------------------------ solutions des exercices115def test_solutions_exercices():116    # 15.1117    assert c15.mois_inventaire(825, 569) == pytest.approx(4.35, abs=0.005) and c15.taux_absorption(825, 569) * 100 == pytest.approx(23.0, abs=0.05)118    assert c15.mois_inventaire(825 * 1.2, 569 * 0.9) == pytest.approx(5.80, abs=0.005)119    # 15.3120    vm = 9265 / 3; d = 40 / c15.JOURS_PAR_MOIS121    assert vm * d == pytest.approx(4059, abs=1) and (20350 - vm * d) / 20350 * 100 == pytest.approx(80.1, abs=0.05)122    # 15.4123    a = (1 + math.sqrt(2)) / 2; b = (3 + math.sqrt(10) + 4) / 3124    assert (b - a) / max(a, b) == pytest.approx(0.644, abs=0.001)125    Z = np.array([[0, 0], [0, 1], [1, 0], [4, 0], [4, 1], [5, 0]], float); lab = np.array([0, 0, 0, 1, 1, 1])126    assert c15.silhouette_manuelle(Z, lab)[2] == pytest.approx(0.6437, abs=1e-4)127    # 15.5128    F = ((4.0245 - 3.7360) / 44) / (3.7360 / 540); F0 = ((6.4024 - 3.7360) / 48) / (3.7360 / 540)129    assert F == pytest.approx(0.948, abs=0.001) and F0 == pytest.approx(8.03, abs=0.005)130    assert stats.f.ppf(0.95, 44, 540) == pytest.approx(1.40, abs=0.005) and stats.f.ppf(0.95, 48, 540) == pytest.approx(1.38, abs=0.005)131    # 15.6132    assert 1 - 0.85 ** 5 == pytest.approx(0.556, abs=0.001) and 1 - 0.906 ** 5 == pytest.approx(0.390, abs=0.001) and 1 - 0.915 ** 5 == pytest.approx(0.359, abs=0.001)133    assert 1 - 0.85 ** 10 == pytest.approx(0.803, abs=0.001) and 1 - 0.915 ** 10 == pytest.approx(0.589, abs=0.001)134    assert 1 - 875.25 / 1039 == pytest.approx(0.158, abs=0.001) and 1295 / 1054 - 1 == pytest.approx(0.229, abs=0.001)135    # probabilité d'au moins un rejet sur six tests indépendants au seuil de 5 %136    assert 1 - 0.95 ** 6 == pytest.approx(0.265, abs=0.001)137
Files · 2fa255cfbe38460cb046bb32c81cf4f586081ba2
Menu

Theme