Ch. 36 livré et validé (compilation isolée, tests, fusion des registres)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
68 changed files +6,026 −98
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 | petits[nom] = {"post": p, "pred": pr, "largeur95_pct": (pr["haut95"] - pr["bas95"]) / pr["mediane"] * 100} | 353 | petits[nom] = {"post": p, "pred": pr, "largeur95_pct": (pr["haut95"] - pr["bas95"]) / pr["mediane"] * 100} | ||
| 354 | # Hull seul (186), a priori faible | 354 | # Hull seul (186), a priori faible | ||
| 355 | p_hull = ajuster_nig(se["hull"]); pred_hull = predictive_t(p_hull, x_sujet(s, p_hull["colonnes"])) | 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 | 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), | 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 | "n_rec": len(rec), "p_vois": p_vois, "micro": micro, "cols_micro": cols_m, "petits": petits, "p_hull": p_hull, | 359 | "n_rec": len(rec), "p_vois": p_vois, "micro": micro, "cols_micro": cols_m, "petits": petits, "p_hull": p_hull, | ||
| 358 | "pred_hull": pred_hull, "n_micro": len(micro)} | 360 | "pred_hull": pred_hull, "n_micro": len(micro)} | ||
@@ -459,28 +461,37 @@ def exercices(ex: dict, hier: dict) -> dict: | |||||
| 459 | w = (n / sig ** 2) / prec | 461 | w = (n / sig ** 2) / prec | ||
| 460 | 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), | 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 | "n_equivalent_prior": sig ** 2 / s0 ** 2, "valeur_165": mn * 165, "sd_165": sn * 165} | 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 | # ex 2 : lecture d'intervalles — probabilité a posteriori que la valeur dépasse 650 000 $ sous une postérieure log-normale | 467 | # ex 2 : lecture d'intervalles — probabilité a posteriori que la valeur dépasse 650 000 $ sous une postérieure log-normale | ||
| 463 | mu, sd = ex["pred600"]["loc"], ex["pred600"]["echelle"] | 468 | mu, sd = ex["pred600"]["loc"], ex["pred600"]["echelle"] | ||
| 464 | r["ex2"] = {"p_sup_650": float(1 - stats.t.cdf((math.log(650_000) - mu) / sd, ex["pred600"]["nu"])), | 469 | r["ex2"] = {"p_sup_650": float(1 - stats.t.cdf((math.log(650_000) - mu) / sd, ex["pred600"]["nu"])), | ||
| 465 | "p_inf_550": float(stats.t.cdf((math.log(550_000) - mu) / sd, ex["pred600"]["nu"])), | 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 | # ex 3 : facteur de rétrécissement | 473 | # ex 3 : facteur de rétrécissement | ||
| 468 | s2, t2 = 0.083 ** 2, 0.15 ** 2 | 474 | s2, t2 = 0.083 ** 2, 0.15 ** 2 | ||
| 469 | r["ex3"] = {n_: facteur_retrecissement(n_, s2, t2) for n_ in (5, 10, 20, 47)} | 475 | r["ex3"] = {n_: facteur_retrecissement(n_, s2, t2) for n_ in (5, 10, 20, 47)} | ||
| 470 | r["ex3"]["alpha_5"] = facteur_retrecissement(5, s2, t2) * (-0.30) + (1 - facteur_retrecissement(5, s2, t2)) * (-0.08) | 476 | r["ex3"]["alpha_5"] = facteur_retrecissement(5, s2, t2) * (-0.30) + (1 - facteur_retrecissement(5, s2, t2)) * (-0.08) | ||
| 471 | r["ex3"]["tau_005"] = {n_: facteur_retrecissement(n_, s2, 0.05 ** 2) for n_ in (5, 10, 20, 47)} | 477 | r["ex3"]["tau_005"] = {n_: facteur_retrecissement(n_, s2, 0.05 ** 2) for n_ in (5, 10, 20, 47)} | ||
| 472 | r["ex3"]["sd_cond_5"] = math.sqrt(1 / (5 / s2 + 1 / t2)); r["ex3"]["sd_fixe_5"] = math.sqrt(s2 / 5) | 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 | # ex 4 : prédictive Student-t | 480 | # ex 4 : prédictive Student-t | ||
| 474 | p = ex["p600"]; xs = ex["xs"]; q = float(xs @ p["V_n"] @ xs) | 481 | p = ex["p600"]; xs = ex["xs"]; q = float(xs @ p["V_n"] @ xs) | ||
| 475 | 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"], | 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 | "t975": float(stats.t.ppf(0.975, p["nu"])), "z975": 1.959964, "bas95": ex["pred600"]["bas95"], "haut95": ex["pred600"]["haut95"], | 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 | # ex 5 : R̂ sur des moyennes/variances de chaînes données | 488 | # ex 5 : R̂ sur des moyennes/variances de chaînes données | ||
| 479 | moy = np.array([-0.262, -0.258, -0.271, -0.249]); var = np.array([0.0049, 0.0052, 0.0047, 0.0055]); n_ = 1000 | 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 | B = n_ * moy.var(ddof=1); W = var.mean(); vp = (n_ - 1) / n_ * W + B / n_ | 490 | B = n_ * moy.var(ddof=1); W = var.mean(); vp = (n_ - 1) / n_ * W + B / n_ | ||
| 481 | r["ex5"] = {"B": B, "W": W, "var_plus": vp, "rhat": math.sqrt(vp / W)} | 491 | r["ex5"] = {"B": B, "W": W, "var_plus": vp, "rhat": math.sqrt(vp / W)} | ||
| 482 | moy2 = np.array([-0.262, -0.258, -0.371, -0.249]); B2 = n_ * moy2.var(ddof=1); vp2 = (n_ - 1) / n_ * W + B2 / n_ | 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 | r["ex5"]["rhat_divergent"] = math.sqrt(vp2 / W); r["ex5"]["B2"] = B2 | 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 | # ex 6 : script (κ) — valeurs du tableau des petits échantillons | 495 | # ex 6 : script (κ) — valeurs du tableau des petits échantillons | ||
| 485 | 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")], | 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 | "mediane": v_["pred"]["mediane"], "largeur95_pct": v_["largeur95_pct"]} for k, v_ in ex["petits"].items()} | 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 | po = pe["post"]; i = ex["cols_micro"].index("ln_S"); iq = ex["cols_micro"].index("qualite"); ig = ex["cols_micro"].index("garage") | 537 | po = pe["post"]; i = ex["cols_micro"].index("ln_S"); iq = ex["cols_micro"].index("qualite"); ig = ex["cols_micro"].index("garage") | ||
| 527 | 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}) ; " | 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 | 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} %") | 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 | print("\n=== modèle hiérarchique (Gibbs par blocs) : résumé ===") | 541 | print("\n=== modèle hiérarchique (Gibbs par blocs) : résumé ===") | ||
| 531 | print(hi["resume"].round(4).to_string()) | 542 | print(hi["resume"].round(4).to_string()) | ||
| 532 | print("\ndiagnostics : Gibbs par blocs vs un paramètre à la fois"); print(hi["diag"].round(3).to_string()) | 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 | out["ex1"] = {**p, **m, "P_sup_5pct": float(1 - stats.norm.cdf(math.log(0.05 / p["mediane"]) / p["sigma_log"]))} | 379 | out["ex1"] = {**p, **m, "P_sup_5pct": float(1 - stats.norm.cdf(math.log(0.05 / p["mediane"]) / p["sigma_log"]))} | ||
| 380 | # Ex. 2 : PERT vs triangulaire pour la rotation (0,08 ; 0,15 ; 0,25) | 380 | # Ex. 2 : PERT vs triangulaire pour la rotation (0,08 ; 0,15 ; 0,25) | ||
| 381 | t = moments_loi("triangulaire", MC_LOIS["rotation_annuelle"][1]); pe = pert(0.08, 0.15, 0.25) | 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 | # Ex. 3 : nombre de tirages pour une médiane à ±5 000 $ (95 %) et pour P(RCD<1,20) à ±1 point | 385 | # Ex. 3 : nombre de tirages pour une médiane à ±5 000 $ (95 %) et pour P(RCD<1,20) à ±1 point | ||
| 384 | V = mc["valeur"]; e = erreur_type_quantile(V, 0.5) | 386 | V = mc["valeur"]; e = erreur_type_quantile(V, 0.5) | ||
| 385 | se1_med = e["se_theorique"] * math.sqrt(len(V)) # écart-type « pour N = 1 » | 387 | se1_med = e["se_theorique"] * math.sqrt(len(V)) # écart-type « pour N = 1 » | ||
| 386 | 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), | 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 | "N_proba_1pt": n_requis(0.5, 0.01), "N_proba_0.5pt": n_requis(0.5, 0.005)} | 389 | "N_proba_1pt": n_requis(0.5, 0.01), "N_proba_0.5pt": n_requis(0.5, 0.005)} | ||
| 388 | # 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 | 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 | # Ex. 5 : VaR/CVaR 10 % de valeur et loi normale de même moyenne/écart-type ; comparaison | 393 | # Ex. 5 : VaR/CVaR 10 % de valeur et loi normale de même moyenne/écart-type ; comparaison | ||
| 391 | st = mc["stats"]; mr = mesures_risque(mc, r["dcf"]["valeur"], alphas=(0.10,)).iloc[0].to_dict() | 394 | st = mc["stats"]; mr = mesures_risque(mc, r["dcf"]["valeur"], alphas=(0.10,)).iloc[0].to_dict() | ||
| 392 | 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)} | 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 | lois = dict(MC_LOIS); lois["taux_actualisation"] = ("normale", {"mu": 0.0725, "sigma": 0.010}) | 397 | lois = dict(MC_LOIS); lois["taux_actualisation"] = ("normale", {"mu": 0.0725, "sigma": 0.010}) | ||
| 395 | x = tirer(N_MC, GRAINE, len(r["hyp"]["capex"]), lois); ev = evaluer(r, x); Vx = ev["valeur"] | 398 | x = tirer(N_MC, GRAINE, len(r["hyp"]["capex"]), lois); ev = evaluer(r, x); Vx = ev["valeur"] | ||
| 396 | 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()), | 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 | return out | 402 | return out | ||
| 399 | 403 | ||||
| 400 | # ----------------------------------------------------------------------------- tout | 404 | # ----------------------------------------------------------------------------- tout | ||
@@ -440,6 +444,7 @@ def imprimer(D: dict) -> None: | |||||
| 440 | print("\nErreur Monte-Carlo théorique (N = 10 000) :\n", D["erreurs"].to_string(index=False)) | 444 | print("\nErreur Monte-Carlo théorique (N = 10 000) :\n", D["erreurs"].to_string(index=False)) | ||
| 441 | rep = D["repetitions"] | 445 | rep = D["repetitions"] | ||
| 442 | 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}") | 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 | 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}") | 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 | print(" graine du traité (20261031) :", {k: (round(v, 4) if isinstance(v, float) else v) for k, v in D["graine_traite"].items()}) | 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 | print("\nConvergence (une suite, graine 20261031) :\n", D["convergence"].to_string(index=False)) | 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 | ax.set_xlabel("Poids de la comparaison 2 × 2"); ax.set_title(titre, fontsize=8.5); ax.grid(True, lw=0.4) | 72 | ax.set_xlabel("Poids de la comparaison 2 × 2"); ax.set_title(titre, fontsize=8.5); ax.grid(True, lw=0.4) | ||
| 73 | axes[0].set_ylabel("Estimation 2 × 2 (points de log)") | 73 | axes[0].set_ylabel("Estimation 2 × 2 (points de log)") | ||
| 74 | h_, l_ = axes[0].get_legend_handles_labels() | 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 | savefig(fig, "ch36/ch36-fig-bacon.pdf") | 76 | savefig(fig, "ch36/ch36-fig-bacon.pdf") | ||
| 77 | 77 | ||||
| 78 | def fig_synthetique(res): | 78 | def fig_synthetique(res): | ||
modified
analysis/python/figures/ch37_figures.py
+1 −1
@@ -29,7 +29,7 @@ def fig_arbre(res): | |||||
| 29 | arbre = res["ajustes"]["Arbre (profondeur 3)"] | 29 | arbre = res["ajustes"]["Arbre (profondeur 3)"] | ||
| 30 | noms = [c37.ETIQUETTES[c].replace(" (m²)", " (m²)") for c in res["Xm"].columns] | 30 | noms = [c37.ETIQUETTES[c].replace(" (m²)", " (m²)") for c in res["Xm"].columns] | ||
| 31 | fig, ax = plt.subplots(figsize=(LARGEUR, 3.6)) | 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 | for t in ax.texts: | 33 | for t in ax.texts: | ||
| 34 | s = t.get_text() | 34 | s = t.get_text() | ||
| 35 | s = re.sub(r"samples = (\d+)", lambda m: f"n = {m.group(1)}", s) | 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 | ax.axhline(0, color=palette.ink2, lw=0.6) | 18 | ax.axhline(0, color=palette.ink2, lw=0.6) | ||
| 19 | ax.set_title(titre, loc="left", fontsize=9) | 19 | ax.set_title(titre, loc="left", fontsize=9) | ||
| 20 | ax.set_xlabel("Année"); ax.yaxis.set_major_formatter(plt.FuncFormatter(fmt_dollars)) | 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 | textcoords="offset points", ha="right", va="top" if fl["van"] < 0 else "bottom", fontsize=8, color=palette.ink) | 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 | 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") | 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 | for i, (_, r) in enumerate(comp5.iterrows()): | 68 | for i, (_, r) in enumerate(comp5.iterrows()): | ||
| 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) | 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) | 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, 11.5, f"Médiane des 67 triplex : {rev['mrb_127']:.2f} (pointillé)".replace(".", ","), fontsize=7, color=palette.ink2) | 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); ax.set_xlim(12, 31); ax.legend(loc="center right", fontsize=6.8) | 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 | ax.set_title("A. Les cinq comparables dans la distribution", loc="left", fontsize=8.5) | 73 | ax.set_title("A. Les cinq comparables dans la distribution", loc="left", fontsize=8.5) | ||
| 74 | ax = axes[1] | 74 | ax = axes[1] | ||
| 75 | rho5 = comp5["loyer_mensuel_moyen_contrat"] / comp5["loyer_mensuel_moyen_marche"]; rho_tri = tri["loyer_mensuel_moyen_contrat"] / tri["loyer_mensuel_moyen_marche"] | 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 | assert cl["noi"] == pytest.approx(cl["rbe"] - cl["depenses"]) | 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 | class TestConversion: | 118 | class TestConversion: | ||
| 103 | def test_bloc(self, res): | 119 | def test_bloc(self, res): | ||
| 104 | b = res["faisabilite"]["bloc"] | 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 | ## Chapitre 25 — ventes de terrains vacants « Gatineau » — ajouté le 2026-10-31 | 71 | ## Chapitre 25 — ventes de terrains vacants « Gatineau » — ajouté le 2026-10-31 | ||
| 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. | 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 | \end{equation} | 187 | \end{equation} | ||
| 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 : | 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 | \begin{equation}\label{eq:36:cs-agreg} | 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 | 190 | +\begin{aligned} | ||
| 191 | −\widehat{\mathrm{ATT}}(k) = \sum_{g} w_{g,k}\,\widehat{\mathrm{ATT}}(g, g + k), \qquad | 191 | +\widehat{\mathrm{ATT}}_g &= \frac{1}{T - g + 1}\sum_{t \geq g}\widehat{\mathrm{ATT}}(g, t), \qquad | ||
| 192 | −\widehat{\mathrm{ATT}} = \sum_g \frac{n_g}{\sum_{g'} n_{g'}}\,\widehat{\mathrm{ATT}}_g , | 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 | \end{equation} | 195 | \end{equation} | ||
| 194 | 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. | 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 | \begin{figure}[htbp] | 226 | \begin{figure}[htbp] | ||
| 225 | \centering | 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 | % axe du temps : t = 0 (2016T1) … 41 (2026T2) | 230 | % axe du temps : t = 0 (2016T1) … 41 (2026T2) | ||
| 228 | \draw[->, thick, brandGray] (0,0) -- (43,0) node[right, text=brandGray] {trimestre}; | 231 | \draw[->, thick, brandGray] (0,0) -- (43,0) node[right, text=brandGray] {trimestre}; | ||
| 229 | \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};} | 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 | % bandes de traitement | 233 | % bandes de traitement | ||
| 231 | \fill[brandPrimary!25] (26,0.55) rectangle (42,1.05); \draw[brandPrimary, thick] (26,0.55) -- (26,1.05); | 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 | \node at (34,0.8) {traitée dès 2022T3 ($k = 0$ à $15$)}; | 236 | \node at (34,0.8) {traitée dès 2022T3 ($k = 0$ à $15$)}; | ||
| 234 | \fill[brandSecondary!35] (34,1.25) rectangle (42,1.75); \draw[brandSecondary!80!black, thick] (34,1.25) -- (34,1.75); | 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 | \node at (38,1.5) {dès 2024T3}; | 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 | \draw[brandGray, dashed] (0,2.2) -- (42,2.2); | 241 | \draw[brandGray, dashed] (0,2.2) -- (42,2.2); | ||
| 239 | % fenêtres du 2 x 2 | 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)}; | 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)}; | ||
| 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)}; | 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 | \draw[decorate, decoration={brace, amplitude=3pt, mirror}, brandGray] (10,0.3) -- (17.9,0.3) node[midway, above=3pt, text=brandGray] {placebo (2019T3)}; | 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 | \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} | 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 | \figsource{Schéma de l'auteur ; plan décrit dans \texttt{analysis/python/cases/ch36\_panel.py}.} | 248 | \figsource{Schéma de l'auteur ; plan décrit dans \texttt{analysis/python/cases/ch36\_panel.py}.} | ||
| 246 | \end{figure} | 249 | \end{figure} | ||
| 247 | 250 | ||||
| 248 | \begin{exemple}{Cinq estimateurs confrontés à un effet connu}{choc-cas4} | 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 | \begin{figure}[htbp] | 257 | \begin{figure}[htbp] | ||
| 254 | \centering | 258 | \centering | ||
@@ -257,12 +261,12 @@ L'exemple procède en huit étapes : description du plan et des trajectoires ; d | |||||
| 257 | \figsource{\synth{script \texttt{analysis/python/figures/ch36\_figures.py}, graine 20261031}} | 261 | \figsource{\synth{script \texttt{analysis/python/figures/ch36\_figures.py}, graine 20261031}} | ||
| 258 | \end{figure} | 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 | \begin{table}[htbp] | 266 | \begin{table}[htbp] | ||
| 263 | \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} | 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 | \centering | 268 | \centering | ||
| 265 | −\small | 269 | +\footnotesize\setlength{\tabcolsep}{4pt} | ||
| 266 | \begin{tabular}{lS[table-format=2.4]S[table-format=2.4]S[table-format=1.4]} | 270 | \begin{tabular}{lS[table-format=2.4]S[table-format=2.4]S[table-format=1.4]} | ||
| 267 | \toprule | 271 | \toprule | ||
| 268 | Groupe & {Avant (2021T3--2022T2)} & {Après (2023T3--2024T2)} & {Différence} \\ | 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 | \tabsource{\synth{\texttt{analysis/python/cases/ch36\_panel.py}, fonctions \texttt{did\_deux\_periodes} et \texttt{placebo\_temporel} ; \texttt{traite.panel.did\_2x2}}.} | 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 | \end{table} | 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 | \begin{table}[htbp] | 290 | \begin{table}[htbp] | ||
| 287 | \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} | 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 | \centering | 292 | \centering | ||
| 289 | −\small | 293 | +\footnotesize\setlength{\tabcolsep}{4pt} | ||
| 290 | \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]} | 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 | \toprule | 295 | \toprule | ||
| 292 | Spécification & {Paramètres} & {$\hat\delta$} & {ET classique} & {ET HC1} & {ET grappes} \\ | 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 | \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$).} | 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 | \end{table} | 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 | \begin{figure}[htbp] | 308 | \begin{figure}[htbp] | ||
| 305 | \centering | 309 | \centering | ||
@@ -311,7 +315,7 @@ Effets d'immeuble + RMR $\times$ trimestre & 295 & 0.0702 & 0.0054 & 0.0064 & 0. | |||||
| 311 | \begin{table}[htbp] | 315 | \begin{table}[htbp] | ||
| 312 | \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} | 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 | \centering | 317 | \centering | ||
| 314 | −\small | 318 | +\footnotesize\setlength{\tabcolsep}{4pt} | ||
| 315 | \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]} | 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 | \toprule | 320 | \toprule | ||
| 317 | {$k$} & {$\hat\delta_k$} & {ET grappes} & {Vrai} & & {$k$} & {$\hat\delta_k$} & {ET grappes} & {Vrai} \\ | 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 | \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.} | 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 | \end{table} | 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 | \begin{table}[htbp] | 339 | \begin{table}[htbp] | ||
| 336 | \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} | 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 | \centering | 341 | \centering | ||
| 338 | −\small | 342 | +\footnotesize\setlength{\tabcolsep}{4pt} | ||
| 339 | −\begin{tabular}{llS[table-format=1.4]S[table-format=1.4]S[table-format=1.4]} | 343 | +\begin{tabular}{lL{44mm}S[table-format=1.4]S[table-format=1.4]S[table-format=1.4]} | ||
| 340 | \toprule | 344 | \toprule | ||
| 341 | Groupe traité & Groupe témoin (fenêtre) & {Poids $s$} & {$\hat\beta_{2\times 2}$} & {Vrai} \\ | 345 | Groupe traité & Groupe témoin (fenêtre) & {Poids $s$} & {$\hat\beta_{2\times 2}$} & {Vrai} \\ | ||
| 342 | \midrule | 346 | \midrule | ||
| 343 | Cohorte A (2022T3) & Jamais traités (toute la période) & 0.6658 & 0.0844 & 0.0484 \\ | 347 | Cohorte A (2022T3) & Jamais traités (toute la période) & 0.6658 & 0.0844 & 0.0484 \\ | ||
| 344 | Cohorte B (2024T3) & Jamais traités (toute la période) & 0.2721 & 0.1224 & 0.0356 \\ | 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 | Cohorte B & Cohorte A dès 2022T3 (déjà traitée) & 0.0146 & 0.0403 & 0.0356 \\ | 350 | Cohorte B & Cohorte A dès 2022T3 (déjà traitée) & 0.0146 & 0.0403 & 0.0356 \\ | ||
| 347 | \midrule | 351 | \midrule | ||
| 348 | Somme des poids / moyenne pondérée & & 1.0000 & 0.0905 & 0.0454 \\ | 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 | \begin{table}[htbp] | 367 | \begin{table}[htbp] | ||
| 364 | \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} | 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 | \centering | 369 | \centering | ||
| 366 | −\small | 370 | +\footnotesize\setlength{\tabcolsep}{4pt} | ||
| 367 | \begin{tabular}{lS[table-format=+1.4]S[table-format=1.4]S[table-format=1.4]} | 371 | \begin{tabular}{lS[table-format=+1.4]S[table-format=1.4]S[table-format=1.4]} | ||
| 368 | \toprule | 372 | \toprule | ||
| 369 | Quantité & {Estimation} & {ET grappes / bootstrap} & {Vrai} \\ | 373 | Quantité & {Estimation} & {ET grappes / bootstrap} & {Vrai} \\ | ||
| 370 | \midrule | 374 | \midrule | ||
| 371 | TWFE statique & 0.0233 & 0.0061 & 0.0692 \\ | 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 \\ | 376 | +\quad 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 \\ | 377 | +\quad 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 \\ | 378 | +\quad 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 \\ | 379 | +\quad tardive vs précoce déjà traitée (poids 0,327) & -0.0356 & & 0.0425 \\ | ||
| 376 | Callaway--Sant'Anna, cohorte précoce & 0.0893 & 0.0086 & 0.0825 \\ | 380 | Callaway--Sant'Anna, cohorte précoce & 0.0893 & 0.0086 & 0.0825 \\ | ||
| 377 | Callaway--Sant'Anna, cohorte tardive & 0.0310 & 0.0105 & 0.0425 \\ | 381 | Callaway--Sant'Anna, cohorte tardive & 0.0310 & 0.0105 & 0.0425 \\ | ||
| 378 | Callaway--Sant'Anna, global & 0.0601 & 0.0073 & 0.0692 \\ | 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 | \tabsource{\synth{\texttt{analysis/python/cases/ch36\_panel.py}, fonction \texttt{laboratoire} (graine 20261031 + 36)}.} | 385 | \tabsource{\synth{\texttt{analysis/python/cases/ch36\_panel.py}, fonction \texttt{laboratoire} (graine 20261031 + 36)}.} | ||
| 382 | \end{table} | 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 | \begin{table}[htbp] | 390 | \begin{table}[htbp] | ||
| 387 | \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} | 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 | \centering | 392 | \centering | ||
| 389 | −\small | 393 | +\footnotesize\setlength{\tabcolsep}{4pt} | ||
| 390 | \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]} | 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 | \toprule | 395 | \toprule | ||
| 392 | & \multicolumn{2}{c}{Témoins de la même RMR} & \multicolumn{2}{c}{Témoins de toutes les RMR} & \\ | 396 | & \multicolumn{2}{c}{Témoins de la même RMR} & \multicolumn{2}{c}{Témoins de toutes les RMR} & \\ | ||
| 393 | \cmidrule(lr){2-3}\cmidrule(lr){4-5} | 397 | \cmidrule(lr){2-3}\cmidrule(lr){4-5} | ||
| 394 | Agrégat & {$\widehat{\mathrm{ATT}}$} & {ET bootstrap} & {$\widehat{\mathrm{ATT}}$} & {ET bootstrap} & {Vrai} \\ | 398 | Agrégat & {$\widehat{\mathrm{ATT}}$} & {ET bootstrap} & {$\widehat{\mathrm{ATT}}$} & {ET bootstrap} & {Vrai} \\ | ||
| 395 | \midrule | 399 | \midrule | ||
| 396 | −Cohorte A (Montréal, 2022T3, 16 trimestres) & 0.0528 & 0.0154 & 0.0509 & 0.0145 & 0.0484 \\ | 400 | +Cohorte A (Montréal, 2022T3, 16 trim.) & 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 \\ | 401 | +Cohorte B (Gatineau, 2024T3, 8 trim.) & 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 \\ | 402 | +Global (pondéré par les cohortes) & 0.0481 & 0.0116 & 0.0514 & & 0.0454 \\ | ||
| 399 | \bottomrule | 403 | \bottomrule | ||
| 400 | \end{tabular} | 404 | \end{tabular} | ||
| 401 | \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)}.} | 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 | \end{table} | 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 | \begin{figure}[htbp] | 410 | \begin{figure}[htbp] | ||
| 407 | \centering | 411 | \centering | ||
@@ -410,7 +414,7 @@ Global (pondéré par la taille des cohortes) & 0.0481 & 0.0116 & 0.0514 & & 0.0 | |||||
| 410 | \figsource{\synth{script \texttt{analysis/python/figures/ch36\_figures.py}, graine 20261031}} | 414 | \figsource{\synth{script \texttt{analysis/python/figures/ch36\_figures.py}, graine 20261031}} | ||
| 411 | \end{figure} | 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 | \begin{figure}[htbp] | 419 | \begin{figure}[htbp] | ||
| 416 | \centering | 420 | \centering | ||
@@ -419,13 +423,12 @@ Global (pondéré par la taille des cohortes) & 0.0481 & 0.0116 & 0.0514 & & 0.0 | |||||
| 419 | \figsource{\synth{script \texttt{analysis/python/figures/ch36\_figures.py}, graine 20261031 ; données engendrées par \texttt{ch36\_panel.discontinuite}}} | 423 | \figsource{\synth{script \texttt{analysis/python/figures/ch36\_figures.py}, graine 20261031 ; données engendrées par \texttt{ch36\_panel.discontinuite}}} | ||
| 420 | \end{figure} | 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. | 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. | ||
| 423 | −\end{exemple} | ||||
| 424 | 427 | ||||
| 425 | \begin{table}[htbp] | 428 | \begin{table}[htbp] | ||
| 426 | \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} | 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 | \centering | 430 | \centering | ||
| 428 | −\small | 431 | +\footnotesize\setlength{\tabcolsep}{4pt} | ||
| 429 | \begin{tabular}{lS[table-format=1.4]S[table-format=1.4]S[table-format=1.4]S[table-format=+1.4]} | 432 | \begin{tabular}{lS[table-format=1.4]S[table-format=1.4]S[table-format=1.4]S[table-format=+1.4]} | ||
| 430 | \toprule | 433 | \toprule | ||
| 431 | Estimateur (cible) & {Estimation} & {Erreur-type} & {Vrai} & {Écart} \\ | 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 | \begin{table}[htbp] | 464 | \begin{table}[htbp] | ||
| 462 | \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} | 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 | \centering | 466 | \centering | ||
| 464 | −\small | 467 | +\footnotesize\setlength{\tabcolsep}{4pt} | ||
| 465 | \begin{tabular}{lS[table-format=1.4]S[table-format=1.4]S[table-format=1.4]} | 468 | \begin{tabular}{lS[table-format=1.4]S[table-format=1.4]S[table-format=1.4]} | ||
| 466 | \toprule | 469 | \toprule | ||
| 467 | Scénario et estimateur & {Référence} & {Violé} & {Vrai} \\ | 470 | Scénario et estimateur & {Référence} & {Violé} & {Vrai} \\ | ||
modified
manuscript/chapters/ch37.tex
+476 −1
@@ -1,2 +1,477 @@ | |||||
| 1 | \chapter{Apprentissage automatique et explicabilité}\label{ch:37} | 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 | \begin{table}[htbp] | 29 | \begin{table}[htbp] | ||
| 30 | \caption{Systèmes de référence de coordonnées utiles à l'évaluation au Québec et codes EPSG}\label{tab:38:epsg} | 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 | \centering | 31 | \centering | ||
| 32 | −\footnotesize\setlength{\tabcolsep}{3.5pt} | 32 | +\footnotesize\setlength{\tabcolsep}{3pt} | ||
| 33 | −\begin{tabular}{llp{27mm}p{48mm}} | 33 | +\begin{tabularx}{\textwidth}{p{34mm}p{19mm}p{18mm}X} | ||
| 34 | \toprule | 34 | \toprule | ||
| 35 | −Système & {EPSG} & Nature & Emploi et remarques \\ | 35 | +Système & EPSG & Nature & Emploi et remarques \\ | ||
| 36 | \midrule | 36 | \midrule | ||
| 37 | WGS 84 & 4326 & géographique (degrés) & GPS, services Web, GeoJSON ; pas de calcul de distance direct \\ | 37 | WGS 84 & 4326 & géographique (degrés) & GPS, services Web, GeoJSON ; pas de calcul de distance direct \\ | ||
| 38 | NAD83 & 4269 & géographique & datum nord-américain d'origine ; nombreux fichiers anciens \\ | 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 | 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 \\ | 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 | WGS 84 / Pseudo-Mercator & 3857 & projetée, mètres & fonds de tuiles Web ; échelle fausse de 40 \% à $45^\circ$ de latitude : jamais pour mesurer \\ | 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 | \bottomrule | 45 | \bottomrule | ||
| 46 | −\end{tabular} | 46 | +\end{tabularx} | ||
| 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é.} | 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 | \end{table} | 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 | 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)$, | 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 | \begin{equation}\label{eq:38:pip} | 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 | \end{equation} | 131 | \end{equation} | ||
| 129 | 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 : | 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 | \begin{lstlisting} | 133 | \begin{lstlisting} | ||
| 131 | ventes = ventes.to_crs(32189) # NAD83 / MTM zone 9, metres | 134 | ventes = ventes.to_crs(32189) # NAD83 / MTM zone 9, metres | ||
| 132 | zones = gpd.read_file("zones.gpkg").to_crs(ventes.crs) | 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 | \end{lstlisting} | 138 | \end{lstlisting} | ||
| 135 | 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. | 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 | \subsection{Les distances : euclidienne, orthodromique, réseau} | 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 | \begin{equation}\label{eq:38:haversine} | 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 | \end{equation} | 154 | \end{equation} | ||
| 151 | 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. | 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 | \caption{Les cinq opérations SIG élémentaires : définition, algorithme, mise en œuvre et piège caractéristique}\label{tab:38:operations} | 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 | \centering | 183 | \centering | ||
| 180 | \footnotesize\setlength{\tabcolsep}{3pt} | 184 | \footnotesize\setlength{\tabcolsep}{3pt} | ||
| 181 | −\begin{tabularx}{\textwidth}{p{22mm}XXp{36mm}} | 185 | +\begin{tabularx}{\textwidth}{p{20mm}XXp{33mm}} | ||
| 182 | \toprule | 186 | \toprule | ||
| 183 | Opération & Définition et formule & \texttt{geopandas} / \texttt{shapely} & Piège \\ | 187 | Opération & Définition et formule & \texttt{geopandas} / \texttt{shapely} & Piège \\ | ||
| 184 | \midrule | 188 | \midrule | ||
| 185 | 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 \\ | 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 | 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 \\ | 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 | 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 \\ | 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 | 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 \\ | 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 | \begin{table}[htbp] | 302 | \begin{table}[htbp] | ||
| 299 | \caption{Jointure spatiale et distances aux géométries fictives, par secteur du cas 1 (données synthétiques)}\label{tab:38:secteurs} | 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 | \centering | 304 | \centering | ||
| 301 | −\small\setlength{\tabcolsep}{4pt} | 305 | +\footnotesize\setlength{\tabcolsep}{3.5pt} | ||
| 306 | +\resizebox{\textwidth}{!}{% | ||||
| 302 | \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]} | 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 | \toprule | 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 | \midrule | 310 | \midrule | ||
| 306 | Hull & 186 & 14 & 19 & 14 & 2.0 & 74.3 & 3526 \\ | 311 | Hull & 186 & 14 & 19 & 14 & 2.0 & 74.3 & 3526 \\ | ||
| 307 | Gatineau & 200 & 0 & 0 & 1 & 2.8 & 43.6 & 3451 \\ | 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 | \midrule | 316 | \midrule | ||
| 312 | Ensemble & 600 & 31 & 57 & 17 & 3.1 & 43.1 & 3418 \\ | 317 | Ensemble & 600 & 31 & 57 & 17 & 3.1 & 43.1 & 3418 \\ | ||
| 313 | \bottomrule | 318 | \bottomrule | ||
| 314 | −\end{tabular} | 319 | +\end{tabular}} | ||
| 315 | \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}} | 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 | \end{table} | 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 | \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. | 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 | \end{exemple} | 361 | \end{exemple} | ||
| 357 | 362 | ||||
| 358 | \begin{table}[htbp] | 363 | \begin{table}[htbp] | ||
| 359 | \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} | 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 | \centering | 365 | \centering | ||
| 361 | −\footnotesize\setlength{\tabcolsep}{3.5pt} | 366 | +\footnotesize\setlength{\tabcolsep}{3pt} | ||
| 367 | +\resizebox{\textwidth}{!}{% | ||||
| 362 | \begin{tabular}{lrrrrrrr} | 368 | \begin{tabular}{lrrrrrrr} | ||
| 363 | \toprule | 369 | \toprule | ||
| 364 | & \multicolumn{2}{c}{Référence (secteurs, gradient)} & \multicolumn{2}{c}{Sans indicatrices de secteur} & \multicolumn{2}{c}{Sans secteur ni gradient} & {EMD} \\ | 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 | $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} & \\ | 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 | 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$)} & \\ | 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 | \bottomrule | 382 | \bottomrule | ||
| 377 | −\end{tabular} | 383 | +\end{tabular}} | ||
| 378 | \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}} | 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 | \end{table} | 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 | \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} | 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 | \centering | 393 | \centering | ||
| 388 | \footnotesize\setlength{\tabcolsep}{3pt} | 394 | \footnotesize\setlength{\tabcolsep}{3pt} | ||
| 395 | +\resizebox{\textwidth}{!}{% | ||||
| 389 | \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]} | 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 | \toprule | 397 | \toprule | ||
| 391 | & & \multicolumn{2}{c}{$\abs{t} > 1{,}96$ (\%)} & \multicolumn{2}{c}{$\abs{t} > 2{,}58$ (\%)} & {Bonferroni (\%)} & {É.-t. des $t$} \\ | 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 | 2 km, sans secteur & 484 & 73.6 & 78.7 & 52.9 & 65.5 & 29.3 & 4.42 \\ | 409 | 2 km, sans secteur & 484 & 73.6 & 78.7 & 52.9 & 65.5 & 29.3 & 4.42 \\ | ||
| 403 | 2 km, sans secteur ni gradient & 484 & 48.3 & 52.5 & 40.9 & 47.5 & 28.5 & 3.51 \\ | 410 | 2 km, sans secteur ni gradient & 484 & 48.3 & 52.5 & 40.9 & 47.5 & 28.5 & 3.51 \\ | ||
| 404 | \bottomrule | 411 | \bottomrule | ||
| 405 | −\end{tabular} | 412 | +\end{tabular}} | ||
| 406 | \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}} | 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 | \end{table} | 414 | \end{table} | ||
| 408 | 415 | ||||
@@ -427,7 +434,7 @@ est l'outil minimal de l'analyste qui essaie plusieurs rayons, plusieurs zones o | |||||
| 427 | 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}. | 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 | \end{synthetique} | 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 | \begin{figure}[htbp] | 439 | \begin{figure}[htbp] | ||
| 433 | \centering | 440 | \centering | ||
@@ -441,10 +448,11 @@ Le \cref{tab:38:cas4} décompose l'exposition. Trois immeubles sur 48 sont en zo | |||||
| 441 | \begin{table}[htbp] | 448 | \begin{table}[htbp] | ||
| 442 | \caption{Exposition du portefeuille du cas 4 aux zones inondables, par région métropolitaine (données synthétiques)}\label{tab:38:cas4} | 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 | \centering | 450 | \centering | ||
| 444 | −\small\setlength{\tabcolsep}{4pt} | 451 | +\footnotesize\setlength{\tabcolsep}{3pt} | ||
| 452 | +\resizebox{\textwidth}{!}{% | ||||
| 445 | \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]} | 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 | \toprule | 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 | \midrule | 456 | \midrule | ||
| 449 | Montréal & 17 & 65.2 & 1 & 2 & 1.7 & 2.6 & 1.2 \\ | 457 | Montréal & 17 & 65.2 & 1 & 2 & 1.7 & 2.6 & 1.2 \\ | ||
| 450 | Québec & 6 & 25.1 & 1 & 0 & 7.2 & 28.8 & 5.0 \\ | 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 | \midrule | 463 | \midrule | ||
| 456 | Portefeuille & 48 & 143.4 & 3 & 2 & 9.5 & 6.7 & 6.7 \\ | 464 | Portefeuille & 48 & 143.4 & 3 & 2 & 9.5 & 6.7 & 6.7 \\ | ||
| 457 | \bottomrule | 465 | \bottomrule | ||
| 458 | −\end{tabular} | 466 | +\end{tabular}} | ||
| 459 | \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}} | 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 | \end{table} | 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