SPB Git forge

spb/wp14_uqo

Public
2commits 1branches 1releases
909.0 KBsize
maindefault branch
57 min agolast push
Python 57% TeX 42.9%
6.9 KB · 135 lines python
Raw Blame History
1#!/usr/bin/env python32# -*- coding: utf-8 -*-3"""Robustesse (par segment) + figures primes de localisation & validation IA."""4import numpy as np, pandas as pd, warnings, statsmodels.formula.api as smf5import matplotlib; matplotlib.use("Agg"); import matplotlib.pyplot as plt6warnings.filterwarnings("ignore")7plt.rcParams.update({"font.family":"serif","font.size":10,"axes.grid":True,8    "grid.alpha":0.25,"axes.axisbelow":True,"figure.dpi":150,"savefig.bbox":"tight"})9C1,C2,C3="#3B6EA5","#C4462E","#4A8C5B"10exec(open("v5_ml.py").read().split("kf=KFold")[0])11amen_=list(AMEN.keys())12bl=np.load("data/processed/v5_blend_oos.npy"); price=df["price"].values; y=np.log(price)13from sklearn.metrics import r2_score14def stat(mask):15    yy=y[mask];pp=bl[mask];e=np.abs(np.exp(pp-yy)-1)16    return len(yy),r2_score(yy,pp),np.median(e)*100,np.mean(e<=.15)*1001718# ---- robustesse par segment ----19seg=[]20seg.append(("\\textit{Full sample}",np.ones(len(y),bool)))21q1,q2=np.quantile(price,[1/3,2/3])22seg.append(("\\quad Price tercile 1 (low)",price<=q1))23seg.append(("\\quad Price tercile 2 (mid)",(price>q1)&(price<=q2)))24seg.append(("\\quad Price tercile 3 (high)",price>q2))25seg.append(("\\quad Realtor.ca",df["source"].values=="realtor"))26seg.append(("\\quad DuProprio",df["source"].values=="duproprio"))27seg.append(("\\quad Measured area",df["has_sqft"].values==1))28seg.append(("\\quad Imputed area",df["has_sqft"].values==0))29rows=[]30for nm,m in seg:31    n,r2,md,w=stat(m)32    # R^2 non informatif sur une plage de prix restreinte (terciles) -> n/a33    r2c = "n/a$^{\\dagger}$" if "tercile" in nm else f"{r2:.3f}"34    rows.append(f"      {nm} & {n:,} & {r2c} & {md:.1f}\\% & {w:.0f}\\% \\\\".replace(",","{,}"))35tab=r"""% Auto-generated36\begin{table}[htbp]37  \centering38  \begin{threeparttable}39    \caption{Robustness: Blend Performance Across Subsamples}40    \label{tab:robustness}41    \begin{tabular}{@{}lcccc@{}}42      \toprule43      Subsample & $N$ & $R^2_{\text{OOS}}$ & Median AE & Within $\pm$15\% \\44      \midrule45"""+"\n".join(rows)+r"""46      \bottomrule47    \end{tabular}48    \begin{tablenotes}[flushleft]49      \footnotesize50      \item \textit{Notes:} Out-of-sample performance of the text\,+\,vision blend51      (Table~\ref{tab:models}, Panel~B) computed on subsamples of the same 5-fold52      cross-validated predictions. Price terciles are defined on the actual list price.53      ``Measured area'' are listings with a reported or room-reconstructed living area54      (priced by Model~A); ``Imputed area'' listings are priced by the area-free model.55      $^{\dagger}$Within a single price tercile the range of $\ln P$ is heavily56      restricted, so $R^2$---which normalizes by within-group variance---is57      uninformative; the median absolute error is the comparable accuracy metric across58      terciles.59    \end{tablenotes}60  \end{threeparttable}61\end{table}62"""63open("paper/tables/robustness.tex","w").write(tab)64print("-> tables/robustness.tex"); [print(f"  {nm[:28]:<30} n={stat(m)[0]:5d} R2={stat(m)[1]:.3f} md={stat(m)[2]:.1f}%") for nm,m in seg]6566# ---- OLS v5 pour primes de localisation ----67df["ln_price"]=y68df["ln_sqft"]=np.log(df["sqft_final"].fillna(df.groupby("ptype")["sqft_final"].transform("median")).fillna(df["sqft_final"].median()))69df["ln_lot"]=np.log(df["lot_sqft"].fillna(df["lot_sqft"].median()));df["has_lot"]=df["lot_sqft"].notna().astype(int)70df["has_age"]=df["age"].notna().astype(int)71for c in ["beds","baths","storeys","age","age2"]:72    df[c]=df[c].fillna(df.groupby("ptype")[c].transform("median")).fillna(df[c].median())73df["parking"]=df["parking"].fillna(0)74form=("ln_price ~ ln_sqft+ln_lot+has_lot+beds+baths+parking+storeys+age+age2+has_age+C(source)+C(ptype_fe)+"+"+".join(amen_)+"+n_images_c+virtual_tour+"+"+".join(rv)+"+"+"+".join(gv)+"+"+"+".join(vv)+"+C(city_fe)")75mA=smf.ols(form,data=df[df["has_sqft"]==1]).fit(cov_type="HC3")76pct=lambda b:(np.exp(b)-1)*10077cf={k.split("T.")[1].rstrip("]"):pct(v) for k,v in mA.params.items() if k.startswith("C(city_fe)")}78s=pd.Series(cf).sort_values()79top=s.tail(12);bot=s.head(8)80fig,ax=plt.subplots(figsize=(6.6,4.6))81sel=pd.concat([bot,top])82cols=[C2 if v<0 else C1 for v in sel.values]83ax.barh(range(len(sel)),sel.values,color=cols,edgecolor="white")84ax.set_yticks(range(len(sel)));ax.set_yticklabels([c.title() for c in sel.index],fontsize=8)85ax.axvline(0,color="black",lw=0.8)86ax.set_xlabel("Location premium vs reference city (\\%)")87ax.set_title("Municipality fixed effects (largest and smallest)",fontsize=11)88fig.savefig("paper/figures/fig_premia.pdf");plt.close(fig);print("-> fig_premia.pdf")8990# ---- validation IA : prix moyen par score visuel ----91ext=df[df["v_is_ext"]==1]92fig,axs=plt.subplots(1,2,figsize=(7.6,3.5))93for ax,col,lab,c in [(axs[0],"v_curb","Vision curb appeal (1--10)",C3),(axs[1],"v_cond","Vision condition score (1--10)",C1)]:94    g=ext.groupby(col)["price"].median()/1e395    g=g[g.index>=3]96    ax.plot(g.index,g.values,"-o",color=c,lw=2)97    ax.set_xlabel(lab);ax.set_ylabel("Median price (\\$000)")98axs[0].set_title("Price rises with visual curb appeal",fontsize=10)99axs[1].set_title("Price rises with visual condition",fontsize=10)100fig.savefig("paper/figures/fig_validation.pdf");plt.close(fig);print("-> fig_validation.pdf")101102# ---- table résumé features IA ----103def sh(col): return df[col].mean()*100104aitab=r"""% Auto-generated105\begin{table}[htbp]106  \centering107  \begin{threeparttable}108    \caption{Machine-Generated Feature Summary}109    \label{tab:ai_summary}110    \begin{tabular}{@{}lc@{\hskip 1.2cm}lc@{}}111      \toprule112      Text (LLM) & Share & Vision (façade) & Mean / share \\113      \midrule114"""+f"""      Recently renovated & {sh('g_recently_renovated'):.0f}\\% & Condition score (1--10) & {df['v_cond'].mean():.1f} \\\\115      Renovated kitchen & {sh('g_renovated_kitchen'):.0f}\\% & Curb appeal (1--10) & {df['v_curb'].mean():.1f} \\\\116      Hardwood floor & {sh('g_hardwood'):.0f}\\% & Luxury cue & {sh('v_luxury'):.0f}\\% \\\\117      Quartz/granite counter & {sh('g_quartz_granite'):.0f}\\% & Garage visible & {sh('v_garage_vis'):.0f}\\% \\\\118      New construction & {sh('g_new_construction'):.0f}\\% & Brick façade & {sh('v_mat_brique'):.0f}\\% \\\\119      Income property & {sh('g_income_property'):.0f}\\% & Waterfront setting & {sh('v_set_bord_eau'):.0f}\\% \\\\120      Water view & {sh('g_view_water'):.0f}\\% & Exterior shot detected & {sh('v_is_ext'):.0f}\\% \\\\121""" + r"""      \bottomrule122    \end{tabular}123    \begin{tablenotes}[flushleft]124      \footnotesize125      \item \textit{Notes:} Prevalence of selected machine-generated attributes across126      the """ + f"{len(df):,}".replace(",","{,}") + r""" cleaned listings. Text attributes are extracted from the127      description by \texttt{gpt-4o-mini}; vision attributes from the façade photograph by128      \texttt{claude-haiku-4-5}. Scores are 1--10 means; the remainder are shares.129    \end{tablenotes}130  \end{threeparttable}131\end{table}132"""133open("paper/tables/ai_summary.tex","w").write(aitab)134print("-> tables/ai_summary.tex")135