SPB Git

Ch. 36 livré et validé (compilation isolée, tests, fusion des registres)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Simon-Pierre Boucher committed (Oct 31, 2026) parent bc6e342 Browse files

68 changed files +6,026 −98

68 changed files
modified analysis/python/cases/ch39_bayes.py +14 −3
@@ -353,6 +353,8 @@ def exemple_conjugue(v: pd.DataFrame, s: dict, ref: dict) -> dict:
353 353 petits[nom] = {"post": p, "pred": pr, "largeur95_pct": (pr["haut95"] - pr["bas95"]) / pr["mediane"] * 100}
354 354 # Hull seul (186), a priori faible
355 355 p_hull = ajuster_nig(se["hull"]); pred_hull = predictive_t(p_hull, x_sujet(s, p_hull["colonnes"]))
356 + ih = p_hull["colonnes"].index("ln_S"); pred_hull["ln_S"] = float(p_hull["m_n"][ih]); pred_hull["ln_S_sd"] = float(p_hull["sd"][ih])
357 + pred_hull["largeur95_pct"] = (pred_hull["haut95"] - pred_hull["bas95"]) / pred_hull["mediane"] * 100
356 358 return {"p600": p600, "p600_propre": p600_propre, "pred600_propre": predictive_t(p600_propre, xs), "cols": cols, "xs": xs, "tab600": tab, "pred600": pred600, "p_anc": p_anc, "p_rec_seq": p_seq, "n_anc": len(anc),
357 359 "n_rec": len(rec), "p_vois": p_vois, "micro": micro, "cols_micro": cols_m, "petits": petits, "p_hull": p_hull,
358 360 "pred_hull": pred_hull, "n_micro": len(micro)}
@@ -459,28 +461,37 @@ def exercices(ex: dict, hier: dict) -> dict:
459 461 w = (n / sig ** 2) / prec
460 462 r["ex1"] = {"m_n": mn, "s_n": sn, "poids_donnees": w, "ic95": (mn - 1.96 * sn, mn + 1.96 * sn), "ic90": (mn - 1.645 * sn, mn + 1.645 * sn),
461 463 "n_equivalent_prior": sig ** 2 / s0 ** 2, "valeur_165": mn * 165, "sd_165": sn * 165}
464 + prec4 = 1 / 400.0 ** 2 + n / sig ** 2
465 + r["ex1"]["s0_400"] = {"m_n": (m0 / 400.0 ** 2 + n * xbar / sig ** 2) / prec4, "s_n": math.sqrt(1 / prec4), "poids_donnees": (n / sig ** 2) / prec4,
466 + "n_equivalent_prior": sig ** 2 / 400.0 ** 2}
462 467 # ex 2 : lecture d'intervalles — probabilité a posteriori que la valeur dépasse 650 000 $ sous une postérieure log-normale
463 468 mu, sd = ex["pred600"]["loc"], ex["pred600"]["echelle"]
464 469 r["ex2"] = {"p_sup_650": float(1 - stats.t.cdf((math.log(650_000) - mu) / sd, ex["pred600"]["nu"])),
465 470 "p_inf_550": float(stats.t.cdf((math.log(550_000) - mu) / sd, ex["pred600"]["nu"])),
466 − "q10": math.exp(mu + stats.t.ppf(0.10, ex["pred600"]["nu"]) * sd), "q90": math.exp(mu + stats.t.ppf(0.90, ex["pred600"]["nu"]) * sd)}
471 + "q10": math.exp(mu + stats.t.ppf(0.10, ex["pred600"]["nu"]) * sd), "q90": math.exp(mu + stats.t.ppf(0.90, ex["pred600"]["nu"]) * sd),
472 + "z_650": (math.log(650_000) - mu) / sd, "z_550": (math.log(550_000) - mu) / sd}
467 473 # ex 3 : facteur de rétrécissement
468 474 s2, t2 = 0.083 ** 2, 0.15 ** 2
469 475 r["ex3"] = {n_: facteur_retrecissement(n_, s2, t2) for n_ in (5, 10, 20, 47)}
470 476 r["ex3"]["alpha_5"] = facteur_retrecissement(5, s2, t2) * (-0.30) + (1 - facteur_retrecissement(5, s2, t2)) * (-0.08)
471 477 r["ex3"]["tau_005"] = {n_: facteur_retrecissement(n_, s2, 0.05 ** 2) for n_ in (5, 10, 20, 47)}
472 478 r["ex3"]["sd_cond_5"] = math.sqrt(1 / (5 / s2 + 1 / t2)); r["ex3"]["sd_fixe_5"] = math.sqrt(s2 / 5)
479 + r["ex3"]["n_demi"] = {"tau_015": s2 / t2, "tau_005": s2 / 0.05 ** 2}
473 480 # ex 4 : prédictive Student-t
474 481 p = ex["p600"]; xs = ex["xs"]; q = float(xs @ p["V_n"] @ xs)
475 482 r["ex4"] = {"a_n": p["a_n"], "b_n": p["b_n"], "nu": p["nu"], "xVx": q, "b_sur_a": p["b_n"] / p["a_n"], "echelle": ex["pred600"]["echelle"],
476 483 "t975": float(stats.t.ppf(0.975, p["nu"])), "z975": 1.959964, "bas95": ex["pred600"]["bas95"], "haut95": ex["pred600"]["haut95"],
477 − "nu_petit": 20, "t975_20": float(stats.t.ppf(0.975, 20)), "mediane": ex["pred600"]["mediane"]}
484 + "nu_petit": 20, "t975_20": float(stats.t.ppf(0.975, 20)), "mediane": ex["pred600"]["mediane"],
485 + "bas95_normal": math.exp(ex["pred600"]["loc"] - 1.959964 * ex["pred600"]["echelle"]), "haut95_normal": math.exp(ex["pred600"]["loc"] + 1.959964 * ex["pred600"]["echelle"]),
486 + "bas95_t20": math.exp(ex["pred600"]["loc"] - float(stats.t.ppf(0.975, 20)) * ex["pred600"]["echelle"]), "haut95_t20": math.exp(ex["pred600"]["loc"] + float(stats.t.ppf(0.975, 20)) * ex["pred600"]["echelle"]),
487 + "elargissement_pct": (float(stats.t.ppf(0.975, 20)) / float(stats.t.ppf(0.975, p["nu"])) - 1) * 100}
478 488 # ex 5 : R̂ sur des moyennes/variances de chaînes données
479 489 moy = np.array([-0.262, -0.258, -0.271, -0.249]); var = np.array([0.0049, 0.0052, 0.0047, 0.0055]); n_ = 1000
480 490 B = n_ * moy.var(ddof=1); W = var.mean(); vp = (n_ - 1) / n_ * W + B / n_
481 491 r["ex5"] = {"B": B, "W": W, "var_plus": vp, "rhat": math.sqrt(vp / W)}
482 492 moy2 = np.array([-0.262, -0.258, -0.371, -0.249]); B2 = n_ * moy2.var(ddof=1); vp2 = (n_ - 1) / n_ * W + B2 / n_
483 493 r["ex5"]["rhat_divergent"] = math.sqrt(vp2 / W); r["ex5"]["B2"] = B2
494 + r["ex5"]["ess_rho08"] = 4000 / (1 + 2 * 0.8 / (1 - 0.8))
484 495 # ex 6 : script (κ) — valeurs du tableau des petits échantillons
485 496 r["ex6"] = {k: {"ln_S": v_["post"]["m_n"][ex["cols_micro"].index("ln_S")], "ln_S_sd": v_["post"]["sd"][ex["cols_micro"].index("ln_S")],
486 497 "mediane": v_["pred"]["mediane"], "largeur95_pct": v_["largeur95_pct"]} for k, v_ in ex["petits"].items()}
@@ -526,7 +537,7 @@ def imprimer(r: dict) -> None:
526 537 po = pe["post"]; i = ex["cols_micro"].index("ln_S"); iq = ex["cols_micro"].index("qualite"); ig = ex["cols_micro"].index("garage")
527 538 print(f" {nom:9s}: ln_S {po['m_n'][i]:.3f} ({po['sd'][i]:.3f}) ; qualité {po['m_n'][iq]:.3f} ({po['sd'][iq]:.3f}) ; garage {po['m_n'][ig]:.3f} ({po['sd'][ig]:.3f}) ; "
528 539 f"E[σ] {po['sigma_moy']:.4f} ; sujet médiane {_d(pe['pred']['mediane'])} [{_d(pe['pred']['bas95'])} ; {_d(pe['pred']['haut95'])}] largeur {pe['largeur95_pct']:.1f} %")
529 − ph = ex["pred_hull"]; print(f" Hull seul ({ex['p_hull']['n']}) : médiane {_d(ph['mediane'])} [{_d(ph['bas95'])} ; {_d(ph['haut95'])}]")
540 + ph = ex["pred_hull"]; print(f" Hull seul ({ex['p_hull']['n']}) : ln_S {ph['ln_S']:.3f} ({ph['ln_S_sd']:.3f}) ; médiane {_d(ph['mediane'])} [{_d(ph['bas95'])} ; {_d(ph['haut95'])}] largeur {ph['largeur95_pct']:.1f} %")
530 541 print("\n=== modèle hiérarchique (Gibbs par blocs) : résumé ===")
531 542 print(hi["resume"].round(4).to_string())
532 543 print("\ndiagnostics : Gibbs par blocs vs un paramètre à la fois"); print(hi["diag"].round(3).to_string())
modified analysis/python/cases/ch40_montecarlo.py +8 −3
@@ -379,14 +379,17 @@ def exercices(r: dict, mc: dict) -> dict:
379 379 out["ex1"] = {**p, **m, "P_sup_5pct": float(1 - stats.norm.cdf(math.log(0.05 / p["mediane"]) / p["sigma_log"]))}
380 380 # Ex. 2 : PERT vs triangulaire pour la rotation (0,08 ; 0,15 ; 0,25)
381 381 t = moments_loi("triangulaire", MC_LOIS["rotation_annuelle"][1]); pe = pert(0.08, 0.15, 0.25)
382 − out["ex2"] = {"tri": t, "pert": {k: pe[k] for k in ["alpha", "beta", "moyenne", "ecart_type", "q05", "q95"]}}
382 + a_, m_, b_ = 0.08, 0.15, 0.25
383 + out["ex2"] = {"tri": t, "pert": {k: pe[k] for k in ["alpha", "beta", "moyenne", "ecart_type", "q05", "q95"]},
384 + "P_tri_sup_20": (b_ - 0.20) ** 2 / ((b_ - a_) * (b_ - m_)), "P_pert_sup_20": float(pe["loi"].sf(0.20))}
383 385 # Ex. 3 : nombre de tirages pour une médiane à ±5 000 $ (95 %) et pour P(RCD<1,20) à ±1 point
384 386 V = mc["valeur"]; e = erreur_type_quantile(V, 0.5)
385 387 se1_med = e["se_theorique"] * math.sqrt(len(V)) # écart-type « pour N = 1 »
386 388 out["ex3"] = {"se_mediane_10000": e["se_theorique"], "densite": e["densite"], "N_mediane_5000": n_requis(se1_med, 5000), "N_mediane_1000": n_requis(se1_med, 1000),
387 389 "N_proba_1pt": n_requis(0.5, 0.01), "N_proba_0.5pt": n_requis(0.5, 0.005)}
388 390 # Ex. 4 : Cholesky 2×2 pour (r, R_T) avec rho = 0,78 et sigma_RT = 0,0064 ; vérification Sigma = L L' ; Spearman induit
389 − L = cholesky_2x2(0.005, 0.0064, 0.78); out["ex4"] = {"L": L, "Sigma": L @ L.T, "spearman": spearman_depuis_pearson_gaussien(0.78)}
391 + L = cholesky_2x2(0.005, 0.0064, 0.78); eps = np.array([1.2, -0.5]); z = L @ eps
392 + out["ex4"] = {"L": L, "Sigma": L @ L.T, "spearman": spearman_depuis_pearson_gaussien(0.78), "r_tire": 0.0725 + z[0], "RT_tire": 0.05 + z[1]}
390 393 # Ex. 5 : VaR/CVaR 10 % de valeur et loi normale de même moyenne/écart-type ; comparaison
391 394 st = mc["stats"]; mr = mesures_risque(mc, r["dcf"]["valeur"], alphas=(0.10,)).iloc[0].to_dict()
392 395 out["ex5"] = {**mr, "q10_normale": stats.norm.ppf(0.10, st["moyenne"], st["ecart_type"]), "cvar_normale": cvar_normale(st["moyenne"], st["ecart_type"], 0.10)}
@@ -394,7 +397,8 @@ def exercices(r: dict, mc: dict) -> dict:
394 397 lois = dict(MC_LOIS); lois["taux_actualisation"] = ("normale", {"mu": 0.0725, "sigma": 0.010})
395 398 x = tirer(N_MC, GRAINE, len(r["hyp"]["capex"]), lois); ev = evaluer(r, x); Vx = ev["valeur"]
396 399 out["ex6"] = {"mediane": float(np.median(Vx)), "ecart_type": float(Vx.std(ddof=1)), "q05": float(np.quantile(Vx, 0.05)), "q95": float(np.quantile(Vx, 0.95)), "P_V_2M": float((Vx < 2e6).mean()),
397 − "P_V_2M_base": float((V < 2e6).mean()), "cv": float(Vx.std(ddof=1) / Vx.mean())}
400 + "P_V_2M_base": float((V < 2e6).mean()), "cv": float(Vx.std(ddof=1) / Vx.mean()), "ratio_ecart_type": float(Vx.std(ddof=1) / V.std(ddof=1))}
401 + S_r = 0.528; out["ex6"]["S_r_approx"] = 4 * S_r / (1 + 3 * S_r) # modèle additif : la variance due à r quadruple
398 402 return out
399 403
400 404 # ----------------------------------------------------------------------------- tout
@@ -440,6 +444,7 @@ def imprimer(D: dict) -> None:
440 444 print("\nErreur Monte-Carlo théorique (N = 10 000) :\n", D["erreurs"].to_string(index=False))
441 445 rep = D["repetitions"]
442 446 print(f"\nRépétitions ({len(rep)} graines, N = 10 000) : écart-type empirique — moyenne {rep['moyenne'].std(ddof=1):,.0f} | médiane {rep['mediane'].std(ddof=1):,.0f} | q05 {rep['q05'].std(ddof=1):,.0f} | q95 {rep['q95'].std(ddof=1):,.0f} | P(RCD1<1,20) {rep['p_rcd1_120'].std(ddof=1):.4f}")
447 + print(f" moyennes des 300 : moyenne {rep['moyenne'].mean():,.0f} | q05 {rep['q05'].mean():,.0f} | q95 {rep['q95'].mean():,.0f}")
443 448 print(f" moyenne des médianes {rep['mediane'].mean():,.0f} ; min {rep['mediane'].min():,.0f} ; max {rep['mediane'].max():,.0f} ; P(RCD1<1,20) moyenne {rep['p_rcd1_120'].mean():.4f}")
444 449 print(" graine du traité (20261031) :", {k: (round(v, 4) if isinstance(v, float) else v) for k, v in D["graine_traite"].items()})
445 450 print("\nConvergence (une suite, graine 20261031) :\n", D["convergence"].to_string(index=False))
modified analysis/python/figures/ch36_figures.py +1 −1
@@ -72,7 +72,7 @@ def fig_bacon(res):
72 72 ax.set_xlabel("Poids de la comparaison 2 × 2"); ax.set_title(titre, fontsize=8.5); ax.grid(True, lw=0.4)
73 73 axes[0].set_ylabel("Estimation 2 × 2 (points de log)")
74 74 h_, l_ = axes[0].get_legend_handles_labels()
75 − fig.legend(h_, l_, loc="lower center", ncol=3, fontsize=6.8, bbox_to_anchor=(0.5, -0.1), frameon=False)
75 + fig.legend(h_, l_, loc="lower center", ncol=3, fontsize=6.8, bbox_to_anchor=(0.5, -0.2), frameon=False)
76 76 savefig(fig, "ch36/ch36-fig-bacon.pdf")
77 77
78 78 def fig_synthetique(res):
modified analysis/python/figures/ch37_figures.py +1 −1
@@ -29,7 +29,7 @@ def fig_arbre(res):
29 29 arbre = res["ajustes"]["Arbre (profondeur 3)"]
30 30 noms = [c37.ETIQUETTES[c].replace(" (m²)", " (m²)") for c in res["Xm"].columns]
31 31 fig, ax = plt.subplots(figsize=(LARGEUR, 3.6))
32 − plot_tree(arbre, feature_names=noms, filled=False, impurity=False, rounded=True, fontsize=6.4, ax=ax, precision=3)
32 + plot_tree(arbre, feature_names=noms, filled=False, impurity=False, rounded=True, fontsize=6, ax=ax, precision=3)
33 33 for t in ax.texts:
34 34 s = t.get_text()
35 35 s = re.sub(r"samples = (\d+)", lambda m: f"n = {m.group(1)}", s)
modified analysis/python/figures/ch44_flux.py +1 −1
@@ -18,7 +18,7 @@ def panneau(ax, fl, titre, couleur):
18 18 ax.axhline(0, color=palette.ink2, lw=0.6)
19 19 ax.set_title(titre, loc="left", fontsize=9)
20 20 ax.set_xlabel("Année"); ax.yaxis.set_major_formatter(plt.FuncFormatter(fmt_dollars))
21 − ax.annotate(f"VAN {fmt_dollars(fl['van'])}".replace("-", "−"), xy=(t[-1], fl["cumul_actualise"][-1]), xytext=(-6, -14 if fl["van"] < 0 else 10),
21 + ax.annotate(f"VAN {fmt_dollars(fl['van'])}".replace("-", "−"), xy=(t[-1], fl["cumul_actualise"][-1]), xytext=(4, -22 if fl["van"] < 0 else 10),
22 22 textcoords="offset points", ha="right", va="top" if fl["van"] < 0 else "bottom", fontsize=8, color=palette.ink)
23 23
24 24
modified analysis/python/figures/ch46_figures.py +3 −3
@@ -67,9 +67,9 @@ def fig_mrb(D):
67 67 ax.hist(c.loc[(c["quartier"] == "Limoilou") & (c["nb_logements"] == 3), "mrb"], bins=np.arange(12, 32, 1.0), color=palette.cat[0], alpha=0.8, label="28 triplex de Limoilou")
68 68 for i, (_, r) in enumerate(comp5.iterrows()):
69 69 ax.axvline(r["mrb"], color=palette.cat[1], lw=1.2, label="5 comparables (MRB 20,75 à 23,35)" if i == 0 else None)
70 − ax.axvline(rec["mrb_implicite"], color=palette.ink, lw=1.6, ls="--"); ax.text(12.3, 12.7, f"Sujet, valeur conclue : {rec['mrb_implicite']:.2f} (tireté)".replace(".", ","), fontsize=7, color=palette.ink)
71 − ax.axvline(rev["mrb_127"], color=palette.ink2, lw=1, ls=":"); ax.text(12.3, 11.5, f"Médiane des 67 triplex : {rev['mrb_127']:.2f} (pointillé)".replace(".", ","), fontsize=7, color=palette.ink2)
72 − ax.set_xlabel("Multiplicateur du revenu brut (prix / RBP contractuel)"); ax.set_ylabel("Nombre de ventes"); ax.set_ylim(0, 14); ax.set_xlim(12, 31); ax.legend(loc="center right", fontsize=6.8)
70 + ax.axvline(rec["mrb_implicite"], color=palette.ink, lw=1.6, ls="--"); ax.text(12.3, 13.2, f"Sujet, valeur conclue : {rec['mrb_implicite']:.2f} (tireté)".replace(".", ","), fontsize=7, color=palette.ink)
71 + ax.axvline(rev["mrb_127"], color=palette.ink2, lw=1, ls=":"); ax.text(12.3, 12.1, f"Médiane des 67 triplex : {rev['mrb_127']:.2f} (pointillé)".replace(".", ","), fontsize=7, color=palette.ink2)
72 + ax.set_xlabel("Multiplicateur du revenu brut (prix / RBP contractuel)"); ax.set_ylabel("Nombre de ventes"); ax.set_ylim(0, 14.5); ax.set_xlim(12, 31); ax.legend(loc="upper right", fontsize=6.5)
73 73 ax.set_title("A. Les cinq comparables dans la distribution", loc="left", fontsize=8.5)
74 74 ax = axes[1]
75 75 rho5 = comp5["loyer_mensuel_moyen_contrat"] / comp5["loyer_mensuel_moyen_marche"]; rho_tri = tri["loyer_mensuel_moyen_contrat"] / tri["loyer_mensuel_moyen_marche"]
added analysis/python/tests/test_ch36.py +202 −0
@@ -0,0 +1,202 @@
1 +"""Chapitre 36 — tests des fonctions de traite.panel (récupération d'effets connus sur données simulées) et des chiffres cités
2 +dans le texte, les tableaux et les solutions. Tolérances : coefficients ±0,0005 (ou relatives déclarées), erreurs-types ±0,0005,
3 +identités (Bacon, within = indicatrices) à 1e-8."""
4 +import sys, pathlib, math
5 +import numpy as np, pandas as pd, pytest
6 +sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "cases"))
7 +from traite import panel as pn
8 +from traite import hedonique as h
9 +import ch36_panel as c36
10 +
11 +# ----------------------------------------------------------------------------- fonctions génériques (données simulées)
12 +
13 +def _panel(N=30, T=12, seed=3, delta=0.05, dyn=0.0):
14 + rng = np.random.default_rng(seed); alpha = rng.normal(10, 0.5, N); lam = np.cumsum(rng.normal(0.01, 0.01, T))
15 + g = np.array([np.nan] * 10 + [4] * 10 + [8] * 10, float); rows = []
16 + for i in range(N):
17 + for t in range(T):
18 + k = t - g[i] if not np.isnan(g[i]) else np.nan
19 + d = (delta + dyn * k) if (not np.isnan(k) and k >= 0) else 0.0
20 + rows.append({"u": i, "t": t, "g": g[i], "vrai": d, "y": alpha[i] + lam[t] + d + rng.normal(0, 0.01)})
21 + df = pd.DataFrame(rows); df["D"] = ((df["t"] >= df["g"]) & df["g"].notna()).astype(float); return df
22 +
23 +def test_within_egal_indicatrices_et_mco_grappes_reduit_a_hc1():
24 + df = _panel()
25 + r_tw = pn.twfe(df, "y", ["D"], "u", "t")
26 + yw, Xw = pn.within_deux_voies(df["y"].values, df[["D"]].values, df["u"].values, df["t"].values)
27 + bw = np.linalg.lstsq(Xw, yw, rcond=None)[0][0]
28 + assert abs(bw - r_tw["coef"].loc["D", "coef"]) < 1e-8
29 + X = np.column_stack([np.ones(len(df)), df["D"].values]); r = pn.mco_grappes(X, df["y"].values, np.arange(len(df)))
30 + assert np.allclose(r["se_grappes"], r["se_hc1"], rtol=1e-6) # une grappe par observation = HC1
31 +
32 +def test_twfe_recupere_effet_constant():
33 + df = _panel(delta=0.05); r = pn.twfe(df, "y", ["D"], "u", "t")
34 + assert abs(r["coef"].loc["D", "coef"] - 0.05) < 0.005
35 +
36 +def test_did_2x2_identite_cellules_et_regression():
37 + df = _panel(); df["tr"] = (df["g"] == 4).astype(int); d = df[df["g"] != 8]
38 + r = pn.did_2x2(d, "y", "u", "t", "tr", [1, 2, 3], [4, 5, 6])
39 + assert abs(r["estimateur"] - r["coef_reg"]) < 1e-10 and abs(r["estimateur"] - 0.05) < 0.01
40 +
41 +def test_event_study_retrouve_le_profil():
42 + df = _panel(delta=0.02, dyn=0.01); es = pn.event_study(df, "y", "u", "t", "g", fenetre=(-3, 3))
43 + for k in range(0, 3):
44 + assert abs(es.loc[k, "coef"] - (0.02 + 0.01 * k)) < 0.01
45 + assert all(abs(es.loc[k, "coef"]) < 0.01 for k in (-3, -2))
46 +
47 +def test_bacon_identite_et_cs_corrige_le_twfe():
48 + df = _panel(N=30, T=16, delta=0.0, dyn=0.02) # effets croissants : TWFE biaisé
49 + b = pn.bacon(df, "y", "u", "t", "g")
50 + assert abs(b["somme_poids"] - 1) < 1e-8 and abs(b["beta_pondere"] - b["beta_twfe"]) < 1e-8
51 + interdite = b["table"].loc[b["table"]["type"].str.startswith("tardive"), "beta"].iloc[0]
52 + vrai = df.loc[df["D"] == 1, "vrai"].mean()
53 + cs = pn.callaway_santanna(df, "y", "u", "t", "g", B=50)
54 + assert abs(cs["globale"] - vrai) < 0.01 and abs(b["beta_twfe"] - vrai) > 0.02 and interdite < 0
55 +
56 +def test_controle_synthetique_retrouve_combinaison_exacte():
57 + rng = np.random.default_rng(1); T, J, T0 = 30, 6, 20
58 + Y0 = np.cumsum(rng.normal(0, 0.02, (T, J)), axis=0); w = np.array([0.5, 0.3, 0.2, 0, 0, 0])
59 + y1 = Y0 @ w + np.concatenate([np.zeros(T0), 0.05 * np.ones(T - T0)])
60 + r = pn.controle_synthetique(Y0, y1, T0)
61 + assert np.allclose(r["poids"], w, atol=0.02) and abs(r["effet_moyen_post"] - 0.05) < 0.005 and r["rmspe_pre"] < 1e-3
62 +
63 +def test_rdd_retrouve_le_saut():
64 + rng = np.random.default_rng(2); x = rng.uniform(-1, 1, 4000); y = 1 + 0.2 * x + 0.1 * (x >= 0) + rng.normal(0, 0.05, 4000)
65 + r = pn.rdd_local_lineaire(x, y, 0.5)
66 + assert abs(r["tau"] - 0.1) < 3 * r["se_hc1"] and abs(r["tau"] - 0.1) < 0.02
67 +
68 +# ----------------------------------------------------------------------------- chiffres du chapitre
69 +
70 +@pytest.fixture(scope="module")
71 +def res():
72 + return c36.main(verbose=False)
73 +
74 +def test_plan_et_verite(res):
75 + v = res["verite"]; tr = res["traites"]
76 + assert tr["A"] == ["P04", "P06", "P11", "P14", "P20", "P21", "P28", "P42"] and tr["B"] == ["P10", "P24", "P29", "P33", "P37"]
77 + assert len(res["panel"]) == 2016 and res["panel"]["id_immeuble"].nunique() == 48
78 + assert v["att_global"] == pytest.approx(0.0454, abs=0.0001) and v["att_A"] == pytest.approx(0.0484, abs=0.0001)
79 + assert v["att_B"] == pytest.approx(0.0356, abs=0.0001) and v["att_fenetre_2x2"] == pytest.approx(0.04625, abs=1e-6)
80 + assert c36.effet_vrai(3) == 0.04 and c36.effet_vrai(15) == pytest.approx(0.07) and c36.effet_vrai(-1) == 0
81 + assert 100 * (math.exp(0.04) - 1) == pytest.approx(4.08, abs=0.005)
82 +
83 +def test_did_2x2(res):
84 + r = res["did_rmr"]; c = r["cellules"]
85 + assert c.loc[1, 0] == pytest.approx(10.1379, abs=0.0001) and c.loc[1, 1] == pytest.approx(10.2450, abs=0.0001)
86 + assert c.loc[0, 0] == pytest.approx(10.3696, abs=0.0001) and c.loc[0, 1] == pytest.approx(10.4234, abs=0.0001)
87 + assert r["estimateur"] == pytest.approx(0.0534, abs=0.0001) and r["se_hc1"] == pytest.approx(0.0201, abs=0.0001)
88 + assert r["n_traites"] == 8 and r["n_temoins"] == 9
89 + t = res["did_tous"]; assert t["estimateur"] == pytest.approx(0.0468, abs=0.0001) and t["se_hc1"] == pytest.approx(0.0177, abs=0.0001) and t["n_temoins"] == 35
90 + p = res["placebo"]; assert p["estimateur"] == pytest.approx(0.0155, abs=0.0001) and p["se_hc1"] == pytest.approx(0.0101, abs=0.0001)
91 +
92 +def test_twfe_et_erreurs_types(res):
93 + tw = res["twfe"]; c = tw["commun"]["coef"].loc["D"]; r = tw["regional"]["coef"].loc["D"]
94 + assert c["coef"] == pytest.approx(0.0905, abs=0.0001) and tw["within_deux_voies"] == pytest.approx(c["coef"], abs=1e-8)
95 + assert c["se"] == pytest.approx(0.0047, abs=0.0001) and c["se_hc1"] == pytest.approx(0.0053, abs=0.0001) and c["se_grappes"] == pytest.approx(0.0198, abs=0.0001)
96 + assert tw["commun"]["k"] == 90 and tw["regional"]["k"] == 295
97 + assert r["coef"] == pytest.approx(0.0702, abs=0.0001) and r["se"] == pytest.approx(0.0054, abs=0.0001)
98 + assert r["se_hc1"] == pytest.approx(0.0064, abs=0.0001) and r["se_grappes"] == pytest.approx(0.0264, abs=0.0001)
99 + assert r["se_grappes"] / r["se"] == pytest.approx(4.9, abs=0.05) and c["se_grappes"] / c["se"] == pytest.approx(4.2, abs=0.05)
100 +
101 +def test_event_study(res):
102 + es = res["es"]
103 + attendu = {-8: (-0.0175, 0.0204), -7: (-0.0169, 0.0173), -6: (-0.0182, 0.0148), -5: (-0.0121, 0.0131), -4: (-0.0214, 0.0140),
104 + -3: (-0.0183, 0.0116), -2: (-0.0073, 0.0074), 0: (0.0088, 0.0060), 1: (0.0235, 0.0084), 2: (0.0312, 0.0108),
105 + 3: (0.0410, 0.0138), 4: (0.0354, 0.0141), 5: (0.0439, 0.0149), 6: (0.0476, 0.0155), 7: (0.0561, 0.0158), 8: (0.0817, 0.0270)}
106 + for k, (b, s) in attendu.items():
107 + assert es.loc[k, "coef"] == pytest.approx(b, abs=0.0001) and es.loc[k, "se_grappes"] == pytest.approx(s, abs=0.0001)
108 + assert es.loc[-1, "coef"] == 0 and es.loc[8, "vrai"] == pytest.approx(0.0612, abs=0.0001) and es.loc[4, "vrai"] == pytest.approx(0.0425)
109 + pre = es.loc[-7:-2]; pente = np.polyfit(pre.index.values.astype(float), pre["coef"].values, 1)[0]
110 + assert pente == pytest.approx(0.0011, abs=0.0001)
111 +
112 +def test_bacon_cas4(res):
113 + b = res["bacon"]; t = b["table"].set_index("type")
114 + assert b["somme_poids"] == pytest.approx(1, abs=1e-10) and b["beta_pondere"] == pytest.approx(b["beta_twfe"], abs=1e-10)
115 + assert b["beta_twfe"] == pytest.approx(0.0905, abs=0.0001)
116 + tj = b["table"][b["table"]["temoins"] == "jamais"].set_index("traites")
117 + assert tj.loc[26, "poids"] == pytest.approx(0.6658, abs=0.0001) and tj.loc[26, "beta"] == pytest.approx(0.0844, abs=0.0001)
118 + assert tj.loc[34, "poids"] == pytest.approx(0.2721, abs=0.0001) and tj.loc[34, "beta"] == pytest.approx(0.1224, abs=0.0001)
119 + assert t.loc["précoce vs tardive (pas encore traitée)", "poids"] == pytest.approx(0.0476, abs=0.0001) and t.loc["précoce vs tardive (pas encore traitée)", "beta"] == pytest.approx(0.0080, abs=0.0001)
120 + assert t.loc["tardive vs précoce (déjà traitée)", "poids"] == pytest.approx(0.0146, abs=0.0001) and t.loc["tardive vs précoce (déjà traitée)", "beta"] == pytest.approx(0.0403, abs=0.0001)
121 + # vérité de la comparaison A vs B avant 2024T3 : moyenne de δ(k) pour k = 0..7
122 + assert c36.effet_vrai(np.arange(0, 8)).mean() == pytest.approx(0.0356, abs=0.0001)
123 +
124 +def test_callaway_santanna(res):
125 + c = res["cs"]
126 + assert c["globale"] == pytest.approx(0.0481, abs=0.0001) and c["globale_se_boot"] == pytest.approx(0.0116, abs=0.0002)
127 + pc = c["par_cohorte"]
128 + assert pc.loc[26, "att"] == pytest.approx(0.0528, abs=0.0001) and pc.loc[26, "se_boot"] == pytest.approx(0.0154, abs=0.0002)
129 + assert pc.loc[34, "att"] == pytest.approx(0.0406, abs=0.0001) and pc.loc[34, "se_boot"] == pytest.approx(0.0183, abs=0.0002)
130 + d = c["dynamique"]
131 + assert d.loc[3, "att"] == pytest.approx(0.0417, abs=0.0001) and d.loc[15, "att"] == pytest.approx(0.0740, abs=0.0001)
132 + c2 = res["cs_tous"]
133 + assert c2["globale"] == pytest.approx(0.0514, abs=0.0001) and c2["par_cohorte"].loc[26, "att"] == pytest.approx(0.0509, abs=0.0001)
134 + assert c2["par_cohorte"].loc[34, "att"] == pytest.approx(0.0521, abs=0.0001)
135 +
136 +def test_controle_synthetique_cas4(res):
137 + s = res["sc"]
138 + assert s["effet_moyen_post"] == pytest.approx(0.0363, abs=0.0001) and s["vrai_post"] == pytest.approx(0.0484, abs=0.0001)
139 + assert s["rmspe_pre"] == pytest.approx(0.0031, abs=0.0001) and s["rmspe_post"] == pytest.approx(0.0397, abs=0.0001)
140 + assert s["ratio_traite"] == pytest.approx(12.9, abs=0.1) and s["rang_ratio"] == 1 and len(s["placebos"]) == 35
141 + w = s["poids"]; assert abs(w.sum() - 1) < 1e-8 and (w >= -1e-12).all() and int((w > 0.005).sum()) == 11
142 + for k, v in {"P02": 0.142, "P09": 0.136, "P16": 0.123, "P25": 0.120, "P19": 0.114, "P43": 0.110, "P45": 0.105, "P03": 0.071}.items():
143 + assert w[k] == pytest.approx(v, abs=0.001)
144 +
145 +def test_discontinuite(res):
146 + r = res["rdd"]; e = r["estimations"]
147 + assert r["saut_vrai"] == 0.05 and r["naif"] == pytest.approx(0.1136, abs=0.0001)
148 + assert e[0.5]["tau"] == pytest.approx(0.034, abs=0.0005) and e[0.5]["se_hc1"] == pytest.approx(0.0343, abs=0.0001) and e[0.5]["n_eff"] == 93
149 + assert e[1.0]["tau"] == pytest.approx(0.0316, abs=0.0001) and e[1.0]["se_hc1"] == pytest.approx(0.0247, abs=0.0001) and e[1.0]["n_eff"] == 202
150 + assert e[2.0]["tau"] == pytest.approx(0.058, abs=0.0005) and e[2.0]["se_hc1"] == pytest.approx(0.0183, abs=0.0001) and e[2.0]["n_eff"] == 400
151 + for hb in e: assert abs(e[hb]["tau"] - 0.05) < 1.0 * e[hb]["se_hc1"]
152 +
153 +def test_laboratoire(res):
154 + L = res["labo"]
155 + assert L["twfe"] == pytest.approx(0.0233, abs=0.0001) and L["se_grappes"] == pytest.approx(0.0061, abs=0.0001)
156 + assert L["verite"] == pytest.approx(0.0692, abs=0.0001) and L["verite_k"] == pytest.approx(0.0825, abs=0.0001) and L["verite_l"] == pytest.approx(0.0425, abs=0.0001)
157 + assert L["cs"]["globale"] == pytest.approx(0.0601, abs=0.0001) and L["cs"]["globale_se_boot"] == pytest.approx(0.0073, abs=0.0002)
158 + pc = L["cs"]["par_cohorte"]; assert pc.loc[10, "att"] == pytest.approx(0.0893, abs=0.0001) and pc.loc[26, "att"] == pytest.approx(0.0310, abs=0.0001)
159 + t = L["bacon"]["table"].set_index("type")
160 + assert t.loc["tardive vs précoce (déjà traitée)", "beta"] == pytest.approx(-0.0356, abs=0.0001) and t.loc["tardive vs précoce (déjà traitée)", "poids"] == pytest.approx(0.3265, abs=0.0001)
161 + assert t.loc["précoce vs tardive (pas encore traitée)", "beta"] == pytest.approx(0.0404, abs=0.0001)
162 + assert abs(L["bacon"]["beta_pondere"] - L["twfe"]) < 1e-8 and abs(L["twfe"] - L["verite"]) / L["se_grappes"] > 7
163 +
164 +def test_sensibilite_et_temoins(res):
165 + sv = res["sens"]
166 + assert sv["did_2x2"]["estimateur"] == pytest.approx(0.0734, abs=0.0001) and sv["twfe_regional"]["coef"].loc["D", "coef"] == pytest.approx(0.1227, abs=0.0001)
167 + assert sv["pente_pre"] == pytest.approx(0.0036, abs=0.0001) and sv["twfe_tendance"]["coef"].loc["D", "coef"] == pytest.approx(0.0609, abs=0.0001)
168 + assert sv["did_detendance"] == pytest.approx(0.0426, abs=0.0001) and sv["ecart_ans"] == 2.0
169 + es = sv["event_study"]; assert es.loc[-8, "coef"] == pytest.approx(-0.0609, abs=0.0001) and es.loc[7, "coef"] == pytest.approx(0.0761, abs=0.0001)
170 + # biais du 2 × 2 = tendance × écart entre fenêtres (0,01 × 2 ans)
171 + assert sv["did_2x2"]["estimateur"] - res["did_rmr"]["estimateur"] == pytest.approx(0.02, abs=1e-6)
172 + t = res["temoins"].set_index("temoins")
173 + assert t.loc["jamais traités, toutes RMR", "estimateur"] == pytest.approx(0.0468, abs=0.0001) and t.loc["jamais traités, toutes RMR", "n_temoins"] == 35
174 + assert t.loc["jamais traités, Montréal", "estimateur"] == pytest.approx(0.0534, abs=0.0001)
175 + assert t.loc["jamais traités, hors Montréal", "estimateur"] == pytest.approx(0.0445, abs=0.0001) and t.loc["jamais traités, hors Montréal", "n_temoins"] == 26
176 + assert t.loc["pas encore traités (cohorte B)", "estimateur"] == pytest.approx(0.0308, abs=0.0001) and t.loc["pas encore traités (cohorte B)", "n_temoins"] == 5
177 +
178 +# ----------------------------------------------------------------------------- solutions
179 +
180 +def test_solutions_exercices(res):
181 + ex = res["ex"]
182 + assert ex["ex1"]["did"] == pytest.approx(0.05) and ex["ex1"]["pct"] == pytest.approx(5.13, abs=0.005)
183 + assert np.allclose(ex["ex2"]["yw"], [-0.15, 0.15, -0.05, 0.05, -0.05, 0.05]) and np.allclose(ex["ex2"]["xw"], [-0.5, 0.5, -0.5, 0.5, 0, 0])
184 + assert ex["ex2"]["b_within"] == pytest.approx(0.20) and ex["ex2"]["b_pooled"] == pytest.approx(0.425)
185 + assert ex["ex3"][(42, 0.5)] == pytest.approx(4.64, abs=0.005) and ex["ex3"][(42, 0.9)] == pytest.approx(6.16, abs=0.005) and ex["ex3"][(8, 0.5)] == pytest.approx(2.12, abs=0.005)
186 + assert (4.9 ** 2 - 1) / 41 == pytest.approx(0.56, abs=0.005)
187 + e5 = ex["ex5"]; assert e5["Dk"] == 0.75 and e5["Dl"] == 0.25 and e5["VD"] == pytest.approx(0.0625) and e5["s_kl"] == pytest.approx(0.5) and e5["s_lk"] == pytest.approx(0.5)
188 + # (c) espérances : β_kl = 0,055 ; β_lk = 0,03 − (0,13 − 0,055) = −0,045 ; TWFE = 0,005 ; ATT global 0,0675
189 + att_k = 0.01 * np.arange(1, 16).mean(); att_l = 0.01 * np.arange(1, 6).mean()
190 + assert att_k == pytest.approx(0.08) and att_l == pytest.approx(0.03)
191 + b_kl = 0.01 * np.arange(1, 11).mean(); b_lk = att_l - (0.01 * np.arange(11, 16).mean() - b_kl)
192 + assert b_kl == pytest.approx(0.055) and b_lk == pytest.approx(-0.045) and 0.5 * b_kl + 0.5 * b_lk == pytest.approx(0.005)
193 + assert (15 * att_k + 5 * att_l) / 20 == pytest.approx(0.0675)
194 + e6 = ex["ex6"]; assert e6["att"][2] == pytest.approx(0.045) and e6["att"][3] == pytest.approx(0.0817, abs=0.0001) and e6["att"][0] == pytest.approx(-0.015) and e6["moyenne_post"] == pytest.approx(0.0633, abs=0.0001)
195 +
196 +def test_solution_exercice_7(res):
197 + t = res["ex7"].set_index("variante")
198 + assert t.loc["35 donneurs, six RMR", "effet_post"] == pytest.approx(0.0363, abs=0.0001) and t.loc["35 donneurs, six RMR", "n_poids_sup_1pct"] == 11
199 + assert t.loc["9 donneurs, Montréal", "rmspe_pre"] == pytest.approx(0.0067, abs=0.0001) and t.loc["9 donneurs, Montréal", "effet_post"] == pytest.approx(0.0452, abs=0.0001)
200 + assert t.loc["9 donneurs, Montréal", "n_poids_sup_1pct"] == 7 and t.loc["9 donneurs, Montréal", "trois_premiers"].startswith("P09 0.32, P27 0.20")
201 + assert t.loc["26 donneurs, hors Montréal", "rmspe_pre"] == pytest.approx(0.0035, abs=0.0001) and t.loc["26 donneurs, hors Montréal", "effet_post"] == pytest.approx(0.0375, abs=0.0001)
202 + assert t.loc["placebo 2020T3, 35 donneurs", "rmspe_pre"] == pytest.approx(0.0027, abs=0.0001) and t.loc["placebo 2020T3, 35 donneurs", "effet_post"] == pytest.approx(-0.0055, abs=0.0001)
added analysis/python/tests/test_ch38.py +247 −0
@@ -0,0 +1,247 @@
1 +"""Chapitre 38 — tests des fonctions de traite.geo et des chiffres cités dans manuscript/chapters/ch38.tex et solutions/ch38.tex.
2 +Tolérances : distances ±1 m sauf indication, coefficients ±0,0005, parts ±0,001, GVF ±0,001."""
3 +import sys, pathlib
4 +import numpy as np, pytest
5 +sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "cases"))
6 +from traite import geo
7 +import ch38_sig as c38
8 +
9 +pytestmark = pytest.mark.filterwarnings("ignore")
10 +
11 +
12 +@pytest.fixture(scope="module")
13 +def r():
14 + return c38.tout()
15 +
16 +# ----------------------------------------------------------------------------- fonctions génériques (traite.geo)
17 +
18 +def test_haversine_et_equirectangulaire():
19 + # un degré de latitude ≈ 111,19 km sur la sphère de rayon 6371,0088 km ; méridien et équateur
20 + assert geo.haversine_km(0, 0, 1, 0) == pytest.approx(np.pi / 180 * geo.R_TERRE_KM, rel=1e-9)
21 + assert geo.haversine_km(0, 0, 0, 90) == pytest.approx(np.pi / 2 * geo.R_TERRE_KM, rel=1e-9)
22 + # antipodes et point identique
23 + assert geo.haversine_km(10, 20, -10, -160) == pytest.approx(np.pi * geo.R_TERRE_KM, rel=1e-6)
24 + assert float(geo.haversine_km(45.0, -75.0, 45.0, -75.0)) == 0.0
25 + xy = geo.equirectangulaire_km([45.0, 46.0], [-75.0, -75.0], 45.0, -75.0)
26 + assert xy[1, 1] == pytest.approx(np.pi / 180 * geo.R_TERRE_KM, rel=1e-9) and xy[1, 0] == 0.0
27 +
28 +
29 +def test_facteur_echelle_tm():
30 + assert float(geo.facteur_echelle_tm(45.0, -76.5, -76.5)) == pytest.approx(0.9999) # méridien central
31 + k = float(geo.facteur_echelle_tm(45.4215, -75.7395, -76.5)); assert k == pytest.approx(0.999943, abs=1e-6)
32 + # croissant avec l'écart au méridien, décroissant avec la latitude
33 + assert geo.facteur_echelle_tm(45, -75.0, -76.5) > geo.facteur_echelle_tm(45, -76.0, -76.5)
34 + assert geo.facteur_echelle_tm(60, -75.0, -76.5) < geo.facteur_echelle_tm(45, -75.0, -76.5)
35 +
36 +
37 +def test_point_dans_polygone_carre_et_concave():
38 + carre = np.array([(0, 0), (1, 0), (1, 1), (0, 1)], float)
39 + pts = np.array([(0.5, 0.5), (1.5, 0.5), (-0.1, 0.2), (0.99, 0.01)])
40 + assert list(geo.point_dans_polygone(pts, carre)) == [True, False, False, True]
41 + poly = np.array([(0, 0), (4, 0), (4, 3), (2, 1), (0, 3)], float) # exercice 3
42 + assert list(geo.point_dans_polygone(np.array([(1, 1), (2, 2), (3, 2.5)]), poly)) == [True, False, False]
43 + # cohérence avec shapely sur des points aléatoires
44 + from shapely.geometry import Polygon, Point
45 + rng = np.random.default_rng(1); P = rng.uniform(-1, 5, (500, 2)); sp = Polygon(poly)
46 + assert np.array_equal(geo.point_dans_polygone(P, poly), np.array([sp.contains(Point(*p)) for p in P]))
47 +
48 +
49 +def test_idw_exact_et_moyenne():
50 + co = np.array([(0, 0), (1, 0), (0, 1)], float); z = np.array([1.0, 2.0, 3.0])
51 + assert geo.idw(co, z, co)[1] == 2.0 # valeur exacte aux observations
52 + assert float(geo.idw(co[:2], z[:2], np.array([[0.5, 0.0]]))[0]) == pytest.approx(1.5) # équidistant : moyenne
53 + # un point plus proche de (1, 0) tire vers 2
54 + assert geo.idw(co[:2], z[:2], np.array([[0.9, 0.0]]), puissance=2)[0] > 1.9
55 +
56 +
57 +def test_grille_hexagonale_couvre_et_aire():
58 + cells = geo.grille_hexagonale(0, 0, 10, 10, 1.0)
59 + from shapely.geometry import Polygon, Point
60 + polys = [Polygon(c) for c in cells]
61 + assert polys[0].area == pytest.approx(3 * np.sqrt(3) / 2, rel=1e-9)
62 + rng = np.random.default_rng(2); P = rng.uniform(0, 10, (300, 2))
63 + assert all(any(p.contains(Point(*q)) for p in polys) for q in P) # chaque point tombe dans une cellule
64 + # pavage : aires d'intersection nulles entre cellules distinctes
65 + assert polys[0].intersection(polys[1]).area < 1e-9
66 +
67 +
68 +def test_classifications_et_gvf():
69 + z = np.array([1, 2, 3, 10, 11, 12, 30, 31, 32], float)
70 + bj = geo.bornes_jenks(z, 3)
71 + assert list(geo.classer(z, bj)) == [0, 0, 0, 1, 1, 1, 2, 2, 2] and geo.gvf(z, bj) > 0.99
72 + be = geo.bornes_intervalles_egaux(z, 3); assert be[0] == 1 and be[-1] == 32 and len(be) == 4
73 + bq = geo.bornes_quantiles(z, 3); assert np.bincount(geo.classer(z, bq)).tolist() == [3, 3, 3]
74 + assert geo.gvf(z, bj) >= geo.gvf(z, be) - 1e-9 and geo.gvf(z, bj) >= geo.gvf(z, bq) - 1e-9 # Jenks optimal
75 + assert geo.gvf(z, np.array([1.0, 32.0])) == pytest.approx(0.0)
76 +
77 +
78 +def test_gravite_annuelle_emd():
79 + A = geo.accessibilite_gravitaire(np.array([[0.0, 0.0]]), np.array([[1.5, 0.0], [6.0, 0.0], [9.0, 0.0]]), [100, 40, 30], 0.25)
80 + assert float(A[0]) == pytest.approx(80.816, abs=0.001) # exercice 4
81 + assert geo.probabilite_annuelle(0.70) == pytest.approx(0.04702, abs=1e-5)
82 + assert geo.probabilite_annuelle(0.20) == pytest.approx(0.00889, abs=1e-5)
83 + assert geo.probabilite_annuelle(0.07) == pytest.approx(0.00290, abs=1e-5)
84 + assert geo.probabilite_annuelle(1 - 0.5 ** 25) == pytest.approx(0.5)
85 + assert geo.effet_minimal_detectable(1.0) == pytest.approx(2.8016, abs=1e-3)
86 +
87 +# ----------------------------------------------------------------------------- chiffres du chapitre (cas 1)
88 +
89 +def test_projection_et_distances(r):
90 + b = r["base"]
91 + assert b["crs_nom"] == "NAD83 / MTM zone 9"
92 + assert b["xs"] == pytest.approx(364315.8, abs=0.5) and b["ys"] == pytest.approx(5031566.3, abs=0.5)
93 + assert b["k_sujet"] == pytest.approx(0.999943, abs=1e-6)
94 + assert b["etendue_km"][0] == pytest.approx(42.2, abs=0.05) and b["etendue_km"][1] == pytest.approx(27.5, abs=0.05)
95 + e = b["ecarts"]
96 + assert e["mtm9"]["max_m"] < 1.5 and e["equirect"]["max_m"] == pytest.approx(82.8, abs=0.5) and e["haversine"]["max_m"] == pytest.approx(70.8, abs=0.5)
97 + assert e["equirect"]["max_rel_pct"] == pytest.approx(0.51, abs=0.01) and e["haversine"]["max_rel_pct"] == pytest.approx(0.28, abs=0.01)
98 + m = b["manhattan"]; assert m["moyenne_ratio"] == pytest.approx(1.33, abs=0.005) and m["mediane_ratio"] == pytest.approx(1.37, abs=0.005) and m["max_ratio"] <= np.sqrt(2) + 1e-9
99 +
100 +
101 +def test_geometries_et_jointure(r):
102 + a = r["geom"]["aires_km2"]; L = r["geom"]["longueurs_km"]
103 + assert a["A"] == pytest.approx(31.0, abs=0.05) and a["B"] == pytest.approx(50.3, abs=0.05) and a["corridor_300"] == pytest.approx(15.0, abs=0.05)
104 + assert L["cours_eau"] == pytest.approx(43.8, abs=0.05) and L["corridor"] == pytest.approx(24.5, abs=0.05)
105 + s = r["secteurs"]
106 + assert s.loc["Total", "zone_A"] == 31 and s.loc["Total", "zone_B"] == 57 and s.loc["Total", "corridor_300"] == 17
107 + assert s.loc["Hull", "zone_A"] == 14 and s.loc["Aylmer", "zone_A"] == 17 and s.loc["Hull", "zone_B"] == 19 and s.loc["Aylmer", "zone_B"] == 38
108 + assert s.loc["Hull", "corridor_300"] == 14 and s.loc["Gatineau", "zone_A"] == 0
109 + assert s.loc["Hull", "dist_corr_med_km"] == pytest.approx(2.0, abs=0.05) and s.loc["Buckingham", "dist_corr_med_km"] == pytest.approx(16.8, abs=0.05)
110 + assert s.loc["Hull", "acces_moy"] == pytest.approx(74.3, abs=0.1) and s.loc["Aylmer", "acces_moy"] == pytest.approx(26.4, abs=0.1)
111 + v = r["ventes"]; assert (v["zone_A"] == v["zone_A_rayon"]).all() # lancer de rayon = shapely
112 + assert int(v["zone_A"].sum() + v["zone_B"].sum()) == 88
113 +
114 +
115 +def test_grille_classification_et_surfacique(r):
116 + g = r["grille"]
117 + assert (g["n_cellules"], g["n_occupees"], g["n_cartographiees"]) == (374, 69, 30) and g["aire_cellule_km2"] == pytest.approx(3.74, abs=0.005)
118 + cl = g["classif"]
119 + assert np.allclose(np.round(cl["Jenks"]["bornes"]), [2546, 3004, 3358, 3548, 3925, 4106]) and cl["Jenks"]["effectifs"].tolist() == [4, 5, 8, 9, 4]
120 + assert np.allclose(np.round(cl["quantiles"]["bornes"]), [2546, 3165, 3441, 3555, 3650, 4106]) and cl["quantiles"]["effectifs"].tolist() == [6, 6, 6, 6, 6]
121 + assert cl["intervalles égaux"]["effectifs"].tolist() == [4, 2, 9, 11, 4]
122 + assert cl["Jenks"]["gvf"] == pytest.approx(0.964, abs=0.001) and cl["intervalles égaux"]["gvf"] == pytest.approx(0.952, abs=0.001) and cl["quantiles"]["gvf"] == pytest.approx(0.909, abs=0.001)
123 + ps = g["pond_surf"]; assert ps["total_exact"] == 600 and ps["eam"] == pytest.approx(2.95, abs=0.01) and ps["max"] == pytest.approx(14.9, abs=0.05) and ps["total_est"] == pytest.approx(598.3, abs=0.05)
124 + ok = g["cartographiees"]; assert np.abs(ok["residu_moy"]).max() < 0.051
125 +
126 +
127 +def test_idw_sujet(r):
128 + i = r["idw"]
129 + assert i[1.0]["sujet"] == pytest.approx(-0.054, abs=0.0005) and i[2.0]["sujet"] == pytest.approx(-0.060, abs=0.0005) and i[3.0]["sujet"] == pytest.approx(-0.066, abs=0.0005)
130 + assert i[1.0]["eqm_loo"] == pytest.approx(0.086, abs=0.0005) and i[2.0]["eqm_loo"] == pytest.approx(0.090, abs=0.0005) and i[3.0]["eqm_loo"] == pytest.approx(0.095, abs=0.0005)
131 + assert i["eqm_sans_correction"] == pytest.approx(0.103, abs=0.0005)
132 +
133 +
134 +def test_modeles_augmentes(r):
135 + m = r["modeles"]; ref = m["référence"]; ss = m["sans secteur"]; sn = m["sans secteur ni gradient"]
136 + c = ref["coef"]
137 + assert c.loc["zone_A", "coef"] == pytest.approx(0.0054, abs=0.0005) and c.loc["zone_A", "t"] == pytest.approx(0.31, abs=0.01)
138 + assert c.loc["zone_B", "coef"] == pytest.approx(-0.0181, abs=0.0005) and c.loc["corr_300", "coef"] == pytest.approx(-0.0306, abs=0.0005)
139 + assert c.loc["ln_dist_corridor", "coef"] == pytest.approx(-0.0065, abs=0.0005) and c.loc["ln_acces", "coef"] == pytest.approx(0.0106, abs=0.0005)
140 + assert (c["p"] > 0.05).all() and ref["F"] == pytest.approx(0.84, abs=0.01) and ref["p_F"] == pytest.approx(0.52, abs=0.01)
141 + assert ref["r2_base"] == pytest.approx(0.8530, abs=0.0001) and ref["r2"] == pytest.approx(0.8541, abs=0.0001)
142 + assert ref["mde"]["zone_A"] == pytest.approx(0.049, abs=0.001) and ref["mde"]["zone_B"] == pytest.approx(0.038, abs=0.001) and ref["mde"]["corr_300"] == pytest.approx(0.062, abs=0.001)
143 + assert ref["mde"]["ln_acces"] == pytest.approx(0.059, abs=0.001) and ref["mde"]["ln_dist_corridor"] == pytest.approx(0.020, abs=0.001)
144 + c2 = ss["coef"]
145 + assert c2.loc["zone_A", "coef"] == pytest.approx(0.0614, abs=0.0005) and c2.loc["zone_A", "t"] == pytest.approx(3.78, abs=0.01)
146 + assert c2.loc["zone_B", "coef"] == pytest.approx(0.0424, abs=0.0005) and c2.loc["zone_B", "t"] == pytest.approx(2.99, abs=0.01)
147 + assert c2.loc["ln_dist_corridor", "t"] == pytest.approx(-1.98, abs=0.01) and c2.loc["ln_acces", "coef"] == pytest.approx(0.1359, abs=0.0005) and c2.loc["ln_acces", "t"] == pytest.approx(10.9, abs=0.05)
148 + assert ss["F"] == pytest.approx(33.4, abs=0.05) and ss["r2_base"] == pytest.approx(0.7661, abs=0.0001) and ss["r2"] == pytest.approx(0.8182, abs=0.0001)
149 + c3 = sn["coef"]
150 + assert c3.loc["zone_A", "coef"] == pytest.approx(0.0578, abs=0.0005) and c3.loc["zone_B", "coef"] == pytest.approx(0.0502, abs=0.0005)
151 + assert c3.loc["ln_acces", "coef"] == pytest.approx(0.0596, abs=0.0005) and c3.loc["ln_acces", "t"] == pytest.approx(14.1, abs=0.05) and sn["F"] == pytest.approx(99.4, abs=0.05)
152 + assert sn["r2_base"] == pytest.approx(0.6372, abs=0.0001) and sn["r2"] == pytest.approx(0.8040, abs=0.0001)
153 + # effets en pourcentage cités : +0,5, −1,8, −3,0 (référence) ; +6,3, +4,3, +14,6 (sans secteur) ; +5,8, +5,2, +6,0
154 + assert c.loc["zone_A", "effet_pct"] == pytest.approx(0.5, abs=0.05) and c.loc["zone_B", "effet_pct"] == pytest.approx(-1.8, abs=0.05) and c.loc["corr_300", "effet_pct"] == pytest.approx(-3.0, abs=0.05)
155 + assert c2.loc["zone_A", "effet_pct"] == pytest.approx(6.3, abs=0.05) and c2.loc["zone_B", "effet_pct"] == pytest.approx(4.3, abs=0.05) and c2.loc["ln_acces", "effet_pct"] == pytest.approx(14.6, abs=0.05)
156 + assert c3.loc["zone_A", "effet_pct"] == pytest.approx(6.0, abs=0.06) and c3.loc["zone_B", "effet_pct"] == pytest.approx(5.2, abs=0.06) and c3.loc["ln_acces", "effet_pct"] == pytest.approx(6.1, abs=0.06)
157 +
158 +
159 +def test_sensibilite_tampon(r):
160 + t = r["tampon"]; ref = t[t["spec"] == "référence"].set_index("rayon_m"); ss = t[t["spec"] == "sans secteur"].set_index("rayon_m")
161 + assert ref["n_dans_tampon"].loc[[100, 300, 1000]].tolist() == [4, 17, 57]
162 + assert (ref["t"].abs() < 1.96).all() # jamais significatif dans la référence
163 + assert ref.loc[100, "coef"] == pytest.approx(-0.054, abs=0.0005) and ref.loc[400, "coef"] == pytest.approx(0.002, abs=0.0005)
164 + assert 1.96 * ref.loc[100, "se"] == pytest.approx(0.072, abs=0.001) and 1.96 * ref.loc[1000, "se"] == pytest.approx(0.022, abs=0.001)
165 + assert ss.loc[300, "t"] == pytest.approx(-2.13, abs=0.01) and ss.loc[750, "t"] == pytest.approx(-2.72, abs=0.01) and ss.loc[1000, "t"] == pytest.approx(-3.56, abs=0.01)
166 + assert ss.loc[300, "coef"] == pytest.approx(-0.039, abs=0.0005) and ss.loc[1000, "coef"] == pytest.approx(-0.045, abs=0.0005) and (ss["coef"] < 0).all()
167 +
168 +
169 +def test_faux_positifs(r):
170 + tab = r["faux_positifs"]["tableau"].set_index(["rayon_km", "spec", "erreur_type"])
171 + assert tab.loc[(0.5, "référence", "classique"), "n_polygones"] == 107 and tab.loc[(1.0, "référence", "classique"), "n_polygones"] == 346 and tab.loc[(2.0, "référence", "classique"), "n_polygones"] == 484
172 + g = lambda r_, s_, e_, col="part_5pct": float(tab.loc[(r_, s_, e_), col])
173 + assert g(1.0, "référence", "classique") == pytest.approx(0.029, abs=0.001) and g(2.0, "référence", "classique") == pytest.approx(0.035, abs=0.001) and g(0.5, "référence", "classique") == pytest.approx(0.112, abs=0.001)
174 + assert g(0.5, "référence", "HC1") == pytest.approx(0.262, abs=0.001) and g(1.0, "référence", "HC1") == pytest.approx(0.072, abs=0.001)
175 + assert g(1.0, "sans secteur", "classique") == pytest.approx(0.364, abs=0.001) and g(2.0, "sans secteur", "classique") == pytest.approx(0.736, abs=0.001)
176 + assert g(2.0, "sans secteur", "classique", "part_bonferroni") == pytest.approx(0.293, abs=0.001) and g(2.0, "référence", "classique", "part_bonferroni") == 0.0
177 + assert g(1.0, "référence", "classique", "ecart_type_t") == pytest.approx(1.01, abs=0.005) and g(2.0, "sans secteur", "classique", "ecart_type_t") == pytest.approx(4.42, abs=0.005)
178 + assert g(2.0, "sans secteur ni gradient", "classique") == pytest.approx(0.483, abs=0.001)
179 +
180 +# ----------------------------------------------------------------------------- cas 4 et capitalisation
181 +
182 +def test_cas4(r):
183 + c4 = r["cas4"]; par = c4["par_rmr"]
184 + assert c4["total"]["part_zone_inondable"] == pytest.approx(0.0666, abs=0.0001) and c4["n_inond"] == 3 and c4["n_sol"] == 2
185 + assert par.loc["Québec", "part_inond_rmr"] == pytest.approx(0.288, abs=0.001) and par.loc["Saguenay", "part_inond_rmr"] == pytest.approx(0.185, abs=0.001) and par.loc["Montréal", "part_inond_rmr"] == pytest.approx(0.026, abs=0.001)
186 + assert par.loc["Québec", "part_inond_total"] == pytest.approx(0.050, abs=0.001) and par["val_inond"].sum() == 9546000 and par["valeur"].sum() == 143363000
187 + assert par.loc["Québec", "val_inond"] == 7229000 and par.loc["Montréal", "n"] == 17 and par.loc["Gatineau", "n"] == 8
188 + assert c4["coef_inondable"] == pytest.approx(0.0756, abs=0.0005) and c4["se_inondable"] == pytest.approx(0.0347, abs=0.0005) and c4["effet_pct_inondable"] == pytest.approx(7.9, abs=0.05)
189 + assert c4["effet_pct_sol"] == pytest.approx(5.5, abs=0.05)
190 + assert abs(c4["coef_inondable"] - np.log(1.05)) < c4["se_inondable"] # à moins d'une erreur-type du générateur
191 + d = c4["distances_km"]
192 + assert d.loc["Montréal", "Québec"] == pytest.approx(236, abs=0.5) and d.loc["Québec", "Trois-Rivières"] == pytest.approx(116, abs=0.5) and d.loc["Gatineau", "Saguenay"] == pytest.approx(478, abs=0.5)
193 + assert c4["etendue_km"][0] == pytest.approx(379, abs=1) and c4["etendue_km"][1] == pytest.approx(343, abs=1)
194 +
195 +
196 +def test_capitalisation(r):
197 + cap = r["capitalisation"]
198 + assert np.allclose(cap["p_annuelle"] * 100, [4.70, 0.89, 0.29], atol=0.005)
199 + assert np.allclose(cap["decote_pct"] * 100, [12.82, 1.29, 0.16], atol=0.005)
200 + assert np.allclose(cap["decote_dollars"], [81805, 8246, 1009], atol=0.6)
201 + # identité : décote = p1 × D / R
202 + assert np.allclose(cap["decote_pct"], cap["p_annuelle"] * cap["dommage_pct"] / 0.055)
203 +
204 +# ----------------------------------------------------------------------------- solutions des exercices
205 +
206 +def test_solutions_1_a_5():
207 + # ex. 1 : fuseaux et facteurs d'échelle
208 + k = {v: float(geo.facteur_echelle_tm(la, lo, l0)) for v, (la, lo, l0) in {"Mtl": (45.51, -73.60, -73.5), "Qc": (46.82, -71.19, -70.5), "Gat": (45.42, -75.74, -76.5), "Sag": (48.38, -71.10, -70.5)}.items()}
209 + assert k["Mtl"] == pytest.approx(0.999901, abs=1e-6) and k["Qc"] == pytest.approx(0.999934, abs=1e-6) and k["Gat"] == pytest.approx(0.999943, abs=1e-6) and k["Sag"] == pytest.approx(0.999924, abs=1e-6)
210 + assert (k["Mtl"] - 1) * 10000 == pytest.approx(-0.99, abs=0.01)
211 + # ex. 2 : trois distances
212 + xy = geo.equirectangulaire_km([45.4620], [-75.6612], 45.4215, -75.7395)[0]
213 + assert xy[0] == pytest.approx(6.111, abs=0.001) and xy[1] == pytest.approx(4.503, abs=0.001) and np.hypot(*xy) == pytest.approx(7.591, abs=0.001)
214 + assert float(geo.haversine_km(45.4215, -75.7395, 45.4620, -75.6612)) == pytest.approx(7.589, abs=0.001)
215 + assert np.hypot(45.4620 - 45.4215, -75.6612 + 75.7395) * 111 == pytest.approx(9.785, abs=0.001)
216 + # ex. 3 : aire, périmètre et tampon
217 + from shapely.geometry import Polygon
218 + sp = Polygon([(0, 0), (4, 0), (4, 3), (2, 1), (0, 3)])
219 + assert sp.area == 8.0 and sp.length == pytest.approx(15.657, abs=0.001)
220 + assert sp.buffer(0.5, quad_segs=64).area == pytest.approx(16.560, abs=0.001) and sp.area + 0.5 * sp.length + np.pi * 0.25 == pytest.approx(16.614, abs=0.001)
221 + # ex. 4 : indice gravitaire
222 + for lam, a1, a2 in [(0.25, 80.816, 62.54), (0.5, 49.561, 23.007)]:
223 + A1 = 100 * np.exp(-lam * 1.5) + 40 * np.exp(-lam * 6) + 30 * np.exp(-lam * 9); A2 = 170 * np.exp(-lam * 4)
224 + assert A1 == pytest.approx(a1, abs=0.001) and A2 == pytest.approx(a2, abs=0.001)
225 + assert np.log(80.816) == pytest.approx(4.392, abs=0.001) and np.log(62.54) == pytest.approx(4.136, abs=0.001)
226 + # ex. 5 : capitalisation et EMD
227 + p1 = geo.probabilite_annuelle(0.35); assert p1 == pytest.approx(0.01708, abs=1e-5)
228 + assert p1 * 0.10 / 0.05 == pytest.approx(0.0342, abs=0.0001) and p1 * 0.10 / 0.05 * 520000 == pytest.approx(17767, abs=1) and p1 * 0.2 / 0.05 * 520000 == pytest.approx(35534, abs=1)
229 + se = 0.09 / np.sqrt(500 * 0.048 * 0.952 * 0.9); assert se == pytest.approx(0.0199, abs=0.0001) and geo.effet_minimal_detectable(se) == pytest.approx(0.0556, abs=0.0001)
230 +
231 +
232 +def test_solutions_6_et_7():
233 + e6 = c38.exercice_6(); t = e6["tableau_3km"].set_index(["spec", "erreur_type"])
234 + assert int(t.loc[("référence", "classique"), "n_polygones"]) == 995
235 + assert float(t.loc[("référence", "classique"), "part_5pct"]) == pytest.approx(0.045, abs=0.001) and float(t.loc[("référence", "HC1"), "part_5pct"]) == pytest.approx(0.053, abs=0.001)
236 + assert float(t.loc[("sans secteur", "classique"), "part_5pct"]) == pytest.approx(0.892, abs=0.001) and float(t.loc[("sans secteur", "classique"), "part_bonferroni"]) == pytest.approx(0.505, abs=0.001)
237 + assert float(t.loc[("sans secteur ni gradient", "classique"), "part_5pct"]) == pytest.approx(0.601, abs=0.001) and float(t.loc[("sans secteur ni gradient", "classique"), "part_bonferroni"]) == pytest.approx(0.325, abs=0.001)
238 + assert float(t.loc[("référence", "classique"), "ecart_type_t"]) == pytest.approx(0.98, abs=0.005) and float(t.loc[("sans secteur", "classique"), "ecart_type_t"]) == pytest.approx(6.28, abs=0.005)
239 + c = e6["comparaison_05km"]
240 + assert c["classique"]["n"] == 107 and c["classique"]["part_5pct"] == pytest.approx(0.112, abs=0.001) and c["HC1"]["part_5pct"] == pytest.approx(0.262, abs=0.001) and c["HC3"]["part_5pct"] == pytest.approx(0.234, abs=0.001)
241 + assert c["HC3"]["ecart_type"] == pytest.approx(1.42, abs=0.005)
242 + e7 = c38.exercice_7().set_index("grille")
243 + assert e7.loc["hexagones R = 800 m", ["cellules", "occupees", "n_min5"]].tolist() == [813, 123, 43] and e7.loc["hexagones R = 800 m", "gvf"] == pytest.approx(0.962, abs=0.001)
244 + assert e7.loc["hexagones R = 2000 m", ["cellules", "occupees", "n_min5"]].tolist() == [154, 34, 19] and e7.loc["hexagones R = 2000 m", "gvf"] == pytest.approx(0.975, abs=0.001)
245 + assert e7.loc["carrés de 2 km", ["cellules", "occupees", "n_min5"]].tolist() == [368, 64, 36] and e7.loc["carrés de 2 km", "gvf"] == pytest.approx(0.962, abs=0.001)
246 + assert np.allclose(e7["part_inter"].values, [0.356, 0.286, 0.250, 0.302], atol=0.001)
247 + assert np.allclose(e7.loc["hexagones R = 800 m", "bornes_jenks"], [2407, 2981, 3388, 3602, 3970, 4207])
added analysis/python/tests/test_ch40.py +183 −0
@@ -0,0 +1,183 @@
1 +"""Tests du chapitre 40 (Monte-Carlo et stress tests — données synthétiques, cas 3 et cas 4) : égalité stricte avec les chiffres du
2 +chapitre 47 (mêmes lois MC_LOIS, même graine 20261031), moments théoriques des lois, Cholesky/copule, erreur de Monte-Carlo,
3 +hypercube latin, mesures de risque et vérification analytique du RCD, Sobol, portefeuille, exercices.
4 +Tolérances déclarées : montants ±1 $ (valeurs citées), moments de lois ±1e-9 (formules fermées), fréquences ±1e-4, indices de Sobol ±0,02."""
5 +import sys, pathlib, math
6 +import numpy as np, pandas as pd, pytest
7 +from scipy import stats
8 +sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "cases"))
9 +from traite import cas3, cas4
10 +import ch40_montecarlo as m
11 +import ch47_dossier as c47
12 +
13 +@pytest.fixture(scope="module")
14 +def r():
15 + return cas3.tout()
16 +
17 +@pytest.fixture(scope="module")
18 +def mc(r):
19 + return m.monte_carlo(r, m.N_MC, m.GRAINE)
20 +
21 +# ----------------------------------------------------------------------------- cohérence avec le chapitre 47
22 +
23 +def test_memes_lois_et_graine_que_ch47():
24 + assert m.MC_LOIS is c47.MC_LOIS and m.GRAINE == c47.GRAINE == 20261031 and m.R_T_PLANCHER == c47.R_T_PLANCHER
25 +
26 +def test_chiffres_identiques_au_ch47(r, mc):
27 + st, pr = mc["stats"], mc["probabilites"]
28 + assert st["mediane"] == pytest.approx(2_191_607.87, abs=1) and st["q05"] == pytest.approx(1_735_194.21, abs=1) and st["q95"] == pytest.approx(2_876_902.35, abs=1)
29 + assert st["moyenne"] == pytest.approx(2_233_505.68, abs=1) and st["ecart_type"] == pytest.approx(350_442.57, abs=1)
30 + assert pr["rcd1_sous_1.20"] == pytest.approx(0.4936, abs=1e-4) and pr["rcd1_sous_1.00"] == 0.0 and pr["rcd6_choc200_sous_1.20"] == pytest.approx(0.0140, abs=1e-4)
31 + assert pr["V_sous_cap"] == pytest.approx(0.3094, abs=1e-4) and pr["V_sous_2M"] == pytest.approx(0.2645, abs=1e-4) and pr["V_sous_role"] == pytest.approx(0.5118, abs=1e-4)
32 + # evaluer() (chemin du ch. 40) reproduit monte_carlo() (chemin du ch. 47) tirage par tirage
33 + e = m.evaluer(r, mc["tirages"])
34 + assert np.array_equal(e["valeur"], mc["valeur"]) and np.allclose(e["rcd1"], mc["rcd"][:, 0])
35 +
36 +def test_valeur_deterministe_reproduit_dcf(r):
37 + assert m.valeur_deterministe(r) == pytest.approx(r["dcf"]["valeur"], rel=1e-12)
38 + assert m.valeur_deterministe(r, taux_actualisation=0.08, taux_terminal=0.055) == pytest.approx(1_947_889, abs=1) # scénario « hausse des taux » du ch. 30
39 +
40 +# ----------------------------------------------------------------------------- lois
41 +
42 +def test_moments_lois_formules_fermees():
43 + ln = m.moments_loi("lognormale", {"mediane": 0.039, "sigma_log": 0.30})
44 + assert ln["moyenne"] == pytest.approx(0.039 * math.exp(0.045), abs=1e-12) and ln["q05"] == pytest.approx(stats.lognorm.ppf(0.05, 0.30, scale=0.039), abs=1e-12)
45 + assert ln["ecart_type"] == pytest.approx(stats.lognorm.std(0.30, scale=0.039), abs=1e-12)
46 + tri = m.moments_loi("triangulaire", {"min": 0.08, "mode": 0.15, "max": 0.25}); d = stats.triang(c=0.07 / 0.17, loc=0.08, scale=0.17)
47 + assert tri["moyenne"] == pytest.approx(0.16, abs=1e-12) and tri["ecart_type"] == pytest.approx(d.std(), abs=1e-12)
48 + assert float(tri["q05"]) == pytest.approx(d.ppf(0.05), abs=1e-12) and float(tri["q95"]) == pytest.approx(d.ppf(0.95), abs=1e-12)
49 + pe = m.pert(0.08, 0.15, 0.25)
50 + assert pe["moyenne"] == pytest.approx((0.08 + 4 * 0.15 + 0.25) / 6, abs=1e-12) and pe["moyenne"] == pytest.approx(pe["loi"].mean(), abs=1e-12)
51 + assert pe["ecart_type"] == pytest.approx(pe["loi"].std(), abs=1e-12)
52 + assert pe["loi"].pdf(0.15) >= max(pe["loi"].pdf(0.149), pe["loi"].pdf(0.151)) # mode en m
53 + # élicitation : inversion exacte des quantiles
54 + p = m.parametres_depuis_quantiles(0.024, 0.063, "lognormale"); mm = m.moments_loi("lognormale", p)
55 + assert mm["q05"] == pytest.approx(0.024, abs=1e-12) and mm["q95"] == pytest.approx(0.063, abs=1e-12)
56 + pn = m.parametres_depuis_quantiles(-0.01, 0.03, "normale"); assert pn["mu"] == pytest.approx(0.01) and pn["mu"] + pn["sigma"] * m.Z90 == pytest.approx(0.03, abs=1e-12)
57 +
58 +def test_quantile_loi_inverse_les_tirages():
59 + for nom, (typ, p) in m.MC_LOIS.items():
60 + u = np.array([0.05, 0.5, 0.95]); q = m.quantile_loi(typ, p, u)
61 + if typ == "normale": assert np.allclose(q, stats.norm.ppf(u, p["mu"], p["sigma"]))
62 + if typ == "lognormale": assert np.allclose(q, stats.lognorm.ppf(u, p["sigma_log"], scale=p["mediane"]))
63 + if typ == "triangulaire": assert np.allclose(q, stats.triang.ppf(u, c=(p["mode"] - p["min"]) / (p["max"] - p["min"]), loc=p["min"], scale=p["max"] - p["min"]))
64 +
65 +# ----------------------------------------------------------------------------- corrélations
66 +
67 +def test_cholesky_et_copule(mc):
68 + L = m.cholesky_2x2(0.005, 0.0064, 0.78); S = L @ L.T
69 + assert S[0, 0] == pytest.approx(0.005**2) and S[1, 1] == pytest.approx(0.0064**2) and S[0, 1] == pytest.approx(0.78 * 0.005 * 0.0064)
70 + assert np.allclose(L, np.linalg.cholesky(S))
71 + c = m.correlation_r_RT(mc["tirages"])
72 + assert c["rho_theorique"] == pytest.approx(0.005 / math.sqrt(0.005**2 + 0.004**2), abs=1e-12) and c["rho_empirique"] == pytest.approx(c["rho_theorique"], abs=0.01)
73 + assert c["spearman_empirique"] == pytest.approx(c["spearman_theorique"], abs=0.01) and c["part_plancher"] < 0.02
74 + assert m.spearman_depuis_pearson_gaussien(0.0) == 0.0 and m.spearman_depuis_pearson_gaussien(1.0) == pytest.approx(1.0)
75 + # copule gaussienne : marginales exactes et corrélation de rang ≈ (6/pi) arcsin(rho/2)
76 + corr = np.array([[1.0, 0.78], [0.78, 1.0]]); X = m.copule_gaussienne(50_000, 3, corr, [("normale", {"mu": 0.0725, "sigma": 0.005}), ("lognormale", {"mediane": 0.039, "sigma_log": 0.30})])
77 + assert X[:, 0].mean() == pytest.approx(0.0725, abs=1e-4) and np.median(X[:, 1]) == pytest.approx(0.039, abs=5e-4)
78 + assert stats.spearmanr(X[:, 0], X[:, 1])[0] == pytest.approx(m.spearman_depuis_pearson_gaussien(0.78), abs=0.01)
79 +
80 +# ----------------------------------------------------------------------------- erreur de Monte-Carlo, convergence, hypercube latin
81 +
82 +def test_erreur_monte_carlo(r, mc):
83 + e = m.erreurs_theoriques(mc).set_index("statistique")
84 + assert e.loc["moyenne", "se_theorique"] == pytest.approx(mc["stats"]["ecart_type"] / 100, rel=1e-9)
85 + assert e.loc["mediane", "se_theorique"] == pytest.approx(4135, abs=30) and e.loc["q95", "se_theorique"] == pytest.approx(10621, abs=80)
86 + assert e.loc["P(RCD1<1,20)", "se_theorique"] == pytest.approx(math.sqrt(0.4936 * 0.5064 / 10_000), abs=1e-5)
87 + rep = m.repetitions(r, 10_000, 30, graine0=1)
88 + assert rep["mediane"].std(ddof=1) == pytest.approx(e.loc["mediane", "se_theorique"], rel=0.35) # accord théorie/empirique (30 répétitions)
89 + assert abs(rep["p_rcd1_120"].mean() - 0.5) < 0.004 # valeur exacte 0,5
90 + assert m.n_requis(413_451, 5000) == 26_267 and m.n_requis(0.5, 0.01) == 9_604 and m.n_requis(0.5, 0.005) == 38_415
91 +
92 +def test_convergence_et_pente(r):
93 + c = m.convergence(r, N_max=20_000, points=(100, 1000, 10_000, 20_000))
94 + assert len(c) == 4 and c["mediane"].iloc[-1] == pytest.approx(2_204_000, abs=15_000)
95 + s = m.se_empirique_selon_N(r, Ns=(250, 1000, 4000), k=12)
96 + assert -0.75 < s.attrs["pente_log_mediane"] < -0.3
97 +
98 +def test_hypercube_latin(r):
99 + rng = np.random.default_rng(0); U = m.hypercube_latin(500, 3, rng)
100 + for k in range(3): # exactement un point par strate
101 + assert np.array_equal(np.sort(np.floor(U[:, k] * 500).astype(int)), np.arange(500))
102 + x = m.tirer_lhs(2000, 1, 3)
103 + assert set(x) == set(m.tirer(2000, 1, 3)) and x["facteur_capex"].shape == (2000, 3)
104 + assert x["inoccupation_pertes"].mean() == pytest.approx(0.039 * math.exp(0.045), rel=0.01) and x["taux_actualisation"].mean() == pytest.approx(0.0725, abs=2e-5)
105 + assert np.all(x["taux_terminal"] >= m.R_T_PLANCHER)
106 + cmp = m.comparer_lhs(r, n=500, k=20).set_index("statistique")
107 + assert cmp.loc["moyenne", "ratio_variance"] > 5 and cmp.loc["p_rcd1_120", "se_lhs"] == 0.0 and cmp.loc["p_rcd1_120", "moy_lhs"] == 0.5
108 +
109 +# ----------------------------------------------------------------------------- risque, covenants, stress
110 +
111 +def test_mesures_risque_et_rcd_analytique(r, mc):
112 + mr = m.mesures_risque(mc, r["dcf"]["valeur"]).set_index("alpha")
113 + assert mr.loc[0.05, "VaR"] == pytest.approx(r["dcf"]["valeur"] - mc["stats"]["q05"], abs=1e-6) and mr.loc[0.05, "CVaR"] >= mr.loc[0.05, "VaR"]
114 + assert mr.loc[0.05, "VaR"] == pytest.approx(473_372, abs=1) and mr.loc[0.05, "CVaR"] == pytest.approx(557_097, abs=1)
115 + assert mr.loc[0.10, "VaR"] == pytest.approx(385_254, abs=1) and mr.loc[0.10, "CVaR"] == pytest.approx(491_208, abs=1)
116 + assert m.cvar_normale(0, 1, 0.05) == pytest.approx(-stats.norm.pdf(stats.norm.ppf(0.05)) / 0.05)
117 + an = m.rcd1_analytique(r)
118 + assert an["rcd1_base"] == pytest.approx(1.20, abs=1e-9) and an["v_seuil_120"] == pytest.approx(0.039, abs=1e-9)
119 + assert an["P_rcd1_sous_120_exacte"] == pytest.approx(0.5, abs=1e-12) and an["v_seuil_100"] == pytest.approx(0.1333, abs=1e-4) and an["P_rcd1_sous_100_exacte"] < 1e-4
120 + # la fonction affine reproduit le RCD1 simulé pour chaque tirage
121 + v = mc["tirages"]["inoccupation_pertes"]; b, hyp, f = r["base"], r["hyp"], r["financement"]
122 + de_hg = b["depenses_hors_reserve"] - 0.04 * b["rbe"]
123 + rcd_affine = (0.96 * (b["rbp_contrat"] * (1 - v) + hyp["autres_revenus_annuels"]) - de_hg - b["reserve"]) / f["service_dette"]
124 + assert np.allclose(rcd_affine, mc["rcd"][:, 0], rtol=1e-10)
125 +
126 +def test_stress_et_inverse(r, mc):
127 + st = m.stress_deterministes(r, mc)
128 + assert st["valeur"].iloc[0] == pytest.approx(r["dcf"]["valeur"], rel=1e-12) and st["P_V_inferieure"].is_monotonic_decreasing or True
129 + assert st.loc[st["scenario"].str.startswith("Hausse"), "valeur"].iloc[0] == pytest.approx(1_947_889, abs=1)
130 + assert st["P_V_inferieure"].iloc[-1] < 0.01 and st["P_V_inferieure"].iloc[1] == pytest.approx(0.2072, abs=1e-4)
131 + inv = m.stress_inverse(r)
132 + assert m.valeur_deterministe(r, taux_actualisation=inv["r_valeur_2M"], taux_terminal=inv["r_valeur_2M"] - 0.0225) == pytest.approx(2_000_000, abs=1)
133 + assert m.valeur_deterministe(r, taux_terminal=inv["RT_valeur_cap"]) == pytest.approx(r["valeur_capitalisation"], abs=1)
134 + assert inv["v_rcd1_egal_1"] == pytest.approx(0.1333, abs=1e-4)
135 +
136 +# ----------------------------------------------------------------------------- sensibilité globale
137 +
138 +def test_sobol_et_rangs(r, mc):
139 + so = m.sobol(r, n=5000).set_index("intrant")
140 + assert 0.9 < so["S1"].sum() < 1.05 and so["S1"].idxmax() == "taux_actualisation"
141 + assert so.loc["taux_actualisation", "S1"] == pytest.approx(0.528, abs=0.03) and so.loc["croissance_loyers_marche", "S1"] == pytest.approx(0.248, abs=0.03)
142 + assert (so["ST"] >= so["S1"] - 0.02).all()
143 + rg = m.correlations_rang(mc).set_index("intrant")
144 + assert rg.loc["taux_terminal", "spearman"] == pytest.approx(-0.78, abs=0.01) and rg.loc["taux_actualisation", "spearman"] == pytest.approx(-0.71, abs=0.01)
145 + assert rg.loc["croissance_loyers_marche", "spearman"] == pytest.approx(0.49, abs=0.01)
146 + reg = m.regression_standardisee(mc); assert reg["R2"] == pytest.approx(0.955, abs=0.01)
147 +
148 +# ----------------------------------------------------------------------------- portefeuille (cas 4)
149 +
150 +def test_portefeuille_structure_de_correlation():
151 + d = cas4.charger(); pf = d["portefeuille"]; cr = cas4.croissance_rne(d["series"])
152 + s = m.simuler_portefeuille(pf, 4000, 1, croissance=cr)
153 + assert s["valeurs"].shape == (4000, 48) and s["stats"]["nb_renouvellements_stress"] == int((pf["echeance_dette"] <= 2028).sum()) == 23
154 + assert s["stats"]["nb_rcd_det_sous_seuil"] == 5 and s["stats"]["nb_rcd_det_sous_1"] == 1
155 + # corrélation empirique des chocs : rho_rmr intra-RMR, rho_prov inter-RMR (sur les log-valeurs, à sigma_g près)
156 + V = np.log(s["valeurs"]); c = np.corrcoef(V.T); rmr = pf["rmr"].values
157 + intra = [c[i, j] for i in range(48) for j in range(i) if rmr[i] == rmr[j]]; inter = [c[i, j] for i in range(48) for j in range(i) if rmr[i] != rmr[j]]
158 + assert np.mean(intra) == pytest.approx(0.60, abs=0.05) and np.mean(inter) == pytest.approx(0.30, abs=0.05)
159 + ind = m.simuler_portefeuille(pf, 4000, 1, rho_rmr=0.0, rho_prov=0.0, croissance=cr)["stats"]; par = m.simuler_portefeuille(pf, 4000, 1, rho_rmr=1.0, rho_prov=1.0, croissance=cr)["stats"]
160 + assert ind["ecart_type"] < s["stats"]["ecart_type"] < par["ecart_type"] and ind["ratio_diversification"] < 0.3 and par["ratio_diversification"] > 0.95
161 + # l'espérance du nombre de bris ne dépend pas de la corrélation (±1 immeuble d'erreur MC)
162 + assert abs(ind["nb_rcd_sous_seuil_moyen"] - par["nb_rcd_sous_seuil_moyen"]) < 0.5
163 + # variance d'une somme à corrélation commune
164 + sig = np.array([1.0, 2.0, 3.0]); assert m.variance_somme_correlee(sig, 1.0) == pytest.approx(6.0) and m.variance_somme_correlee(sig, 0.0) == pytest.approx(math.sqrt(14))
165 + with pytest.raises(AssertionError): m.simuler_portefeuille(pf, 10, 1, rho_rmr=0.2, rho_prov=0.5)
166 +
167 +def test_portefeuille_valeurs_citees():
168 + d = cas4.charger(); cr = cas4.croissance_rne(d["series"])
169 + s = m.simuler_portefeuille(d["portefeuille"], m.N_MC, m.GRAINE, croissance=cr)["stats"]
170 + assert s["valeur_2026"] == 143_363_000 and s["valeur_deterministe"] == pytest.approx(147_935_699, abs=1)
171 + assert s["ecart_type"] == pytest.approx(10_295_448, abs=1) and s["VaR05"] == pytest.approx(14_063_788, abs=1) and s["CVaR05"] == pytest.approx(17_388_940, abs=1)
172 + assert s["ratio_diversification"] == pytest.approx(0.643, abs=1e-3) and s["nb_rcd_sous_seuil_moyen"] == pytest.approx(10.19, abs=0.01)
173 +
174 +# ----------------------------------------------------------------------------- exercices
175 +
176 +def test_exercices(r, mc):
177 + ex = m.exercices(r, mc)
178 + assert ex["ex1"]["mediane"] == pytest.approx(0.03888, abs=1e-5) and ex["ex1"]["P_sup_5pct"] == pytest.approx(0.1957, abs=1e-4)
179 + assert ex["ex2"]["pert"]["alpha"] == pytest.approx(2.647, abs=1e-3) and ex["ex2"]["P_tri_sup_20"] == pytest.approx(0.147, abs=1e-3) and ex["ex2"]["P_pert_sup_20"] == pytest.approx(0.092, abs=1e-3)
180 + assert ex["ex3"]["N_mediane_5000"] == 26_267 and ex["ex3"]["N_proba_1pt"] == 9_604
181 + assert ex["ex4"]["r_tire"] == pytest.approx(0.0785, abs=1e-6) and ex["ex4"]["RT_tire"] == pytest.approx(0.053988, abs=1e-5)
182 + assert ex["ex5"]["VaR"] == pytest.approx(385_254, abs=1) and ex["ex5"]["cvar_normale"] == pytest.approx(1_618_485, abs=1)
183 + assert ex["ex6"]["mediane"] == pytest.approx(2_184_377, abs=1) and ex["ex6"]["ecart_type"] == pytest.approx(546_896, abs=1) and ex["ex6"]["P_V_2M"] == pytest.approx(0.3558, abs=1e-4)
modified analysis/python/tests/test_ch42.py +16 −0
@@ -99,6 +99,22 @@ class TestLocative:
99 99 assert cl["noi"] == pytest.approx(cl["rbe"] - cl["depenses"])
100 100
101 101
102 +class TestFinancement:
103 + def test_divise_vs_indivise(self, res):
104 + fa, fi = res["fin_divise"], res["fin_indivise"]
105 + assert fa["mise_de_fonds"] == 19000 and fa["pret"] == 361000
106 + assert fa["paiement"] == pytest.approx(1922, abs=1) and fa["cout_mensuel"] == pytest.approx(3049, abs=1)
107 + assert fa["tam"] == pytest.approx(0.0612) and fa["paiement_tam"] == pytest.approx(2336, abs=1)
108 + assert fi["mise_de_fonds"] == 76000 and fi["pret"] == 304000 and fi["paiement"] == pytest.approx(1619, abs=1)
109 + assert fi["mise_de_fonds"] - fa["mise_de_fonds"] == 57000
110 + # convention canadienne : composition semestrielle (traite.finance.Hypotheque)
111 + h = fin.Hypotheque(361000, 0.0412, 300)
112 + assert fa["paiement"] == pytest.approx(h.paiement) and h.solde(300) == pytest.approx(0.0, abs=1e-6)
113 + # droits de mutation 2026 pour 380 000 $ (cités dans le texte)
114 + dm = 0.005 * 62900 + 0.01 * (315000 - 62900) + 0.015 * (380000 - 315000)
115 + assert dm == pytest.approx(3810.5, abs=0.01)
116 +
117 +
102 118 class TestConversion:
103 119 def test_bloc(self, res):
104 120 b = res["faisabilite"]["bloc"]
modified assets/figures/ch36/ch36-fig-bacon.pdf +0 −0

