#!/usr/bin/env python3 # Author: Simon-Pierre Boucher — contact@spboucher.ai """Step 05 — LaTeX tables. Converts the step-02/03 CSV outputs into booktabs tables under ``results/tables/``. The paper inputs these files directly, so every number in the manuscript is machine-generated. Usage: python scripts/05_make_tables.py """ import sys from pathlib import Path import pandas as pd sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) from wp10 import config # noqa: E402 RES = config.REPRODUCED TAB = config.TABLES def _stars(coef: float, se: float) -> str: t = abs(coef / se) if se > 0 else 0 return "***" if t > 2.576 else "**" if t > 1.96 else "*" if t > 1.645 else "" def _f(x, nd=3): return f"{x:,.{nd}f}" def t_summary(): s = pd.read_csv(RES / "summary_stats.csv") lines = [r"\begin{tabular}{lrrrrrr}", r"\toprule", r"Variable & $n$ & Mean & SD & P10 & Median & P90 \\", r"\midrule"] for _, r in s.iterrows(): big = r["mean"] > 1000 nd = 0 if big else 3 if r["mean"] < 10 else 1 label = (str(r["variable"]).replace("($)", "(\\$)") .replace("(m2)", "(m$^2$)")) lines.append( f"{label} & {r['n']:,.0f} & {_f(r['mean'], nd)} & " f"{_f(r['sd'], nd)} & {_f(r['p10'], nd)} & {_f(r['p50'], nd)} & " f"{_f(r['p90'], nd)} \\\\") lines += [r"\bottomrule", r"\end{tabular}"] (TAB / "summary_stats.tex").write_text("\n".join(lines)) def t_iaao(): d = pd.read_csv(RES / "iaao_overall.csv") lines = [r"\begin{tabular}{lrcccc}", r"\toprule", r"Sample & $n$ & Median ratio & COD & PRD & PRB \\", r"\midrule"] for _, r in d.iterrows(): lines.append( f"{r['group']} & {r['n']:,.0f} & {_f(r['median_ratio'])} & " f"{_f(r['cod'], 1)} & {_f(r['prd'])} & {_f(r['prb'], 4)} \\\\") lines.append( f" & & \\footnotesize[{_f(r['median_lo'])}, {_f(r['median_hi'])}]" f" & \\footnotesize[{_f(r['cod_lo'], 1)}, {_f(r['cod_hi'], 1)}]" f" & \\footnotesize[{_f(r['prd_lo'])}, {_f(r['prd_hi'])}]" f" & \\footnotesize[{_f(r['prb_lo'], 4)}, {_f(r['prb_hi'], 4)}] \\\\") lines += [r"\bottomrule", r"\end{tabular}"] (TAB / "iaao.tex").write_text("\n".join(lines)) def t_cities(): d = pd.read_csv(RES / "iaao_cities.csv").sort_values("n", ascending=False) lines = [r"\begin{tabular}{lrccccc}", r"\toprule", r"Municipality & $n$ & Median ratio & COD & PRD & PRB & " r"\makecell{Share of years\\PRB $<$ 0} \\", r"\midrule"] for _, r in d.iterrows(): lines.append( f"{r['muni']} & {r['n']:,.0f} & {_f(r['median_ratio'])} & " f"{_f(r['cod'], 1)} & {_f(r['prd'])} & {_f(r['prb'], 4)} & " f"{_f(r['share_years_prb_neg'], 2)} \\\\") lines += [r"\bottomrule", r"\end{tabular}"] (TAB / "iaao_cities.tex").write_text("\n".join(lines)) def t_vertical(): v = pd.read_csv(RES / "vertical.csv") pf = pd.read_csv(RES / "paglin_fogarty.csv", index_col=0).squeeze() pf = {k: (v if k == "estimator" else float(v)) for k, v in pf.items()} q = pd.read_csv(RES / "quantile.csv") def row(label, vals): return label + " & " + " & ".join(vals) + r" \\" cols = list(v["estimator"]) lines = [r"\begin{tabular}{l" + "c" * len(cols) + "}", r"\toprule", row("", [f"({i+1})" for i in range(len(cols))]), row("", [c.replace(" (pooled)", "").replace(" (cell)", "") .replace(" (rank instrument)", "") for c in cols]), r"\midrule", row(r"$\beta$ (ln sale price)", [f"{_f(r['beta'])}{_stars(r['beta'] - 1, r['se'])}" for _, r in v.iterrows()]), row("", [f"({_f(r['se'])})" for _, r in v.iterrows()]), row(r"$\gamma = \beta - 1$", [f"{r['gamma']:+.3f}" for _, r in v.iterrows()]), r"\midrule", row("Cell fixed effects", ["No", "Yes", "Yes"]), row("Error-in-price robust", ["No", "No", "Yes"]), row("Observations", [f"{r['n']:,.0f}" for _, r in v.iterrows()]), row("$R^2$", [f"{_f(r['r2'])}" for _, r in v.iterrows()]), r"\bottomrule", r"\end{tabular}"] (TAB / "vertical.tex").write_text("\n".join(lines)) # stars in this table test H0: beta = 1 (proportionality), noted in caption lines = [r"\begin{tabular}{lcc}", r"\toprule", r" & Coefficient & Std.\ error \\", r"\midrule", f"Intercept ($\\$$) & {pf['intercept']:,.0f} & " f"{pf['intercept_se']:,.0f} \\\\", f"Slope on sale price & {_f(pf['slope'])} & " f"{_f(pf['slope_se'])} \\\\", f"Observations & \\multicolumn{{2}}{{c}}{{{pf['n']:,.0f}}} \\\\", r"\bottomrule", r"\end{tabular}"] (TAB / "paglin_fogarty.tex").write_text("\n".join(lines)) lines = [r"\begin{tabular}{lccccc}", r"\toprule", "Quantile $\\tau$ & " + " & ".join(f"{t:.2f}" for t in q["tau"]) + r" \\", r"\midrule", r"$\beta(\tau)$ & " + " & ".join( f"{_f(r['beta'])}{_stars(r['beta'] - 1, r['se'])}" for _, r in q.iterrows()) + r" \\", " & " + " & ".join(f"({_f(r['se'], 4)})" for _, r in q.iterrows()) + r" \\", r"\bottomrule", r"\end{tabular}"] (TAB / "quantile.tex").write_text("\n".join(lines)) def t_heterogeneity(): h = pd.read_csv(RES / "heterogeneity.csv") panels = { "Property class": ["Single-family", "Condominium", "Plex (2–5 units)", "Cottage"], "Building age": ["Age < 20 y", "Age 20–60 y", "Age > 60 y"], "Assessed land share": ["Land share < 0.2", "Land share 0.2–0.4", "Land share > 0.4"], "Roll lag at sale": ["Roll lag < 24 m", "Roll lag 24–48 m", "Roll lag > 48 m"], "Municipality size": ["Muni < 1k sales", "Muni 1k–10k sales", "Muni > 10k sales"], "Sale year": [f"Sales {y}" for y in range(2021, 2027)], } lines = [r"\begin{tabular}{lccr}", r"\toprule", r"Subsample & $\gamma$ & Std.\ error & $n$ \\"] for panel, keys in panels.items(): lines.append(r"\midrule") lines.append(r"\multicolumn{4}{l}{\itshape " + panel + r"}\\") for k in keys: r = h[h["group"] == k] if r.empty: continue r = r.iloc[0] label = (k.replace("<", "$<$").replace(">", "$>$") .replace("–", "--")) lines.append( f"\\quad {label} & {r['gamma']:+.3f}" f"{_stars(r['gamma'], r['se'])} & ({_f(r['se'])}) & " f"{r['n']:,.0f} \\\\") lines += [r"\bottomrule", r"\end{tabular}"] (TAB / "heterogeneity.tex").write_text("\n".join(lines)) def t_taxshift(): t = pd.read_csv(RES / "taxshift.csv") lines = [r"\begin{tabular}{lcccc}", r"\toprule", r"Price decile & $n$ & \makecell{Mean excess\\burden (\%)} & " r"\makecell{Median excess\\burden (\%)} & Std.\ error \\", r"\midrule"] for _, r in t.iterrows(): lines.append( f"{int(r['decile'])} & {r['n']:,.0f} & " f"{100 * r['mean_rel']:+.1f} & {100 * r['median_rel']:+.1f} & " f"({100 * r['se']:.2f}) \\\\") lines += [r"\bottomrule", r"\end{tabular}"] (TAB / "taxshift.tex").write_text("\n".join(lines)) def t_horizontal(): h = pd.read_csv(RES / "horizontal.csv", index_col=0) meta = pd.read_csv(RES / "horizontal_meta.csv", index_col=0).squeeze() labels = {"age_dec": "Building age (decades)", "land_share": "Assessed land share", "is_condo": "Condominium", "is_plex": "Plex (2--5 units)", "is_cottage": "Cottage"} lines = [r"\begin{tabular}{lcc}", r"\toprule", r" & Coefficient & Std.\ error \\", r"\midrule"] for k, lab in labels.items(): r = h.loc[k] lines.append(f"{lab} & {r['coef']:+.4f}{_stars(r['coef'], r['se'])} & " f"({_f(r['se'], 4)}) \\\\") lines += [r"\midrule", f"Mean of dependent variable & \\multicolumn{{2}}{{c}}" f"{{{float(meta['mean_dep']):.3f}}} \\\\", f"Observations & \\multicolumn{{2}}{{c}}{{{float(meta['n']):,.0f}}} \\\\", r"\bottomrule", r"\end{tabular}"] (TAB / "horizontal.tex").write_text("\n".join(lines)) def t_robustness(): r = pd.read_csv(RES / "robustness.csv") lines = [r"\begin{tabular}{lccccr}", r"\toprule", r"Sample variant & $\gamma_{\text{FE}}$ & Std.\ err. & " r"$\gamma_{\text{IV}}$ & Std.\ err. & $n$ \\", r"\midrule"] for _, x in r.iterrows(): label = (str(x["variant"]).replace("<=", "$\\le$") .replace(">=", "$\\ge$").replace("$100k", "\\$100k")) lines.append( f"{label} & {x['gamma_fe']:+.3f}{_stars(x['gamma_fe'], x['se_fe'])}" f" & ({_f(x['se_fe'])}) & " f"{x['gamma_iv']:+.3f}{_stars(x['gamma_iv'], x['se_iv'])} & " f"({_f(x['se_iv'])}) & {x['n']:,.0f} \\\\") lines += [r"\bottomrule", r"\end{tabular}"] (TAB / "robustness.tex").write_text("\n".join(lines)) def main() -> None: config.ensure_dirs() t_summary() t_iaao() t_cities() t_vertical() t_heterogeneity() t_taxshift() t_horizontal() t_robustness() made = sorted(p.name for p in TAB.glob("*.tex")) print(f"{len(made)} tables written:", ", ".join(made)) if __name__ == "__main__": main()