SPB Git

spb/wp2_uqo

Public

UQO Working Paper No. 2 — Decoding Real Estate Descriptions: text-based hedonic analysis of housing listings.

20.1 KB · 388 lines python
1#!/usr/bin/env python32# Author: Simon-Pierre Boucher — contact@spboucher.ai3#4"""Step 8 — LaTeX tables and figure for the revision extensions (step 7).56Inputs : results/revision_*.csv7Outputs: paper/tables/tab_language.tex8         paper/tables/tab_spec_summary.tex9         paper/tables/tab_extended_specs.tex10         paper/tables/tab_block_tests.tex11         paper/tables/tab_oos.tex12         paper/tables/tab_reference_sensitivity.tex13         figures/fig12_specification_stability.{pdf,png}14"""1516import sys17from pathlib import Path1819import matplotlib20import numpy as np21import pandas as pd2223matplotlib.use("Agg")24import matplotlib.pyplot as plt2526sys.path.insert(0, str(Path(__file__).resolve().parents[1]))2728from src import config29from src.references import ENGLISH_LABELS, REFERENCES3031TABLES_DIR = config.ROOT / "paper" / "tables"32HEADER = ("% Author: Simon-Pierre Boucher — contact@spboucher.ai\n"33          "% Generated by scripts/08_revision_tables.py — do not edit by hand.\n")34R = config.RESULTS_DIR3536plt.rcParams.update({37    "font.family": "serif", "font.serif": ["Times New Roman", "Times", "DejaVu Serif"],38    "font.size": 10, "axes.labelsize": 11, "xtick.labelsize": 9, "ytick.labelsize": 9,39    "legend.fontsize": 8.5, "figure.dpi": 300, "savefig.dpi": 300, "savefig.bbox": "tight",40    "savefig.pad_inches": 0.08, "axes.spines.top": False, "axes.spines.right": False,41})424344def tex(label):45    return label.replace("&", "\\&")464748def num(x, dec=3):49    s = f"{x:,.{dec}f}".replace(",", "{,}")50    return s.replace("-", "$-$")515253def stars(p):54    return "***" if p < 0.001 else "**" if p < 0.01 else "*" if p < 0.05 else ""555657def write(name, content):58    (TABLES_DIR / name).write_text(HEADER + content)59    print(f"  -> {TABLES_DIR / name}")606162# --------------------------------------------------------------------------63def tab_language():64    d = pd.read_csv(R / "revision_language.csv")65    base = d[d.model == "all-MiniLM-L6-v2"].set_index("language")66    multi = d[d.model == "paraphrase-multilingual-MiniLM-L12-v2"].set_index("language")67    rows = []68    for lang, name in [("fr", "French"), ("en", "English"), ("mixed", "Mixed / uninformative")]:69        b, m = base.loc[lang], multi.loc[lang]70        n_tex = f"{int(b['n']):,}".replace(",", "{,}")71        price_tex = f"{b['median_price']:,.0f}".replace(",", "{,}")72        rows.append(f"{name} & {n_tex} & {b['share'] * 100:.1f} & {b['mean_length']:.0f} & "73                    f"\\${price_tex} & {b['mean_log_price']:.3f} & "74                    f"{b['mean_similarity']:.3f} & {m['mean_similarity']:.3f} \\\\")75    body = "\n".join(rows)76    e_b = base.loc["eta2_language_on_mean_similarity", "mean_similarity"]77    e_m = multi.loc["eta2_language_on_mean_similarity", "mean_similarity"]78    c_b = base.loc["corr_mean_similarity_english", "mean_similarity"]79    c_m = multi.loc["corr_mean_similarity_english", "mean_similarity"]80    l_b = base.loc["corr_mean_similarity_length", "mean_similarity"]81    l_m = multi.loc["corr_mean_similarity_length", "mean_similarity"]82    p_b = base.loc["pc1_share_of_similarity_variance", "mean_similarity"]83    p_m = multi.loc["pc1_share_of_similarity_variance", "mean_similarity"]84    content = rf"""\begin{{table}}[!htbp]85\centering86\caption{{Language of the listing descriptions and mean cosine similarity to the 20 French references.}}87\label{{tab:language}}88\small89\begin{{adjustbox}}{{max width=\textwidth}}90\begin{{tabular}}{{lR{{1.2cm}}R{{1.1cm}}R{{1.3cm}}R{{1.6cm}}R{{1.3cm}}R{{1.5cm}}R{{1.7cm}}}}91\toprule92& & & & & & \multicolumn{{2}}{{c}}{{\textbf{{Mean similarity}}}} \\93\cmidrule(lr){{7-8}}94\textbf{{Language}} & $\boldsymbol{{n}}$ & \textbf{{\%}} & \textbf{{Length}} & \textbf{{Median price}} & $\boldsymbol{{\overline{{\ln P}}}}$ & \textbf{{MiniLM-L6}} & \textbf{{Multilingual}} \\95\midrule96{body}97\midrule98\multicolumn{{6}}{{l}}{{Share of the variance of the mean similarity explained by language ($\eta^2$)}} & {e_b:.3f} & {e_m:.3f} \\99\multicolumn{{6}}{{l}}{{Correlation of the mean similarity with the English indicator}} & {num(c_b)} & {num(c_m)} \\100\multicolumn{{6}}{{l}}{{Correlation of the mean similarity with description length}} & {l_b:.3f} & {l_m:.3f} \\101\multicolumn{{6}}{{l}}{{Share of the variance of the 20 similarities on their first principal component}} & {p_b:.3f} & {p_m:.3f} \\102\bottomrule103\end{{tabular}}104\end{{adjustbox}}105\begin{{minipage}}{{\textwidth}}106\vspace{{4pt}}\footnotesize107\textit{{Notes:}} Language is classified from French and English stop-word counts (ratio rule of 1.5; ``mixed'' listings are mostly short boilerplate such as ``See listing broker(s)''). Length is the mean number of characters. MiniLM-L6 is the paper's baseline encoder (all-MiniLM-L6-v2); Multilingual is paraphrase-multilingual-MiniLM-L12-v2 \citep{{reimers2020multilingual}}. The mean similarity is the average of the 20 cosine similarities of a listing.108\end{{minipage}}109\end{{table}}110"""111    write("tab_language.tex", content)112113114# --------------------------------------------------------------------------115SPEC_ORDER = [116    ("D (baseline)", "D: baseline (Table~\\ref{tab:model_comparison})"),117    ("D (baseline), FSA-clustered SE", "D, standard errors clustered by FSA"),118    ("D-L: + language", "D-L: D + language indicators"),119    ("D-L, FSA-clustered SE", "D-L, standard errors clustered by FSA"),120    ("D-L + FSA FE", "D-L + FSA fixed effects"),121    ("D-L + municipality FE", "D-L + municipality fixed effects"),122    ("D-L + FSA FE, lot area (m2)", "D-L + FSA FE, unit-corrected lot area"),123    ("D-L + FSA FE, floor-area subsample", "D-L + FSA FE + floor area (subsample)"),124    ("French-only, D + FSA FE", "French listings only, D + FSA FE"),125    ("English-only, D + FSA FE", "English listings only, D + FSA FE"),126    ("ML-D: multilingual similarities", "ML-D: multilingual encoder, as D"),127    ("ML-D-L: + language", "ML-D-L: multilingual encoder + language"),128    ("ML-D-L + FSA FE", "ML-D-L + FSA fixed effects"),129    ("ML-D-L + FSA FE, lot area (m2)", "ML-D-L + FSA FE, unit-corrected lot area"),130]131132133def tab_spec_summary():134    s = pd.read_csv(R / "revision_spec_summary.csv").set_index("spec")135    rows = []136    for key, label in SPEC_ORDER:137        r = s.loc[key]138        cov = "cluster" if r["cov"] != "HC3" else "HC3"139        r2w = "---" if pd.isna(r["r2_within"]) else f"{r['r2_within']:.3f}"140        n_tex = f"{int(r['n']):,}".replace(",", "{,}")141        rows.append(f"{label} & {n_tex} & {r['r2']:.3f} & {r2w} & {r['delta_r2_sims']:.3f} & "142                    f"{r['block_F']:.1f} & {int(r['n_sig_5pct'])}/20 & {cov} \\\\")143    body = "\n".join(rows)144    content = rf"""\begin{{table}}[!htbp]145\centering146\caption{{Incremental explanatory power of the 20 semantic similarities across extended specifications.}}147\label{{tab:spec_summary}}148\small149\begin{{adjustbox}}{{max width=\textwidth}}150\begin{{tabular}}{{lR{{1.2cm}}R{{1.0cm}}R{{1.2cm}}R{{1.4cm}}R{{1.3cm}}R{{1.1cm}}l}}151\toprule152\textbf{{Specification}} & $\boldsymbol{{n}}$ & $\boldsymbol{{R^2}}$ & \textbf{{Within}} $\boldsymbol{{R^2}}$ & $\boldsymbol{{\Delta R^2}}$ \textbf{{(sem.)}} & \textbf{{Block}} $\boldsymbol{{F}}$ & \textbf{{Sig.}} & \textbf{{Cov.}} \\153\midrule154{body}155\bottomrule156\end{{tabular}}157\end{{adjustbox}}158\begin{{minipage}}{{\textwidth}}159\vspace{{4pt}}\footnotesize160\textit{{Notes:}} All specifications include the six structural variables and description length; ``D-L'' adds indicators for English and mixed-language listings; ``ML'' replaces the all-MiniLM-L6-v2 similarities by those computed with paraphrase-multilingual-MiniLM-L12-v2. Fixed effects are absorbed by within-group demeaning (415 forward sortation areas, FSA, or 1{{,}}061 municipalities); $R^2$ is then measured against the total variance of log price and ``Within $R^2$'' against the within-group variance. $\Delta R^2$ (sem.) is the gain in $R^2$ from adding the 20 similarities to the otherwise identical specification. Block $F$ is the robust Wald test (HC3 or clustered by FSA/municipality) that the 20 semantic coefficients are jointly zero; all $p < 0.001$. ``Sig.'' counts similarities individually significant at the 5\% level. The unit-corrected lot area converts the raw field to square metres (m$^2$, sq.\ ft., acres, hectares) and enters in logs with a missing-value indicator; the floor-area subsample is restricted to the 6{{,}}228 listings reporting interior floor area.161\end{{minipage}}162\end{{table}}163"""164    write("tab_spec_summary.tex", content)165166167# --------------------------------------------------------------------------168EXT_COLS = [("D (baseline)", "D"), ("D-L: + language", "D-L"), ("D-L + FSA FE", "D-L + FSA FE"),169            ("French-only, D + FSA FE", "French only"), ("English-only, D + FSA FE", "English only"),170            ("ML-D-L + FSA FE", "ML-D-L + FSA FE")]171DIM_ORDER = [ENGLISH_LABELS[k] for k in REFERENCES]172173174def tab_extended_specs():175    sp = pd.read_csv(R / "revision_specifications.csv")176    sims = sp[sp.variable.str.startswith(("sim_", "ml_"))]177    imp = sims.pivot_table(index="label", columns="spec", values="impact_pct")178    pv = sims.pivot_table(index="label", columns="spec", values="p_value")179    summ = pd.read_csv(R / "revision_spec_summary.csv").set_index("spec")180    rows = []181    for lab in DIM_ORDER:182        cells = []183        for key, _ in EXT_COLS:184            v, p = imp.loc[lab, key], pv.loc[lab, key]185            cells.append(f"{num(v, 1)}{stars(p)}")186        rows.append(f"{tex(lab)} & " + " & ".join(cells) + " \\\\")187    body = "\n".join(rows)188    foot_n = " & ".join(f"{int(summ.loc[k, 'n']):,}".replace(",", "{,}") for k, _ in EXT_COLS)189    foot_r2 = " & ".join(f"{summ.loc[k, 'r2']:.3f}" for k, _ in EXT_COLS)190    foot_d = " & ".join(f"{summ.loc[k, 'delta_r2_sims']:.3f}" for k, _ in EXT_COLS)191    head = " & ".join(f"\\textbf{{{lbl}}}" for _, lbl in EXT_COLS)192    content = rf"""\begin{{table}}[!htbp]193\centering194\caption{{Semantic implicit price gradients (\% per standard deviation) across specifications and encoders.}}195\label{{tab:extended_specs}}196\small197\begin{{adjustbox}}{{max width=\textwidth}}198\begin{{tabular}}{{lR{{1.5cm}}R{{1.5cm}}R{{1.8cm}}R{{1.6cm}}R{{1.6cm}}R{{1.9cm}}}}199\toprule200\textbf{{Dimension}} & {head} \\201\midrule202{body}203\midrule204$n$ & {foot_n} \\205$R^2$ & {foot_r2} \\206$\Delta R^2$ (semantic block) & {foot_d} \\207\bottomrule208\end{{tabular}}209\end{{adjustbox}}210\begin{{minipage}}{{\textwidth}}211\vspace{{4pt}}\footnotesize212\textit{{Notes:}} Entries are $100\,(e^{{\hat\gamma_j}} - 1)$ for a one-standard-deviation increase in similarity $j$ (standard deviation computed on the estimation sample). D is the baseline full model (HC3 standard errors); D-L adds English and mixed-language indicators (HC3); ``+ FSA FE'' absorbs 415 forward-sortation-area fixed effects with standard errors clustered by FSA; the French-only and English-only columns re-estimate D with FSA fixed effects on each language subsample; ML-D-L replaces the encoder by paraphrase-multilingual-MiniLM-L12-v2. \signote213\end{{minipage}}214\end{{table}}215"""216    write("tab_extended_specs.tex", content)217218219# --------------------------------------------------------------------------220def tab_block_tests():221    b = pd.read_csv(R / "revision_block_tests.csv")222    order = ["Quality & Standing", "Condition", "Physical Attributes", "Location & Setting",223             "Style & Character", "Market Signals", "All 20 dimensions"]224    hc = b[b.spec == "D-L (HC3)"].set_index("domain")225    fe = b[b.spec == "D-L + FSA FE (clustered)"].set_index("domain")226    rows = []227    for dom in order:228        rows.append(f"{tex(dom)} & {int(hc.loc[dom, 'n_dims'])} & {hc.loc[dom, 'F']:.2f} & "229                    f"{'$< 0.001$' if hc.loc[dom, 'p_value'] < 0.001 else f'{hc.loc[dom, 'p_value']:.3f}'} & "230                    f"{fe.loc[dom, 'F']:.2f} & "231                    f"{'$< 0.001$' if fe.loc[dom, 'p_value'] < 0.001 else f'{fe.loc[dom, 'p_value']:.3f}'} \\\\")232    body = "\n".join(rows)233    content = rf"""\begin{{table}}[!htbp]234\centering235\caption{{Domain-level block tests of the semantic dimensions.}}236\label{{tab:block_tests}}237\small238\begin{{tabular}}{{lR{{1.0cm}}R{{1.4cm}}R{{1.6cm}}R{{1.4cm}}R{{1.6cm}}}}239\toprule240& & \multicolumn{{2}}{{c}}{{\textbf{{D-L (HC3)}}}} & \multicolumn{{2}}{{c}}{{\textbf{{D-L + FSA FE (clustered)}}}} \\241\cmidrule(lr){{3-4}} \cmidrule(lr){{5-6}}242\textbf{{Domain}} & $\boldsymbol{{q}}$ & $\boldsymbol{{F}}$ & $\boldsymbol{{p}}$ & $\boldsymbol{{F}}$ & $\boldsymbol{{p}}$ \\243\midrule244{body}245\bottomrule246\end{{tabular}}247\begin{{minipage}}{{\textwidth}}248\vspace{{4pt}}\footnotesize249\textit{{Notes:}} Wald tests that the $q$ coefficients of the dimensions belonging to each domain of Table~\ref{{tab:references}} are jointly zero, under the D-L specification with HC3 standard errors and under D-L with 415 FSA fixed effects and standard errors clustered by FSA.250\end{{minipage}}251\end{{table}}252"""253    write("tab_block_tests.tex", content)254255256# --------------------------------------------------------------------------257def tab_oos():258    o = pd.read_csv(R / "revision_oos.csv")259    rnd = o[o.scheme == "random_5fold"].set_index("specification")260    spa = o[o.scheme == "spatial_fsa_5fold"].set_index("specification")261    order = ["A (raw lot-size field)", "A: structural", "B: + length", "B-L: + length + language",262             "D: + length + 20 similarities", "D-L: D + language", "PCA-20 + length",263             "PCA-20 + length + language", "ML-D: multilingual similarities",264             "ML-D-L: multilingual similarities + language"]265    names = {"A (raw lot-size field)": "A, raw lot-size field (for reference)",266             "A: structural": "A: structural", "B: + length": "B: A + description length",267             "B-L: + length + language": "B-L: B + language indicators",268             "D: + length + 20 similarities": "D: B + 20 similarities",269             "D-L: D + language": "D-L: D + language indicators",270             "PCA-20 + length": "B + 20 principal components of the embeddings",271             "PCA-20 + length + language": "B-L + 20 principal components",272             "ML-D: multilingual similarities": "ML-D: B + 20 multilingual similarities",273             "ML-D-L: multilingual similarities + language": "ML-D-L: B-L + 20 multilingual similarities"}274    rows = []275    for k in order:276        r, s = rnd.loc[k], spa.loc[k]277        rows.append(f"{names[k]} & {num(r['r2_oos'])} & {r['rmse_log']:.3f} & {r['mdape_pct']:.1f} & "278                    f"{num(s['r2_oos'])} & {s['rmse_log']:.3f} & {s['mdape_pct']:.1f} \\\\")279    body = "\n".join(rows)280    content = rf"""\begin{{table}}[!htbp]281\centering282\caption{{Out-of-sample performance: random and spatial five-fold cross-validation.}}283\label{{tab:oos}}284\small285\begin{{adjustbox}}{{max width=\textwidth}}286\begin{{tabular}}{{lR{{1.3cm}}R{{1.3cm}}R{{1.4cm}}R{{1.3cm}}R{{1.3cm}}R{{1.4cm}}}}287\toprule288& \multicolumn{{3}}{{c}}{{\textbf{{Random folds}}}} & \multicolumn{{3}}{{c}}{{\textbf{{FSA-grouped folds}}}} \\289\cmidrule(lr){{2-4}} \cmidrule(lr){{5-7}}290\textbf{{Specification}} & $\boldsymbol{{R^2_{{\text{{oos}}}}}}$ & \textbf{{RMSE}} & \textbf{{MdAPE}} & $\boldsymbol{{R^2_{{\text{{oos}}}}}}$ & \textbf{{RMSE}} & \textbf{{MdAPE}} \\291\midrule292{body}293\bottomrule294\end{{tabular}}295\end{{adjustbox}}296\begin{{minipage}}{{\textwidth}}297\vspace{{4pt}}\footnotesize298\textit{{Notes:}} Five-fold cross-validation on the $n = 17{{,}}087$ listings; predictions for each fold are made from a model estimated on the other four folds, and the metrics are computed on the pooled out-of-fold predictions (Equations~\ref{{eq:r2oos}}--\ref{{eq:mdape}}). Random folds are drawn with seed 42; FSA-grouped folds hold out entire forward sortation areas, so that no held-out listing shares an FSA with the training data. RMSE is in log points; MdAPE is the median absolute percentage error of the price level. Except for the first row, the lot-size variable is the unit-corrected log lot area with a missing indicator; the raw field (mixed units, maximum $7.2 \times 10^8$) yields exploding out-of-fold predictions and a negative $R^2$. Principal components are estimated on the training folds only.299\end{{minipage}}300\end{{table}}301"""302    write("tab_oos.tex", content)303304305# --------------------------------------------------------------------------306def tab_reference_sensitivity():307    s = pd.read_csv(R / "revision_reference_sensitivity_summary.csv").set_index("variant")308    names = {"first_half": "First half of each reference's phrases",309             "second_half": "Second half of each reference's phrases",310             "reversed": "Phrases in reverse order"}311    rows = []312    for k in ["first_half", "second_half", "reversed"]:313        r = s.loc[k]314        rows.append(f"{names[k]} & {r['mean_corr_similarity']:.3f} & {r['r2']:.4f} & {r['delta_r2_sims']:.3f} & "315                    f"{r['block_F']:.1f} & {r['corr_coef_with_baseline']:.3f} & "316                    f"{r['sign_agreement_sig16'] * 100:.0f}\\% & {int(r['n_sig_5pct'])}/20 \\\\")317    body = "\n".join(rows)318    content = rf"""\begin{{table}}[!htbp]319\centering320\caption{{Sensitivity of Model~D to the phrasing of the reference descriptions.}}321\label{{tab:reference_sensitivity}}322\small323\begin{{adjustbox}}{{max width=\textwidth}}324\begin{{tabular}}{{lR{{1.6cm}}R{{1.1cm}}R{{1.4cm}}R{{1.3cm}}R{{1.7cm}}R{{1.6cm}}R{{1.1cm}}}}325\toprule326\textbf{{Reference variant}} & \textbf{{Corr.\ of similarities}} & $\boldsymbol{{R^2}}$ & $\boldsymbol{{\Delta R^2}}$ \textbf{{(sem.)}} & \textbf{{Block}} $\boldsymbol{{F}}$ & \textbf{{Corr.\ of coefficients}} & \textbf{{Sign agreement}} & \textbf{{Sig.}} \\327\midrule328Baseline (full references) & 1.000 & 0.5122 & 0.048 & 54.3 & 1.000 & 100\% & 16/20 \\329{body}330\bottomrule331\end{{tabular}}332\end{{adjustbox}}333\begin{{minipage}}{{\textwidth}}334\vspace{{4pt}}\footnotesize335\textit{{Notes:}} Each reference text in Appendix Table~\ref{{tab:reference_texts}} is split into its comma-separated phrases and re-encoded with all-MiniLM-L6-v2 after the indicated perturbation; Model~D is then re-estimated with the 20 perturbed similarities. ``Corr.\ of similarities'' is the mean, over the 20 dimensions, of the correlation between perturbed and baseline similarity scores; ``Corr.\ of coefficients'' is the correlation between the 20 perturbed and baseline semantic coefficients; ``Sign agreement'' is the share of the 16 dimensions significant in the baseline whose coefficient keeps its sign; ``Sig.'' counts dimensions significant at 5\%. Block $F$ is the HC3 Wald test of the 20 semantic coefficients.336\end{{minipage}}337\end{{table}}338"""339    write("tab_reference_sensitivity.tex", content)340341342# --------------------------------------------------------------------------343def fig12_specification_stability():344    sp = pd.read_csv(R / "revision_specifications.csv")345    sims = sp[sp.variable.str.startswith(("sim_", "ml_"))]346    imp = sims.pivot_table(index="label", columns="spec", values="impact_pct")347    pv = sims.pivot_table(index="label", columns="spec", values="p_value")348    order = imp["D (baseline)"].sort_values().index.tolist()349    specs = [("D (baseline)", "D: baseline (MiniLM-L6)", "#1565C0", "o"),350             ("D-L + FSA FE", "D-L + FSA FE (MiniLM-L6)", "#2E7D32", "s"),351             ("ML-D-L + FSA FE", "ML-D-L + FSA FE (multilingual)", "#C62828", "D")]352    fig, ax = plt.subplots(figsize=(6.4, 6.0))353    yy = np.arange(len(order))354    for j, (key, lbl, col, mk) in enumerate(specs):355        off = (j - 1) * 0.24356        vals = imp.loc[order, key].values357        sig = pv.loc[order, key].values < 0.05358        ax.scatter(vals[sig], yy[sig] + off, s=26, color=col, marker=mk, label=lbl, zorder=3)359        ax.scatter(vals[~sig], yy[~sig] + off, s=26, facecolors="none", edgecolors=col,360                   marker=mk, zorder=3)361    for y in yy[:-1]:362        ax.axhline(y + 0.5, color="#DDDDDD", lw=0.5, zorder=1)363    ax.axvline(0, color="black", lw=0.8)364    ax.set_yticks(yy)365    ax.set_yticklabels(order, fontsize=8.5)366    ax.set_xlabel("Price impact of a one-standard-deviation increase in similarity (%)")367    ax.legend(loc="lower right", frameon=False)368    ax.set_ylim(-0.7, len(order) - 0.3)369    plt.tight_layout()370    for ext in ("pdf", "png"):371        plt.savefig(config.FIGURES_DIR / f"fig12_specification_stability.{ext}")372    plt.close()373    print("  -> figures/fig12_specification_stability")374375376def main():377    tab_language()378    tab_spec_summary()379    tab_extended_specs()380    tab_block_tests()381    tab_oos()382    tab_reference_sensitivity()383    fig12_specification_stability()384385386if __name__ == "__main__":387    main()388
Files · d5a5099f2f004278a7a1dba0ebaf2515519ee167
Menu

Theme