Binary file not shown.

modified assets/figures/ch46/ch46-fig-cascade.pdf +0 −0

Binary file not shown.

modified assets/figures/ch46/ch46-fig-fourchettes.pdf +0 −0

Binary file not shown.

modified assets/figures/ch46/ch46-fig-loyers.pdf +0 −0

Binary file not shown.

modified assets/figures/ch46/ch46-fig-mrb.pdf +0 −0

Binary file not shown.

modified assets/figures/ch46/ch46-fig-occupation.pdf +0 −0

Binary file not shown.

modified data/synthetic/README.md +3 −0
@@ -70,3 +70,6 @@ Les relations entre variables (élasticités, primes de secteur, décote d'ancie
70 70
71 71 ## Chapitre 25 — ventes de terrains vacants « Gatineau » — ajouté le 2026-10-31
72 72 `ch25_terrains_ventes.csv` (200 ventes de terrains vacants résidentiels, 2023-01 → 2026-08, cinq secteurs : Hull 70, Gatineau 60, Aylmer 40, Buckingham 15, Masson-Angers 15 ; 156 desservis) est généré par `analysis/python/cases/ch25_terrain.py` (`generer_terrains`, graine 20261031). Variables : superficie (m²), façade (m), desservi (aqueduc et égout, 0/1), topographie (plat/pente/roc), forme (régulier/irrégulier), logements constructibles (1 à 3), coordonnées fictives, prix. Loi : ln P = ln(225 000) + 0,50 ln(T/560) + 0,08 ln(F/18) − 0,30·(non desservi) − 0,06·pente − 0,12·roc − 0,05·irrégulier + 0,12·(logements − 1) + prime de secteur (log du rapport des médianes APCIQ T3 2026 à celle de Hull, comme le cas 1) + 0,0036·mois depuis 2024-07-01 + ε, σ = 0,10 ; prix arrondis à 500 $. Le prix de référence de 225 000 $ (terrain de 560 m², desservi, plat, régulier, Hull, juillet 2024) est une **hypothèse pédagogique** choisie pour la cohérence interne avec l'extraction terrain/bâtiment du cas 1 (part du terrain ≈ 45-50 % dans les ventes synthétiques de Hull) ; **aucune vente réelle de terrain à Gatineau n'a servi à la calibration** (la grille de terrains de l'auteur n'est pas dans ce dépôt). Les élasticités et décotes sont des hypothèses vraisemblables, non des estimations.
73 +
74 +## Chapitre 43 — « Rivière-Fictive » (zones inondables) — ajouté le 2026-10-31
75 +`ch43_riverain_lots.csv` (625 lots d'une grille 25 × 25 de 40 m ; rivière au sud, lac non inondable à l'est ; classes d'exposition par rangée : très élevée 25, élevée 50, modérée 50, faible 50, hors zone 450 ; `rive` = 1 à moins de 120 m de la rivière ou dans les trois colonnes du lac, 141 lots dont 54 hors zone) et `ch43_riverain_ventes.csv` (1 014 ventes 2016-01 → 2026-08, Poisson(1,6) par lot) sont générés par `analysis/python/cases/ch43_risques.py` (graine 20261031). Loi : ln P = ln 390 000 + 0,55 ln(S/140) + 0,10 ln(T/560) − 0,005·âge + 0,06(qualité − 3) + 0,035·garage + 0,04·rive + décote(classe) × (1 + 0,5·après) + ln(1,035)·t + ε, σ = 0,07 ; décotes avant l'inondation du 2019-05-01 : −0,10 / −0,07 / −0,04 / −0,015 (très élevée / élevée / modérée / faible), stigmate = ×1,5 après. Hypothèses pédagogiques situées dans les fourchettes de la littérature citée au ch. 43 (Bin–Polasky 2004, Bernstein et al. 2019, Ortega–Taspinar 2018) ; aucun marché réel n'est décrit. Le même script injecte, en mémoire seulement (sans modifier `cas4_portefeuille_series.csv`), un choc d'inondation sur les trois immeubles `zone_inondable == 1` du cas 4 : −0,25 / −0,12 / −0,06 (2019T2–T4) puis −0,05 en ln RNE dès 2020T1.
modified manuscript/chapters/ch36.tex +41 −38
@@ -187,9 +187,11 @@ La solution proposée par \textcite{callawaySantanna2021} consiste à ne jamais
187 187 \end{equation}
188 188 Chaque $\widehat{\mathrm{ATT}}(g, t)$ est un estimateur $2 \times 2$ propre : il compare une cohorte, entre sa dernière période non traitée et la période $t$, à des unités qui ne sont traitées à aucune de ces deux dates. Pour $t < g$, la même formule fournit des placebos. L'hypothèse requise est celle des tendances parallèles entre chaque cohorte et ses témoins, et elle peut être restreinte aux témoins d'une même strate, par exemple d'une même RMR, lorsque les tendances régionales diffèrent ; c'est ce que fait l'exemple intégral. Les agrégations usuelles sont l'effet par cohorte, l'effet dynamique par temps relatif et l'effet global :
189 189 \begin{equation}\label{eq:36:cs-agreg}
190 −\widehat{\mathrm{ATT}}_g = \frac{1}{T - g + 1}\sum_{t \geq g}\widehat{\mathrm{ATT}}(g, t), \qquad
191 −\widehat{\mathrm{ATT}}(k) = \sum_{g} w_{g,k}\,\widehat{\mathrm{ATT}}(g, g + k), \qquad
192 −\widehat{\mathrm{ATT}} = \sum_g \frac{n_g}{\sum_{g'} n_{g'}}\,\widehat{\mathrm{ATT}}_g ,
190 +\begin{aligned}
191 +\widehat{\mathrm{ATT}}_g &= \frac{1}{T - g + 1}\sum_{t \geq g}\widehat{\mathrm{ATT}}(g, t), \qquad
192 +\widehat{\mathrm{ATT}}(k) = \sum_{g} w_{g,k}\,\widehat{\mathrm{ATT}}(g, g + k), \\
193 +\widehat{\mathrm{ATT}} &= \sum_g \frac{n_g}{\sum_{g'} n_{g'}}\,\widehat{\mathrm{ATT}}_g ,
194 +\end{aligned}
193 195 \end{equation}
194 196 où $w_{g,k}$ est la part de la cohorte $g$ parmi les cohortes observées $k$ périodes après leur traitement. Chaque agrégation est une moyenne à poids positifs et connus d'effets bien définis, ce qui est précisément ce que le TWFE statique ne garantit pas. L'inférence repose sur un bootstrap par blocs d'unités, qui respecte l'autocorrélation intra-immeuble ; la mise en œuvre de la bibliothèque (\texttt{traite.panel.callaway\_santanna}) rééchantillonne séparément chaque cohorte et chaque strate de témoins afin de conserver le plan. L'estimateur de \textcite{sunAbraham2021} poursuit le même but dans le cadre d'une régression, en interagissant les indicatrices de temps relatif avec les indicatrices de cohorte puis en agrégeant ; celui de \textcite{dechaisemartinDhaultfoeuille2020} généralise aux traitements qui s'allument et s'éteignent. Dans un plan simple comme celui de ce chapitre, deux cohortes et un groupe jamais traité, les trois approches donnent des résultats très voisins ; la version par cohortes a l'avantage de la transparence, chaque chiffre se recalculant à la main comme une différence de deux moyennes.
195 197
@@ -223,32 +225,34 @@ Les séries trimestrielles de revenu net d'exploitation des 48 immeubles du cas
223 225
224 226 \begin{figure}[htbp]
225 227 \centering
226 −\begin{tikzpicture}[x=2.9mm, y=1cm, every node/.style={font=\sffamily\footnotesize}]
228 +\resizebox{\textwidth}{!}{%
229 +\begin{tikzpicture}[x=2.6mm, y=1cm, every node/.style={font=\sffamily\footnotesize}]
227 230 % axe du temps : t = 0 (2016T1) … 41 (2026T2)
228 231 \draw[->, thick, brandGray] (0,0) -- (43,0) node[right, text=brandGray] {trimestre};
229 232 \foreach \t/\l in {0/2016T1, 8/2018T1, 16/2020T1, 24/2022T1, 32/2024T1, 40/2026T1} {\draw[brandGray] (\t,0.08) -- (\t,-0.08) node[below, text=brandGray] {\l};}
230 233 % bandes de traitement
231 234 \fill[brandPrimary!25] (26,0.55) rectangle (42,1.05); \draw[brandPrimary, thick] (26,0.55) -- (26,1.05);
232 −\node[left] at (0,0.8) {Cohorte A (8 immeubles, Montréal)};
235 +\node[left] at (-0.5,0.8) {Cohorte A (8 immeubles, Montréal)};
233 236 \node at (34,0.8) {traitée dès 2022T3 ($k = 0$ à $15$)};
234 237 \fill[brandSecondary!35] (34,1.25) rectangle (42,1.75); \draw[brandSecondary!80!black, thick] (34,1.25) -- (34,1.75);
235 −\node[left] at (0,1.5) {Cohorte B (5 immeubles, Gatineau)};
238 +\node[left] at (-0.5,1.5) {Cohorte B (5 immeubles, Gatineau)};
236 239 \node at (38,1.5) {dès 2024T3};
237 −\node[left] at (0,2.2) {Jamais traités (35 immeubles, 6 RMR)};
240 +\node[left] at (-0.5,2.2) {Jamais traités (35 immeubles, 6 RMR)};
238 241 \draw[brandGray, dashed] (0,2.2) -- (42,2.2);
239 242 % fenêtres du 2 x 2
240 −\draw[decorate, decoration={brace, amplitude=3pt}, brandDark] (22,-0.75) -- (25.9,-0.75) node[midway, below=3pt, text=brandDark] {avant (2021T3--2022T2)};
241 −\draw[decorate, decoration={brace, amplitude=3pt}, brandDark] (30,-0.75) -- (33.9,-0.75) node[midway, below=3pt, text=brandDark] {après (2023T3--2024T2)};
243 +\draw[decorate, decoration={brace, amplitude=3pt}, brandDark] (22,-0.95) -- (25.9,-0.95) node[midway, below=3pt, text=brandDark, xshift=-6mm] {avant (2021T3--2022T2)};
244 +\draw[decorate, decoration={brace, amplitude=3pt}, brandDark] (30,-0.95) -- (33.9,-0.95) node[midway, below=3pt, text=brandDark, xshift=6mm] {après (2023T3--2024T2)};
242 245 \draw[decorate, decoration={brace, amplitude=3pt, mirror}, brandGray] (10,0.3) -- (17.9,0.3) node[midway, above=3pt, text=brandGray] {placebo (2019T3)};
243 −\end{tikzpicture}
246 +\end{tikzpicture}}
244 247 \caption{Chronologie du plan simulé : deux cohortes traitées à des dates différentes, un groupe jamais traité, et les fenêtres retenues pour l'estimateur $2 \times 2$ (deuxième année après la mise en service, lorsque le régime déclaré est atteint) et pour le placebo temporel.}\label{fig:36:chronologie}
245 248 \figsource{Schéma de l'auteur ; plan décrit dans \texttt{analysis/python/cases/ch36\_panel.py}.}
246 249 \end{figure}
247 250
248 251 \begin{exemple}{Cinq estimateurs confrontés à un effet connu}{choc-cas4}
249 −L'exemple procède en huit étapes : description du plan et des trajectoires ; doubles différences $2 \times 2$ et placebo ; effets fixes bidirectionnels et erreurs-types ; étude d'événement ; décomposition de Goodman-Bacon ; estimateur de Callaway et Sant'Anna ; contrôle synthétique ; discontinuité spatiale sur une simulation annexe. Le \cref{tab:36:bilan} rassemble les résultats.
252 +L'exemple procède en huit étapes : description du plan et des trajectoires ; doubles différences $2 \times 2$ et placebo ; effets fixes bidirectionnels et erreurs-types ; étude d'événement ; décomposition de Goodman-Bacon ; estimateur de Callaway et Sant'Anna ; contrôle synthétique ; discontinuité spatiale sur une simulation annexe. Chaque estimateur vise une cible précise (effet moyen global, effet d'une cohorte, effet sur une fenêtre, effet dynamique) et doit être comparé à la vérité correspondante, non à un effet « en général » ; le \cref{tab:36:bilan} rassemble les résultats et leurs cibles.
253 +\end{exemple}
250 254
251 −\paragraph{Étape 1 : le plan et la vérité.} La \cref{fig:36:chronologie} résume le plan. L'effet moyen vrai sur les observations traitées, c'est-à-dire l'ATT de l'\cref{eq:36:att} pris sur l'ensemble des trimestres post-traitement des deux cohortes, vaut $0{,}0454$ en logarithme : $0{,}0484$ pour la cohorte A, qui compte seize trimestres de traitement et atteint $\delta(15) = 0{,}07$ au dernier, et $0{,}0356$ pour la cohorte B, qui n'en compte que huit. Sur la fenêtre « après » de l'estimateur $2 \times 2$, la deuxième année suivant la mise en service de la cohorte A ($k = 4$ à $7$), l'effet vrai moyen est de $0{,}0462$. La \cref{fig:36:trajectoires} montre les trajectoires moyennes : le logarithme du revenu net des immeubles traités de Montréal se détache de celui des neuf immeubles témoins de la même RMR après 2022T3, et la courbe en pointillé, qui retranche l'effet déclaré, indique ce que les traités auraient connu sans traitement ; le lecteur voit que ce contrefactuel n'est pas exactement parallèle aux témoins, parce que les marches aléatoires de huit immeubles et de neuf autres ne s'annulent pas, et c'est tout le problème.
255 +\paragraph{Étape 1 : le plan et la vérité} La \cref{fig:36:chronologie} résume le plan. L'effet moyen vrai sur les observations traitées, c'est-à-dire l'ATT de l'\cref{eq:36:att} pris sur l'ensemble des trimestres post-traitement des deux cohortes, vaut $0{,}0454$ en logarithme : $0{,}0484$ pour la cohorte A, qui compte seize trimestres de traitement et atteint $\delta(15) = 0{,}07$ au dernier, et $0{,}0356$ pour la cohorte B, qui n'en compte que huit. Sur la fenêtre « après » de l'estimateur $2 \times 2$, la deuxième année suivant la mise en service de la cohorte A ($k = 4$ à $7$), l'effet vrai moyen est de $0{,}0462$. La \cref{fig:36:trajectoires} montre les trajectoires moyennes : le logarithme du revenu net des immeubles traités de Montréal se détache de celui des neuf immeubles témoins de la même RMR après 2022T3, et la courbe en pointillé, qui retranche l'effet déclaré, indique ce que les traités auraient connu sans traitement ; le lecteur voit que ce contrefactuel n'est pas exactement parallèle aux témoins, parce que les marches aléatoires de huit immeubles et de neuf autres ne s'annulent pas, et c'est tout le problème.
252 256
253 257 \begin{figure}[htbp]
254 258 \centering
@@ -257,12 +261,12 @@ L'exemple procède en huit étapes : description du plan et des trajectoires ; d
257 261 \figsource{\synth{script \texttt{analysis/python/figures/ch36\_figures.py}, graine 20261031}}
258 262 \end{figure}
259 263
260 −\paragraph{Étape 2 : doubles différences $2 \times 2$ et placebo.} Le \cref{tab:36:did} donne les quatre moyennes de cellule de l'\cref{eq:8:did} pour la cohorte A, avec pour témoins les neuf immeubles jamais traités de Montréal. Le logarithme moyen du revenu net des traités passe de $10{,}1379$ à $10{,}2450$ entre les deux fenêtres, soit $+0{,}1071$ ; celui des témoins de $10{,}3696$ à $10{,}4234$, soit $+0{,}0538$. La double différence vaut $0{,}0534$, à comparer au vrai $0{,}0462$ : l'erreur, $0{,}0072$, est bien inférieure à l'erreur-type de $0{,}0201$ obtenue par la régression des différences longues de l'\cref{eq:36:did-twfe} (dix-sept observations, HC1). En pourcentage, l'estimation est de $5{,}5$ \% pour un effet vrai de $4{,}7$ \%. Avec les 35 témoins de toutes les RMR, l'estimateur tombe à $0{,}0468$, plus proche encore de la vérité, mais pour une mauvaise raison que la \cref{sec:36:sensibilite} démêle : les tendances régionales des autres RMR ne sont pas celles de Montréal, et deux biais de signe opposé se compensent partiellement. Le placebo temporel, qui place une fausse mise en service en 2019T3 et compare 2018T3--2019T2 à 2019T3--2020T2, donne $0{,}0155$ avec une erreur-type de $0{,}0101$ : non significatif ($t = 1{,}5$), mais pas négligeable, et le signe positif annonce ce que l'étude d'événement va préciser.
264 +\paragraph{Étape 2 : doubles différences $2 \times 2$ et placebo} Le \cref{tab:36:did} donne les quatre moyennes de cellule de l'\cref{eq:8:did} pour la cohorte A, avec pour témoins les neuf immeubles jamais traités de Montréal. Le logarithme moyen du revenu net des traités passe de $10{,}1379$ à $10{,}2450$ entre les deux fenêtres, soit $+0{,}1071$ ; celui des témoins de $10{,}3696$ à $10{,}4234$, soit $+0{,}0538$. La double différence vaut $0{,}0534$, à comparer au vrai $0{,}0462$ : l'erreur, $0{,}0072$, est bien inférieure à l'erreur-type de $0{,}0201$ obtenue par la régression des différences longues de l'\cref{eq:36:did-twfe} (dix-sept observations, HC1). En pourcentage, l'estimation est de $5{,}5$ \% pour un effet vrai de $4{,}7$ \%. Avec les 35 témoins de toutes les RMR, l'estimateur tombe à $0{,}0468$, plus proche encore de la vérité, mais pour une mauvaise raison que la \cref{sec:36:sensibilite} démêle : les tendances régionales des autres RMR ne sont pas celles de Montréal, et deux biais de signe opposé se compensent partiellement. Le placebo temporel, qui place une fausse mise en service en 2019T3 et compare 2018T3--2019T2 à 2019T3--2020T2, donne $0{,}0155$ avec une erreur-type de $0{,}0101$ : non significatif ($t = 1{,}5$), mais pas négligeable, et le signe positif annonce ce que l'étude d'événement va préciser.
261 265
262 266 \begin{table}[htbp]
263 267 \caption{Doubles différences $2 \times 2$ pour la cohorte A : moyennes de cellule du logarithme du revenu net d'exploitation (données synthétiques)}\label{tab:36:did}
264 268 \centering
265 −\small
269 +\footnotesize\setlength{\tabcolsep}{4pt}
266 270 \begin{tabular}{lS[table-format=2.4]S[table-format=2.4]S[table-format=1.4]}
267 271 \toprule
268 272 Groupe & {Avant (2021T3--2022T2)} & {Après (2023T3--2024T2)} & {Différence} \\
@@ -281,12 +285,12 @@ Effet vrai moyen sur la fenêtre & & & 0.0462 \\
281 285 \tabsource{\synth{\texttt{analysis/python/cases/ch36\_panel.py}, fonctions \texttt{did\_deux\_periodes} et \texttt{placebo\_temporel} ; \texttt{traite.panel.did\_2x2}}.}
282 286 \end{table}
283 287
284 −\paragraph{Étape 3 : effets fixes bidirectionnels et erreurs-types.} Le \cref{tab:36:twfe} estime l'\cref{eq:36:did-twfe} sur les 2 016 observations avec l'indicatrice $D_{it}$ des deux cohortes. Avec des effets de période communs à toutes les RMR, le coefficient vaut $0{,}0905$, le double de l'effet vrai de $0{,}0454$ ; la transformation within bidirectionnelle de l'\cref{eq:36:within} donne exactement le même chiffre, ce qui vérifie la \cref{prop:36-within}. L'écart n'est pas un accident d'échantillonnage : les deux cohortes sont dans les RMR dont le générateur fait croître les revenus le plus vite, Gatineau et Montréal, et un effet de période commun attribue à $D_{it}$ la différence entre leur tendance et la tendance moyenne des six RMR, accumulée sur dix ans. Remplacer les effets de période par des effets RMR $\times$ trimestre, 246 indicatrices qui laissent chaque région suivre sa propre trajectoire, ramène le coefficient à $0{,}0702$. Il reste supérieur à la vérité de $0{,}025$, mais l'erreur-type en grappes est de $0{,}0264$ : l'écart est de l'ordre d'un écart-type, et l'étude d'événement en donnera la raison. Les trois colonnes d'erreurs-types sont la leçon la plus importante du tableau. L'erreur-type classique, $0{,}0054$, et l'erreur-type HC1, $0{,}0064$, supposent 2 016 observations indépendantes ; l'erreur-type en grappes par immeuble, $0{,}0264$, est $4{,}9$ fois plus grande, un rapport conforme à l'\cref{eq:36:moulton} pour $m = 42$ et une corrélation intra-immeuble de l'ordre de $0{,}5$. Quiconque aurait rapporté le premier chiffre aurait annoncé un effet mesuré à $\pm 1$ \% près alors que l'incertitude réelle est de $\pm 5$ \%.
288 +\paragraph{Étape 3 : effets fixes bidirectionnels et erreurs-types} Le \cref{tab:36:twfe} estime l'\cref{eq:36:did-twfe} sur les 2 016 observations avec l'indicatrice $D_{it}$ des deux cohortes. Avec des effets de période communs à toutes les RMR, le coefficient vaut $0{,}0905$, le double de l'effet vrai de $0{,}0454$ ; la transformation within bidirectionnelle de l'\cref{eq:36:within} donne exactement le même chiffre, ce qui vérifie la \cref{prop:36-within}. L'écart n'est pas un accident d'échantillonnage : les deux cohortes sont dans les RMR dont le générateur fait croître les revenus le plus vite, Gatineau et Montréal, et un effet de période commun attribue à $D_{it}$ la différence entre leur tendance et la tendance moyenne des six RMR, accumulée sur dix ans. Remplacer les effets de période par des effets RMR $\times$ trimestre, 246 indicatrices qui laissent chaque région suivre sa propre trajectoire, ramène le coefficient à $0{,}0702$. Il reste supérieur à la vérité de $0{,}025$, mais l'erreur-type en grappes est de $0{,}0264$ : l'écart est de l'ordre d'un écart-type, et l'étude d'événement en donnera la raison. Les trois colonnes d'erreurs-types sont la leçon la plus importante du tableau. L'erreur-type classique, $0{,}0054$, et l'erreur-type HC1, $0{,}0064$, supposent 2 016 observations indépendantes ; l'erreur-type en grappes par immeuble, $0{,}0264$, est $4{,}9$ fois plus grande, un rapport conforme à l'\cref{eq:36:moulton} pour $m = 42$ et une corrélation intra-immeuble de l'ordre de $0{,}5$. Quiconque aurait rapporté le premier chiffre aurait annoncé un effet mesuré à $\pm 1$ \% près alors que l'incertitude réelle est de $\pm 5$ \%.
285 289
286 290 \begin{table}[htbp]
287 291 \caption{Estimateur à effets fixes bidirectionnels de l'effet statique du traitement, selon les effets de période et le calcul des erreurs-types (données synthétiques ; effet vrai moyen $0{,}0454$)}\label{tab:36:twfe}
288 292 \centering
289 −\small
293 +\footnotesize\setlength{\tabcolsep}{4pt}
290 294 \begin{tabular}{lS[table-format=3.0]S[table-format=1.4]S[table-format=1.4]S[table-format=1.4]S[table-format=1.4]}
291 295 \toprule
292 296 Spécification & {Paramètres} & {$\hat\delta$} & {ET classique} & {ET HC1} & {ET grappes} \\
@@ -299,7 +303,7 @@ Effets d'immeuble + RMR $\times$ trimestre & 295 & 0.0702 & 0.0054 & 0.0064 & 0.
299 303 \tabsource{\synth{\texttt{analysis/python/cases/ch36\_panel.py}, fonction \texttt{twfe\_statique} ; \texttt{traite.panel.twfe} et \texttt{mco\_grappes} (CR1, 48 grappes)} ; contrôle croisé \texttt{linearmodels.PanelOLS} et \texttt{statsmodels} (coefficient identique, erreurs-types en grappes $0{,}0196$ et $0{,}0198$).}
300 304 \end{table}
301 305
302 −\paragraph{Étape 4 : étude d'événement.} La \cref{fig:36:eventstudy}(a) et le \cref{tab:36:event} présentent l'\cref{eq:36:event} avec effets RMR $\times$ trimestre, fenêtre de $-8$ à $+8$ trimestres (les deux extrémités regroupant les observations plus lointaines) et référence $k = -1$. Les coefficients post-traitement suivent le profil déclaré de près : $0{,}0088$ à $k = 0$ pour un vrai $0{,}01$, $0{,}0235$ pour $0{,}02$, $0{,}0312$ pour $0{,}03$, $0{,}0410$ pour $0{,}04$, puis $0{,}0354$, $0{,}0439$, $0{,}0476$ et $0{,}0561$ pour des vrais de $0{,}0425$ à $0{,}05$ ; l'indicatrice regroupée $k \geq 8$, qui ne concerne que la cohorte A, donne $0{,}0817$ pour une vraie moyenne de $0{,}0612$, avec une erreur-type de $0{,}027$ qui traduit l'accumulation des marches aléatoires. La montée en régime sur quatre trimestres, que l'estimateur statique ne pouvait pas voir, est parfaitement lisible. Les coefficients pré-traitement sont tous négatifs, entre $-0{,}007$ et $-0{,}021$, aucun n'est individuellement significatif, et ils n'ont pas de pente marquée : le profil est compatible avec des tendances parallèles, mais il révèle que le trimestre de référence, 2022T2 pour la cohorte A, est un point relativement haut des traités, puisque tous les trimestres antérieurs leur sont inférieurs d'environ $0{,}015$ une fois la tendance régionale retirée. C'est le hasard du tirage des marches aléatoires, et c'est lui qui explique que l'estimateur statique du \cref{tab:36:twfe}, qui compare la moyenne de \emph{tous} les trimestres post-traitement à celle de \emph{tous} les trimestres antérieurs, excède la vérité de $0{,}02$ environ : il hérite du creux pré-traitement. L'étude d'événement, qui mesure tout par rapport à $k = -1$, n'en souffre pas, mais elle en souffrirait si le hasard avait placé le creux en $k = -1$. La leçon est qu'il faut regarder les deux et comprendre d'où vient leur écart, non choisir celle qui arrange.
306 +\paragraph{Étape 4 : étude d'événement} La \cref{fig:36:eventstudy}(a) et le \cref{tab:36:event} présentent l'\cref{eq:36:event} avec effets RMR $\times$ trimestre, fenêtre de $-8$ à $+8$ trimestres (les deux extrémités regroupant les observations plus lointaines) et référence $k = -1$. Les coefficients post-traitement suivent le profil déclaré de près : $0{,}0088$ à $k = 0$ pour un vrai $0{,}01$, $0{,}0235$ pour $0{,}02$, $0{,}0312$ pour $0{,}03$, $0{,}0410$ pour $0{,}04$, puis $0{,}0354$, $0{,}0439$, $0{,}0476$ et $0{,}0561$ pour des vrais de $0{,}0425$ à $0{,}05$ ; l'indicatrice regroupée $k \geq 8$, qui ne concerne que la cohorte A, donne $0{,}0817$ pour une vraie moyenne de $0{,}0612$, avec une erreur-type de $0{,}027$ qui traduit l'accumulation des marches aléatoires. La montée en régime sur quatre trimestres, que l'estimateur statique ne pouvait pas voir, est parfaitement lisible. Les coefficients pré-traitement sont tous négatifs, entre $-0{,}007$ et $-0{,}021$, aucun n'est individuellement significatif, et ils n'ont pas de pente marquée : le profil est compatible avec des tendances parallèles, mais il révèle que le trimestre de référence, 2022T2 pour la cohorte A, est un point relativement haut des traités, puisque tous les trimestres antérieurs leur sont inférieurs d'environ $0{,}015$ une fois la tendance régionale retirée. C'est le hasard du tirage des marches aléatoires, et c'est lui qui explique que l'estimateur statique du \cref{tab:36:twfe}, qui compare la moyenne de \emph{tous} les trimestres post-traitement à celle de \emph{tous} les trimestres antérieurs, excède la vérité de $0{,}02$ environ : il hérite du creux pré-traitement. L'étude d'événement, qui mesure tout par rapport à $k = -1$, n'en souffre pas, mais elle en souffrirait si le hasard avait placé le creux en $k = -1$. La leçon est qu'il faut regarder les deux et comprendre d'où vient leur écart, non choisir celle qui arrange.
303 307
304 308 \begin{figure}[htbp]
305 309 \centering
@@ -311,7 +315,7 @@ Effets d'immeuble + RMR $\times$ trimestre & 295 & 0.0702 & 0.0054 & 0.0064 & 0.
311 315 \begin{table}[htbp]
312 316 \caption{Coefficients de l'étude d'événement (effets d'immeuble et RMR $\times$ trimestre) et effet déclaré, par trimestre écoulé depuis la mise en service (données synthétiques)}\label{tab:36:event}
313 317 \centering
314 −\small
318 +\footnotesize\setlength{\tabcolsep}{4pt}
315 319 \begin{tabular}{S[table-format=+1.0]S[table-format=+1.4]S[table-format=1.4]S[table-format=1.4]cS[table-format=+1.0]S[table-format=+1.4]S[table-format=1.4]S[table-format=1.4]}
316 320 \toprule
317 321 {$k$} & {$\hat\delta_k$} & {ET grappes} & {Vrai} & & {$k$} & {$\hat\delta_k$} & {ET grappes} & {Vrai} \\
@@ -330,19 +334,19 @@ Effets d'immeuble + RMR $\times$ trimestre & 295 & 0.0702 & 0.0054 & 0.0064 & 0.
330 334 \tabsource{\synth{\texttt{analysis/python/cases/ch36\_panel.py}, fonction \texttt{etude\_evenement} ; \texttt{traite.panel.event\_study}} ; la colonne « vrai » est la moyenne de $\delta(k)$ sur les observations regroupées dans chaque indicatrice.}
331 335 \end{table}
332 336
333 −\paragraph{Étape 5 : décomposition de Goodman-Bacon.} Le \cref{tab:36:bacon} décompose l'estimateur TWFE à effets de période communs, $0{,}0905$, selon l'\cref{eq:36:bacon}. Les quatre comparaisons $2 \times 2$ se recombinent exactement : $0{,}6658 \times 0{,}0844 + 0{,}2721 \times 0{,}1224 + 0{,}0476 \times 0{,}0080 + 0{,}0146 \times 0{,}0403 = 0{,}0905$, et les poids somment à un, ce que le test unitaire vérifie à $10^{-10}$. La lecture est instructive. Les deux comparaisons contre les jamais traités dominent (94 \% du poids) et sont toutes deux très au-dessus de la vérité, à $0{,}084$ et $0{,}122$, parce qu'elles comparent des immeubles de Montréal et de Gatineau à des témoins de six RMR sur toute la décennie : c'est le biais de tendance régionale déjà identifié. La comparaison « précoce contre tardive pas encore traitée » oppose Montréal à Gatineau avant 2024T3 et donne $0{,}008$, parce que Gatineau croît plus vite que Montréal dans le générateur et que cette différence de tendance s'oppose à l'effet. La comparaison interdite, « tardive contre précoce déjà traitée », donne $0{,}040$ pour une vérité de $0{,}036$ ; son poids est faible ($1{,}5$ \%) et son biais ici modeste, parce que l'effet de la cohorte A n'augmente que de $0{,}0025$ par trimestre pendant que la cohorte B est traitée. La \cref{fig:36:bacon}(a) place ces quatre points dans le plan poids--estimation. Sur le cas 4, la décomposition montre donc que le défaut du TWFE tient d'abord aux tendances régionales, non à l'échelonnement ; pour isoler ce dernier, le panneau (b) et le \cref{tab:36:labo} recourent à un laboratoire.
337 +\paragraph{Étape 5 : décomposition de Goodman-Bacon} Le \cref{tab:36:bacon} décompose l'estimateur TWFE à effets de période communs, $0{,}0905$, selon l'\cref{eq:36:bacon}. Les quatre comparaisons $2 \times 2$ se recombinent exactement : $0{,}6658 \times 0{,}0844 + 0{,}2721 \times 0{,}1224 + 0{,}0476 \times 0{,}0080 + 0{,}0146 \times 0{,}0403 = 0{,}0905$, et les poids somment à un, ce que le test unitaire vérifie à $10^{-10}$. La lecture est instructive. Les deux comparaisons contre les jamais traités dominent (94 \% du poids) et sont toutes deux très au-dessus de la vérité, à $0{,}084$ et $0{,}122$, parce qu'elles comparent des immeubles de Montréal et de Gatineau à des témoins de six RMR sur toute la décennie : c'est le biais de tendance régionale déjà identifié. La comparaison « précoce contre tardive pas encore traitée » oppose Montréal à Gatineau avant 2024T3 et donne $0{,}008$, parce que Gatineau croît plus vite que Montréal dans le générateur et que cette différence de tendance s'oppose à l'effet. La comparaison interdite, « tardive contre précoce déjà traitée », donne $0{,}040$ pour une vérité de $0{,}036$ ; son poids est faible ($1{,}5$ \%) et son biais ici modeste, parce que l'effet de la cohorte A n'augmente que de $0{,}0025$ par trimestre pendant que la cohorte B est traitée. La \cref{fig:36:bacon}(a) place ces quatre points dans le plan poids--estimation. Sur le cas 4, la décomposition montre donc que le défaut du TWFE tient d'abord aux tendances régionales, non à l'échelonnement ; pour isoler ce dernier, le panneau (b) et le \cref{tab:36:labo} recourent à un laboratoire.
334 338
335 339 \begin{table}[htbp]
336 340 \caption{Décomposition de Goodman-Bacon de l'estimateur TWFE à effets de période communs ($\hat\delta = 0{,}0905$), cas 4 (données synthétiques)}\label{tab:36:bacon}
337 341 \centering
338 −\small
339 −\begin{tabular}{llS[table-format=1.4]S[table-format=1.4]S[table-format=1.4]}
342 +\footnotesize\setlength{\tabcolsep}{4pt}
343 +\begin{tabular}{lL{44mm}S[table-format=1.4]S[table-format=1.4]S[table-format=1.4]}
340 344 \toprule
341 345 Groupe traité & Groupe témoin (fenêtre) & {Poids $s$} & {$\hat\beta_{2\times 2}$} & {Vrai} \\
342 346 \midrule
343 347 Cohorte A (2022T3) & Jamais traités (toute la période) & 0.6658 & 0.0844 & 0.0484 \\
344 348 Cohorte B (2024T3) & Jamais traités (toute la période) & 0.2721 & 0.1224 & 0.0356 \\
345 −Cohorte A & Cohorte B avant 2024T3 (pas encore traitée) & 0.0476 & 0.0080 & 0.0280 \\
349 +Cohorte A & Cohorte B avant 2024T3 (pas encore traitée) & 0.0476 & 0.0080 & 0.0356 \\
346 350 Cohorte B & Cohorte A dès 2022T3 (déjà traitée) & 0.0146 & 0.0403 & 0.0356 \\
347 351 \midrule
348 352 Somme des poids / moyenne pondérée & & 1.0000 & 0.0905 & 0.0454 \\
@@ -363,16 +367,16 @@ Le laboratoire du \cref{tab:36:labo} est un panel artificiel de 50 unités et 42
363 367 \begin{table}[htbp]
364 368 \caption{Laboratoire à adoption échelonnée et effets croissants : TWFE statique, comparaisons de Goodman-Bacon et estimateur de Callaway et Sant'Anna (panel artificiel)}\label{tab:36:labo}
365 369 \centering
366 −\small
370 +\footnotesize\setlength{\tabcolsep}{4pt}
367 371 \begin{tabular}{lS[table-format=+1.4]S[table-format=1.4]S[table-format=1.4]}
368 372 \toprule
369 373 Quantité & {Estimation} & {ET grappes / bootstrap} & {Vrai} \\
370 374 \midrule
371 375 TWFE statique & 0.0233 & 0.0061 & 0.0692 \\
372 −\quad comparaison précoce vs jamais traités (poids 0,204) & 0.0785 & & 0.0825 \\
373 −\quad comparaison tardive vs jamais traités (poids 0,265) & 0.0402 & & 0.0425 \\
374 −\quad comparaison précoce vs tardive pas encore traitée (poids 0,204) & 0.0404 & & 0.0425 \\
375 −\quad comparaison tardive vs précoce déjà traitée (poids 0,327) & -0.0356 & & 0.0425 \\
376 +\quad précoce vs jamais traités (poids 0,204) & 0.0785 & & 0.0825 \\
377 +\quad tardive vs jamais traités (poids 0,265) & 0.0402 & & 0.0425 \\
378 +\quad précoce vs tardive pas encore traitée (poids 0,204) & 0.0404 & & 0.0425 \\
379 +\quad tardive vs précoce déjà traitée (poids 0,327) & -0.0356 & & 0.0425 \\
376 380 Callaway--Sant'Anna, cohorte précoce & 0.0893 & 0.0086 & 0.0825 \\
377 381 Callaway--Sant'Anna, cohorte tardive & 0.0310 & 0.0105 & 0.0425 \\
378 382 Callaway--Sant'Anna, global & 0.0601 & 0.0073 & 0.0692 \\
@@ -381,27 +385,27 @@ Callaway--Sant'Anna, global & 0.0601 & 0.0073 & 0.0692 \\
381 385 \tabsource{\synth{\texttt{analysis/python/cases/ch36\_panel.py}, fonction \texttt{laboratoire} (graine 20261031 + 36)}.}
382 386 \end{table}
383 387
384 −\paragraph{Étape 6 : Callaway et Sant'Anna sur le cas 4.} Revenons au portefeuille. Le \cref{tab:36:cs} applique l'\cref{eq:36:cs} avec, pour chaque cohorte, les témoins jamais traités de sa propre RMR, neuf à Montréal et trois à Gatineau, ce qui est la façon la plus directe d'imposer des tendances parallèles \emph{régionales}. L'effet de la cohorte A est estimé à $0{,}0528$ (vrai $0{,}0484$), celui de la cohorte B à $0{,}0406$ (vrai $0{,}0356$), et l'effet global, pondéré par les tailles des cohortes, à $0{,}0481$ pour un vrai de $0{,}0454$ : l'erreur est de $0{,}003$, le quart de l'erreur-type bootstrap de $0{,}0116$. L'agrégation dynamique, \cref{fig:36:eventstudy}(b), reproduit le profil de l'étude d'événement avec une précision comparable et le prolonge jusqu'à $k = 15$ pour la seule cohorte A, où l'on voit l'estimation suivre la pente de $0{,}0025$ par trimestre avec un bruit croissant. Avec les 35 témoins de toutes les RMR, l'estimateur donne $0{,}0509$ pour A mais $0{,}0521$ pour B, dont l'effet vrai n'est que de $0{,}0356$ : Gatineau croît plus vite que la moyenne des autres RMR et la cohorte B s'en trouve créditée, exactement comme dans la comparaison correspondante du \cref{tab:36:bacon}. Le choix des témoins est une hypothèse d'identification à part entière, et la restriction à la même RMR en est une plus crédible que l'assemblage de six marchés.
388 +\paragraph{Étape 6 : Callaway et Sant'Anna sur le cas 4} Revenons au portefeuille. Le \cref{tab:36:cs} applique l'\cref{eq:36:cs} avec, pour chaque cohorte, les témoins jamais traités de sa propre RMR, neuf à Montréal et trois à Gatineau, ce qui est la façon la plus directe d'imposer des tendances parallèles \emph{régionales}. L'effet de la cohorte A est estimé à $0{,}0528$ (vrai $0{,}0484$), celui de la cohorte B à $0{,}0406$ (vrai $0{,}0356$), et l'effet global, pondéré par les tailles des cohortes, à $0{,}0481$ pour un vrai de $0{,}0454$ : l'erreur est de $0{,}003$, le quart de l'erreur-type bootstrap de $0{,}0116$. L'agrégation dynamique, \cref{fig:36:eventstudy}(b), reproduit le profil de l'étude d'événement avec une précision comparable et le prolonge jusqu'à $k = 15$ pour la seule cohorte A, où l'on voit l'estimation suivre la pente de $0{,}0025$ par trimestre avec un bruit croissant. Avec les 35 témoins de toutes les RMR, l'estimateur donne $0{,}0509$ pour A mais $0{,}0521$ pour B, dont l'effet vrai n'est que de $0{,}0356$ : Gatineau croît plus vite que la moyenne des autres RMR et la cohorte B s'en trouve créditée, exactement comme dans la comparaison correspondante du \cref{tab:36:bacon}. Le choix des témoins est une hypothèse d'identification à part entière, et la restriction à la même RMR en est une plus crédible que l'assemblage de six marchés.
385 389
386 390 \begin{table}[htbp]
387 391 \caption{Estimateur de Callaway et Sant'Anna par cohorte et global, selon le groupe témoin, cas 4 (données synthétiques)}\label{tab:36:cs}
388 392 \centering
389 −\small
393 +\footnotesize\setlength{\tabcolsep}{4pt}
390 394 \begin{tabular}{lS[table-format=1.4]S[table-format=1.4]S[table-format=1.4]S[table-format=1.4]S[table-format=1.4]}
391 395 \toprule
392 396 & \multicolumn{2}{c}{Témoins de la même RMR} & \multicolumn{2}{c}{Témoins de toutes les RMR} & \\
393 397 \cmidrule(lr){2-3}\cmidrule(lr){4-5}
394 398 Agrégat & {$\widehat{\mathrm{ATT}}$} & {ET bootstrap} & {$\widehat{\mathrm{ATT}}$} & {ET bootstrap} & {Vrai} \\
395 399 \midrule
396 −Cohorte A (Montréal, 2022T3, 16 trimestres) & 0.0528 & 0.0154 & 0.0509 & 0.0145 & 0.0484 \\
397 −Cohorte B (Gatineau, 2024T3, 8 trimestres) & 0.0406 & 0.0183 & 0.0521 & 0.0133 & 0.0356 \\
398 −Global (pondéré par la taille des cohortes) & 0.0481 & 0.0116 & 0.0514 & & 0.0454 \\
400 +Cohorte A (Montréal, 2022T3, 16 trim.) & 0.0528 & 0.0154 & 0.0509 & 0.0145 & 0.0484 \\
401 +Cohorte B (Gatineau, 2024T3, 8 trim.) & 0.0406 & 0.0183 & 0.0521 & 0.0133 & 0.0356 \\
402 +Global (pondéré par les cohortes) & 0.0481 & 0.0116 & 0.0514 & & 0.0454 \\
399 403 \bottomrule
400 404 \end{tabular}
401 405 \tabsource{\synth{\texttt{traite.panel.callaway\_santanna} ; base universelle $g - 1$, témoins jamais traités, bootstrap stratifié de 499 tirages (99 pour la variante)}.}
402 406 \end{table}
403 407
404 −\paragraph{Étape 7 : contrôle synthétique.} La cohorte A est ici traitée comme une seule unité, la moyenne des logarithmes de ses huit immeubles, et le bassin de donneurs est constitué des 35 immeubles jamais traités, chaque série étant centrée sur sa moyenne des 26 trimestres pré-traitement. Les poids de l'\cref{eq:36:synth} se concentrent sur onze donneurs (\cref{fig:36:synthetique}(c)) : P02 ($0{,}142$) et P45 ($0{,}105$) à Gatineau, P09 ($0{,}136$), P16 ($0{,}123$) et P38 ($0{,}027$) à Montréal, P25 ($0{,}120$) et P19 ($0{,}114$) à Trois-Rivières, P43 ($0{,}110$) au Saguenay, P03 ($0{,}071$), P35 et P05. L'ajustement pré-traitement est excellent, avec un RMSPE de $0{,}0031$, ce qui n'étonne pas lorsque 35 donneurs servent à reproduire une moyenne de huit séries lisses. Après la mise en service, l'écart moyen entre la cohorte et son contrôle synthétique est de $0{,}0363$ pour un effet vrai moyen de $0{,}0484$ sur les seize trimestres : le contrôle synthétique sous-estime de $0{,}012$, là où les estimateurs par régression surestimaient. La raison se lit sur le panneau (a) : la méthode ajuste toute la trajectoire pré-traitement, y compris les derniers trimestres où les traités sont, par hasard, au-dessus de leur tendance ; le contrefactuel synthétique prolonge ce point haut, alors que les traités retombent ensuite vers leur tendance, et la différence est portée au débit du traitement. L'inférence par placebos (panneau (b)) est néanmoins nette : le rapport des RMSPE post et pré de la cohorte vaut $12{,}9$, le plus élevé des 36 unités, soit une valeur $p$ de permutation de $1/36 = 0{,}028$. Le contrôle synthétique confirme qu'il s'est passé quelque chose, situe l'effet dans le bon ordre de grandeur, et montre par les poids quels immeubles ont servi de témoins ; il ne remplace pas l'étude d'événement pour la dynamique, et il est, comme elle, à la merci de la configuration des derniers trimestres pré-traitement.
408 +\paragraph{Étape 7 : contrôle synthétique} La cohorte A est ici traitée comme une seule unité, la moyenne des logarithmes de ses huit immeubles, et le bassin de donneurs est constitué des 35 immeubles jamais traités, chaque série étant centrée sur sa moyenne des 26 trimestres pré-traitement. Les poids de l'\cref{eq:36:synth} se concentrent sur onze donneurs (\cref{fig:36:synthetique}(c)) : P02 ($0{,}142$) et P45 ($0{,}105$) à Gatineau, P09 ($0{,}136$), P16 ($0{,}123$) et P38 ($0{,}027$) à Montréal, P25 ($0{,}120$) et P19 ($0{,}114$) à Trois-Rivières, P43 ($0{,}110$) au Saguenay, P03 ($0{,}071$), P35 et P05. L'ajustement pré-traitement est excellent, avec un RMSPE de $0{,}0031$, ce qui n'étonne pas lorsque 35 donneurs servent à reproduire une moyenne de huit séries lisses. Après la mise en service, l'écart moyen entre la cohorte et son contrôle synthétique est de $0{,}0363$ pour un effet vrai moyen de $0{,}0484$ sur les seize trimestres : le contrôle synthétique sous-estime de $0{,}012$, là où les estimateurs par régression surestimaient. La raison se lit sur le panneau (a) : la méthode ajuste toute la trajectoire pré-traitement, y compris les derniers trimestres où les traités sont, par hasard, au-dessus de leur tendance ; le contrefactuel synthétique prolonge ce point haut, alors que les traités retombent ensuite vers leur tendance, et la différence est portée au débit du traitement. L'inférence par placebos (panneau (b)) est néanmoins nette : le rapport des RMSPE post et pré de la cohorte vaut $12{,}9$, le plus élevé des 36 unités, soit une valeur $p$ de permutation de $1/36 = 0{,}028$. Le contrôle synthétique confirme qu'il s'est passé quelque chose, situe l'effet dans le bon ordre de grandeur, et montre par les poids quels immeubles ont servi de témoins ; il ne remplace pas l'étude d'événement pour la dynamique, et il est, comme elle, à la merci de la configuration des derniers trimestres pré-traitement.
405 409
406 410 \begin{figure}[htbp]
407 411 \centering
@@ -410,7 +414,7 @@ Global (pondéré par la taille des cohortes) & 0.0481 & 0.0116 & 0.0514 & & 0.0
410 414 \figsource{\synth{script \texttt{analysis/python/figures/ch36\_figures.py}, graine 20261031}}
411 415 \end{figure}
412 416
413 −\paragraph{Étape 8 : discontinuité spatiale.} Le portefeuille du cas 4 ne comporte pas de frontière ; la \cref{fig:36:discontinuite} repose donc sur une simulation transversale annexe, déclarée dans le script : 400 immeubles fictifs répartis uniformément à moins de deux kilomètres d'une frontière de zone, dont le logarithme de la valeur suit $12{,}9 + 0{,}03\,x + 0{,}05\,\ind[x \geq 0] + \varepsilon$, avec $x$ la distance signée en kilomètres (positive du côté de la zone de densité supérieure) et $\varepsilon$ normal d'écart-type $0{,}08$. La pente de $0{,}03$ par kilomètre représente un gradient continu de localisation, le saut de $0{,}05$ l'effet de la règle. La comparaison naïve des deux côtés donne $0{,}114$, plus du double de la vérité, parce qu'elle attribue à la règle tout le gradient. L'\cref{eq:36:rdd} donne $0{,}034$ (erreur-type $0{,}034$, 93 immeubles effectifs) pour une bande de 500 mètres, $0{,}032$ ($0{,}025$, 202 immeubles) pour un kilomètre et $0{,}058$ ($0{,}018$, 400 immeubles) pour deux kilomètres. Les trois estimations encadrent la vérité à moins d'un écart-type, et la progression des erreurs-types illustre l'arbitrage : la bande de deux kilomètres est la plus précise mais elle repose sur l'hypothèse, ici vraie par construction, que la fonction de prix est linéaire sur toute la largeur. Sur des données réelles, la linéarité n'est jamais garantie, et l'usage est de rapporter la fourchette.
417 +\paragraph{Étape 8 : discontinuité spatiale} Le portefeuille du cas 4 ne comporte pas de frontière ; la \cref{fig:36:discontinuite} repose donc sur une simulation transversale annexe, déclarée dans le script : 400 immeubles fictifs répartis uniformément à moins de deux kilomètres d'une frontière de zone, dont le logarithme de la valeur suit $12{,}9 + 0{,}03\,x + 0{,}05\,\ind[x \geq 0] + \varepsilon$, avec $x$ la distance signée en kilomètres (positive du côté de la zone de densité supérieure) et $\varepsilon$ normal d'écart-type $0{,}08$. La pente de $0{,}03$ par kilomètre représente un gradient continu de localisation, le saut de $0{,}05$ l'effet de la règle. La comparaison naïve des deux côtés donne $0{,}114$, plus du double de la vérité, parce qu'elle attribue à la règle tout le gradient. L'\cref{eq:36:rdd} donne $0{,}034$ (erreur-type $0{,}034$, 93 immeubles effectifs) pour une bande de 500 mètres, $0{,}032$ ($0{,}025$, 202 immeubles) pour un kilomètre et $0{,}058$ ($0{,}018$, 400 immeubles) pour deux kilomètres. Les trois estimations encadrent la vérité à moins d'un écart-type, et la progression des erreurs-types illustre l'arbitrage : la bande de deux kilomètres est la plus précise mais elle repose sur l'hypothèse, ici vraie par construction, que la fonction de prix est linéaire sur toute la largeur. Sur des données réelles, la linéarité n'est jamais garantie, et l'usage est de rapporter la fourchette.
414 418
415 419 \begin{figure}[htbp]
416 420 \centering
@@ -419,13 +423,12 @@ Global (pondéré par la taille des cohortes) & 0.0481 & 0.0116 & 0.0514 & & 0.0
419 423 \figsource{\synth{script \texttt{analysis/python/figures/ch36\_figures.py}, graine 20261031 ; données engendrées par \texttt{ch36\_panel.discontinuite}}}
420 424 \end{figure}
421 425
422 −\paragraph{Bilan.} Le \cref{tab:36:bilan} aligne les estimateurs sur la vérité correspondante. Trois constats. Les estimateurs qui imposent des tendances parallèles \emph{régionales} (2 $\times$ 2 avec témoins de Montréal, étude d'événement et TWFE avec effets RMR $\times$ trimestre, Callaway--Sant'Anna par RMR) sont tous à moins d'un écart-type de la vérité ; ceux qui mélangent les régions ne le sont que par compensation d'erreurs. L'estimateur statique, quel que soit son habillage, est le plus exposé aux accidents de trajectoire pré-traitement, parce qu'il moyenne sur toute la période ; l'étude d'événement et l'estimateur par cohortes, qui raisonnent par rapport à une base datée, rendent ces accidents visibles. Enfin, aucun estimateur n'est précis : avec huit immeubles traités et des revenus qui suivent des marches aléatoires, les erreurs-types honnêtes sont de l'ordre de $0{,}015$ à $0{,}025$, soit $\pm 3$ à $\pm 5$ points de pourcentage autour d'un effet de $4$ à $5$ \%. C'est le réalisme qu'un rapport doit afficher : l'effet est positif, de l'ordre de quelques pour cent, et sa valeur exacte n'est pas connue.
423 −\end{exemple}
426 +\paragraph{Bilan} Le \cref{tab:36:bilan} aligne les estimateurs sur la vérité correspondante. Trois constats. Les estimateurs qui imposent des tendances parallèles \emph{régionales} (2 $\times$ 2 avec témoins de Montréal, étude d'événement et TWFE avec effets RMR $\times$ trimestre, Callaway--Sant'Anna par RMR) sont tous à moins d'un écart-type de la vérité ; ceux qui mélangent les régions ne le sont que par compensation d'erreurs. L'estimateur statique, quel que soit son habillage, est le plus exposé aux accidents de trajectoire pré-traitement, parce qu'il moyenne sur toute la période ; l'étude d'événement et l'estimateur par cohortes, qui raisonnent par rapport à une base datée, rendent ces accidents visibles. Enfin, aucun estimateur n'est précis : avec huit immeubles traités et des revenus qui suivent des marches aléatoires, les erreurs-types honnêtes sont de l'ordre de $0{,}015$ à $0{,}025$, soit $\pm 3$ à $\pm 5$ points de pourcentage autour d'un effet de $4$ à $5$ \%. C'est le réalisme qu'un rapport doit afficher : l'effet est positif, de l'ordre de quelques pour cent, et sa valeur exacte n'est pas connue.
424 427
425 428 \begin{table}[htbp]
426 429 \caption{Bilan : estimateurs de l'effet du traitement sur le logarithme du revenu net d'exploitation et vérité correspondante (données synthétiques)}\label{tab:36:bilan}
427 430 \centering
428 −\small
431 +\footnotesize\setlength{\tabcolsep}{4pt}
429 432 \begin{tabular}{lS[table-format=1.4]S[table-format=1.4]S[table-format=1.4]S[table-format=+1.4]}
430 433 \toprule
431 434 Estimateur (cible) & {Estimation} & {Erreur-type} & {Vrai} & {Écart} \\
@@ -461,7 +464,7 @@ Deux corrections sont possibles lorsque la pente pré-traitement est jugée rée
461 464 \begin{table}[htbp]
462 465 \caption{Sensibilité aux hypothèses d'identification : tendances parallèles violées ($+1$ point de croissance par an pour les traités) et choix du groupe témoin (données synthétiques)}\label{tab:36:sensibilite}
463 466 \centering
464 −\small
467 +\footnotesize\setlength{\tabcolsep}{4pt}
465 468 \begin{tabular}{lS[table-format=1.4]S[table-format=1.4]S[table-format=1.4]}
466 469 \toprule
467 470 Scénario et estimateur & {Référence} & {Violé} & {Vrai} \\
modified manuscript/chapters/ch37.tex +476 −1
@@ -1,2 +1,477 @@
1 1 \chapter{Apprentissage automatique et explicabilité}\label{ch:37}
2 −\stubchapter
2 +
3 +Les chapitres précédents de cette partie ont étendu le modèle hédonique dans trois directions : les indices et les ventes répétées (\cref{ch:35}), les données de panel et l'identification causale (\cref{ch:36}), et, plus tôt, la dépendance spatiale (\cref{ch:19}). Toutes ces extensions gardent la forme d'une régression dont l'analyste écrit l'équation. Le présent chapitre examine ce qui se passe lorsqu'on renonce à écrire l'équation et qu'on laisse un algorithme la découvrir : arbres de régression, forêts aléatoires et \emph{gradient boosting} sont aujourd'hui les méthodes les plus utilisées dans les modèles d'évaluation automatisés (AVM) des prêteurs, des portails immobiliers et de plusieurs administrations fiscales, et l'évaluateur agréé les rencontre à la fois comme outils et comme adversaires, lorsqu'une valeur produite par une « boîte noire » lui est opposée. L'objectif est double. Il s'agit d'abord de comprendre ces méthodes assez bien pour les estimer, les valider et en lire les hyperparamètres, ce qui exige une dérivation du boosting comme descente de gradient dans l'espace des fonctions et une discussion serrée de la validation croisée, en particulier lorsque les données sont organisées dans l'espace et dans le temps. Il s'agit ensuite de les rendre explicables, par les valeurs de Shapley, la dépendance partielle et l'importance par permutation, parce qu'une prédiction qu'on ne peut pas expliquer n'est pas une opinion de valeur au sens des normes professionnelles. L'exemple intégral oppose, sur les 600 ventes synthétiques du cas 1, le modèle hédonique de référence du \cref{ch:18} à une forêt et à deux algorithmes de boosting ; comme la loi qui a engendré ces ventes est connue et additive en logarithme, le lecteur verra l'hédonique bien spécifié l'emporter, puis, lorsqu'une non-linéarité déclarée est introduite dans le jeu, le boosting prendre l'avantage et l'explicabilité servir à respécifier le modèle économétrique. Les méthodes bayésiennes (\cref{ch:39}) et la simulation (\cref{ch:40}) complètent la partie.
4 +
5 +\begin{objectifs}
6 +À la fin de ce chapitre, le lecteur sait :
7 +\begin{itemize}
8 +\item distinguer la prédiction de l'inférence, décomposer l'erreur de prédiction en biais et variance, et situer les arbres, les forêts aléatoires et le boosting dans cet arbitrage ;
9 +\item dériver le gradient boosting comme une descente de gradient fonctionnelle, lire ses hyperparamètres (profondeur, taux d'apprentissage, nombre d'arbres, sous-échantillonnage, taille des feuilles) et reconnaître le surapprentissage sur une courbe d'apprentissage ;
10 +\item concevoir une validation croisée honnête, aléatoire, spatiale par secteur et temporelle, repérer les fuites de données et mesurer la performance par la racine de l'erreur quadratique en logarithme, l'erreur absolue médiane et les statistiques de ratio de l'IAAO ;
11 +\item définir les valeurs de Shapley par leurs axiomes, les calculer pour un modèle d'arbres par TreeSHAP, produire et lire un graphique en essaim, une cascade pour un sujet, une courbe de dépendance partielle et une importance par permutation ;
12 +\item comparer, sur un même jeu de ventes, un modèle hédonique et des modèles d'arbres, comprendre pourquoi un modèle économétrique bien spécifié résiste et dans quelles conditions l'apprentissage gagne ;
13 +\item situer les modèles d'évaluation automatisés dans les normes de l'IAAO et dans le cadre québécois, et énoncer leurs limites : extrapolation hors support, dérive temporelle, équité et opacité devant un tribunal.
14 +\end{itemize}
15 +\end{objectifs}
16 +
17 +\section{Prédire n'est pas estimer}\label{sec:37:prediction}
18 +
19 +\subsection{Deux questions, deux critères}
20 +
21 +L'économétrie du \cref{ch:18} cherche des paramètres : l'élasticité de la superficie, la prime d'un secteur, la dépréciation annuelle. Elle juge un modèle à la précision et à l'absence de biais de ces paramètres, et les diagnostics qu'elle déploie (hétéroscédasticité, colinéarité, observations influentes) servent cette fin. L'apprentissage automatique supervisé pose une autre question : étant donné un vecteur de caractéristiques $\mathbf{x}$, quelle est la meilleure prédiction $\hat f(\mathbf{x})$ du prix d'un logement que l'on n'a pas encore vu ? Le critère est alors le \motcle{risque de prédiction} (en anglais \emph{generalization error}), l'erreur attendue sur une nouvelle vente tirée de la même population,
22 +\begin{equation}\label{eq:37:risque}
23 +R(f) = \E\!\left[\big(Y - f(\mathbf{X})\big)^{2}\right],
24 +\end{equation}
25 +où l'espérance porte sur la loi jointe de $(\mathbf{X}, Y)$ et où $Y$ est, dans tout ce chapitre, le logarithme du prix. \textcite{mullainathanSpiess2017} ont résumé la différence par une formule : l'économètre veut $\hat\beta$, l'apprentissage veut $\hat y$. La distinction n'est pas une opposition de camps mais une question de mandat. L'évaluateur qui dérive des ajustements pour une grille de comparaison (\cref{ch:17}) a besoin de $\hat\beta$ ; celui qui produit une valeur pour le sujet, ou mille valeurs pour un rôle, a besoin de $\hat y$ ; et celui qui dépose une opinion de valeur devant un tiers a besoin des deux, parce qu'une prédiction sans explication n'est pas défendable. \textcite{atheyImbens2019} ont montré ce que l'économie peut retenir de ces méthodes : une discipline de validation hors échantillon que la régression classique néglige, des formes fonctionnelles flexibles, et des outils pour détecter l'hétérogénéité, dont les forêts causales de \textcite{wagerAthey2018} sont l'exemple le plus abouti.
26 +
27 +\subsection{La décomposition biais-variance}
28 +
29 +Le risque \eqref{eq:37:risque} d'un estimateur $\hat f$ obtenu à partir d'un échantillon d'apprentissage se décompose, en un point $\mathbf{x}_0$, en trois termes :
30 +\begin{equation}\label{eq:37:biais-variance}
31 +\E\!\left[\big(Y - \hat f(\mathbf{x}_0)\big)^{2}\right] = \underbrace{\big(f(\mathbf{x}_0) - \E[\hat f(\mathbf{x}_0)]\big)^{2}}_{\text{biais}^2} + \underbrace{\Var\big(\hat f(\mathbf{x}_0)\big)}_{\text{variance}} + \underbrace{\sigma^{2}}_{\text{bruit}},
32 +\end{equation}
33 +où $f(\mathbf{x}) = \E[Y \mid \mathbf{X} = \mathbf{x}]$ est la vraie fonction de régression, l'espérance et la variance portant sur les échantillons d'apprentissage possibles. Le troisième terme est le bruit de transaction, irréductible, que le \cref{ch:18} a mesuré à $\sigma \approx 0{,}083$ sur le cas 1 et qu'aucun modèle ne peut abaisser. Les deux premiers s'échangent : un modèle rigide, comme une régression à peu de paramètres, a un biais possiblement élevé si sa forme est fausse mais une variance faible ; un modèle souple, comme un arbre profond, peut approcher n'importe quelle fonction mais sa prédiction en un point dépend fortement de l'échantillon tiré. \textcite{hastieTibshiraniFriedman2009} en font le fil conducteur de tout l'apprentissage statistique : les méthodes de ce chapitre sont des manières de réduire la variance d'un estimateur souple (moyennage, dans les forêts) ou de contrôler sa souplesse pas à pas (boosting avec rétrécissement), et la validation croisée est l'instrument qui mesure où l'on se trouve dans l'arbitrage. Une conséquence mérite d'être énoncée dès maintenant : si la vraie fonction est additive en logarithme, ce qui est le cas des ventes synthétiques du cas 1 et une approximation raisonnable de bien des marchés homogènes, le modèle semi-logarithmique du \cref{ch:18} a un biais nul et une variance minimale, et aucune méthode souple ne peut le battre en espérance ; elle ne peut que s'en approcher. C'est l'enseignement central de l'exemple intégral.
34 +
35 +\section{Arbres, forêts aléatoires et boosting}\label{sec:37:arbres}
36 +
37 +\subsection{L'arbre de régression}
38 +
39 +Un \motcle{arbre de régression} (\emph{regression tree}) partitionne l'espace des caractéristiques en $M$ régions rectangulaires $R_1, \dots, R_M$ par une suite de divisions binaires, chacune portant sur une seule variable et un seul seuil, et prédit dans chaque région une constante, la moyenne des observations qui y tombent :
40 +\begin{equation}\label{eq:37:arbre}
41 +\hat f(\mathbf{x}) = \sum_{m=1}^{M} \hat c_m\, \mathbb{1}\{\mathbf{x} \in R_m\}, \qquad \hat c_m = \frac{1}{n_m}\sum_{i : \mathbf{x}_i \in R_m} y_i .
42 +\end{equation}
43 +La construction est gloutonne : à chaque nœud, l'algorithme examine toutes les variables $j$ et tous les seuils $s$ et retient la division qui réduit le plus la somme des carrés des écarts,
44 +\begin{equation}\label{eq:37:division}
45 +(j^{\ast}, s^{\ast}) = \argmin_{j, s}\ \Bigg[\sum_{i : x_{ij} \le s} (y_i - \bar y_{\text{g}})^{2} + \sum_{i : x_{ij} > s} (y_i - \bar y_{\text{d}})^{2}\Bigg],
46 +\end{equation}
47 +où $\bar y_{\text{g}}$ et $\bar y_{\text{d}}$ sont les moyennes des deux groupes, puis recommence dans chaque fils jusqu'à ce qu'un critère d'arrêt soit atteint : profondeur maximale, nombre minimal d'observations par feuille, ou gain minimal. L'arbre a trois vertus pour l'évaluateur. Il est invariant aux transformations monotones des variables, de sorte qu'il est indifférent d'entrer la superficie en mètres carrés ou en logarithme ; il capte naturellement les interactions, puisqu'une division sur l'âge dans la branche des maisons de qualité élevée agit différemment de la même division dans la branche des maisons modestes ; et il se lit comme un arbre de décision, ce que la \cref{fig:37:arbre} illustre sur les 600 ventes du cas 1. Il a un défaut rédhibitoire : sa variance. Deux échantillons voisins produisent des arbres aux divisions différentes dès la racine, et la prédiction en un point saute d'une constante à une autre. L'arbre de profondeur 3 de la figure, avec ses huit feuilles, prédit un même prix d'environ \dollars{473000} pour toutes les maisons de 117 à \msq{147} situées à moins de 25 kilomètres du centre, quels que soient leur âge, leur qualité et leur secteur, ce qui n'est évidemment pas la fonction de prix du \cref{ch:18}, et son erreur hors échantillon le dit (\cref{sec:37:exemple}).
48 +
49 +\begin{figure}[htbp]
50 +\centering
51 +\includegraphics[width=\textwidth]{ch37/ch37-fig-arbre.pdf}
52 +\caption{Arbre de régression de profondeur 3 estimé sur le logarithme du prix des 600 ventes du cas 1 (feuilles d'au moins cinq ventes). La première division porte sur la superficie habitable (147,5 m²), les suivantes sur la distance au centre, la latitude, de nouveau la superficie et le terrain ; chaque feuille prédit une constante, convertie ici en dollars. Huit prix possibles pour six cents maisons : l'arbre est lisible mais grossier.}\label{fig:37:arbre}
53 +\figsource{\synth{script \texttt{analysis/python/figures/ch37\_figures.py}, graine 20261031}.}
54 +\end{figure}
55 +
56 +\subsection{Moyenner des arbres : bagging et forêts aléatoires}
57 +
58 +Le remède à la variance d'un arbre est d'en estimer beaucoup et de les moyenner. Le \emph{bagging} (\emph{bootstrap aggregating}) tire $B$ échantillons bootstrap de l'échantillon d'apprentissage, estime un arbre profond sur chacun et prédit par la moyenne des $B$ prédictions. La \motcle{forêt aléatoire} (\emph{random forest}) de \textcite{breiman2001} ajoute une seconde source de hasard : à chaque division, seul un sous-ensemble de $m$ variables tirées au hasard parmi les $p$ disponibles est candidat. La raison tient à une identité élémentaire. Si les $B$ arbres ont chacun une variance $\sigma_a^2$ en un point et une corrélation deux à deux $\rho$, la variance de leur moyenne est
59 +\begin{equation}\label{eq:37:variance-foret}
60 +\Var\!\left(\frac{1}{B}\sum_{b=1}^{B} \hat f_b(\mathbf{x}_0)\right) = \rho\,\sigma_a^{2} + \frac{1 - \rho}{B}\,\sigma_a^{2} .
61 +\end{equation}
62 +Le second terme s'annule lorsque $B$ croît ; le premier ne dépend que de la corrélation entre arbres. Or des arbres estimés sur des bootstraps du même échantillon se ressemblent, parce qu'ils choisissent presque tous les mêmes variables dominantes à la racine ; restreindre les variables candidates les décorrèle, au prix d'un léger biais pour chaque arbre, et abaisse le plancher $\rho\sigma_a^2$. C'est l'arbitrage biais-variance de l'\cref{eq:37:biais-variance} appliqué à l'ensemble. Les hyperparamètres d'une forêt sont donc le nombre d'arbres $B$, que l'on prend grand sans risque de surapprentissage puisque la moyenne converge, le nombre $m$ de variables par division, souvent $p/3$ en régression, et la taille minimale des feuilles, qui règle la profondeur. Les observations laissées hors du bootstrap d'un arbre, dites \emph{hors sac} (\emph{out-of-bag}), fournissent une estimation gratuite de l'erreur de prédiction, qui joue le rôle d'une validation croisée interne. \textcite{hongChoiKim2020} ont appliqué la forêt aléatoire à l'évaluation de masse des logements de Séoul, et \textcite{paceHayunga2020} ont montré qu'arbres et forêts, appliqués aux résidus d'un modèle hédonique ou spatial, y détectent de l'information structurée que le modèle paramétrique a laissée : la forêt est aussi un diagnostic de spécification.
63 +
64 +\subsection{Le gradient boosting comme descente de gradient fonctionnelle}
65 +
66 +Le \motcle{boosting} procède à l'inverse du bagging : au lieu de moyenner des arbres profonds estimés indépendamment, il additionne séquentiellement des arbres peu profonds, chacun corrigeant les erreurs de la somme des précédents. \textcite{friedman2001} en a donné l'interprétation qui fonde toutes les implémentations modernes : le boosting est une descente de gradient, non dans l'espace des paramètres, mais dans l'espace des fonctions. Soit une fonction de perte $L(y, F)$, par exemple le carré de l'erreur $\tfrac{1}{2}(y - F)^2$, et un modèle additif
67 +\begin{equation}\label{eq:37:boosting}
68 +F_M(\mathbf{x}) = F_0 + \sum_{m=1}^{M} \nu\, h_m(\mathbf{x}), \qquad F_0 = \bar y ,
69 +\end{equation}
70 +où chaque $h_m$ est un arbre de régression de petite taille et $\nu \in (0, 1]$ un \motcle{taux d'apprentissage} (\emph{learning rate}, ou rétrécissement). On cherche à minimiser le risque empirique $\sum_i L(y_i, F(\mathbf{x}_i))$ en $F$. Si l'on pouvait modifier librement les $n$ valeurs $F(\mathbf{x}_i)$, la descente de gradient ferait un pas dans la direction opposée au gradient de la perte par rapport à ces valeurs, c'est-à-dire vers les \motcle{pseudo-résidus}
71 +\begin{equation}\label{eq:37:gradient}
72 +r_{im} = -\left.\frac{\partial L\big(y_i, F(\mathbf{x}_i)\big)}{\partial F(\mathbf{x}_i)}\right|_{F = F_{m-1}}, \qquad \text{soit } r_{im} = y_i - F_{m-1}(\mathbf{x}_i) \text{ pour la perte quadratique.}
73 +\end{equation}
74 +Mais une fonction définie seulement aux $n$ points d'apprentissage ne prédit rien ailleurs ; le boosting projette donc ce gradient sur une classe de fonctions, les arbres, en estimant $h_m$ par moindres carrés sur les pseudo-résidus, puis fait le pas $F_m = F_{m-1} + \nu\, h_m$. Pour la perte quadratique, chaque itération ajuste simplement un petit arbre aux résidus courants ; pour la perte absolue ou la perte de Huber, les pseudo-résidus sont des signes ou des résidus tronqués, ce qui rend l'algorithme robuste aux ventes aberrantes, un avantage que le \cref{ch:14} fait apprécier. Le taux d'apprentissage est le pas de la descente ; un petit pas exige plus d'arbres mais généralise mieux, parce que chaque arbre n'explique qu'une fraction des résidus et que l'ensemble ne s'ajuste pas au bruit d'un coup. Le sous-échantillonnage, qui estime chaque arbre sur une fraction tirée au hasard des observations (\emph{stochastic gradient boosting}), ajoute une régularisation de même nature que le bagging. La profondeur des arbres fixe l'\emph{ordre d'interaction} du modèle : des souches (profondeur 1) produisent un modèle strictement additif en les variables, des arbres de profondeur 2 admettent des interactions de deux variables, et ainsi de suite ; on verra à la \cref{sec:37:sensibilite} que, sur un jeu engendré par une loi additive, les souches sont précisément le meilleur boosting, ce qui n'est pas un hasard.
75 +
76 +\textcite{chenGuestrin2016} ont réécrit cette descente avec un développement au second ordre de la perte et une pénalité explicite sur la complexité des arbres ; c'est l'algorithme XGBoost. À l'itération $m$, l'objectif à minimiser en l'arbre $h$ est
77 +\begin{equation}\label{eq:37:xgb-objectif}
78 +\mathcal{O}_m(h) = \sum_{i=1}^{n}\Big[g_i\, h(\mathbf{x}_i) + \tfrac{1}{2} k_i\, h(\mathbf{x}_i)^{2}\Big] + \gamma\, T + \tfrac{1}{2}\lambda \sum_{t=1}^{T} w_t^{2},
79 +\end{equation}
80 +où $g_i$ et $k_i$ sont les dérivées première et seconde de la perte en $F_{m-1}(\mathbf{x}_i)$ (pour la perte quadratique, $g_i = F_{m-1}(\mathbf{x}_i) - y_i$ et $k_i = 1$), $T$ le nombre de feuilles de l'arbre, $w_t$ la valeur de la feuille $t$, et $\gamma$, $\lambda$ des paramètres de régularisation. L'objectif étant quadratique en $w_t$, la valeur optimale de chaque feuille est $w_t^{\ast} = -\sum_{i \in t} g_i / (\sum_{i \in t} k_i + \lambda)$, et le gain d'une division qui sépare une feuille en une partie gauche $G$ et une partie droite $D$ s'écrit en forme fermée,
81 +\begin{equation}\label{eq:37:xgb-gain}
82 +\text{Gain} = \frac{1}{2}\left[\frac{\big(\sum_{i \in G} g_i\big)^{2}}{\sum_{i \in G} k_i + \lambda} + \frac{\big(\sum_{i \in D} g_i\big)^{2}}{\sum_{i \in D} k_i + \lambda} - \frac{\big(\sum_{i \in G \cup D} g_i\big)^{2}}{\sum_{i \in G \cup D} k_i + \lambda}\right] - \gamma ,
83 +\end{equation}
84 +une division n'étant retenue que si son gain est positif. Avec $\lambda = \gamma = 0$ et la perte quadratique, ce gain est exactement la réduction de la somme des carrés de l'\cref{eq:37:division} appliquée aux résidus : XGBoost est le gradient boosting de Friedman muni d'une régularisation de type ridge sur les feuilles et d'une pénalité sur leur nombre. Le \cref{tab:37:familles} résume les trois familles et leurs hyperparamètres. La leçon pratique est que ces hyperparamètres ne sont pas des détails d'implémentation : la profondeur fixe les interactions que le modèle peut représenter, le taux d'apprentissage et le nombre d'arbres règlent conjointement la quantité de signal absorbée, la taille minimale des feuilles gouverne le comportement aux bords du support, et aucun ne se choisit autrement que par validation.
85 +
86 +\begin{table}[htbp]
87 +\caption{Trois familles de modèles d'arbres : principe, hyperparamètres et comportement}\label{tab:37:familles}
88 +\centering\small
89 +\begin{tabularx}{\textwidth}{lYYY}
90 +\toprule
91 + & Arbre de régression & Forêt aléatoire & Gradient boosting \\
92 +\midrule
93 +Principe & partition récursive, constante par feuille \eqref{eq:37:arbre} & moyenne de $B$ arbres profonds estimés sur des bootstraps avec $m < p$ variables candidates par division & somme de $M$ petits arbres ajustés séquentiellement aux pseudo-résidus \eqref{eq:37:gradient}, pas $\nu$ \\
94 +Référence & \textcite{hastieTibshiraniFriedman2009}, chap. 9 & \textcite{breiman2001} & \textcite{friedman2001} ; \textcite{chenGuestrin2016} \\
95 +Hyperparamètres & profondeur ; taille min. des feuilles & $B$ ; $m$ ; taille min. des feuilles & $M$ ; $\nu$ ; profondeur ; sous-échantillon ; taille min. des feuilles ; $\lambda$, $\gamma$ (XGBoost) \\
96 +Arbitrage biais-variance & variance très élevée & variance réduite par moyennage ; biais légèrement accru par $m < p$ & biais réduit pas à pas ; variance contrôlée par $\nu$, $M$ et le sous-échantillonnage \\
97 +Surapprentissage & dès que la profondeur croît & peu sensible à $B$ ; sensible à la taille des feuilles & oui, si $M$ trop grand pour $\nu$ ; arrêt précoce par validation \\
98 +Interactions & toutes, implicitement & toutes & d'ordre $\le$ profondeur \\
99 +Extrapolation & constante hors support & constante hors support & constante hors support \\
100 +Valeurs de référence (exemple) & profondeur 3, feuilles $\ge 5$ & 500 arbres, $m = p/2$, feuilles $\ge 2$ & 1 200 arbres de profondeur 2, $\nu = 0{,}03$, sous-échantillon 80 \%, feuilles $\ge 5$ \\
101 +\bottomrule
102 +\end{tabularx}
103 +\tabsource{synthèse de l'auteur d'après les références citées ; valeurs de l'exemple : \texttt{analysis/python/cases/ch37\_ml.py}.}
104 +\end{table}
105 +
106 +\section{Validation, fuites de données et surapprentissage}\label{sec:37:validation}
107 +
108 +\subsection{La validation croisée et ce qu'elle estime}
109 +
110 +Le risque \eqref{eq:37:risque} n'est pas observable ; l'erreur sur l'échantillon d'apprentissage le sous-estime, d'autant plus que le modèle est souple, puisqu'un arbre assez profond reproduit chaque prix observé. La \motcle{validation croisée} à $K$ plis partitionne les $n$ observations en $K$ groupes, estime le modèle $K$ fois en excluant chaque groupe à tour de rôle et prédit les observations exclues par le modèle qui ne les a pas vues :
111 +\begin{equation}\label{eq:37:cv}
112 +\widehat{R}_{\text{CV}} = \frac{1}{n}\sum_{k=1}^{K}\sum_{i \in \mathcal{F}_k}\big(y_i - \hat f^{(-k)}(\mathbf{x}_i)\big)^{2},
113 +\end{equation}
114 +où $\hat f^{(-k)}$ est estimé sans le pli $\mathcal{F}_k$. Chaque observation est prédite exactement une fois, hors échantillon, et la collection des $n$ prédictions ainsi obtenues sert à toutes les métriques de la \cref{sec:37:metriques}. Le choix de $K$ arbitre entre le biais (avec $K$ petit, chaque modèle est estimé sur une fraction réduite des données et le risque est surestimé) et la variance et le coût de calcul ; $K = 10$ est la convention de l'ouvrage, et le \cref{ch:18} l'a déjà appliquée au modèle hédonique de référence, avec les mêmes dix plis que ceux de ce chapitre, tirés avec la graine 20261031 : la comparaison entre modèles porte donc sur des prédictions des mêmes ventes par des modèles estimés sur les mêmes sous-échantillons, ce qui élimine une source de bruit de la comparaison.
115 +
116 +\subsection{Les fuites de données}
117 +
118 +L'\cref{eq:37:cv} n'estime le risque sur une nouvelle vente que si les plis reproduisent la situation dans laquelle le modèle sera utilisé. Toute information qui circule de l'échantillon de test vers l'échantillon d'apprentissage, et que le modèle n'aurait pas eue en situation réelle, est une \motcle{fuite de données} (\emph{data leakage}), et elle rend la validation trop optimiste. Trois fuites menacent l'évaluation immobilière. La première est \emph{temporelle} : un modèle estimé sur des ventes de 2023 à 2026 pour prédire des ventes de 2025 a vu l'avenir ; or l'usage réel est prospectif, le modèle estimé sur le passé prédisant le présent ou l'avenir proche, et la tendance des prix ne se prolonge pas d'elle-même dans un modèle d'arbres. La deuxième est \emph{spatiale} : des ventes voisines partagent des attributs de voisinage non observés (\cref{ch:19}), et si une vente de test a ses voisines immédiates dans l'échantillon d'apprentissage, le modèle, surtout s'il dispose des coordonnées, les retrouve et prédit bien ; mais le sujet d'une évaluation réelle n'a pas toujours un voisin vendu à cent mètres, et un modèle destiné à un secteur peu documenté doit être validé en lui retirant ce secteur. La troisième est une fuite par la \emph{cible} : une variable construite à partir du prix, comme la valeur au rôle lorsque celle-ci a été ajustée sur les ventes connues, ou un indice de quartier calculé avec les prix de l'échantillon complet, transporte le prix de test dans les régresseurs. Dans un article qui a fait date en écologie mais dont la portée est générale, Roberts et ses coauteurs ont montré qu'avec des données structurées dans l'espace, le temps ou une hiérarchie, la validation croisée aléatoire surestime systématiquement la performance et que la validation par \emph{blocs} respectant la structure est la seule honnête\footnote{Roberts, D. R., Bahn, V., Ciuti, S., Boyce, M. S., Elith, J., Guillera-Arroita, G. \emph{et al.} (2017), « Cross-validation strategies for data with temporal, spatial, hierarchical, or phylogenetic structure », \emph{Ecography}, 40(8), 913-929 ; DOI 10.1111/ecog.02881 (vérifié par l'API Crossref le 31 octobre 2026 ; consigné dans \texttt{references/lib-ch37.bib}).}. Ce chapitre met en œuvre trois schémas : dix plis aléatoires, qui mesurent la performance \emph{interpolative} dans un marché bien documenté ; dix blocs spatiaux contigus, obtenus en coupant chaque secteur en deux par les $k$-moyennes sur les coordonnées, de sorte que chaque secteur reste représenté à l'estimation mais que le voisinage immédiat d'une vente testée ne l'est jamais ; et une partition temporelle, les 499 ventes de 2023 à 2025 prédisant les 101 ventes de 2026. La \cref{sec:37:exemple} y ajoute, comme épreuve extrême, l'exclusion d'un secteur entier.
119 +
120 +\subsection{Surapprentissage et arrêt précoce}
121 +
122 +Le \motcle{surapprentissage} (\emph{overfitting}) est la situation où un modèle continue d'améliorer son ajustement à l'échantillon d'apprentissage alors que son erreur hors échantillon se dégrade : il apprend le bruit. Pour le boosting, le phénomène se lit sur une \emph{courbe d'apprentissage} qui porte l'erreur d'entraînement et l'erreur de validation croisée contre le nombre d'arbres, et la \cref{fig:37:apprentissage} en montre trois sur le cas 1, pour des arbres de profondeur 1, 2 et 4 avec $\nu = 0{,}03$. L'erreur d'entraînement décroît sans fin ; l'erreur de validation passe par un minimum puis remonte, d'autant plus tôt que les arbres sont profonds : 367 itérations pour la profondeur 4, dont l'erreur d'entraînement finit à 0,012 pour une erreur de validation de 0,097 ; 730 pour la profondeur 2 ; au-delà de 1 200 pour les souches, dont le minimum, 0,089, est aussi le plus bas des trois. L'\motcle{arrêt précoce} (\emph{early stopping}) consiste à retenir le nombre d'arbres qui minimise l'erreur de validation ; c'est l'hyperparamètre le plus important du boosting, et il se lit sur cette courbe. La droite pointillée est l'erreur de validation croisée du modèle hédonique de référence, 0,084 : aucun boosting ne l'atteint, et la \cref{sec:37:exemple} dira pourquoi.
123 +
124 +\begin{figure}[htbp]
125 +\centering
126 +\includegraphics[width=\textwidth]{ch37/ch37-fig-apprentissage.pdf}
127 +\caption{Courbes d'apprentissage du gradient boosting sur les 600 ventes du cas 1 : racine de l'erreur quadratique moyenne du logarithme du prix à l'entraînement (tiretés) et en validation croisée à dix plis (traits pleins) selon le nombre d'arbres, pour des arbres de profondeur 1, 2 et 4 ($\nu = 0{,}03$, sous-échantillon 80 \%, feuilles d'au moins cinq ventes). Le point marque le minimum de l'erreur de validation. Plus les arbres sont profonds, plus tôt le modèle surapprend ; aucune courbe n'atteint l'erreur du modèle hédonique de référence (pointillé).}\label{fig:37:apprentissage}
128 +\figsource{\synth{script \texttt{analysis/python/figures/ch37\_figures.py}, graine 20261031}.}
129 +\end{figure}
130 +
131 +\subsection{Les métriques}\label{sec:37:metriques}
132 +
133 +Les prédictions hors échantillon sont évaluées par des métriques qui répondent à des questions différentes. La racine de l'erreur quadratique moyenne en logarithme et l'erreur absolue médiane en pourcentage,
134 +\begin{equation}\label{eq:37:metriques}
135 +\mathrm{RMSE}_{\ln} = \sqrt{\frac{1}{n}\sum_{i=1}^{n}\big(\hat y_i - y_i\big)^{2}}, \qquad \mathrm{MdAPE} = \operatorname{méd}_i \abs{\frac{\hat P_i - P_i}{P_i}} ,
136 +\end{equation}
137 +mesurent respectivement la dispersion typique de l'erreur, dans l'unité qui rend les erreurs comparables entre maisons chères et modestes, et l'erreur que subit la vente médiane, insensible aux ventes aberrantes ; la première se compare directement à l'écart-type résiduel $\hat\sigma$ du modèle hédonique et au bruit de génération, la seconde est celle que publient la plupart des fournisseurs d'AVM. Les statistiques de ratio de l'IAAO, le coefficient de dispersion (COD), le différentiel relatif aux prix (PRD) et le biais relatif aux prix (PRB), définies aux \cref{eq:18:cod,eq:18:prd,eq:18:prb} et calculées par \texttt{traite.hedonique.statistiques\_ratio}, mesurent l'uniformité horizontale et verticale des ratios prédit/observé, et c'est en elles que le \emph{Standard on Automated Valuation Models} exprime ses exigences (\cref{sec:37:avm}). Les prédictions en niveau sont retransformées du logarithme avec la correction de Duan \eqref{eq:18:duan} ; pour les modèles d'arbres, dont les résidus d'apprentissage sont artificiellement petits, le facteur est estimé sur les résidus hors échantillon, ce qui donne 1,0034 pour l'hédonique, conforme au 1,00335 du \cref{ch:18}, et de 1,0020 à 1,0078 pour les autres modèles. Enfin, le \emph{biais moyen} en logarithme et la médiane des ratios détectent une erreur de niveau, que les métriques de dispersion ne voient pas et que la validation temporelle fera apparaître.
138 +
139 +\section{Explicabilité : valeurs de Shapley, dépendance partielle, permutation}\label{sec:37:explicabilite}
140 +
141 +\subsection{La valeur de Shapley}
142 +
143 +Un modèle d'arbres n'a pas de coefficients. Pour dire ce qu'il a appris et pourquoi il prédit telle valeur pour tel logement, il faut des outils d'\motcle{explicabilité} (\emph{explainability}) qui s'appliquent à toute fonction $\hat f$, quelle que soit sa forme. Le plus fondé théoriquement vient de la théorie des jeux coopératifs. Lloyd Shapley a posé en 1953 la question du partage équitable du gain d'une coalition entre ses membres\footnote{Shapley, L. S. (1953), « A Value for $n$-Person Games », dans Kuhn, H. W. et Tucker, A. W. (dir.), \emph{Contributions to the Theory of Games}, vol.~II, Annals of Mathematics Studies 28, Princeton University Press, p.~307-318 ; DOI 10.1515/9781400881970-018 (vérifié par l'API Crossref le 31 octobre 2026 ; \texttt{lib-ch37.bib}).} et a montré qu'une unique règle satisfait quatre axiomes.
144 +
145 +\begin{definition}{Valeur de Shapley}{37:shapley}
146 +Soit un ensemble $N$ de $p$ joueurs et une fonction caractéristique $v : 2^{N} \to \mathbb{R}$ donnant le gain de chaque coalition, avec $v(\varnothing) = 0$. La valeur de Shapley du joueur $j$ est sa contribution marginale moyenne sur toutes les façons d'ordonner les joueurs,
147 +\begin{equation}\label{eq:37:shapley}
148 +\phi_j(v) = \sum_{S \subseteq N \setminus \{j\}} \frac{\abs{S}!\,(p - \abs{S} - 1)!}{p!}\,\big[v(S \cup \{j\}) - v(S)\big].
149 +\end{equation}
150 +C'est l'unique règle de partage qui satisfait l'\emph{efficacité} ($\sum_j \phi_j = v(N)$), la \emph{symétrie} (deux joueurs interchangeables reçoivent la même part), le \emph{joueur nul} (un joueur qui n'ajoute rien à aucune coalition reçoit zéro) et l'\emph{additivité} (la part dans la somme de deux jeux est la somme des parts).
151 +\end{definition}
152 +
153 +\textcite{lundbergLee2017} ont montré que la plupart des méthodes d'explication locale proposées pour les modèles d'apprentissage sont des cas particuliers d'une même classe, les \emph{explications additives}, et que la seule qui respecte les axiomes de Shapley est celle où les joueurs sont les variables, le gain d'une coalition $S$ est la prédiction attendue lorsque seules les variables de $S$ sont connues, $v(S) = \E[\hat f(\mathbf{X}) \mid \mathbf{X}_S = \mathbf{x}_S]$, et la valeur de base est la prédiction moyenne. Les \motcle{valeurs SHAP} (\emph{SHapley Additive exPlanations}) $\phi_j(\mathbf{x})$ d'une observation décomposent alors sa prédiction :
154 +\begin{equation}\label{eq:37:efficacite}
155 +\hat f(\mathbf{x}) = \phi_0 + \sum_{j=1}^{p} \phi_j(\mathbf{x}), \qquad \phi_0 = \E[\hat f(\mathbf{X})] .
156 +\end{equation}
157 +Dans un modèle en logarithme du prix, $\phi_j$ est la contribution de la variable $j$, en points de logarithme, à l'écart entre la prédiction du logement et la prédiction moyenne ; $\exp(\phi_j) - 1$ est l'effet en pourcentage, et la cascade des $\exp(\phi_j)$ depuis $\exp(\phi_0)$ reconstitue le prix prédit. L'analogie avec le modèle hédonique est exacte : dans un modèle linéaire en $\mathbf{x}$, la valeur SHAP de la variable $j$ est $\hat\beta_j (x_j - \bar x_j)$, le prix implicite multiplié par l'écart à la moyenne, de sorte que SHAP généralise la décomposition « maison de référence plus contributions des écarts » du \cref{tab:18:prix-implicites} à des modèles sans coefficients. Le calcul exact de l'\cref{eq:37:shapley} exige $2^{p}$ coalitions, mais \textcite{lundberg2020} ont montré que, pour les modèles d'arbres, les valeurs SHAP se calculent exactement en temps polynomial en parcourant la structure des arbres : c'est l'algorithme \motcle{TreeSHAP}, qui fournit aussi les \emph{valeurs d'interaction} SHAP, décomposant chaque $\phi_j$ en un effet principal et des effets partagés avec chaque autre variable. Deux lectures globales en découlent : l'\emph{importance SHAP}, moyenne de $\abs{\phi_j}$ sur l'échantillon, qui classe les variables par leur contribution absolue moyenne ; et le \emph{graphique en essaim} (\emph{beeswarm}), qui porte pour chaque variable les $n$ valeurs $\phi_j$ colorées par la valeur de la variable, et montre donc à la fois l'ampleur et le sens de l'effet, ainsi que sa non-linéarité éventuelle. Pour un logement unique, la \emph{cascade} (\emph{waterfall}) ordonne les contributions depuis la base jusqu'à la prédiction ; c'est l'explication locale que l'évaluateur peut joindre à un rapport.
158 +
159 +\subsection{Dépendance partielle et importance par permutation}
160 +
161 +La \motcle{dépendance partielle} (\emph{partial dependence}), introduite par \textcite{friedman2001} avec le boosting, répond à une question différente : comment la prédiction varie-t-elle, en moyenne, lorsqu'une variable change et que les autres gardent leurs valeurs observées ?
162 +\begin{equation}\label{eq:37:pdp}
163 +\mathrm{PD}_j(z) = \frac{1}{n}\sum_{i=1}^{n} \hat f\big(z, \mathbf{x}_{i, -j}\big),
164 +\end{equation}
165 +où $\mathbf{x}_{i,-j}$ désigne les autres variables de l'observation $i$. La courbe $z \mapsto \mathrm{PD}_j(z)$ est l'analogue, pour un modèle quelconque, de la courbe de prix selon la superficie que le \cref{ch:18} a tracée pour quatre formes fonctionnelles (\cref{fig:18:formes}) ; pour un modèle additif, elle coïncide, à une constante près, avec la composante de la variable. Elle a deux limites, que \textcite{molnar2025} discute : elle moyenne, et peut donc masquer une interaction (deux sous-populations aux effets opposés donnent une courbe plate), ce à quoi remédient les courbes individuelles d'espérance conditionnelle (ICE), une par observation ; et elle évalue le modèle en des combinaisons de variables qui n'existent pas dans les données lorsque la variable $j$ est corrélée aux autres, par exemple une maison de \msq{260} sur un terrain de \msq{200}, ce qui est une forme d'extrapolation. L'\motcle{importance par permutation}, due aussi à \textcite{breiman2001}, mesure de combien l'erreur de prédiction augmente lorsque les valeurs d'une variable sont mélangées au hasard entre les observations, ce qui rompt son lien avec la cible tout en préservant sa distribution :
166 +\begin{equation}\label{eq:37:permutation}
167 +\mathrm{IP}_j = \E\big[L\big(Y, \hat f(\mathbf{X}^{(\pi_j)})\big)\big] - \E\big[L\big(Y, \hat f(\mathbf{X})\big)\big],
168 +\end{equation}
169 +où $\mathbf{X}^{(\pi_j)}$ est la matrice des régresseurs dont la colonne $j$ a été permutée. Elle est mesurée ici hors échantillon, dans chaque pli de validation, ce qui évite d'attribuer de l'importance à des variables que le modèle utilise pour mémoriser le bruit. Sa limite est la même que celle de la dépendance partielle : lorsque deux variables sont fortement corrélées, permuter l'une crée des observations impossibles et le modèle peut reporter l'information sur l'autre, de sorte que l'importance se partage entre elles de façon instable. L'exemple en donne une illustration nette : les coordonnées, la distance au centre et les indicatrices de secteur portent la même information de localisation, et le boosting l'attribue presque entièrement aux coordonnées, au point que les indicatrices de secteur, qui sont le mécanisme même de la loi de génération, reçoivent une importance nulle. L'explicabilité décrit ce que le modèle fait, pas ce que le marché fait ; la différence est au cœur de la \cref{sec:37:limites}.
170 +
171 +\section{Exemple intégral : hédonique, forêt et boosting sur les 600 ventes du cas 1}\label{sec:37:exemple}
172 +
173 +\begin{synthetique}
174 +Les 600 ventes du cas 1 (\texttt{data/synthetic/cas1\_gatineau\_ventes.csv}) sont fictives, engendrées par \texttt{analysis/python/traite/synth.py} (graine 20261031) selon la loi semi-logarithmique additive déclarée dans \texttt{data/synthetic/README.md} et rappelée au \cref{ch:18} : élasticité de 0,55 pour la superficie habitable, 0,10 pour le terrain, polynôme d'âge, effets proportionnels de la qualité et des équipements, primes de secteur calibrées sur les médianes de l'APCIQ \parencite{apciqgatineaut32026}, gradient de distance au-delà de 3 km, tendance temporelle, bruit normal d'écart-type 0,085. Aucune interaction ni rupture n'y figure : c'est ce qui rend l'hédonique bien spécifié imbattable en espérance et ce qui fait de la \cref{sec:37:scenario}, où une interaction et une rupture déclarées sont ajoutées, un contrepoint nécessaire. Les régresseurs offerts aux modèles d'arbres sont les variables brutes (superficie, terrain, âge, qualité, garage, sous-sol, piscine, type, distance au centre, latitude, longitude, mois depuis la date de référence, cinq indicatrices de secteur), sans transformation. Tous les chiffres de cette section et de la suivante sont imprimés par \texttt{analysis/python/cases/ch37\_ml.py} et vérifiés par \texttt{analysis/python/tests/test\_ch37.py} ; les modèles proviennent de scikit-learn 1.6 et de xgboost 3.2, les valeurs SHAP de shap 0.48 (TreeSHAP appliqué au gradient boosting de scikit-learn, la bibliothèque shap ne lisant pas encore le format des modèles de xgboost 3.2). Les plis aléatoires sont ceux de \texttt{ch18\_hedonique.validation\_croisee}.
175 +\end{synthetique}
176 +
177 +\begin{exemple}{Cinq modèles, trois validations, un sujet}{37:ml-cas1}
178 +L'exemple procède en cinq étapes : comparaison des modèles en validation croisée aléatoire ; validation spatiale et temporelle ; exclusion d'un secteur ; explication du boosting par SHAP, dépendance partielle et permutation ; prédiction du sujet par chaque modèle avec sa cascade SHAP.
179 +
180 +\emph{Étape 1 : validation croisée aléatoire.} Le \cref{tab:37:comparaison} rapporte, pour les cinq modèles et les mêmes dix plis, les métriques de la \cref{sec:37:metriques}. Le modèle hédonique de référence reproduit exactement les chiffres du \cref{ch:18} : racine de l'erreur quadratique moyenne de 0,0839 en logarithme et de \dollars{40889} en niveau, erreur absolue médiane de 5,7 \%, COD de 6,83, PRD de 1,007, PRB de $-0{,}059$. L'arbre seul est loin derrière (0,147 ; COD 11,6) : huit constantes ne décrivent pas un marché, et son COD sort même de la plage de 5 à 10 que l'IAAO admet pour l'unifamilial homogène. La forêt aléatoire réduit l'erreur à 0,107 (COD 8,64), ce qui est la réduction de variance de l'\cref{eq:37:variance-foret} à l'œuvre ; le gradient boosting fait mieux, 0,093 (COD 7,42), et XGBoost légèrement mieux encore, 0,092 (COD 7,33), la régularisation des feuilles l'aidant un peu. Mais aucun n'approche l'hédonique, et l'écart n'est pas un détail : l'erreur quadratique du meilleur modèle d'arbres est de 20 \% supérieure à celle de la régression, et son COD de plus d'un demi-point. La \cref{fig:37:erreurs} montre la distribution des erreurs et la \cref{fig:37:predit-observe} les nuages prédit-observé. Deux traits méritent l'attention au-delà des moyennes. D'abord la \emph{régressivité} : le PRB de la forêt est de $-0{,}196$ et celui du boosting de $-0{,}088$, contre $-0{,}059$ pour l'hédonique, et les PRD sont de 1,015 et 1,009 ; les modèles d'arbres surestiment les maisons modestes et sous-estiment les maisons chères, parce que leurs feuilles sont des moyennes locales qui tirent les extrêmes vers le centre, et parce qu'ils ne peuvent prédire au-delà du plus grand prix observé dans une feuille. Le \cref{ch:18} a montré qu'une part du PRB négatif est mécanique ; la part supplémentaire est ici propre aux arbres, et c'est précisément le genre d'iniquité verticale que le Standard on Ratio Studies demande de surveiller. Ensuite le niveau : les médianes des ratios sont toutes comprises entre 0,998 et 1,012, donc aucun modèle n'est biaisé en moyenne dans ce schéma, ce qui ne sera plus vrai en validation temporelle.
181 +
182 +\emph{Étape 2 : validation spatiale et temporelle.} Les colonnes de droite du \cref{tab:37:comparaison} rapportent les deux schémas qui respectent la structure des données. En blocs spatiaux stratifiés par secteur, chaque vente est prédite par un modèle qui n'a vu aucune vente de son demi-secteur ; toutes les erreurs augmentent, mais inégalement : l'hédonique passe de 0,0839 à 0,0857 (COD 7,00), parce que ses indicatrices de secteur restent estimées sur l'autre moitié du secteur et que sa structure additive ne dépend pas du voisinage ; le boosting passe de 0,093 à 0,097 (COD 7,62) et la forêt de 0,107 à 0,112 (COD 8,96), parce que ces modèles avaient tiré une partie de leur performance aléatoire du voisinage immédiat, par les coordonnées, information qui leur est désormais retirée. La validation aléatoire surestimait donc légèrement leur performance, comme l'annonçait la \cref{sec:37:validation}. La partition temporelle est plus sévère encore et révèle une faiblesse structurelle. Les 101 ventes de 2026 prédites par des modèles estimés sur 2023-2025 ont, avec l'hédonique, une erreur de 0,0855 et un biais moyen de $-1{,}0$ \% (médiane des ratios 0,988) : la tendance linéaire estimée sur le passé se prolonge et, dans ce jeu, elle est vraie. Les modèles d'arbres, eux, ne peuvent pas extrapoler : au-delà du dernier mois observé, ils prédisent la constante de la feuille extrême, et leur biais est de $-3{,}4$ \% pour le boosting, $-6{,}0$ \% pour la forêt et $-8{,}5$ \% pour l'arbre, avec des médianes de ratios de 0,966, 0,934 et 0,931, sous la plage de 0,90 à 1,10 pour l'arbre et très près de sa borne pour la forêt. Le biais s'aggrave au fil des mois : pour le boosting, il va de $-4{,}2$ \% en janvier 2026 à $-7{,}6$ \% en août, tandis que celui de l'hédonique oscille entre $-3$ et $+1$ \% sans tendance. Deux remèdes s'imposent et le \cref{tab:37:temporel} les mesure. Le premier est de retirer le temps du modèle d'arbres et de l'estimer séparément : un boosting estimé sur $\ln P - \hat\beta_{\text{mois}}\,\text{mois}$, la tendance étant celle de l'hédonique estimé sur l'échantillon d'apprentissage (0,342 \% par mois), puis la tendance rajoutée, ramène le biais à $-1{,}8$ \% et l'erreur à 0,093 ; c'est la logique des AVM de production, qui séparent l'indice de marché du modèle de niveau (\cref{ch:35}). Le second est de régler la taille minimale des feuilles : avec des feuilles d'une seule vente, la feuille extrême du temps est estimée sur une poignée d'observations, quatorze ventes se situent au-delà du dix-septième mois dans l'échantillon d'apprentissage, et le biais du boosting sur 2026 atteint $-13{,}4$ \% ; avec des feuilles d'au moins cinq ventes, celui du modèle de référence, il est de $-3{,}4$ \%. Un hyperparamètre qui ne change presque rien en validation aléatoire (0,0931 dans les deux cas, \cref{tab:37:sensibilite}) change tout au bord du support.
183 +
184 +\emph{Étape 3 : un secteur exclu.} Le \cref{tab:37:secteurs} pousse la logique spatiale à l'extrême en retirant un secteur entier à l'estimation et en lui demandant une prédiction. Le modèle hédonique n'a, par construction, aucune information sur un secteur absent : son indicatrice est retirée et le secteur est prédit au niveau du secteur de référence (Hull, ou Gatineau lorsque c'est Hull qui est exclu), ce qui est un \emph{jugement} de l'analyste, non un résultat du modèle. Les biais qui en résultent sont exactement les primes de secteur omises : $+23{,}4$ \% pour Buckingham et $+26{,}1$ \% pour Masson-Angers, dont les prix sont bien inférieurs à ceux de Hull, $-5{,}9$ \% pour Aylmer, et presque rien pour Gatineau, dont la médiane est proche de celle de Hull ; le COD global monte à 10,44. Les modèles d'arbres, qui disposent des coordonnées et de la distance au centre, font implicitement le jugement que l'analyste a dû faire explicitement : ils situent Masson-Angers près de Buckingham et à grande distance du centre, et prédisent un niveau intermédiaire. Le boosting a un biais de $-0{,}6$ \% sur Buckingham et de $+7{,}2$ \% sur Masson-Angers, mais de $-6{,}8$ \% sur Gatineau et $-7{,}1$ \% sur Aylmer, que leur position géographique n'aide pas à distinguer de leurs voisins moins chers ; son COD global est de 8,59, meilleur que celui de l'hédonique dans ce schéma, et celui de la forêt de 10,21. Aucun modèle n'est bon : prédire un sous-marché sans y observer de vente est une extrapolation, et le mieux que l'on puisse dire du boosting est que son extrapolation géographique est moins fausse, ici, que le choix d'un secteur de référence. L'évaluateur retiendra que la question « ce modèle peut-il prédire là où il n'a pas de données ? » appelle une réponse par la validation, non par la confiance, et que la réponse dépend du modèle, du secteur et de la géographie des prix.
185 +
186 +\emph{Étape 4 : expliquer le boosting.} La \cref{fig:37:beeswarm} présente le graphique en essaim des valeurs SHAP du gradient boosting estimé sur les 600 ventes, et le \cref{tab:37:importances} compare l'importance SHAP et l'importance par permutation. La base $\phi_0$ vaut 13,0732, soit la moyenne des logarithmes des prix, dont l'exponentielle est \dollars{476010} : c'est la « vente moyenne » du jeu, une maison de \msq{145} et 42 ans vendue au printemps 2024. La superficie domine (importance moyenne de 0,111 en logarithme), suivie de l'âge (0,043), du mois de vente (0,040), de la longitude (0,032), de la qualité (0,031), de la distance au centre (0,027) et de la latitude (0,027) ; le terrain, le garage, le type et le sous-sol suivent ; la piscine est faible et les cinq indicatrices de secteur sont pratiquement nulles (au plus 0,0016). Ce dernier résultat est instructif : la loi de génération contient des primes de secteur de $-20$ à $+11$ \%, et le modèle les a parfaitement apprises, mais il les a lues dans les coordonnées et la distance au centre, qui portent la même information de façon continue et se prêtent mieux aux divisions binaires ; l'importance d'une variable mesure son usage par le modèle, non son rôle dans le marché, et deux variables redondantes se partagent l'importance de façon arbitraire. La colonne de permutation confirme le classement, avec une concentration encore plus marquée sur la superficie (accroissement de l'erreur quadratique de 0,0348, quatre fois l'erreur de base de 0,0087), ce qui signifie que la superficie est la seule variable sans substitut dans le modèle. L'essaim montre le sens des effets : les grandes superficies à droite, les âges élevés à gauche, les ventes récentes à droite ; la coloration de la longitude est révélatrice, les longitudes les plus à l'est (Buckingham et Masson-Angers) ayant des valeurs SHAP négatives et les plus à l'ouest (Aylmer) positives, de sorte que la variable « longitude » est devenue, dans ce modèle, le codage de la prime de secteur. La \cref{fig:37:pdp} trace la dépendance partielle de la superficie et de l'âge pour l'hédonique, la forêt, le boosting et la loi de génération elle-même, toutes centrées. Pour la superficie, les trois modèles suivent la courbe vraie à moins de quatre points de logarithme près sur tout le support ; la forêt s'aplatit aux extrémités, au-dessous de \msq{100} et au-dessus de \msq{220}, où elle n'a presque plus de ventes et où ses feuilles moyennent des maisons de tailles différentes, ce qui est la source de sa régressivité. Pour l'âge, le boosting reproduit la courbe en U très ouvert du polynôme de génération, mais par paliers, et la forêt la lisse davantage. L'explicabilité joue ici son rôle de diagnostic : elle montre que le boosting a appris la bonne forme, qu'il n'a trouvé aucune interaction notable (la plus forte valeur d'interaction SHAP moyenne, superficie et latitude, vaut 0,003, soit un trentième de l'effet principal de la superficie), et donc qu'un modèle additif en logarithme est suffisant pour ce marché. C'est exactement la conclusion que le \cref{ch:18} tirait de ses diagnostics, obtenue ici sans écrire d'équation.
187 +
188 +\emph{Étape 5 : le sujet.} Le \cref{tab:37:sujet} rapporte la prédiction du sujet du cas 1, le cottage de \msq{165} construit en 1998 à Hull évalué au 1\ier{} septembre 2026, par chaque modèle estimé sur les 600 ventes. L'hédonique prédit \dollars{637953} avec la correction de Duan en échantillon du \cref{ch:18} (\dollars{638011} avec le facteur hors échantillon de ce chapitre) ; XGBoost prédit \dollars{613976}, le boosting \dollars{605825}, la forêt \dollars{586818} et l'arbre \dollars{549573}. Tous les modèles d'arbres se situent sous l'hédonique, de 3,8 à 13,9 \%, et l'ordre est celui de la souplesse des modèles. Deux mécanismes expliquent l'écart. Le premier est la compression vers la moyenne déjà vue : le sujet est une maison au-dessus de la médiane du marché (\dollars{479250}), vendue à la date la plus récente du jeu, et les feuilles qui le concernent moyennent des maisons un peu plus petites et un peu plus anciennes. Le second est temporel : la date d'évaluation, septembre 2026, est postérieure d'un mois à la dernière vente observée, et les modèles d'arbres lui appliquent la valeur de la feuille extrême du temps, où l'hédonique prolonge sa tendance ; la \cref{sec:37:exemple} a mesuré à quelques points de pourcentage ce que cette différence coûte sur les ventes de 2026. La \cref{fig:37:waterfall} donne la cascade SHAP du sujet pour le boosting, et le \cref{tab:37:sujet} en détaille les contributions. Partant de la vente moyenne à \dollars{476010}, la date de vente ajoute $+10{,}4$ \% ($\phi = 0{,}0986$), la superficie $+9{,}1$ \% (0,0871), l'âge, inférieur à la moyenne de 42 ans, $+3{,}3$ \% (0,0322), la longitude, celle de Hull, $+1{,}3$ \%, le garage, le type à étages et le sous-sol fini de $+0{,}6$ à $+0{,}8$ \% chacun, tandis que le terrain de \msq{620}, inférieur à la moyenne de \msq{683}, retranche $1{,}3$ \% ; la qualité moyenne, la latitude, l'absence de piscine et les indicatrices de secteur contribuent ensemble pour moins de 0,1 \%. La somme des $\phi_j$ est de 0,2391, et $\exp(13{,}0732 + 0{,}2391) = \dollars{604595}$ est la prédiction brute, que le facteur de Duan de 1,0020 porte à \dollars{605825} : l'axiome d'efficacité est vérifié à $10^{-13}$ près par le script. La comparaison avec le \cref{tab:18:prix-implicites} est éclairante. Le modèle hédonique dit que le garage \emph{vaut} \dollars{21031} au sujet, parce que son coefficient est un prix implicite marginal ; la cascade SHAP dit que le garage \emph{contribue} pour $+0{,}8$ \%, soit environ \dollars{4800}, à l'écart entre le sujet et la vente moyenne, parce que la vente moyenne a déjà 0,7 place de garage et que la contribution est mesurée par rapport à elle. Les deux lectures sont correctes et ne répondent pas à la même question ; confondre une valeur SHAP avec un ajustement de grille est l'erreur la plus fréquente dans l'usage de ces outils en évaluation.
189 +\end{exemple}
190 +
191 +\begin{table}[htbp]
192 +\caption{Cinq modèles sur les 600 ventes du cas 1 : performance hors échantillon selon trois schémas de validation (données synthétiques)}\label{tab:37:comparaison}
193 +\centering\footnotesize\setlength{\tabcolsep}{3pt}
194 +\begin{tabular}{@{}l S[table-format=1.4] S[table-format=5.0] S[table-format=2.2] S[table-format=2.2] S[table-format=1.3] S[table-format=-1.3] S[table-format=1.4] S[table-format=2.2] S[table-format=1.4] S[table-format=-1.3] S[table-format=2.2]@{}}
195 +\toprule
196 + & \multicolumn{6}{c}{Plis aléatoires (10)} & \multicolumn{2}{c}{Blocs spatiaux (10)} & \multicolumn{3}{c}{Prospectif (2026, $n = 101$)} \\
197 +\cmidrule(lr){2-7}\cmidrule(lr){8-9}\cmidrule(lr){10-12}
198 +Modèle & {RMSE$_{\ln}$} & {RMSE (\$)} & {MdAPE (\%)} & {COD} & {PRD} & {PRB} & {RMSE$_{\ln}$} & {COD} & {RMSE$_{\ln}$} & {Biais} & {COD} \\
199 +\midrule
200 +Hédonique (référence) & 0.0839 & 40889 & 5.68 & 6.83 & 1.007 & -0.059 & 0.0857 & 7.00 & 0.0855 & -0.010 & 6.96 \\
201 +Arbre (profondeur 3) & 0.1468 & 70779 & 9.85 & 11.63 & 1.021 & -0.228 & 0.1537 & 12.12 & 0.1737 & -0.085 & 12.35 \\
202 +Forêt aléatoire & 0.1067 & 52058 & 7.52 & 8.64 & 1.015 & -0.196 & 0.1120 & 8.96 & 0.1283 & -0.060 & 9.47 \\
203 +Gradient boosting & 0.0931 & 45644 & 6.22 & 7.42 & 1.009 & -0.088 & 0.0967 & 7.62 & 0.0950 & -0.034 & 7.41 \\
204 +XGBoost & 0.0921 & 45221 & 6.09 & 7.33 & 1.009 & -0.081 & 0.0948 & 7.53 & 0.0937 & -0.029 & 7.50 \\
205 +\bottomrule
206 +\end{tabular}
207 +\tabsource{\synth{\texttt{analysis/python/cases/ch37\_ml.py} ; plis aléatoires identiques à ceux du \cref{ch:18}}. RMSE$_{\ln}$ : racine de l'erreur quadratique moyenne du logarithme ; RMSE (\$) après retransformation de Duan (facteur hors échantillon) ; biais : erreur moyenne du logarithme. Hyperparamètres au \cref{tab:37:familles}.}
208 +\end{table}
209 +
210 +\begin{figure}[htbp]
211 +\centering
212 +\includegraphics[width=\textwidth]{ch37/ch37-fig-erreurs.pdf}
213 +\caption{Distribution des erreurs de prédiction hors échantillon (en pourcentage du prix observé) des cinq modèles, pour les trois schémas de validation ; le COD figure au-dessus de chaque boîte. Les boîtes s'élargissent de l'hédonique à l'arbre ; en validation prospective, les médianes des modèles d'arbres glissent sous zéro parce qu'ils ne prolongent pas la tendance du marché.}\label{fig:37:erreurs}
214 +\figsource{\synth{script \texttt{analysis/python/figures/ch37\_figures.py}, graine 20261031}.}
215 +\end{figure}
216 +
217 +\begin{figure}[htbp]
218 +\centering
219 +\includegraphics[width=\textwidth]{ch37/ch37-fig-predit-observe.pdf}
220 +\caption{Prix prédit hors échantillon (plis aléatoires, correction de Duan) contre prix observé pour l'hédonique de référence, la forêt aléatoire et le gradient boosting, avec la diagonale et les bandes à $\pm 18$ \%. Le nuage de la forêt est le plus large et s'incurve aux extrémités, signe de la compression des prédictions vers la moyenne que mesure son PRB.}\label{fig:37:predit-observe}
221 +\figsource{\synth{script \texttt{analysis/python/figures/ch37\_figures.py}, graine 20261031}.}
222 +\end{figure}
223 +
224 +\begin{table}[htbp]
225 +\caption{Validation prospective (ventes de 2026 prédites à partir de 2023-2025) : l'extrapolation temporelle et ses remèdes (données synthétiques)}\label{tab:37:temporel}
226 +\centering\small
227 +\begin{tabularx}{\textwidth}{@{}Y S[table-format=1.4] S[table-format=-2.1] S[table-format=1.3] S[table-format=2.2]@{}}
228 +\toprule
229 +Modèle ($n_{\text{test}} = 101$) & {RMSE$_{\ln}$} & {Biais (\%)} & {Ratio médian} & {COD} \\
230 +\midrule
231 +Hédonique de référence (tendance linéaire prolongée) & 0.0855 & -1.0 & 0.988 & 6.96 \\
232 +Gradient boosting, feuilles $\ge 5$ (référence) & 0.0950 & -3.4 & 0.966 & 7.41 \\
233 +Gradient boosting, feuilles $\ge 1$ & 0.1631 & -13.4 & {---} & 8.02 \\
234 +Gradient boosting détendancé ($\ln P - \hat\beta_{\text{mois}}\,\text{mois}$, tendance rajoutée) & 0.0928 & -1.8 & {---} & 7.44 \\
235 +Forêt aléatoire & 0.1283 & -6.0 & 0.934 & 9.47 \\
236 +Arbre (profondeur 3) & 0.1737 & -8.5 & 0.931 & 12.35 \\
237 +\bottomrule
238 +\end{tabularx}
239 +\tabsource{\synth{\texttt{analysis/python/cases/ch37\_ml.py}}. Biais : erreur moyenne du logarithme, en points de pourcentage ; $\hat\beta_{\text{mois}} = 0{,}00342$ estimé sur les 499 ventes de 2023-2025 ; 14 ventes d'apprentissage sont postérieures au dix-septième mois.}
240 +\end{table}
241 +
242 +\begin{table}[htbp]
243 +\caption{Un secteur exclu à la fois : biais moyen de la prédiction (points de pourcentage du logarithme) et COD global (données synthétiques)}\label{tab:37:secteurs}
244 +\centering\small
245 +\begin{tabular}{@{}l S[table-format=-2.1] S[table-format=-2.1] S[table-format=-2.1] S[table-format=-2.1] S[table-format=-2.1] S[table-format=2.2]@{}}
246 +\toprule
247 +Modèle & {Hull} & {Gatineau} & {Aylmer} & {Buckingham} & {Masson-Angers} & {COD} \\
248 +\midrule
249 +Hédonique (secteur exclu prédit au niveau de référence) & -2.3 & 1.1 & -5.9 & 23.4 & 26.1 & 10.44 \\
250 +Arbre (profondeur 3) & -1.2 & -20.7 & -6.7 & -2.5 & 12.9 & 15.65 \\
251 +Forêt aléatoire & -1.0 & -4.9 & -8.5 & 2.5 & 12.8 & 10.21 \\
252 +Gradient boosting & 1.3 & -6.8 & -7.1 & -0.6 & 7.2 & 8.59 \\
253 +XGBoost & 1.0 & -6.7 & -8.3 & -2.2 & 10.2 & 8.87 \\
254 +\midrule
255 +Prime de secteur vraie (log, par rapport à Hull) & 0.0 & -2.1 & 11.0 & -18.4 & -20.4 & {} \\
256 +\bottomrule
257 +\end{tabular}
258 +\tabsource{\synth{\texttt{analysis/python/cases/ch37\_ml.py}}. Hédonique : l'indicatrice du secteur exclu est retirée et le secteur est prédit comme Hull (ou comme Gatineau lorsque Hull est exclu) ; les biais de Buckingham et de Masson-Angers sont, au bruit près, l'opposé des primes omises.}
259 +\end{table}
260 +
261 +\begin{figure}[htbp]
262 +\centering
263 +\includegraphics[width=\textwidth]{ch37/ch37-fig-shap-beeswarm.pdf}
264 +\caption{Valeurs SHAP (TreeSHAP) des 600 ventes pour le gradient boosting estimé sur l'ensemble du jeu, onze variables les plus importantes, classées par la moyenne de $\abs{\phi}$ (colonne de droite, en points de pourcentage du logarithme) ; chaque point est une vente, colorée selon la valeur de la variable. La superficie domine ; la longitude et la distance au centre ont absorbé les primes de secteur, dont les indicatrices n'apparaissent pas.}\label{fig:37:beeswarm}
265 +\figsource{\synth{script \texttt{analysis/python/figures/ch37\_figures.py}, graine 20261031}.}
266 +\end{figure}
267 +
268 +\begin{table}[htbp]
269 +\caption{Importance des variables dans le gradient boosting : moyenne de $\abs{\phi}$ (SHAP) et importance par permutation hors échantillon (données synthétiques)}\label{tab:37:importances}
270 +\centering\small
271 +\begin{tabular}{@{}l S[table-format=1.4] S[table-format=1.4] S[table-format=1.4] l@{}}
272 +\toprule
273 +Variable & {SHAP, moy. $\abs{\phi}$} & {Permutation, $\Delta$EQM} & {Forêt : $\Delta$EQM} & Lecture \\
274 +\midrule
275 +Superficie habitable & 0.1110 & 0.0348 & 0.0300 & dominante et sans substitut \\
276 +Âge & 0.0432 & 0.0061 & 0.0037 & courbe en U très ouvert \\
277 +Mois de vente & 0.0395 & 0.0046 & 0.0033 & tendance, apprise par paliers \\
278 +Longitude & 0.0323 & 0.0042 & 0.0027 & codage est-ouest des secteurs \\
279 +Qualité & 0.0312 & 0.0042 & 0.0013 & effet proportionnel \\
280 +Distance au centre & 0.0274 & 0.0028 & 0.0023 & redondante avec les coordonnées \\
281 +Latitude & 0.0271 & 0.0039 & 0.0031 & idem \\
282 +Terrain & 0.0204 & 0.0019 & 0.0013 & élasticité faible \\
283 +Garage & 0.0154 & {$<$ 0,002} & {$<$ 0,002} & \\
284 +Type à étages ; sous-sol ; piscine & {0,0097 ; 0,0076 ; 0,0038} & {$<$ 0,002} & {$<$ 0,002} & \\
285 +Indicatrices de secteur (5) & {$\le$ 0,0016} & {$\approx$ 0} & {$\approx$ 0} & absorbées par lat., long., distance \\
286 +\bottomrule
287 +\end{tabular}
288 +\tabsource{\synth{\texttt{analysis/python/cases/ch37\_ml.py}}. Permutation : accroissement de l'erreur quadratique moyenne du logarithme en validation croisée à dix plis, dix permutations par pli ; erreur de base 0,0087 (boosting) et 0,0114 (forêt).}
289 +\end{table}
290 +
291 +\begin{figure}[htbp]
292 +\centering
293 +\includegraphics[width=\textwidth]{ch37/ch37-fig-pdp.pdf}
294 +\caption{Dépendance partielle du logarithme du prix, centrée, selon la superficie habitable (gauche) et l'âge (centre) pour l'hédonique de référence, la forêt aléatoire, le gradient boosting et la loi de génération ; à droite, l'âge dans le scénario non linéaire de la \cref{sec:37:scenario}, où la loi contient un « mur » de $-10$ \% entre 25 et 45 ans et une prime de $+12$ \% pour les maisons de qualité élevée de plus de 40 ans : le boosting suit la rupture, le polynôme hédonique ne le peut pas. Les traits sous les courbes marquent les vingtiles de la distribution observée.}\label{fig:37:pdp}
295 +\figsource{\synth{script \texttt{analysis/python/figures/ch37\_figures.py}, graine 20261031}.}
296 +\end{figure}
297 +
298 +\begin{table}[htbp]
299 +\caption{Prédiction du sujet du cas 1 par chaque modèle et cascade SHAP du gradient boosting (données synthétiques)}\label{tab:37:sujet}
300 +\centering\small\setlength{\tabcolsep}{4pt}
301 +\begin{tabular}{@{}l S[table-format=2.4] S[table-format=6.0] S[table-format=1.4] S[table-format=6.0] S[table-format=-2.1]@{}}
302 +\toprule
303 +Modèle (estimé sur 600 ventes) & {$\ln \hat P$} & {$\exp(\ln\hat P)$ (\$)} & {Facteur} & {Prédiction (\$)} & {Écart à l'hédonique (\%)} \\
304 +\midrule
305 +Hédonique (référence, \cref{ch:18}) & 13.3627 & 635821 & 1.0034 & 637953 & 0.0 \\
306 +XGBoost & 13.3244 & 611957 & 1.0033 & 613976 & -3.8 \\
307 +Gradient boosting & 13.3123 & 604595 & 1.0020 & 605825 & -5.0 \\
308 +Forêt aléatoire & 13.2776 & 583938 & 1.0049 & 586818 & -8.0 \\
309 +Arbre (profondeur 3) & 13.2092 & 545332 & 1.0078 & 549573 & -13.9 \\
310 +\midrule
311 +\multicolumn{6}{@{}l}{\emph{Cascade SHAP du gradient boosting} (base $\phi_0 = 13{,}0732$, soit \dollars{476010})} \\
312 +Contribution & {$\phi_j$} & {Effet (\%)} & {} & {Cumul (\$)} & {} \\
313 +\midrule
314 +Mois de vente (septembre 2026) & 0.0986 & 10.4 & {} & 525340 & {} \\
315 +Superficie habitable (165 m²) & 0.0871 & 9.1 & {} & 573148 & {} \\
316 +Âge (28 ans) & 0.0322 & 3.3 & {} & 591904 & {} \\
317 +Longitude (Hull) & 0.0132 & 1.3 & {} & 599769 & {} \\
318 +Terrain (620 m²) & -0.0127 & -1.3 & {} & 592200 & {} \\
319 +Garage (1 place) & 0.0081 & 0.8 & {} & 597016 & {} \\
320 +Type à étages & 0.0075 & 0.8 & {} & 601511 & {} \\
321 +Sous-sol fini & 0.0056 & 0.6 & {} & 604889 & {} \\
322 +Autres (qualité, latitude, piscine, distance, secteurs) & -0.0005 & -0.1 & {} & 604595 & {} \\
323 +\bottomrule
324 +\end{tabular}
325 +\tabsource{\synth{\texttt{analysis/python/cases/ch37\_ml.py}}. Facteur : correction de Duan (en échantillon pour l'hédonique, comme au \cref{ch:18} ; hors échantillon pour les autres). Effet : $100\,[\exp(\phi_j) - 1]$ ; cumul : $\exp(\phi_0 + \sum \phi)$ après chaque contribution, avant le facteur de Duan.}
326 +\end{table}
327 +
328 +\begin{figure}[htbp]
329 +\centering
330 +\includegraphics[width=\textwidth]{ch37/ch37-fig-waterfall.pdf}
331 +\caption{Cascade SHAP de la prédiction du sujet du cas 1 par le gradient boosting, en dollars : depuis la vente moyenne du jeu (base, \dollars{476010}), chaque variable multiplie le prix par $\exp(\phi_j)$ ; la date d'évaluation et la superficie expliquent l'essentiel de l'écart, le terrain plus petit que la moyenne retranche 1,3 \%. La prédiction brute, \dollars{604595}, reçoit ensuite le facteur de Duan.}\label{fig:37:waterfall}
332 +\figsource{\synth{script \texttt{analysis/python/figures/ch37\_figures.py}, graine 20261031}.}
333 +\end{figure}
334 +
335 +\section{Quand l'apprentissage gagne : une non-linéarité déclarée}\label{sec:37:scenario}
336 +
337 +L'exemple intégral enseigne qu'un modèle économétrique bien spécifié est la référence à battre, non l'inverse, et qu'un modèle souple paie en variance la souplesse dont il n'a pas l'usage. Il faut aussitôt montrer le cas contraire, celui où la forme vraie n'est pas celle que l'analyste a écrite, sans quoi la leçon serait fausse. Pour cela, le script ajoute à la loi de génération deux termes déclarés, choisis pour être plausibles et impossibles à représenter dans la spécification du \cref{ch:18} : une \emph{prime « patrimoine rénové »} de $+0{,}12$ en logarithme pour les maisons de qualité 4 ou 5 âgées de 40 ans et plus, soit une interaction entre la qualité et l'âge, qui touche 76 ventes ; et un \emph{« mur de la mi-vie »} de $-0{,}10$ pour les maisons de 25 à 45 ans, une rupture qui représente la période où toiture, fenêtres et systèmes arrivent en fin de vie sans que la maison soit encore « ancienne », et qui touche 235 ventes. Le nouveau logarithme du prix est $\ln P^{\dagger} = \ln P + 0{,}12\cdot\mathbb{1}[\text{qualité} \ge 4,\ \text{âge} \ge 40] - 0{,}10\cdot\mathbb{1}[25 \le \text{âge} \le 45]$, et les mêmes modèles sont réestimés sur les mêmes dix plis.
338 +
339 +\begin{table}[htbp]
340 +\caption{Scénario non linéaire : validation croisée aléatoire des modèles sur $\ln P^{\dagger}$ (données synthétiques perturbées par une interaction et une rupture déclarées)}\label{tab:37:scenario}
341 +\centering\small
342 +\begin{tabularx}{\textwidth}{@{}Y S[table-format=1.4] S[table-format=1.2] S[table-format=1.2] S[table-format=-1.3]@{}}
343 +\toprule
344 +Modèle & {RMSE$_{\ln}$} & {MdAPE (\%)} & {COD} & {PRB} \\
345 +\midrule
346 +Hédonique de référence (spécification du \cref{ch:18}, désormais mal spécifiée) & 0.1003 & 6.96 & 8.15 & -0.079 \\
347 +Forêt aléatoire & 0.1145 & 7.54 & 9.17 & -0.208 \\
348 +Gradient boosting (profondeur 2) & 0.0944 & 6.26 & 7.46 & -0.083 \\
349 +XGBoost (profondeur 2) & 0.0932 & 6.17 & 7.40 & -0.081 \\
350 +Hédonique respécifié (deux indicatrices ajoutées) & 0.0841 & 5.62 & 6.85 & -0.054 \\
351 +\bottomrule
352 +\end{tabularx}
353 +\tabsource{\synth{\texttt{analysis/python/cases/ch37\_ml.py}}. Coefficients du modèle respécifié : prime 0,129 (e.-t. HC1 0,012 ; vrai 0,12), mur $-0{,}092$ (0,008 ; vrai $-0{,}10$) ; $\hat\sigma = 0{,}083$, $R^2 = 0{,}864$.}
354 +\end{table}
355 +
356 +Le \cref{tab:37:scenario} renverse le classement. L'hédonique de référence, dont le polynôme d'âge ne peut ni creuser un mur de 20 ans ni réserver une prime aux maisons de qualité élevée, voit son erreur monter de 0,084 à 0,100 et son COD de 6,83 à 8,15 ; les deux algorithmes de boosting, dont les arbres de profondeur 2 représentent exactement une interaction de deux variables et des seuils sur l'âge, restent à 0,094 et 0,093 (COD 7,46 et 7,40) et battent désormais la régression de sept dixièmes de point de COD ; la forêt, trop lissée, reste derrière. La \cref{fig:37:pdp} (droite) montre pourquoi : la dépendance partielle de l'âge du boosting dessine le creux entre 25 et 45 ans que la loi de génération contient, tandis que le polynôme de l'hédonique passe en travers. L'interaction est tout aussi visible dans les valeurs d'interaction SHAP, où la paire âge-qualité, absente du classement dans le cas de base, devient la première avec une valeur moyenne de 0,0068, le double de la suivante. C'est ici que l'explicabilité prend sa pleine utilité pour l'évaluateur : elle ne sert pas seulement à justifier la prédiction d'une boîte noire, elle indique \emph{quoi} ajouter au modèle économétrique. La dernière ligne du tableau en est la preuve. L'hédonique respécifié avec les deux indicatrices que la dépendance partielle et les interactions SHAP suggèrent retrouve une erreur de 0,084 et un COD de 6,85, identiques à ceux du cas de base, et estime la prime à 0,129 (erreur-type 0,012) et le mur à $-0{,}092$ (0,008), les deux paramètres vrais, 0,12 et $-0{,}10$, étant dans les intervalles de confiance. Il bat à nouveau le boosting, pour la même raison qu'à la \cref{sec:37:exemple} : une fois la forme connue, l'estimateur paramétrique n'a plus de biais et a la plus petite variance. L'enseignement est donc double. Sur un marché dont la structure est bien comprise et additive en logarithme, la régression hédonique soigneusement spécifiée et diagnostiquée demeure le meilleur outil, et de loin le plus défendable. Lorsque la structure est inconnue, non linéaire ou traversée d'interactions, ce qui est fréquent sur des marchés hétérogènes ou des types d'immeubles mal documentés, le boosting la découvre sans qu'on la lui décrive, et l'explicabilité permet d'en faire une hypothèse économétrique testable. \textcite{mayerBourassaHoesliScognamiglio2019} sont parvenus à une conclusion voisine en comparant, sur un grand échantillon suisse, des méthodes d'estimation et de mise à jour hédoniques au gradient boosting : le boosting l'emporte en précision lorsque les données sont riches et la structure complexe, et les modèles économétriques restent compétitifs et plus stables dans le temps. \textcite{mccluskey2013} avaient, plus tôt, mis en garde contre la généralisation hâtive d'un gain de précision obtenu sur un seul jeu : la comparaison doit être refaite sur chaque marché, avec une validation adaptée à l'usage.
357 +
358 +\section{Les modèles d'évaluation automatisés et les normes}\label{sec:37:avm}
359 +
360 +Un \motcle{modèle d'évaluation automatisé} (AVM, \emph{automated valuation model}) est un système qui produit une estimation de valeur à partir d'une base de données et d'un modèle statistique, sans inspection ni intervention humaine sur chaque immeuble ; les méthodes de ce chapitre en sont aujourd'hui le moteur le plus courant, aux côtés des modèles hédoniques et des modèles de comparables automatisés. L'IAAO leur consacre une norme spécifique, le \emph{Standard on Automated Valuation Models}, révisée en 2018 \parencite{iaaoavm2018}, qui complète le \emph{Standard on Mass Appraisal of Real Property}, révisé en 2025 \parencite{iaaomass2025}, et qui renvoie au \emph{Standard on Ratio Studies} \parencite{iaaoratio2013} pour les seuils de qualité. Sans reproduire ces documents, protégés, on peut en résumer la logique telle qu'elle a été consultée le 31 octobre 2026. La norme sur les AVM traite de la qualité des données d'entrée et de leur nettoyage (\cref{ch:14}), de la spécification et de la calibration du modèle, de sa documentation, et surtout de l'évaluation de sa performance, pour laquelle elle recommande les statistiques de ratio, le COD comme mesure de variabilité et le PRD et le PRB, assortis d'intervalles de confiance, comme mesures d'équité verticale ; elle insiste sur le fait que ces statistiques doivent être calculées sur des ventes que le modèle n'a pas utilisées pour sa calibration, ce qui est la validation hors échantillon de la \cref{sec:37:validation}, et sur la nécessité de les stratifier par type d'immeuble, par secteur et par tranche de valeur. Les seuils sont ceux du \cref{tab:18:ratios} : un COD de 5,0 à 10,0 pour l'unifamilial de secteurs homogènes, de 5,0 à 15,0 pour les secteurs hétérogènes, un PRD de 0,98 à 1,03 et un PRB entre $-0{,}05$ et $+0{,}05$. Mesurés à cette aune, les résultats de l'exemple sont parlants : tous les modèles d'ensemble ont un COD dans la plage la plus exigeante, mais la forêt aléatoire a un PRD de 1,015 et un PRB de $-0{,}196$ en validation aléatoire, hors des plages, et le boosting un PRB de $-0{,}088$, également hors de la plage ; seul l'hédonique s'en approche, et encore avec l'artefact que le \cref{ch:18} a expliqué. La régressivité des modèles d'arbres n'est donc pas un détail technique : c'est, du point de vue de la norme, leur principal défaut, et un AVM fondé sur une forêt devrait en être corrigé, par exemple par une calibration de second stade qui régresse les prix observés sur les prédictions hors échantillon et redresse la pente.
361 +
362 +Du côté des normes professionnelles, le cadre est celui du \cref{ch:10}. Les Normes de pratique professionnelle de l'OEAQ exigent que l'évaluateur justifie le rejet d'une méthode traditionnelle et expose tout écart avec les directives (règle 2.3, éléments 10 et 11) \parencite{oeaqNormes}, ce qui signifie qu'un AVM ne remplace pas les méthodes de la partie IV à VI dans un rapport mais peut les appuyer, et que son emploi, ses données, sa validation et ses limites doivent être décrits. Les normes canadiennes CUSPAP 2026 comportent une norme d'évaluation de masse \parencite{cuspap2026}, et les normes internationales en vigueur depuis le 31 janvier 2025 \parencite{ivs2025} comme le Red Book de la RICS, dont la section VPS~5 porte sur les modèles d'évaluation, convergent vers la même exigence : l'évaluateur reste responsable de la valeur qu'il signe, quel que soit l'outil, et doit comprendre et pouvoir expliquer le modèle qu'il utilise. \textcite{boginShui2020} ont comparé, sur des prêts hypothécaires américains, la précision des évaluations traditionnelles et des AVM en zones rurales, où les ventes sont rares et hétérogènes, et ont trouvé que les deux se dégradent mais que les AVM se dégradent davantage : c'est, à grande échelle, la leçon de l'exclusion d'un secteur de la \cref{sec:37:exemple}. L'usage raisonnable d'un AVM, pour l'évaluateur québécois, est triple : comme indication de valeur supplémentaire à réconcilier (\cref{ch:20}), avec son intervalle et sa validation ; comme outil de contrôle de la cohérence d'un rôle ou d'un portefeuille (\cref{ch:48}) ; et comme instrument de détection de la structure du marché, par l'explicabilité, pour mieux spécifier les modèles que l'on signe.
363 +
364 +\begin{quebec}[Modèles, rôle d'évaluation et contestation]
365 +Le rôle d'évaluation foncière québécois est, par nature, une évaluation de masse (\cref{ch:09}). Le Manuel d'évaluation foncière du Québec consacre sa partie 3 au processus d'évaluation, dont les étapes d'analyse du marché et de modélisation \parencite{mefqpartie3} ; son contenu est protégé et n'est pas reproduit ici, et rien dans les textes consultés n'impose ni n'interdit une famille particulière de modèles statistiques : la loi fixe des exigences de résultat, la proportion médiane de 95 à 105 \% et le plafond de l'écart type relatif du Règlement sur le rôle (\cref{sec:9:masse}), non de moyens. Un modèle d'arbres peut donc servir à équilibrer un rôle, mais son résultat est jugé par les mêmes statistiques que tout autre, et l'exemple a montré qu'il faut y surveiller la régressivité. La question de l'opacité se pose en contestation. La Loi sur la fiscalité municipale ouvre un recours devant le Tribunal administratif du Québec après la demande de révision (art.~138.5), et le Tribunal, qui « peut fixer une valeur inférieure ou supérieure à celles proposées par les parties », inscrit au rôle la valeur réelle divisée par le facteur du rôle (art.~147) \parencite{lfm}. Dans ce cadre, une valeur produite par un modèle n'a la force que de la preuve qui l'accompagne : l'évaluateur qui s'appuie sur un modèle, qu'il soit hédonique ou d'apprentissage, doit pouvoir en exposer les données, la spécification ou les hyperparamètres, la validation et l'application au sujet, et répondre à la question de l'expert adverse ; une cascade SHAP et une dépendance partielle sont des réponses, un score de « confiance » propriétaire n'en est pas une. Les auteurs de ce chapitre n'ont identifié, par recherche documentaire, aucune décision du Tribunal portant spécifiquement sur l'admissibilité d'un modèle d'apprentissage automatique en matière d'évaluation foncière ; cette absence est signalée comme non vérifiée et non comme une règle. Textes de loi consultés sur LégisQuébec le 31 octobre 2026 (LFM à jour au 15 octobre 2026).
366 +\end{quebec}
367 +
368 +\section{Analyse de sensibilité : les hyperparamètres}\label{sec:37:sensibilite}
369 +
370 +Les conclusions de l'exemple dépendent-elles des hyperparamètres retenus ? Le \cref{tab:37:sensibilite} réestime le boosting et la forêt sous treize réglages, toujours sur les mêmes dix plis. Pour le boosting, la profondeur est le réglage décisif, et le résultat est remarquable : les \emph{souches}, arbres de profondeur 1 qui produisent un modèle strictement additif, donnent la plus petite erreur de toute la grille, 0,0887 (COD 7,22), devant la profondeur 2 (0,0931), 3 (0,0970) et 4 (0,0984). La loi de génération étant additive en logarithme, tout degré d'interaction supplémentaire est de la variance pure, et le boosting le confirme sans qu'on le lui dise ; sur le scénario non linéaire de la \cref{sec:37:scenario}, la profondeur 2 l'emporterait au contraire, parce que l'interaction âge-qualité exige deux niveaux. Le nombre d'arbres interagit avec le taux d'apprentissage : à $\nu = 0{,}03$, 300 arbres sous-ajustent (0,0977), 1 200 sont près de l'optimum de la courbe d'apprentissage et 3 000 surajustent légèrement (0,0962) ; à $\nu = 0{,}10$, 1 200 arbres surajustent nettement (0,0988), et l'équivalence approximative « $\nu$ fois $M$ constant » que suggère l'\cref{eq:37:boosting} se vérifie, le produit étant de 36 dans le meilleur réglage. Le sous-échantillonnage à 80 \% améliore marginalement (0,0931 contre 0,0941 sans), et la taille minimale des feuilles ne change rien en validation aléatoire (0,0931 pour 1 comme pour 5), alors qu'elle décidait du biais prospectif au \cref{tab:37:temporel} : c'est l'illustration la plus nette de ce qu'une validation mesure ce qu'on lui demande de mesurer et rien d'autre. Pour la forêt, le nombre de variables par division compte peu entre 50 et 100 \% (0,1067 contre 0,1074) et un tiers des variables avec des feuilles de cinq dégrade (0,1151) ; cent arbres suffisent (0,1065), ce qui confirme que $B$ n'est pas un paramètre de surapprentissage. Aucun réglage de la forêt ne descend sous 0,105 et aucun réglage du boosting sous 0,088 : l'hédonique, à 0,084, n'est jamais rejoint. La sensibilité des conclusions aux hyperparamètres est donc faible pour l'ordre des modèles et forte pour le comportement aux bords ; l'évaluateur qui reçoit un AVM doit demander ses hyperparamètres et la courbe de validation qui les justifie.
371 +
372 +\begin{table}[htbp]
373 +\caption{Sensibilité aux hyperparamètres : boosting et forêt aléatoire en validation croisée aléatoire à dix plis (données synthétiques)}\label{tab:37:sensibilite}
374 +\centering\footnotesize\setlength{\tabcolsep}{3.5pt}
375 +\begin{tabular}{@{}l c S[table-format=4.0] S[table-format=1.2] c c l S[table-format=1.4] S[table-format=1.2] S[table-format=1.2] S[table-format=-1.3]@{}}
376 +\toprule
377 +Modèle & Profondeur & {Arbres} & {$\nu$} & Feuille min. & Sous-éch. & Variables & {RMSE$_{\ln}$} & {MdAPE (\%)} & {COD} & {PRB} \\
378 +\midrule
379 +Boosting & 1 (souches) & 1200 & 0.03 & 5 & 80 \% & toutes & 0.0887 & 6.16 & 7.22 & -0.090 \\
380 +Boosting & 2 & 300 & 0.03 & 5 & 80 \% & toutes & 0.0977 & 6.93 & 7.88 & -0.158 \\
381 +Boosting (référence) & 2 & 1200 & 0.03 & 5 & 80 \% & toutes & 0.0931 & 6.22 & 7.42 & -0.088 \\
382 +Boosting & 2 & 3000 & 0.03 & 5 & 80 \% & toutes & 0.0962 & 6.29 & 7.67 & -0.072 \\
383 +Boosting & 3 & 1200 & 0.03 & 5 & 80 \% & toutes & 0.0970 & 6.38 & 7.77 & -0.081 \\
384 +Boosting & 4 & 1200 & 0.03 & 5 & 80 \% & toutes & 0.0984 & 6.66 & 7.90 & -0.090 \\
385 +Boosting & 2 & 1200 & 0.10 & 5 & 80 \% & toutes & 0.0988 & 6.43 & 7.79 & -0.066 \\
386 +Boosting & 2 & 1200 & 0.03 & 1 & 80 \% & toutes & 0.0931 & 6.26 & 7.42 & -0.088 \\
387 +Boosting & 2 & 1200 & 0.03 & 5 & 100 \% & toutes & 0.0941 & 6.22 & 7.52 & -0.091 \\
388 +Forêt & max. & 500 & {---} & 1 & bootstrap & 100 \% & 0.1074 & 7.79 & 8.69 & -0.166 \\
389 +Forêt (référence) & max. & 500 & {---} & 2 & bootstrap & 50 \% & 0.1067 & 7.52 & 8.64 & -0.196 \\
390 +Forêt & max. & 500 & {---} & 5 & bootstrap & 33 \% & 0.1151 & 7.84 & 9.32 & -0.272 \\
391 +Forêt & max. & 100 & {---} & 2 & bootstrap & 50 \% & 0.1065 & 7.52 & 8.58 & -0.194 \\
392 +\midrule
393 +Hédonique de référence & \multicolumn{6}{l}{16 paramètres, semi-log, MCO} & 0.0839 & 5.68 & 6.83 & -0.059 \\
394 +\bottomrule
395 +\end{tabular}
396 +\tabsource{\synth{\texttt{analysis/python/cases/ch37\_ml.py}}. Scikit-learn 1.6 ; graine 20261031 pour le sous-échantillonnage, le bootstrap et le tirage des variables.}
397 +\end{table}
398 +
399 +\section{Limites : support, dérive, équité, opacité}\label{sec:37:limites}
400 +
401 +Quatre limites bornent l'usage des modèles d'apprentissage en évaluation, et chacune a été touchée du doigt dans l'exemple. La première est l'\emph{extrapolation hors support}. Un modèle d'arbres prédit une constante au-delà de la plus grande valeur observée de chaque variable, et cette constante est estimée sur les quelques observations de la feuille extrême ; la validation prospective l'a montré pour le temps, l'exclusion d'un secteur pour l'espace, et il en va de même pour une maison de \msq{300} dans un échantillon qui s'arrête à \msq{278}, ou pour un immeuble de qualité exceptionnelle. Le modèle hédonique extrapole aussi, mais selon une forme déclarée, que l'analyste peut juger ; le modèle d'arbres extrapole selon un plateau que rien ne justifie. La règle du \cref{ch:16} selon laquelle on ne prédit pas hors du nuage des ventes vaut davantage encore ici, et l'intervalle de prédiction, que les modèles d'arbres ne fournissent pas naturellement et qu'il faut construire par bootstrap, par quantiles ou par les méthodes conformes, ne s'élargit pas de lui-même au bord du support comme le fait celui de l'\cref{eq:18:intervalle}. La deuxième limite est la \emph{dérive} (\emph{drift}) : un modèle estimé à une date décrit le marché de cette date, et les relations entre caractéristiques et prix changent, le temps étant l'exemple le plus simple mais non le seul, les primes de secteur, la valeur d'un sous-sol ou d'une piscine évoluant aussi. Un AVM en production doit être réestimé et revalidé à intervalles réguliers, et surveillé entre deux réestimations par les statistiques de ratio sur les ventes nouvelles ; une baisse de la médiane des ratios de 0,99 à 0,97, comme celle que le boosting subit en huit mois dans la validation prospective, doit déclencher une mise à jour. La troisième limite est l'\emph{équité}. Un modèle qui apprend des prix apprend tout ce qui est dans les prix, y compris, sur des données réelles, les écarts de prix liés à la composition sociale ou ethnique des quartiers, et il les reproduit ; des variables en apparence neutres, les coordonnées, la distance au centre, le code postal, la valeur au rôle, en sont des substituts efficaces, comme l'exemple l'a montré pour les secteurs, et retirer une variable sensible ne retire pas l'information qu'elle porte. L'évaluation foncière vise une valeur de marché qui reflète ces écarts lorsqu'ils existent ; mais un modèle régressif, qui sous-évalue systématiquement les immeubles chers et surévalue les modestes, transfère la charge fiscale des premiers vers les seconds, et c'est précisément ce que le PRD et le PRB mesurent et ce que l'IAAO interdit. L'équité d'un modèle n'est pas un supplément d'éthique ; elle est une statistique de ratio par strate, que le concepteur doit calculer et publier. La quatrième limite est l'\emph{opacité}. Un modèle de 1 200 arbres n'est pas lisible, et les outils de la \cref{sec:37:explicabilite} décrivent ce qu'il fait, non pourquoi le marché fait ce qu'il fait : la longitude qui « explique » 3 points de logarithme dans la cascade du sujet ne signifie pas que les acheteurs paient pour la longitude, mais que le modèle a codé Hull ainsi. Devant un tribunal ou un client, cette distinction entre explication du modèle et explication du marché doit être faite explicitement, et le modèle économétrique, dont chaque coefficient est un prix implicite interprétable et testable, reste l'outil de la preuve. Le meilleur usage des deux est celui que la \cref{sec:37:scenario} a illustré : le modèle souple pour découvrir, le modèle paramétrique pour établir.
402 +
403 +\begin{pratique}
404 +Un modèle d'apprentissage déposé à l'appui d'une conclusion de valeur, ou reçu d'un tiers, doit être accompagné au minimum de : la description des données (source, période, filtres, nombre de ventes retirées, variables et leur construction, traitement des valeurs manquantes) ; la famille de modèle et ses hyperparamètres, avec la courbe de validation qui les justifie ; les métriques hors échantillon, par un schéma de validation adapté à l'usage (aléatoire pour l'interpolation dans un marché documenté, spatial par blocs ou par secteur pour la prédiction dans un secteur peu documenté, prospectif pour l'usage courant), avec le COD, le PRD et le PRB par strate ; la méthode de retransformation et de mise à jour temporelle ; l'importance des variables, la dépendance partielle des variables principales et la cascade des contributions pour le sujet ; les cas où le sujet sort du support des données d'apprentissage ; et le code ou sa description exacte. Un modèle dont on ne connaît que la prédiction et un « score de confiance » n'est pas une preuve.
405 +\end{pratique}
406 +
407 +\begin{erreurs}
408 +\begin{itemize}
409 +\item \emph{Juger un modèle à son erreur d'entraînement ou à son $R^2$ en échantillon.} Un boosting de profondeur 4 atteint une erreur d'entraînement de 0,012 pour une erreur de validation de 0,097 (\cref{fig:37:apprentissage}) ; seule l'erreur hors échantillon compte.
410 +\item \emph{Valider par plis aléatoires un modèle utilisé de façon prospective ou dans un secteur peu documenté.} Les plis aléatoires surestiment la performance des modèles qui exploitent le voisinage et masquent l'incapacité des arbres à prolonger une tendance (\cref{tab:37:comparaison}).
411 +\item \emph{Laisser le temps dans un modèle d'arbres sans traitement.} Le biais atteint $-3$ à $-13$ \% sur les ventes de l'année suivante selon la taille des feuilles ; détendancer ou indexer séparément (\cref{tab:37:temporel}).
412 +\item \emph{Lire une valeur SHAP comme un ajustement de grille.} La contribution du garage dans la cascade du sujet ($+0{,}8$ \%) est mesurée par rapport à la vente moyenne, qui a déjà 0,7 place ; le prix implicite du garage reste celui du \cref{ch:18} (\dollars{21031}).
413 +\item \emph{Conclure qu'une variable « ne compte pas » parce que son importance est nulle.} Les indicatrices de secteur ont une importance nulle alors qu'elles sont le mécanisme de la loi de génération : les coordonnées les ont remplacées.
414 +\item \emph{Choisir les hyperparamètres sur les mêmes plis que ceux qui servent à rapporter la performance sans le dire.} C'est une fuite ; la validation imbriquée ou un jeu de test réservé l'évite, et le \cref{tab:37:sensibilite} doit être lu comme une analyse de sensibilité, non comme une sélection.
415 +\item \emph{Ignorer la régressivité des modèles d'arbres.} Un COD dans la plage ne suffit pas ; le PRB de la forêt ($-0{,}196$) est très hors de la plage de l'IAAO.
416 +\item \emph{Prendre l'explication du modèle pour une explication du marché.} La longitude ne vaut rien aux yeux d'un acheteur ; c'est le secteur qu'elle code.
417 +\item \emph{Généraliser le résultat d'une comparaison.} L'hédonique gagne sur un marché additif, le boosting sur un marché à interactions ; la comparaison doit être refaite sur chaque jeu, avec le schéma de validation de l'usage prévu \parencite{mccluskey2013}.
418 +\end{itemize}
419 +\end{erreurs}
420 +
421 +\begin{limites}
422 +\begin{itemize}
423 +\item Les données sont synthétiques et engendrées par une loi semi-logarithmique additive avec un bruit normal homoscédastique et sans structure spatiale résiduelle ; la domination de l'hédonique dans l'exemple est la conséquence de cette construction et ne préjuge pas de l'ordre des modèles sur un marché réel, où la forme est inconnue. Le scénario non linéaire est tout aussi construit : ses deux termes sont des hypothèses pédagogiques, non des estimations.
424 +\item Les hyperparamètres de référence ont été choisis d'après une grille évaluée sur les mêmes dix plis que ceux qui servent à rapporter la performance (\cref{tab:37:sensibilite}) ; l'optimisme qui en résulte est faible pour une grille de treize points mais n'est pas nul. Une validation imbriquée est laissée en exercice.
425 +\item Les valeurs SHAP ont été calculées sur le gradient boosting de scikit-learn, la bibliothèque shap 0.48 ne lisant pas le format des modèles de xgboost 3.2 au moment d'écrire ; les deux algorithmes ont des performances très voisines et la lecture ne devrait pas en être affectée, mais elle n'a pas été vérifiée sur XGBoost.
426 +\item Les blocs spatiaux sont des moitiés de secteur obtenues par les $k$-moyennes sur des coordonnées fictives ; d'autres découpages donneraient d'autres chiffres, et le choix du découpage fait partie des hypothèses d'une validation spatiale.
427 +\item Les seuils de l'IAAO proviennent de l'édition 2013 du Standard on Ratio Studies, consultée sur une copie tierce alors qu'une révision était annoncée ; le Standard on AVMs a été consulté dans sa version révisée de 2018. Les règles québécoises citées (LFM art.~138.5 et 147) ont été lues sur LégisQuébec le 31 octobre 2026 ; l'absence de décision du TAQ sur l'admissibilité d'un modèle d'apprentissage est un constat de recherche documentaire, non vérifié par une recherche jurisprudentielle exhaustive.
428 +\item Les intervalles de prédiction des modèles d'arbres (bootstrap, régression quantile, prédiction conforme) et les forêts causales de \textcite{wagerAthey2018} ne sont pas traités ; le \cref{ch:39} donne la version bayésienne de l'intervalle pour le modèle hédonique, et le \cref{ch:36} l'identification causale.
429 +\end{itemize}
430 +\end{limites}
431 +
432 +\begin{synthese}
433 +\begin{itemize}
434 +\item Prédire et estimer sont deux questions ; l'apprentissage supervisé minimise le risque de prédiction \eqref{eq:37:risque}, dont la décomposition biais-variance \eqref{eq:37:biais-variance} explique tout : les forêts réduisent la variance par moyennage \eqref{eq:37:variance-foret}, le boosting réduit le biais pas à pas par descente de gradient fonctionnelle \eqref{eq:37:boosting}-\eqref{eq:37:gradient}, XGBoost y ajoute une régularisation explicite \eqref{eq:37:xgb-objectif}-\eqref{eq:37:xgb-gain}.
435 +\item La validation croisée \eqref{eq:37:cv} n'est honnête que si les plis reproduisent l'usage : aléatoire pour l'interpolation, spatiale par blocs ou par secteur pour la prédiction en territoire peu documenté, prospective pour l'usage courant. Les modèles d'arbres n'extrapolent pas : constante hors support, biais de $-3$ à $-13$ \% sur l'année suivante selon la taille des feuilles.
436 +\item Sur les 600 ventes du cas 1, engendrées par une loi additive, l'hédonique de référence (RMSE log 0,084, COD 6,83) bat XGBoost (0,092 ; 7,33), le boosting (0,093 ; 7,42), la forêt (0,107 ; 8,64) et l'arbre (0,147 ; 11,6) ; les modèles d'arbres sont régressifs (PRB de la forêt $-0{,}196$) et prédisent le sujet de 3,8 à 13,9 \% sous l'hédonique.
437 +\item Lorsqu'une interaction et une rupture déclarées sont ajoutées, le boosting (COD 7,46) bat l'hédonique mal spécifié (8,15) ; la dépendance partielle et les interactions SHAP désignent ce qui manque, et l'hédonique respécifié (6,85) reprend l'avantage : le modèle souple découvre, le modèle paramétrique établit.
438 +\item Les valeurs de Shapley \eqref{eq:37:shapley}, uniques sous quatre axiomes, décomposent chaque prédiction en contributions additives \eqref{eq:37:efficacite} calculables exactement pour les arbres (TreeSHAP) ; la dépendance partielle \eqref{eq:37:pdp} et l'importance par permutation \eqref{eq:37:permutation} les complètent. Elles décrivent le modèle, non le marché : les coordonnées ont absorbé les secteurs.
439 +\item Les AVM sont jugés par les statistiques de ratio de l'IAAO, hors échantillon et par strate ; les normes professionnelles exigent que l'évaluateur comprenne, documente et puisse défendre le modèle qu'il utilise, devant un client comme devant le TAQ.
440 +\end{itemize}
441 +\end{synthese}
442 +
443 +\section*{Exercices}
444 +\addcontentsline{toc}{section}{Exercices}
445 +
446 +\begin{exercice}[1]{Une division d'arbre à la main}{ex:37:1}
447 +Les huit premières ventes du cas 1 (G0001 à G0008), classées par superficie habitable, ont les superficies 70, 99, 128, 156, 160, 170, 221 et \msq{241} et les prix \dollars{262500}, \dollars{342500}, \dollars{431500}, \dollars{571500}, \dollars{468500}, \dollars{404500}, \dollars{545500} et \dollars{492000}. (a) Calculez le logarithme de chaque prix et la somme des carrés des écarts à la moyenne du nœud racine. (b) Pour chacun des sept seuils possibles sur la superficie (milieux entre valeurs consécutives), calculez la somme des carrés des deux fils selon l'\cref{eq:37:division} et le gain ; identifiez la meilleure division et les constantes prédites dans chaque fils, en logarithme et en dollars. (c) Expliquez pourquoi le résultat serait identique si la superficie était entrée en logarithme ou en pieds carrés.
448 +\end{exercice}
449 +
450 +\begin{exercice}[1]{Variance d'une forêt}{ex:37:2}
451 +Chaque arbre d'une forêt a, en un point donné, un écart-type de prédiction de 0,12 (logarithme) et les arbres ont une corrélation deux à deux de 0,30. (a) Calculez la variance de la moyenne de 1, 10, 100 et 500 arbres par l'\cref{eq:37:variance-foret}, puis sa limite et l'écart-type correspondant. (b) Combien d'arbres faut-il pour que le second terme ne représente plus que 1 \% du premier ? (c) Expliquez pourquoi réduire le nombre de variables candidates par division peut abaisser la limite, et ce que l'on y perd.
452 +\end{exercice}
453 +
454 +\begin{exercice}[2]{Le boosting pas à pas}{ex:37:3}
455 +Pour une vente dont le logarithme du prix est $y = 13{,}00$, le modèle initial prédit $F_0 = 12{,}80$ ; on suppose que chaque arbre $h_m$ reproduit exactement le pseudo-résidu courant de cette vente, et que le taux d'apprentissage est $\nu = 0{,}10$. (a) Calculez $F_1$, $F_2$, $F_3$ et les résidus successifs. (b) Montrez que le résidu après $m$ itérations est $(1 - \nu)^m (y - F_0)$ et calculez le nombre d'itérations nécessaire pour le ramener à 1 \% de sa valeur initiale. (c) Que se passerait-il avec $\nu = 1$, et pourquoi n'est-ce pas souhaitable sur des données bruitées ? (d) Reliez votre réponse à la courbe d'apprentissage de la \cref{fig:37:apprentissage}.
456 +\end{exercice}
457 +
458 +\begin{exercice}[2]{Valeurs de Shapley à trois joueurs}{ex:37:4}
459 +Un modèle fictif à trois variables, superficie ($S$), qualité ($Q$) et localisation ($L$), donne les gains suivants (en milliers de dollars au-dessus de la base) pour chaque coalition de variables connues : $v(\varnothing) = 0$, $v(S) = 60$, $v(Q) = 30$, $v(L) = 20$, $v(S, Q) = 100$, $v(S, L) = 85$, $v(Q, L) = 45$, $v(S, Q, L) = 130$. (a) Énumérez les six ordres d'arrivée des variables et calculez la contribution marginale de chacune dans chaque ordre. (b) Déduisez-en les valeurs de Shapley par l'\cref{eq:37:shapley} et vérifiez l'axiome d'efficacité. (c) Le jeu contient-il des interactions ? Lesquelles, et de quel signe ? (d) Pourquoi la valeur de Shapley de $S$ diffère-t-elle de $v(S) = 60$ ?
460 +\end{exercice}
461 +
462 +\begin{exercice}[2]{Métriques d'un AVM}{ex:37:5}
463 +Un AVM a produit, pour les dix ventes fictives de l'\cref{ex:18:5} (prix 312 000, 358 500, 401 000, 445 000, 489 500, 532 000, 598 000, 655 000, 742 000 et \dollars{895000}), les prédictions 335 000, 349 000, 388 000, 468 000, 471 000, 561 000, 575 000, 702 000, 718 000 et \dollars{839000}. (a) Calculez les ratios prédit/prix, la RMSE en logarithme, l'erreur absolue moyenne et médiane en pourcentage \eqref{eq:37:metriques}. (b) Calculez la médiane des ratios, le COD, le PRD et le PRB (\cref{eq:18:cod,eq:18:prd,eq:18:prb}). (c) Situez le résultat par rapport aux plages de l'IAAO et expliquez pourquoi un COD inférieur à 5 doit éveiller la méfiance plutôt que l'admiration. (d) Qu'est-ce qui manque à ces dix chiffres pour qu'ils constituent une validation au sens de la \cref{sec:37:validation} ?
464 +\end{exercice}
465 +
466 +\begin{exercice}[3]{Validation spatiale et extrapolation}{ex:37:6}
467 +À l'aide de \texttt{analysis/python/cases/ch37\_ml.py} (fonctions \texttt{plis\_aleatoires}, \texttt{plis\_spatiaux}, \texttt{plis\_secteurs}, \texttt{validation\_secteurs}), reproduisez les COD de l'hédonique, de la forêt et du boosting sous les trois schémas, aléatoire, blocs spatiaux et un secteur exclu. (a) Expliquez, pour chaque modèle, le sens et l'ampleur de la dégradation d'un schéma à l'autre. (b) Montrez que le biais de l'hédonique sur Buckingham et Masson-Angers exclus est, au bruit près, l'opposé de la prime de secteur de la loi de génération, et expliquez pourquoi le boosting fait mieux sur ces deux secteurs et moins bien sur Gatineau et Aylmer. (c) Proposez et mettez en œuvre une variante de l'hédonique qui remplace les indicatrices de secteur par une surface lisse des coordonnées (polynôme ou splines), et comparez son comportement en exclusion d'un secteur ; discutez en quoi cela rapproche les deux approches.
468 +\end{exercice}
469 +
470 +\begin{exercice}[3]{Découvrir, puis établir}{ex:37:7}
471 +Reprenez le scénario non linéaire (\texttt{scenario\_nonlineaire}). (a) Sans connaître les deux termes ajoutés, tracez la dépendance partielle de l'âge du boosting et ses courbes individuelles (ICE) pour les maisons de qualité 2 et de qualité 4, et identifiez la rupture et l'interaction. (b) Classez les paires de variables par leur valeur d'interaction SHAP moyenne et comparez au cas de base. (c) Respécifiez le modèle hédonique en conséquence, avec les seuils que vos courbes suggèrent plutôt que les seuils vrais, et comparez sa validation croisée à celle du boosting et à celle du modèle respécifié avec les seuils vrais (COD 6,85) ; que coûte une erreur de cinq ans sur les bornes du mur ? (d) Mettez en œuvre une validation imbriquée pour le choix de la profondeur du boosting (1 à 4) et comparez l'erreur ainsi obtenue à celle du \cref{tab:37:sensibilite} ; quantifiez l'optimisme de la sélection non imbriquée.
472 +\end{exercice}
473 +
474 +\section*{Pour aller plus loin}
475 +\addcontentsline{toc}{section}{Pour aller plus loin}
476 +
477 +Le manuel de \textcite{hastieTibshiraniFriedman2009} reste l'exposé de référence des arbres, du bagging, des forêts et du boosting, avec la décomposition biais-variance et la validation croisée en fil conducteur ; ses chapitres 9, 10 et 15 suffisent au lecteur de ce chapitre. Les articles fondateurs se lisent encore avec profit : \textcite{breiman2001} pour les forêts, l'erreur hors sac et l'importance par permutation, \textcite{friedman2001} pour la descente de gradient fonctionnelle et la dépendance partielle, \textcite{chenGuestrin2016} pour la formulation régularisée de XGBoost et les détails d'implémentation qui ont fait son succès. Pour l'explicabilité, \textcite{lundbergLee2017} unifient les méthodes d'explication locale sous les axiomes de Shapley, \textcite{lundberg2020} donnent TreeSHAP et les valeurs d'interaction, et le livre en ligne de \textcite{molnar2025} est le guide pratique le plus complet, qui discute aussi les limites de la dépendance partielle et de la permutation en présence de corrélation. Du côté des économistes, \textcite{mullainathanSpiess2017} et \textcite{atheyImbens2019} situent ces méthodes par rapport à l'inférence, et \textcite{wagerAthey2018} montrent comment les forêts servent l'estimation d'effets hétérogènes, ce qui ouvre sur le \cref{ch:36}. En évaluation immobilière, \textcite{paceHayunga2020} utilisent arbres et forêts comme diagnostics des résidus hédoniques et spatiaux, \textcite{mayerBourassaHoesliScognamiglio2019} comparent gradient boosting et méthodes hédoniques de mise à jour sur des données suisses, \textcite{hongChoiKim2020} appliquent la forêt à l'évaluation de masse en Corée, et \textcite{boginShui2020} mesurent la précision relative des AVM et des évaluateurs en zone rurale. Pour la pratique de l'évaluation de masse et ses normes, \textcite{gloudemansAlmy2011} et \textcite{mccluskey2013} accompagnent les trois normes de l'IAAO \parencite{iaaoratio2013,iaaomass2025,iaaoavm2018}.
modified manuscript/chapters/ch38.tex +29 −21
@@ -29,10 +29,10 @@ La projection est l'étape qui permet de calculer des distances et des surfaces
29 29 \begin{table}[htbp]
30 30 \caption{Systèmes de référence de coordonnées utiles à l'évaluation au Québec et codes EPSG}\label{tab:38:epsg}
31 31 \centering
32 −\footnotesize\setlength{\tabcolsep}{3.5pt}
33 −\begin{tabular}{llp{27mm}p{48mm}}
32 +\footnotesize\setlength{\tabcolsep}{3pt}
33 +\begin{tabularx}{\textwidth}{p{34mm}p{19mm}p{18mm}X}
34 34 \toprule
35 −Système & {EPSG} & Nature & Emploi et remarques \\
35 +Système & EPSG & Nature & Emploi et remarques \\
36 36 \midrule
37 37 WGS 84 & 4326 & géographique (degrés) & GPS, services Web, GeoJSON ; pas de calcul de distance direct \\
38 38 NAD83 & 4269 & géographique & datum nord-américain d'origine ; nombreux fichiers anciens \\
@@ -43,7 +43,7 @@ NAD83 / UTM zone 18N, 19N & 26918, 26919 & projetée, mètres & fuseaux de 6°,
43 43 NAD83 / Québec Lambert & 32198 & projetée, mètres & conique conforme, parallèles $46^\circ$ et $60^\circ$, méridien $68{,}5^\circ$ O ; cartes provinciales \\
44 44 WGS 84 / Pseudo-Mercator & 3857 & projetée, mètres & fonds de tuiles Web ; échelle fausse de 40 \% à $45^\circ$ de latitude : jamais pour mesurer \\
45 45 \bottomrule
46 −\end{tabular}
46 +\end{tabularx}
47 47 \tabsource{registre EPSG tel que distribué dans PROJ 9.5.1 (\texttt{pyproj} 3.7.2), interrogé le 31 octobre 2026 ; la fiche en ligne \url{https://epsg.org} refuse l'accès automatisé.}
48 48 \end{table}
49 49
@@ -124,13 +124,17 @@ Les variables spatiales d'un modèle hédonique résultent d'un petit nombre d'o
124 124
125 125 La \motcle{jointure spatiale} attache à chaque objet d'une couche les attributs des objets d'une autre couche qui lui sont liés par une relation géométrique : la vente \emph{est dans} la zone, le lot \emph{touche} le corridor, la parcelle \emph{intersecte} la zone inondable. La relation la plus utile en évaluation est l'appartenance d'un point à un polygone, qui donne à chaque vente la classe de la zone où elle se trouve. L'algorithme classique est le \motcle{lancer de rayon} : on trace depuis le point une demi-droite horizontale et l'on compte les arêtes du polygone qu'elle croise ; le point est intérieur si et seulement si ce nombre est impair. Pour un polygone de sommets $(x_1, y_1), \dots, (x_n, y_n)$ et un point $(x, y)$,
126 126 \begin{equation}\label{eq:38:pip}
127 −\mathbb{1}\{(x, y) \in \mathcal{P}\} = \Big[\,\sum_{i=1}^{n} \mathbb{1}\{(y_i > y) \neq (y_{i+1} > y)\}\cdot\mathbb{1}\Big\{x < x_i + \frac{(y - y_i)(x_{i+1} - x_i)}{y_{i+1} - y_i}\Big\}\Big] \bmod 2 ,
127 +\begin{split}
128 +\mathbb{1}\{(x, y) \in \mathcal{P}\} &= \Big[\sum_{i=1}^{n} c_i\Big] \bmod 2, \\
129 +c_i &= \mathbb{1}\{(y_i > y) \neq (y_{i+1} > y)\}\cdot\mathbb{1}\Big\{x < x_i + \tfrac{(y - y_i)(x_{i+1} - x_i)}{y_{i+1} - y_i}\Big\},
130 +\end{split}
128 131 \end{equation}
129 132 avec $(x_{n+1}, y_{n+1}) = (x_1, y_1)$ ; la première indicatrice retient les arêtes dont l'étendue verticale contient $y$, la seconde celles dont l'intersection avec la droite horizontale est à droite du point. Le calcul est linéaire dans le nombre de sommets, et les bibliothèques géométriques l'accélèrent par un index spatial qui écarte d'emblée les polygones dont la boîte englobante ne contient pas le point. La fonction \texttt{point\_dans\_polygone} de la bibliothèque \texttt{traite.geo} implémente l'\cref{eq:38:pip} ; le script du chapitre vérifie qu'elle donne, sur les 600 ventes et la zone d'exposition fictive, exactement le même résultat que \texttt{shapely}. Dans \texttt{geopandas}, la jointure s'écrit en une ligne :
130 133 \begin{lstlisting}
131 134 ventes = ventes.to_crs(32189) # NAD83 / MTM zone 9, metres
132 135 zones = gpd.read_file("zones.gpkg").to_crs(ventes.crs)
133 −j = gpd.sjoin(ventes, zones[["classe", "geometry"]], how="left", predicate="within")
136 +j = gpd.sjoin(ventes, zones[["classe", "geometry"]],
137 + how="left", predicate="within")
134 138 \end{lstlisting}
135 139 Deux pièges guettent la jointure. Le premier est la \emph{multiplicité} : si des polygones se chevauchent, une vente peut recevoir plusieurs lignes, et le dénombrement qui suit la compte deux fois ; il faut soit dissoudre les polygones par classe, soit retenir la classe la plus sévère. Le second est la \emph{frontière} : le prédicat \texttt{within} exclut les points situés exactement sur le contour, \texttt{intersects} les inclut, et la différence n'est pas théorique lorsque les ventes ont été géocodées au centroïde de parcelles dont certaines bordent la zone. La précision du géocodage (\cref{ch:14}) borne la confiance que l'on peut accorder à l'appartenance d'une vente proche de la limite : une erreur de position de quarante mètres suffit à faire changer de classe les ventes situées à moins de quarante mètres de la frontière, et le \cref{sec:38:exemple} en mesurera la part.
136 140
@@ -144,9 +148,9 @@ c'est-à-dire la somme de Minkowski de $A$ et du disque fermé de rayon $r$. Le
144 148
145 149 \subsection{Les distances : euclidienne, orthodromique, réseau}
146 150
147 −Dans un SRC projeté, la distance entre deux points est euclidienne et \texttt{GeoSeries.distance} la calcule, y compris d'un point à la ligne ou au polygone le plus proche. Sur la sphère, la distance orthodromique entre deux positions en degrés se calcule par la formule du \motcle{haversine},
151 +Dans un SRC projeté, la distance entre deux points est euclidienne et la méthode \texttt{distance} d'une \texttt{GeoSeries} la calcule, y compris d'un point à la ligne ou au polygone le plus proche. Sur la sphère, la distance orthodromique entre deux positions en degrés se calcule par la formule du \motcle{haversine},
148 152 \begin{equation}\label{eq:38:haversine}
149 −d = 2R\arcsin\sqrt{\sin^2\frac{\varphi_2 - \varphi_1}{2} + \cos\varphi_1\cos\varphi_2\sin^2\frac{\lambda_2 - \lambda_1}{2}} ,
153 +d = 2R\arcsin\sqrt{\sin^2(\Delta\varphi/2) + \cos\varphi_1\cos\varphi_2\,\sin^2(\Delta\lambda/2)}, \qquad \Delta\varphi = \varphi_2 - \varphi_1,\ \Delta\lambda = \lambda_2 - \lambda_1,
150 154 \end{equation}
151 155 numériquement stable aux courtes distances, contrairement à la formule de la loi des cosinus sphérique. Avec $R = \qty{6371.0}{\kilo\metre}$, elle s'écarte de la distance géodésique sur l'ellipsoïde WGS84 de 0,3 \% au plus à la latitude de Gatineau, ce qui est négligeable pour l'évaluation mais supérieur à l'écart de la projection MTM. Le \cref{ex:38:2} compare les trois mesures sur un cas simple.
152 156
@@ -178,12 +182,12 @@ où la puissance $p$ règle la localité : $p = 1$ lisse, $p = 3$ ne retient pre
178 182 \caption{Les cinq opérations SIG élémentaires : définition, algorithme, mise en œuvre et piège caractéristique}\label{tab:38:operations}
179 183 \centering
180 184 \footnotesize\setlength{\tabcolsep}{3pt}
181 −\begin{tabularx}{\textwidth}{p{22mm}XXp{36mm}}
185 +\begin{tabularx}{\textwidth}{p{20mm}XXp{33mm}}
182 186 \toprule
183 187 Opération & Définition et formule & \texttt{geopandas} / \texttt{shapely} & Piège \\
184 188 \midrule
185 189 Projection & changement de SRC ; distorsion mesurée par le facteur d'échelle, \cref{eq:38:echelle} & \texttt{to\_crs(epsg)} ; \texttt{crs} & distances en degrés ; couches dans des SRC différents ; Pseudo-Mercator \\
186 −Jointure spatiale & attributs des polygones transférés aux points selon un prédicat ; point dans polygone par lancer de rayon, \cref{eq:38:pip} & \texttt{sjoin(predicate="within")} ; \texttt{within}, \texttt{intersects} & doublons (polygones chevauchants) ; points sur la frontière ; précision du géocodage \\
190 +Jointure spatiale & attributs des polygones transférés aux points selon un prédicat ; point dans polygone par lancer de rayon, \cref{eq:38:pip} & \texttt{sjoin(}\allowbreak\texttt{predicate="within")} ; \texttt{within}, \texttt{intersects} & doublons (polygones chevauchants) ; points sur la frontière ; précision du géocodage \\
187 191 Tampon & $B_r(A) = A \oplus D_r$, \cref{eq:38:tampon} ; couronnes $B_{r_2} \setminus B_{r_1}$ & \texttt{buffer(r)} ; \texttt{difference} & rayon arbitraire ; peu de ventes dans les petits tampons \\
188 192 Distance & euclidienne dans un SRC projeté ; haversine en degrés, \cref{eq:38:haversine} ; réseau $\geq$ euclidienne, \cref{eq:38:reseau} & \texttt{distance(geom)} ; \texttt{sjoin\_nearest} & vol d'oiseau à travers une rivière ; distance à la géométrie la plus proche plutôt qu'à la pertinente \\
189 193 Agrégation, interpolation & statistique par zone ; pondération surfacique, \cref{eq:38:surfacique} ; IDW, \cref{eq:38:idw} ; krigeage (\cref{ch:19}) & \texttt{sjoin} + \texttt{groupby} ; \texttt{overlay} ; \texttt{traite.geo.idw} & unités spatiales modifiables ; uniformité supposée ; cellules presque vides \\
@@ -298,10 +302,11 @@ La jointure spatiale par le prédicat \texttt{within} attribue 31 ventes à la c
298 302 \begin{table}[htbp]
299 303 \caption{Jointure spatiale et distances aux géométries fictives, par secteur du cas 1 (données synthétiques)}\label{tab:38:secteurs}
300 304 \centering
301 −\small\setlength{\tabcolsep}{4pt}
305 +\footnotesize\setlength{\tabcolsep}{3.5pt}
306 +\resizebox{\textwidth}{!}{%
302 307 \begin{tabular}{lS[table-format=3.0]S[table-format=2.0]S[table-format=2.0]S[table-format=2.0]S[table-format=2.1]S[table-format=2.1]S[table-format=4.0]}
303 308 \toprule
304 −Secteur & {Ventes} & {Classe A} & {Classe B} & {Corridor $\leq$ 300 m} & {Dist. corridor méd. (km)} & {Accessibilité $A_i$ moy.} & {Prix méd. (\$/m\textsuperscript{2})} \\
309 +Secteur & {Ventes} & {Classe A} & {Classe B} & {Corr. $\leq$ 300 m} & {Dist. corr. méd. (km)} & {$A_i$ moyen} & {Prix méd. (\$/m\textsuperscript{2})} \\
305 310 \midrule
306 311 Hull & 186 & 14 & 19 & 14 & 2.0 & 74.3 & 3526 \\
307 312 Gatineau & 200 & 0 & 0 & 1 & 2.8 & 43.6 & 3451 \\
@@ -311,7 +316,7 @@ Masson-Angers & 47 & 0 & 0 & 0 & 16.8 & 0.4 & 2686 \\
311 316 \midrule
312 317 Ensemble & 600 & 31 & 57 & 17 & 3.1 & 43.1 & 3418 \\
313 318 \bottomrule
314 −\end{tabular}
319 +\end{tabular}}
315 320 \tabsource{\synth{analysis/python/cases/ch38\_sig.py ; prédicat \texttt{within} ; distances euclidiennes en NAD83 / MTM zone 9 ; indice gravitaire exponentiel, $\lambda = 0{,}25$/km, masses 100, 40 et 30}}
316 321 \end{table}
317 322
@@ -352,13 +357,14 @@ Le \cref{tab:38:coefficients} ajoute au modèle de référence du cas 1 (\texttt
352 357
353 358 \emph{Sans indicatrices de secteur}, tout change. La classe A reçoit un coefficient de $+6{,}3$ \% ($t = 3{,}78$), la classe B de $+4{,}3$ \% ($t = 2{,}99$), le logarithme de la distance au corridor devient significatif au seuil de 5 \% ($-1{,}6$ \%, $t = -1{,}98$) et le logarithme de l'accessibilité vaut $+14{,}6$ \% ($t = 10{,}9$) ; le test conjoint donne $F = 33{,}4$ et le $R^2$ bondit de 0,766 à 0,818. Rien de tout cela n'est un effet du cours d'eau, du corridor ou des pôles. La zone d'exposition fictive a été dessinée pour traverser Aylmer, le secteur à prime positive, et Hull ; 55 des 88 ventes exposées sont à Aylmer, et les indicatrices de classe captent la prime d'Aylmer que le modèle sans secteur a omise. L'indice d'accessibilité, maximal à Hull et presque nul à Buckingham et à Masson-Angers, est une fonction lisse de la position qui épouse la carte des primes de secteur, et le modèle lui attribue ce qu'il ne peut plus attribuer aux indicatrices. Une « zone inondable » fictive qui augmente les prix de 6 \% avec un $t$ de 3,8 est un faux positif de manuel, et il est produit par une spécification que bien des rapports jugeraient acceptable, puisqu'elle contient un gradient de distance au centre et onze caractéristiques physiques.
354 359
355 −\emph{Sans secteur ni gradient}, les coefficients des classes restent de $+5{,}8$ et $+5{,}2$ \% ; l'accessibilité tombe à $+6{,}0$ \% mais avec un $t$ de 14,1, parce qu'elle devient la seule variable de localisation et absorbe à elle seule le gradient omis. Les trois spécifications sont résumées au \cref{tab:38:coefficients}.
360 +\emph{Sans secteur ni gradient}, les effets des classes restent de $+6{,}0$ et $+5{,}2$ \% ; l'accessibilité tombe à $+6{,}1$ \% mais avec un $t$ de 14,1, parce qu'elle devient la seule variable de localisation et absorbe à elle seule le gradient omis. Les trois spécifications sont résumées au \cref{tab:38:coefficients}.
356 361 \end{exemple}
357 362
358 363 \begin{table}[htbp]
359 364 \caption{Coefficients des cinq variables géospatiales fictives ajoutées au modèle hédonique du cas 1 selon la spécification de la localisation (données synthétiques ; effet vrai nul ; erreurs-types HC1 entre parenthèses)}\label{tab:38:coefficients}
360 365 \centering
361 −\footnotesize\setlength{\tabcolsep}{3.5pt}
366 +\footnotesize\setlength{\tabcolsep}{3pt}
367 +\resizebox{\textwidth}{!}{%
362 368 \begin{tabular}{lrrrrrrr}
363 369 \toprule
364 370 & \multicolumn{2}{c}{Référence (secteurs, gradient)} & \multicolumn{2}{c}{Sans indicatrices de secteur} & \multicolumn{2}{c}{Sans secteur ni gradient} & {EMD} \\
@@ -374,7 +380,7 @@ $\ln A_i$ (accessibilité) & 0,0106 (0,0211) & 0,50 & 0,1359 (0,0125) & 10,90 &
374 380 $R^2$ (sans / avec les cinq variables) & \multicolumn{2}{c}{0,8530 / 0,8541} & \multicolumn{2}{c}{0,7661 / 0,8182} & \multicolumn{2}{c}{0,6372 / 0,8040} & \\
375 381 Test $F$ conjoint (valeur $p$) & \multicolumn{2}{c}{0,84 (0,52)} & \multicolumn{2}{c}{33,4 ($< 0{,}001$)} & \multicolumn{2}{c}{99,4 ($< 0{,}001$)} & \\
376 382 \bottomrule
377 −\end{tabular}
383 +\end{tabular}}
378 384 \tabsource{\synth{analysis/python/cases/ch38\_sig.py (fonction \texttt{modeles\_augmentes}) ; MCO semi-log, \cref{eq:38:hedonique-risque} sans interactions ; EMD = effet minimal détectable à 80 \% de puissance, \cref{eq:38:emd}, dans le modèle de référence}}
379 385 \end{table}
380 386
@@ -386,6 +392,7 @@ L'exemple précédent portait sur deux polygones dessinés par l'auteur. Pour me
386 392 \caption{Part des coefficients « significatifs » d'indicatrices de disques aléatoires sans aucun effet, selon le rayon, la spécification de la localisation et l'erreur-type (données synthétiques)}\label{tab:38:faux-positifs}
387 393 \centering
388 394 \footnotesize\setlength{\tabcolsep}{3pt}
395 +\resizebox{\textwidth}{!}{%
389 396 \begin{tabular}{lS[table-format=3.0]S[table-format=2.1]S[table-format=2.1]S[table-format=2.1]S[table-format=2.1]S[table-format=2.1]S[table-format=1.2]}
390 397 \toprule
391 398 & & \multicolumn{2}{c}{$\abs{t} > 1{,}96$ (\%)} & \multicolumn{2}{c}{$\abs{t} > 2{,}58$ (\%)} & {Bonferroni (\%)} & {É.-t. des $t$} \\
@@ -402,7 +409,7 @@ Rayon et spécification & {Disques} & {classique} & {HC1} & {classique} & {HC1}
402 409 2 km, sans secteur & 484 & 73.6 & 78.7 & 52.9 & 65.5 & 29.3 & 4.42 \\
403 410 2 km, sans secteur ni gradient & 484 & 48.3 & 52.5 & 40.9 & 47.5 & 28.5 & 3.51 \\
404 411 \bottomrule
405 −\end{tabular}
412 +\end{tabular}}
406 413 \tabsource{\synth{analysis/python/cases/ch38\_sig.py (fonction \texttt{faux\_positifs}), graine 20261031 ; 500 tirages par rayon, disques de 10 à 590 ventes conservés ; seuil de Bonferroni $\alpha/m$ avec $m$ = nombre de disques ; sous l'hypothèse nulle et une spécification correcte, les parts attendues sont 5 \% et 1 \% et l'écart-type des $t$ vaut 1}}
407 414 \end{table}
408 415
@@ -427,7 +434,7 @@ est l'outil minimal de l'analyste qui essaie plusieurs rayons, plusieurs zones o
427 434 Le portefeuille du cas 4 est synthétique : 48 immeubles fictifs de 6 à 72 logements répartis dans six régions métropolitaines (\texttt{data/synthetic/cas4\_portefeuille.csv}, graine 20261031), avec des coordonnées fictives dispersées autour du centre de chaque RMR, une indicatrice de zone inondable pour trois immeubles et une indicatrice de contamination du sol pour deux ; le générateur relève le taux global de 5 \% en zone inondable et de 3 \% en cas de contamination (\texttt{data/synthetic/README.md}). Les chiffres proviennent de \texttt{traite.cas4} et de \texttt{ch38\_sig.py}.
428 435 \end{synthetique}
429 436
430 −Le changement d'échelle impose un changement de projection : les 48 immeubles, répartis de Gatineau à Saguenay, couvrent trois fuseaux MTM et deux fuseaux UTM, et c'est la projection conique conforme Québec Lambert (EPSG 32198) qui les représente sans distorsion notable ; l'étendue projetée est de 371 kilomètres d'est en ouest et de 335 du sud au nord. La \cref{fig:38:cas4} les situe, avec une taille proportionnelle à la valeur 2026 et des marques distinctes pour les immeubles exposés. Les distances géodésiques entre les centroïdes des RMR, calculées sur l'ellipsoïde, vont de 116 kilomètres (Québec--Trois-Rivières) à 478 kilomètres (Gatineau--Saguenay), avec 236 kilomètres entre Montréal et Québec ; à ces distances, les sous-marchés sont des univers séparés, et le \cref{ch:48} traite leur corrélation comme un paramètre de risque, non comme une dépendance spatiale au sens du \cref{ch:19}.
437 +Le changement d'échelle impose un changement de projection : les 48 immeubles, répartis de Gatineau à Saguenay, couvrent trois fuseaux MTM et deux fuseaux UTM, et c'est la projection conique conforme Québec Lambert (EPSG 32198) qui les représente sans distorsion notable ; l'étendue projetée est de 379 kilomètres d'est en ouest et de 343 du sud au nord. La \cref{fig:38:cas4} les situe, avec une taille proportionnelle à la valeur 2026 et des marques distinctes pour les immeubles exposés. Les distances géodésiques entre les centroïdes des RMR, calculées sur l'ellipsoïde, vont de 116 kilomètres (Québec--Trois-Rivières) à 478 kilomètres (Gatineau--Saguenay), avec 236 kilomètres entre Montréal et Québec ; à ces distances, les sous-marchés sont des univers séparés, et le \cref{ch:48} traite leur corrélation comme un paramètre de risque, non comme une dépendance spatiale au sens du \cref{ch:19}.
431 438
432 439 \begin{figure}[htbp]
433 440 \centering
@@ -441,10 +448,11 @@ Le \cref{tab:38:cas4} décompose l'exposition. Trois immeubles sur 48 sont en zo
441 448 \begin{table}[htbp]
442 449 \caption{Exposition du portefeuille du cas 4 aux zones inondables, par région métropolitaine (données synthétiques)}\label{tab:38:cas4}
443 450 \centering
444 −\small\setlength{\tabcolsep}{4pt}
451 +\footnotesize\setlength{\tabcolsep}{3pt}
452 +\resizebox{\textwidth}{!}{%
445 453 \begin{tabular}{lS[table-format=2.0]S[table-format=3.1]S[table-format=1.0]S[table-format=1.0]S[table-format=1.1]S[table-format=2.1]S[table-format=1.1]}
446 454 \toprule
447 −RMR & {Immeubles} & {Valeur 2026 (M\$)} & {En zone inondable} & {Sol contaminé} & {Valeur exposée (M\$)} & {Part de la RMR (\%)} & {Part du total (\%)} \\
455 +RMR & {Immeubles} & {Valeur 2026 (M\$)} & {Zone inondable} & {Sol contaminé} & {Valeur exposée (M\$)} & {Part de la RMR (\%)} & {Part du total (\%)} \\
448 456 \midrule
449 457 Montréal & 17 & 65.2 & 1 & 2 & 1.7 & 2.6 & 1.2 \\
450 458 Québec & 6 & 25.1 & 1 & 0 & 7.2 & 28.8 & 5.0 \\
@@ -455,7 +463,7 @@ Saguenay & 4 & 3.3 & 1 & 0 & 0.6 & 18.5 & 0.4 \\
455 463 \midrule
456 464 Portefeuille & 48 & 143.4 & 3 & 2 & 9.5 & 6.7 & 6.7 \\
457 465 \bottomrule
458 −\end{tabular}
466 +\end{tabular}}
459 467 \tabsource{\synth{traite.cas4.sommaire et analysis/python/cases/ch38\_sig.py (fonction \texttt{cas4\_geo}) ; valeur exposée = valeur 2026 des immeubles dont \texttt{zone\_inondable} = 1}}
460 468 \end{table}
461 469
modified manuscript/chapters/ch39.tex +548 −1
Large diff (550 lines) is not loaded by default to keep the page fast.Load diff
modified manuscript/chapters/ch40.tex +579 −1
Large diff (581 lines) is not loaded by default to keep the page fast.Load diff
modified manuscript/chapters/ch41.tex +492 −1
Large diff (494 lines) is not loaded by default to keep the page fast.Load diff
modified manuscript/chapters/ch42.tex +250 −18
Large diff (330 lines) is not loaded by default to keep the page fast.Load diff
modified manuscript/chapters/ch43.tex +476 −1
Large diff (478 lines) is not loaded by default to keep the page fast.Load diff
modified manuscript/chapters/ch44.tex +442 −1
Large diff (444 lines) is not loaded by default to keep the page fast.Load diff
modified manuscript/chapters/ch46.tex +439 −1
Large diff (441 lines) is not loaded by default to keep the page fast.Load diff
modified manuscript/chapters/ch48.tex +608 −1
Large diff (610 lines) is not loaded by default to keep the page fast.Load diff
added references/lib-ch36.bib +8 −0
Large diff (8 lines) is not loaded by default to keep the page fast.Load diff
added references/lib-ch38.bib +21 −0
Large diff (21 lines) is not loaded by default to keep the page fast.Load diff
added references/lib-ch42.bib +12 −0
Large diff (12 lines) is not loaded by default to keep the page fast.Load diff
added references/lib-ch43.bib +11 −0
Large diff (11 lines) is not loaded by default to keep the page fast.Load diff
added references/lib-ch44.bib +11 −0
Large diff (11 lines) is not loaded by default to keep the page fast.Load diff
modified references/library.bib +9 −0
Large diff (12 lines) is not loaded by default to keep the page fast.Load diff
modified solutions/ch37.tex +65 −0
Large diff (66 lines) is not loaded by default to keep the page fast.Load diff
modified solutions/ch38.tex +31 −0
Large diff (32 lines) is not loaded by default to keep the page fast.Load diff
modified solutions/ch39.tex +69 −0
Large diff (70 lines) is not loaded by default to keep the page fast.Load diff
modified solutions/ch40.tex +30 −1
Large diff (31 lines) is not loaded by default to keep the page fast.Load diff
modified solutions/ch42.tex +45 −0
Large diff (46 lines) is not loaded by default to keep the page fast.Load diff
modified solutions/ch43.tex +51 −0
Large diff (52 lines) is not loaded by default to keep the page fast.Load diff
modified solutions/ch44.tex +30 −0
Large diff (31 lines) is not loaded by default to keep the page fast.Load diff
modified sources/registry.csv +22 −0
Large diff (26 lines) is not loaded by default to keep the page fast.Load diff
modified verification/calculations.csv +65 −0
Large diff (68 lines) is not loaded by default to keep the page fast.Load diff
added verification/chapters/ch36-calculations.csv +13 −0
Large diff (13 lines) is not loaded by default to keep the page fast.Load diff
added verification/chapters/ch36-claims.csv +10 −0
Large diff (10 lines) is not loaded by default to keep the page fast.Load diff
added verification/chapters/ch36-figures.csv +8 −0
Large diff (8 lines) is not loaded by default to keep the page fast.Load diff
added verification/chapters/ch36-registry.csv +1 −0
Large diff (1 lines) is not loaded by default to keep the page fast.Load diff
added verification/chapters/ch36.md +46 −0
Large diff (46 lines) is not loaded by default to keep the page fast.Load diff
added verification/chapters/ch38-calculations.csv +16 −0
Large diff (16 lines) is not loaded by default to keep the page fast.Load diff
added verification/chapters/ch38-claims.csv +18 −0
Large diff (18 lines) is not loaded by default to keep the page fast.Load diff
added verification/chapters/ch38-figures.csv +7 −0
Large diff (7 lines) is not loaded by default to keep the page fast.Load diff
added verification/chapters/ch38-registry.csv +7 −0
Large diff (7 lines) is not loaded by default to keep the page fast.Load diff
added verification/chapters/ch38.md +42 −0
Large diff (42 lines) is not loaded by default to keep the page fast.Load diff
added verification/chapters/ch42-calculations.csv +15 −0
Large diff (15 lines) is not loaded by default to keep the page fast.Load diff
added verification/chapters/ch42-claims.csv +22 −0
Large diff (22 lines) is not loaded by default to keep the page fast.Load diff
added verification/chapters/ch42-figures.csv +5 −0
Large diff (5 lines) is not loaded by default to keep the page fast.Load diff
added verification/chapters/ch42-registry.csv +3 −0
Large diff (3 lines) is not loaded by default to keep the page fast.Load diff
added verification/chapters/ch42.md +38 −0
Large diff (38 lines) is not loaded by default to keep the page fast.Load diff
added verification/chapters/ch43-calculations.csv +12 −0
Large diff (12 lines) is not loaded by default to keep the page fast.Load diff
added verification/chapters/ch43-claims.csv +27 −0
Large diff (27 lines) is not loaded by default to keep the page fast.Load diff
added verification/chapters/ch43-figures.csv +7 −0
Large diff (7 lines) is not loaded by default to keep the page fast.Load diff
added verification/chapters/ch43-registry.csv +6 −0
Large diff (6 lines) is not loaded by default to keep the page fast.Load diff
added verification/chapters/ch44-calculations.csv +14 −0
Large diff (14 lines) is not loaded by default to keep the page fast.Load diff
added verification/chapters/ch44-claims.csv +27 −0
Large diff (27 lines) is not loaded by default to keep the page fast.Load diff
added verification/chapters/ch44-figures.csv +6 −0
Large diff (6 lines) is not loaded by default to keep the page fast.Load diff
added verification/chapters/ch44-registry.csv +10 −0
Large diff (10 lines) is not loaded by default to keep the page fast.Load diff
modified verification/claims.csv +99 −0
Large diff (102 lines) is not loaded by default to keep the page fast.Load diff
modified verification/figures.csv +28 −0
Large diff (32 lines) is not loaded by default to keep the page fast.Load diff
Menu

Theme