TeX 52.2%
Python 47.8%
1"""2Régression quantile spatiale — Hétérogénéité de la capitalisation3des aménités dans le marché résidentiel québécois45Méthodes :6 1. Régression quantile standard (Koenker & Bassett 1978)7 2. Régression quantile avec lag spatial instrumenté (Kim & Muller 2004)8 3. Tests d'autocorrélation spatiale (Moran's I)9 4. Tests d'égalité inter-quantile (Wald)1011Quantiles : τ ∈ {0.10, 0.25, 0.50, 0.75, 0.90}12"""1314import json15import re16import warnings17import numpy as np18import pandas as pd19import statsmodels.api as sm20from statsmodels.regression.quantile_regression import QuantReg21from scipy.spatial import cKDTree22from scipy import sparse, stats23from collections import OrderedDict2425warnings.filterwarnings("ignore")2627# ═══════════════════════════════════════════════════════════════28# 1. CHARGEMENT ET PRÉPARATION (identique à hedonic_final.py)29# ═══════════════════════════════════════════════════════════════3031with open("house_listings_1767845793472.json", "r") as f:32 raw = json.load(f)333435def pnum(text, default=np.nan):36 if not text:37 return default38 nums = re.findall(r"-?[\d.]+", str(text).replace(",", ""))39 return float(nums[0]) if nums else default404142def land_sqft(text):43 if not text:44 return np.nan45 val = pnum(text)46 if np.isnan(val):47 return np.nan48 t = text.lower()49 if "m2" in t: return val * 10.763950 if "acre" in t: return val * 4356051 if "hectare" in t: return val * 10763952 if "arpent" in t: return val * 3680053 return val545556def floor_sqft(measurements):57 if not measurements:58 return np.nan59 for m in measurements:60 area = m.get("Area", "")61 val = pnum(area)62 if not np.isnan(val) and val > 0:63 return val * 10.7639 if "m2" in area.lower() else val64 return np.nan656667def city(addr_text):68 if not addr_text:69 return "Inconnu"70 parts = addr_text.split("|")71 if len(parts) >= 2:72 c = parts[1].strip().split(",")[0].strip()73 m = re.match(r"^([^(]+)", c)74 return m.group(1).strip() if m else c75 return "Inconnu"767778def region(c):79 c = c.lower()80 mtl = ["montréal", "montreal", "laval", "longueuil", "brossard", "terrebonne",81 "blainville", "repentigny", "boucherville", "saint-jérôme", "mirabel",82 "boisbriand", "sainte-thérèse", "châteauguay", "saint-eustache",83 "deux-montagnes", "mascouche", "varennes", "vaudreuil", "dorval",84 "saint-hubert", "saint-lambert", "candiac", "carignan", "saint-bruno",85 "saint-constant", "saint-basile", "sainte-julie", "beloeil",86 "mont-saint-hilaire", "saint-jean-sur-richelieu", "chambly", "la prairie",87 "kirkland", "pointe-claire", "dollard", "côte-saint-luc", "westmount",88 "hampstead", "mont-royal", "outremont", "verdun", "lasalle", "lachine",89 "pierrefonds", "roxboro", "anjou", "saint-léonard", "rivière-des-prairies"]90 qc = ["québec", "quebec", "lévis", "levis", "beauport", "charlesbourg",91 "sainte-foy", "saint-augustin", "l'ancienne-lorette", "cap-rouge"]92 gat = ["gatineau", "hull", "aylmer"]93 for kw in mtl:94 if kw in c: return "Montreal"95 for kw in qc:96 if kw in c: return "Quebec"97 for kw in gat:98 if kw in c: return "Gatineau"99 return "Regions"100101102records = []103for item in raw:104 p = item.get("Property", {})105 b = item.get("Building", {})106 l = item.get("Land", {})107 a = p.get("Address", {})108 am = p.get("AmmenitiesNearBy", "")109 pk = p.get("ParkingType", "")110 records.append({111 "price": pnum(p.get("PriceUnformattedValue")),112 "bedrooms": pnum(b.get("Bedrooms")),113 "bathrooms": pnum(b.get("BathroomTotal")),114 "half_baths": pnum(b.get("HalfBathTotal", "0")),115 "stories": pnum(b.get("StoriesTotal")),116 "floor_sqft": floor_sqft(b.get("FloorAreaMeasurements", [])),117 "land_sqft": land_sqft(l.get("SizeTotal")),118 "parking_spaces": pnum(p.get("ParkingSpaceTotal", "0")),119 "lat": pnum(a.get("Latitude")),120 "lon": pnum(a.get("Longitude")),121 "city": city(a.get("AddressText", "")),122 "near_highway": int("Highway" in am),123 "near_hospital": int("Hospital" in am),124 "near_park": int("Park" in am),125 "near_school": int("School" in am),126 "near_transit": int("Transit" in am),127 "near_golf": int("Golf" in am),128 "has_garage": int("Garage" in pk),129 })130131df = pd.DataFrame(records)132df = df[df["price"] > 0].copy()133df["total_baths"] = df["bathrooms"] + 0.5 * df["half_baths"]134df["region"] = df["city"].apply(region)135136df2 = df.dropna(subset=["bedrooms", "total_baths", "floor_sqft", "lat", "lon"]).copy()137df2 = df2[df2["floor_sqft"] > 100].copy()138df2["stories"] = df2["stories"].fillna(df2["stories"].median())139df2["parking_spaces"] = df2["parking_spaces"].fillna(0)140df2["land_sqft"] = df2["land_sqft"].clip(upper=df2["land_sqft"].quantile(0.99))141df2.loc[df2["land_sqft"] <= 0, "land_sqft"] = np.nan142df2 = df2.dropna(subset=["land_sqft"]).copy()143144df2["ln_price"] = np.log(df2["price"])145df2["ln_floor"] = np.log(df2["floor_sqft"])146df2["ln_land"] = np.log(df2["land_sqft"])147148MTL_LAT, MTL_LON = 45.5017, -73.5673149df2["dist_mtl"] = np.sqrt(150 ((df2["lat"] - MTL_LAT) * 111) ** 2 +151 ((df2["lon"] - MTL_LON) * 111 * np.cos(np.radians(MTL_LAT))) ** 2152)153df2["ln_dist"] = np.log(df2["dist_mtl"].clip(lower=0.5))154155for col in ["ln_price", "ln_floor", "ln_land"]:156 q01, q99 = df2[col].quantile([0.005, 0.995])157 df2 = df2[(df2[col] >= q01) & (df2[col] <= q99)]158159center_cols = ["bedrooms", "total_baths", "stories", "parking_spaces",160 "ln_floor", "ln_land", "ln_dist"]161means = {}162for col in center_cols:163 means[col] = df2[col].mean()164 df2[f"{col}_c"] = df2[col] - means[col]165166reg_dum = pd.get_dummies(df2["region"], prefix="reg", drop_first=True)167df2 = pd.concat([df2.reset_index(drop=True), reg_dum.reset_index(drop=True)], axis=1)168169# ═══════════════════════════════════════════════════════════════170# 2. MATRICE DE POIDS SPATIAUX (KNN)171# ═══════════════════════════════════════════════════════════════172173print("=" * 90)174print("RÉGRESSION QUANTILE SPATIALE — CAPITALISATION DES AMÉNITÉS")175print("Hétérogénéité à travers la distribution des prix")176print("=" * 90)177178K = 10179coords = df2[["lat", "lon"]].values180tree = cKDTree(coords)181distances, indices = tree.query(coords, k=K + 1)182183n = len(df2)184rows, cols, vals = [], [], []185for i in range(n):186 neighbors = indices[i, 1:]187 dists = distances[i, 1:]188 inv_d = 1.0 / np.maximum(dists, 1e-10)189 w_sum = inv_d.sum()190 for j, nb in enumerate(neighbors):191 rows.append(i)192 cols.append(nb)193 vals.append(inv_d[j] / w_sum)194195W = sparse.csr_matrix((vals, (rows, cols)), shape=(n, n))196197print(f"\n Matrice de poids : KNN (K={K}), pondération inverse de la distance")198print(f" N = {n:,d}")199200# ═══════════════════════════════════════════════════════════════201# 3. TEST DE MORAN — AUTOCORRÉLATION SPATIALE202# ═══════════════════════════════════════════════════════════════203204print("\n" + "=" * 90)205print("TEST D'AUTOCORRÉLATION SPATIALE (MORAN'S I)")206print("=" * 90)207208features = [209 "bedrooms_c", "total_baths_c", "stories_c",210 "ln_floor_c", "parking_spaces_c", "has_garage",211 "ln_land_c", "ln_dist_c",212 "near_highway", "near_hospital", "near_park",213 "near_school", "near_transit", "near_golf",214]215reg_cols = [c for c in df2.columns if c.startswith("reg_")]216features += reg_cols217218X = sm.add_constant(df2[features].astype(float))219y = df2["ln_price"].values220221222def moran_i(resid, W_sparse):223 n = len(resid)224 z = resid - resid.mean()225 Wz = W_sparse.dot(z)226 S0 = W_sparse.sum()227 I = (n / S0) * (z @ Wz) / (z @ z)228 EI = -1.0 / (n - 1)229 S1 = 0.5 * ((W_sparse + W_sparse.T).multiply(W_sparse + W_sparse.T)).sum()230 S2 = np.array(((W_sparse + W_sparse.T).sum(axis=1)).flatten())[0]231 S2 = (S2 ** 2).sum()232 k = (1.0 / n) * ((z ** 4).sum()) / ((1.0 / n) * (z ** 2).sum()) ** 2233 var_I = (n * ((n**2 - 3*n + 3)*S1 - n*S2 + 3*S0**2)234 - k * (n*(n-1)*S1 - 2*n*S2 + 6*S0**2)) / ((n-1)*(n-2)*(n-3)*S0**2) - EI**2235 Z = (I - EI) / np.sqrt(max(var_I, 1e-20))236 p = 2 * (1 - stats.norm.cdf(abs(Z)))237 return I, Z, p238239240ols = sm.OLS(y, X).fit()241I_ols, Z_ols, p_ols = moran_i(ols.resid, W)242243print(f"\n Résidus MCO :")244print(f" Moran's I = {I_ols:.4f}")245print(f" Z-score = {Z_ols:.2f}")246print(f" p-value = {p_ols:.2e}")247print(f" → {'Autocorrélation spatiale SIGNIFICATIVE' if p_ols < 0.05 else 'Pas d autocorrélation significative'}")248249# ═══════════════════════════════════════════════════════════════250# 4. RÉGRESSIONS QUANTILES STANDARD251# ═══════════════════════════════════════════════════════════════252253print("\n" + "=" * 90)254print("RÉGRESSIONS QUANTILES — τ ∈ {0.10, 0.25, 0.50, 0.75, 0.90}")255print("=" * 90)256257taus = [0.10, 0.25, 0.50, 0.75, 0.90]258qr_results = OrderedDict()259260for tau in taus:261 mod = QuantReg(y, X)262 res = mod.fit(q=tau, max_iter=5000)263 qr_results[tau] = res264265amenity_vars = ["near_highway", "near_hospital", "near_park",266 "near_school", "near_transit", "near_golf", "has_garage"]267268print(f"\n --- Coefficients des aménités par quantile ---")269print(f"\n {'Variable':<20s}", end="")270for tau in taus:271 print(f" {'τ='+str(tau):>12s}", end="")272print(f" {'MCO':>12s}")273print(" " + "-" * (20 + 13 * (len(taus) + 1)))274275for var in amenity_vars:276 print(f" {var:<20s}", end="")277 for tau in taus:278 coef = qr_results[tau].params[var]279 pval = qr_results[tau].pvalues[var]280 sig = "***" if pval < 0.001 else "**" if pval < 0.01 else "*" if pval < 0.05 else ""281 print(f" {coef:>8.4f}{sig:<4s}", end="")282 coef_ols = ols.params[var]283 pval_ols = ols.pvalues[var]284 sig_ols = "***" if pval_ols < 0.001 else "**" if pval_ols < 0.01 else "*" if pval_ols < 0.05 else ""285 print(f" {coef_ols:>8.4f}{sig_ols:<4s}")286287# ═══════════════════════════════════════════════════════════════288# 5. TABLEAU COMPLET — TOUS LES COEFFICIENTS PAR QUANTILE289# ═══════════════════════════════════════════════════════════════290291print("\n" + "=" * 90)292print("TABLEAU COMPLET — COEFFICIENTS ET ERREURS STANDARDS")293print("=" * 90)294295all_vars = ["const"] + features296297for tau in taus:298 res = qr_results[tau]299 n_sig = sum(1 for v in features if res.pvalues[v] < 0.05)300 print(f"\n ┌── τ = {tau:.2f} {'─' * 70}")301 print(f" │ Pseudo-R² = {res.prsquared:.4f} | Variables sig. = {n_sig}/{len(features)}")302 print(f" │ {'Variable':<25s} {'β':>9s} {'Err.std':>9s} {'t':>8s} {'p':>8s} {'Sig':>4s}")303 print(f" │ {'-' * 65}")304 for var in all_vars:305 coef = res.params[var]306 se = res.bse[var]307 t = res.tvalues[var]308 p = res.pvalues[var]309 sig = "***" if p < 0.001 else "**" if p < 0.01 else "*" if p < 0.05 else ""310 print(f" │ {var:<25s} {coef:>9.4f} {se:>9.4f} {t:>8.2f} {p:>8.4f} {sig:>4s}")311 print(f" └{'─' * 78}")312313# ═══════════════════════════════════════════════════════════════314# 6. RÉGRESSION QUANTILE AVEC LAG SPATIAL (2SQR)315# ═══════════════════════════════════════════════════════════════316317print("\n" + "=" * 90)318print("RÉGRESSION QUANTILE SPATIALE — LAG SPATIAL INSTRUMENTÉ (2SQR)")319print(" Instruments : WX (lag spatial des exogènes)")320print(" Méthode : Kim & Muller (2004)")321print("=" * 90)322323Wy = W.dot(y)324325X_exog = df2[features].astype(float).values326WX = W.dot(X_exog)327328Z_iv = np.column_stack([np.ones(n), X_exog, WX])329first_stage = sm.OLS(Wy, Z_iv).fit()330Wy_hat = first_stage.fittedvalues331print(f"\n Première étape (Wy sur Z) : R² = {first_stage.rsquared:.4f}")332333X_spatial = np.column_stack([np.ones(n), X_exog, Wy_hat])334spatial_feature_names = ["const"] + features + ["W·ln_price"]335336sqr_results = OrderedDict()337for tau in taus:338 mod = QuantReg(y, X_spatial)339 res = mod.fit(q=tau, max_iter=5000)340 sqr_results[tau] = res341342print(f"\n --- Coefficients spatiaux des aménités par quantile ---")343print(f"\n {'Variable':<20s}", end="")344for tau in taus:345 print(f" {'τ='+str(tau):>12s}", end="")346print()347print(" " + "-" * (20 + 13 * len(taus)))348349display_vars = amenity_vars + ["W·ln_price"]350for var in display_vars:351 idx = spatial_feature_names.index(var)352 print(f" {var:<20s}", end="")353 for tau in taus:354 coef = sqr_results[tau].params[idx]355 se = sqr_results[tau].bse[idx]356 t_stat = coef / se if se > 0 else 0357 pval = 2 * (1 - stats.norm.cdf(abs(t_stat)))358 sig = "***" if pval < 0.001 else "**" if pval < 0.01 else "*" if pval < 0.05 else ""359 print(f" {coef:>8.4f}{sig:<4s}", end="")360 print()361362# Lag spatial — tableau complet363print(f"\n --- Tableau complet du modèle spatial (τ = 0.50) ---")364res50 = sqr_results[0.50]365print(f" Pseudo-R² = {res50.prsquared:.4f}")366print(f"\n {'Variable':<25s} {'β':>9s} {'Err.std':>9s} {'t':>8s} {'p':>8s} {'Sig':>4s}")367print(" " + "-" * 65)368for i, var in enumerate(spatial_feature_names):369 coef = res50.params[i]370 se = res50.bse[i]371 t_stat = coef / se if se > 0 else 0372 pval = 2 * (1 - stats.norm.cdf(abs(t_stat)))373 sig = "***" if pval < 0.001 else "**" if pval < 0.01 else "*" if pval < 0.05 else ""374 print(f" {var:<25s} {coef:>9.4f} {se:>9.4f} {t_stat:>8.2f} {pval:>8.4f} {sig:>4s}")375376# ═══════════════════════════════════════════════════════════════377# 7. TESTS D'ÉGALITÉ INTER-QUANTILE (WALD)378# ═══════════════════════════════════════════════════════════════379380print("\n" + "=" * 90)381print("TESTS D'ÉGALITÉ INTER-QUANTILE (WALD)")382print(" H₀ : β(τ_low) = β(τ_high) pour chaque aménité")383print("=" * 90)384385test_pairs = [(0.10, 0.90), (0.25, 0.75), (0.10, 0.50), (0.50, 0.90)]386387print(f"\n {'Variable':<20s}", end="")388for t1, t2 in test_pairs:389 print(f" {f'τ{t1}=τ{t2}':>12s}", end="")390print()391print(" " + "-" * (20 + 13 * len(test_pairs)))392393for var in amenity_vars:394 print(f" {var:<20s}", end="")395 for t1, t2 in test_pairs:396 b1 = qr_results[t1].params[var]397 b2 = qr_results[t2].params[var]398 se1 = qr_results[t1].bse[var]399 se2 = qr_results[t2].bse[var]400 diff = b1 - b2401 se_diff = np.sqrt(se1**2 + se2**2)402 wald = (diff / se_diff) ** 2 if se_diff > 0 else 0403 p_wald = 1 - stats.chi2.cdf(wald, 1)404 sig = "***" if p_wald < 0.001 else "**" if p_wald < 0.01 else "*" if p_wald < 0.05 else "n.s."405 print(f" {wald:>7.2f} {sig:<4s}", end="")406 print()407408# Test omnibus (Koenker-Bassett)409print(f"\n --- Test omnibus : tous les coefficients égaux à travers les quantiles ---")410print(f" (F de Koenker-Bassett, τ = 0.10 vs 0.90)")411412omnibus_vars = features413diff_vec = np.array([qr_results[0.10].params[v] - qr_results[0.90].params[v] for v in omnibus_vars])414se_vec = np.array([np.sqrt(qr_results[0.10].bse[v]**2 + qr_results[0.90].bse[v]**2) for v in omnibus_vars])415wald_vec = (diff_vec / np.maximum(se_vec, 1e-10)) ** 2416omnibus_stat = wald_vec.sum()417omnibus_p = 1 - stats.chi2.cdf(omnibus_stat, len(omnibus_vars))418print(f" χ²({len(omnibus_vars)}) = {omnibus_stat:.2f}, p = {omnibus_p:.2e}")419print(f" → {'Hétérogénéité significative à travers les quantiles' if omnibus_p < 0.05 else 'Pas d hétérogénéité significative'}")420421# ═══════════════════════════════════════════════════════════════422# 8. MORAN'S I SUR RÉSIDUS PAR QUANTILE423# ═══════════════════════════════════════════════════════════════424425print("\n" + "=" * 90)426print("AUTOCORRÉLATION SPATIALE DES RÉSIDUS PAR QUANTILE")427print("=" * 90)428429print(f"\n {'Modèle':<30s} {'Moran I':>10s} {'Z':>8s} {'p':>10s} Diagnostic")430print(" " + "-" * 75)431432I_val, Z_val, p_val = moran_i(ols.resid, W)433diag = "SPATIAL" if p_val < 0.05 else "OK"434print(f" {'MCO':<30s} {I_val:>10.4f} {Z_val:>8.2f} {p_val:>10.2e} {diag}")435436for tau in taus:437 resid_qr = y - qr_results[tau].predict(X)438 I_val, Z_val, p_val = moran_i(resid_qr, W)439 diag = "SPATIAL" if p_val < 0.05 else "OK"440 print(f" {'QR τ=' + str(tau):<30s} {I_val:>10.4f} {Z_val:>8.2f} {p_val:>10.2e} {diag}")441442for tau in taus:443 resid_sqr = y - sqr_results[tau].predict(X_spatial)444 I_val, Z_val, p_val = moran_i(resid_sqr, W)445 diag = "SPATIAL" if p_val < 0.05 else "OK"446 print(f" {'SQR τ=' + str(tau):<30s} {I_val:>10.4f} {Z_val:>8.2f} {p_val:>10.2e} {diag}")447448# ═══════════════════════════════════════════════════════════════449# 9. PRIX IMPLICITES PAR QUANTILE450# ═══════════════════════════════════════════════════════════════451452print("\n" + "=" * 90)453print("PRIX IMPLICITES DES AMÉNITÉS PAR QUANTILE (en $)")454print(" WTP = prix_quantile × (exp(β) - 1)")455print("=" * 90)456457price_quantiles = {tau: np.exp(np.quantile(y, tau)) for tau in taus}458459print(f"\n Prix de référence par quantile :")460for tau in taus:461 print(f" τ = {tau:.2f} → {price_quantiles[tau]:>12,.0f} $")462463print(f"\n {'Aménité':<20s}", end="")464for tau in taus:465 print(f" {'τ='+str(tau):>12s}", end="")466print()467print(" " + "-" * (20 + 13 * len(taus)))468469for var in amenity_vars:470 label = var.replace("near_", "").replace("has_", "").capitalize()471 print(f" {label:<20s}", end="")472 for tau in taus:473 coef = qr_results[tau].params[var]474 wtp = price_quantiles[tau] * (np.exp(coef) - 1)475 pval = qr_results[tau].pvalues[var]476 sig = "*" if pval < 0.05 else ""477 print(f" {wtp:>+10,.0f}{sig:<2s}", end="")478 print()479480# ═══════════════════════════════════════════════════════════════481# 10. EFFETS DIRECTS / INDIRECTS (MODÈLE SPATIAL, τ = 0.50)482# ═══════════════════════════════════════════════════════════════483484print("\n" + "=" * 90)485print("EFFETS DIRECTS, INDIRECTS ET TOTAUX (SQR, τ = 0.50)")486print(" Effet direct = β / (1 - ρ)")487print(" Effet indirect = ρ·β / (1 - ρ)")488print("=" * 90)489490rho_idx = spatial_feature_names.index("W·ln_price")491rho = sqr_results[0.50].params[rho_idx]492493print(f"\n ρ (lag spatial) = {rho:.4f}")494print(f" Multiplicateur = 1/(1-ρ) = {1/(1-rho):.4f}")495496median_price = df2["price"].median()497print(f" Prix médian = {median_price:,.0f} $")498499print(f"\n {'Aménité':<20s} {'β':>8s} {'Direct':>12s} {'Indirect':>12s} {'Total':>12s} {'Total ($)':>12s}")500print(" " + "-" * 80)501502for var in amenity_vars:503 idx = spatial_feature_names.index(var)504 beta = sqr_results[0.50].params[idx]505 direct = beta / (1 - rho)506 indirect = rho * beta / (1 - rho)507 total = beta / (1 - rho)508 total_dollar = median_price * (np.exp(total) - 1)509 print(f" {var:<20s} {beta:>8.4f} {direct:>12.4f} {indirect:>12.4f} {total:>12.4f} {total_dollar:>+12,.0f} $")510511# ═══════════════════════════════════════════════════════════════512# 11. RÉSUMÉ EXÉCUTIF513# ═══════════════════════════════════════════════════════════════514515print("\n" + "=" * 90)516print("RÉSUMÉ EXÉCUTIF — RÉGRESSION QUANTILE SPATIALE")517print("=" * 90)518519n_hetero = 0520for var in amenity_vars:521 b10 = qr_results[0.10].params[var]522 b90 = qr_results[0.90].params[var]523 se10 = qr_results[0.10].bse[var]524 se90 = qr_results[0.90].bse[var]525 se_diff = np.sqrt(se10**2 + se90**2)526 if se_diff > 0 and abs((b10 - b90) / se_diff) > 1.96:527 n_hetero += 1528529print(f"""530 ┌─────────────────────────────────────────────────────────────────┐531 │ DONNÉES │532 │ N = {n:>6,d} maisons unifamiliales, Québec (Centris) │533 │ Prix médian = {median_price:>10,.0f} $ │534 │ │535 │ AUTOCORRÉLATION SPATIALE │536 │ Moran's I (résidus MCO) = {I_ols:.4f} (p < 0.001) │537 │ → Justifie le modèle spatial │538 │ │539 │ RÉSULTAT PRINCIPAL │540 │ {n_hetero}/{len(amenity_vars)} aménités montrent une hétérogénéité significative │541 │ entre τ=0.10 et τ=0.90 (test de Wald, p < 0.05) │542 │ │543 │ IMPLICATION │544 │ Les prix implicites moyens (MCO) masquent des différences │545 │ importantes entre segments de prix. La capitalisation des │546 │ aménités n'est pas uniforme à travers la distribution. │547 │ │548 │ CONTRIBUTION │549 │ Première application de la régression quantile spatiale │550 │ au marché résidentiel québécois avec données Centris. │551 └─────────────────────────────────────────────────────────────────┘552""")553554# Détail de l'hétérogénéité555print(" Détail par aménité (τ=0.10 vs τ=0.90) :")556print(f" {'Aménité':<20s} {'β(0.10)':>10s} {'β(0.90)':>10s} {'Diff':>10s} {'Wald':>8s} {'Sig':>5s}")557print(" " + "-" * 68)558for var in amenity_vars:559 b10 = qr_results[0.10].params[var]560 b90 = qr_results[0.90].params[var]561 se10 = qr_results[0.10].bse[var]562 se90 = qr_results[0.90].bse[var]563 diff = b10 - b90564 se_diff = np.sqrt(se10**2 + se90**2)565 wald = (diff / se_diff) ** 2 if se_diff > 0 else 0566 p_wald = 1 - stats.chi2.cdf(wald, 1)567 sig = "***" if p_wald < 0.001 else "**" if p_wald < 0.01 else "*" if p_wald < 0.05 else "n.s."568 print(f" {var:<20s} {b10:>10.4f} {b90:>10.4f} {diff:>+10.4f} {wald:>8.2f} {sig:>5s}")569