spb/wp11_uqo Public
UQO Working Paper No. 11 — Half a million prices, twenty models: a systematic assessment of hedonic specifications.
TeX 54.7%
Python 45.2%
1#!/usr/bin/env python32# Author: Simon-Pierre Boucher — contact@spboucher.ai3"""Step 05 — LaTeX tables.45Converts the step-02/03 CSVs into booktabs tables under6``results/tables/``; the paper inputs these files directly.78Usage: python scripts/05_make_tables.py9"""10import sys11from pathlib import Path1213import pandas as pd1415sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))1617from wp11 import config # noqa: E4021819RES = config.REPRODUCED20TAB = config.TABLES212223def _f(x, nd=3):24 return f"{x:,.{nd}f}"252627def _esc(s: str) -> str:28 return (s.replace("–", "--").replace("~", r"$\sim$")29 .replace("<", "$<$").replace(">", "$>$"))303132def t_summary():33 s = pd.read_csv(RES / "summary_stats.csv")34 lines = [r"\begin{tabular}{lrrrrrr}", r"\toprule",35 r"Variable & $n$ & Mean & SD & P10 & Median & P90 \\",36 r"\midrule"]37 for _, r in s.iterrows():38 nd = 0 if r["mean"] > 1000 else 239 label = (str(r["variable"]).replace("($)", "(\\$)")40 .replace("(m2)", "(m$^2$)"))41 lines.append(42 f"{label} & {r['n']:,.0f} & {_f(r['mean'], nd)} & "43 f"{_f(r['sd'], nd)} & {_f(r['p10'], nd)} & {_f(r['p50'], nd)} & "44 f"{_f(r['p90'], nd)} \\\\")45 lines += [r"\bottomrule", r"\end{tabular}"]46 (TAB / "summary_stats.tex").write_text("\n".join(lines))474849def _race_table(fname_csv: str, fname_tex: str):50 r = pd.read_csv(RES / fname_csv)51 lines = [r"\begin{tabular}{lcccc c}", r"\toprule",52 r"Model & MdAPE (\%) & MAPE (\%) & RMSE$_{\ln}$ & "53 r"$R^2_{\ln}$ & Fit (s) \\"]54 for g in ["A. Functional form", "B. Time effects",55 "C. Spatial controls", "D. Estimation method"]:56 sub = r[r["group"] == g]57 if sub.empty:58 continue59 lines.append(r"\midrule")60 lines.append(r"\multicolumn{6}{l}{\itshape " + g + r"}\\")61 best = sub["mdape"].min()62 for _, x in sub.iterrows():63 name = _esc(str(x["name"]))64 md = f"\\textbf{{{_f(x['mdape'], 1)}}}" \65 if x["mdape"] == best else _f(x["mdape"], 1)66 lines.append(67 f"\\quad {name} & {md} & {_f(x['mape'], 1)} & "68 f"{_f(x['rmse_ln'])} & {_f(x['r2_ln'])} & "69 f"{_f(x['seconds'], 0)} \\\\")70 lines += [r"\bottomrule", r"\end{tabular}"]71 (TAB / fname_tex).write_text("\n".join(lines))727374def t_generalization():75 a = pd.read_csv(RES / "horserace_random.csv")76 b = pd.read_csv(RES / "horserace_temporal.csv")77 m = a.merge(b, on=["name", "group"], suffixes=("_r", "_t"))78 m = m[m["name"].str.match(r"(A5|B4|C4|D)")]79 lines = [r"\begin{tabular}{lcc c cc}", r"\toprule",80 r" & \multicolumn{2}{c}{Random holdout} & &"81 r" \multicolumn{2}{c}{Forward-in-time} \\",82 r"\cmidrule{2-3}\cmidrule{5-6}",83 r"Model & MdAPE (\%) & $R^2_{\ln}$ & & MdAPE (\%) & "84 r"$R^2_{\ln}$ \\", r"\midrule"]85 for _, x in m.iterrows():86 lines.append(87 f"{_esc(str(x['name']))} & {_f(x['mdape_r'], 1)} & "88 f"{_f(x['r2_ln_r'])} & & {_f(x['mdape_t'], 1)} & "89 f"{_f(x['r2_ln_t'])} \\\\")90 lines += [r"\bottomrule", r"\end{tabular}"]91 (TAB / "generalization.tex").write_text("\n".join(lines))929394def t_boxcox():95 b = pd.read_csv(RES / "boxcox.csv")96 lines = [r"\begin{tabular}{l" + "c" * len(b) + "}", r"\toprule",97 r"$\lambda$ & " + " & ".join(_f(x, 2) for x in b["lambda"])98 + r" \\", r"\midrule",99 r"Profile log-likelihood$^{\dagger}$ & "100 + " & ".join(f"{(x - b['loglik'].max())/1000:,.1f}"101 for x in b["loglik"]) + r" \\",102 r"\bottomrule", r"\end{tabular}"]103 (TAB / "boxcox.tex").write_text("\n".join(lines))104105106def t_learning():107 l = pd.read_csv(RES / "learning.csv")108 piv = l.pivot_table(index="n_train", columns="model", values="mdape")109 cols = list(piv.columns)110 lines = [r"\begin{tabular}{r" + "c" * len(cols) + "}", r"\toprule",111 "Training sales & " + " & ".join(_esc(c) for c in cols) + r" \\",112 r"\midrule"]113 for n, row in piv.iterrows():114 lines.append(f"{n:,.0f} & "115 + " & ".join(_f(v, 1) for v in row) + r" \\")116 lines += [r"\bottomrule", r"\end{tabular}"]117 (TAB / "learning.tex").write_text("\n".join(lines))118119120def t_segments():121 s = pd.read_csv(RES / "segments.csv")122 piv = s.pivot_table(index="segment", columns="model", values="mdape")123 ns = s.groupby("segment")["n"].first()124 lines = [r"\begin{tabular}{lrcc}", r"\toprule",125 r"Segment & $n$ (test) & OLS quadratic & Gradient boosting \\",126 r"\midrule"]127 for seg, row in piv.iterrows():128 label = _esc(str(seg)).replace("Class: ", "").replace("_", " ")129 lines.append(f"{label.capitalize()} & {ns[seg]:,.0f} & "130 f"{_f(row['OLS quadratic'], 1)} & "131 f"{_f(row['Gradient boosting'], 1)} \\\\")132 lines += [r"\bottomrule", r"\end{tabular}"]133 (TAB / "segments.tex").write_text("\n".join(lines))134135136def main() -> None:137 config.ensure_dirs()138 t_summary()139 _race_table("horserace_random.csv", "horserace_random.tex")140 _race_table("horserace_temporal.csv", "horserace_temporal.tex")141 t_generalization()142 t_boxcox()143 t_learning()144 t_segments()145 made = sorted(p.name for p in TAB.glob("*.tex"))146 print(f"{len(made)} tables written:", ", ".join(made))147148149if __name__ == "__main__":150 main()151