SPB Git

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%
5.5 KB · 151 lines python
Raw Blame History
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