spb/wp10_uqo Public
UQO Working Paper No. 10 — The assessment gap in Quebec: vertical and horizontal inequity in municipal property assessment.
TeX 55.9%
Python 44%
1#!/usr/bin/env python32# Author: Simon-Pierre Boucher — contact@spboucher.ai3"""Step 05 — LaTeX tables.45Converts the step-02/03 CSV outputs into booktabs tables under6``results/tables/``. The paper inputs these files directly, so every number7in the manuscript is machine-generated.89Usage: python scripts/05_make_tables.py10"""11import sys12from pathlib import Path1314import pandas as pd1516sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))1718from wp10 import config # noqa: E4021920RES = config.REPRODUCED21TAB = config.TABLES222324def _stars(coef: float, se: float) -> str:25 t = abs(coef / se) if se > 0 else 026 return "***" if t > 2.576 else "**" if t > 1.96 else "*" if t > 1.645 else ""272829def _f(x, nd=3):30 return f"{x:,.{nd}f}"313233def t_summary():34 s = pd.read_csv(RES / "summary_stats.csv")35 lines = [r"\begin{tabular}{lrrrrrr}", r"\toprule",36 r"Variable & $n$ & Mean & SD & P10 & Median & P90 \\",37 r"\midrule"]38 for _, r in s.iterrows():39 big = r["mean"] > 100040 nd = 0 if big else 3 if r["mean"] < 10 else 141 label = (str(r["variable"]).replace("($)", "(\\$)")42 .replace("(m2)", "(m$^2$)"))43 lines.append(44 f"{label} & {r['n']:,.0f} & {_f(r['mean'], nd)} & "45 f"{_f(r['sd'], nd)} & {_f(r['p10'], nd)} & {_f(r['p50'], nd)} & "46 f"{_f(r['p90'], nd)} \\\\")47 lines += [r"\bottomrule", r"\end{tabular}"]48 (TAB / "summary_stats.tex").write_text("\n".join(lines))495051def t_iaao():52 d = pd.read_csv(RES / "iaao_overall.csv")53 lines = [r"\begin{tabular}{lrcccc}", r"\toprule",54 r"Sample & $n$ & Median ratio & COD & PRD & PRB \\",55 r"\midrule"]56 for _, r in d.iterrows():57 lines.append(58 f"{r['group']} & {r['n']:,.0f} & {_f(r['median_ratio'])} & "59 f"{_f(r['cod'], 1)} & {_f(r['prd'])} & {_f(r['prb'], 4)} \\\\")60 lines.append(61 f" & & \\footnotesize[{_f(r['median_lo'])}, {_f(r['median_hi'])}]"62 f" & \\footnotesize[{_f(r['cod_lo'], 1)}, {_f(r['cod_hi'], 1)}]"63 f" & \\footnotesize[{_f(r['prd_lo'])}, {_f(r['prd_hi'])}]"64 f" & \\footnotesize[{_f(r['prb_lo'], 4)}, {_f(r['prb_hi'], 4)}] \\\\")65 lines += [r"\bottomrule", r"\end{tabular}"]66 (TAB / "iaao.tex").write_text("\n".join(lines))676869def t_cities():70 d = pd.read_csv(RES / "iaao_cities.csv").sort_values("n", ascending=False)71 lines = [r"\begin{tabular}{lrccccc}", r"\toprule",72 r"Municipality & $n$ & Median ratio & COD & PRD & PRB & "73 r"\makecell{Share of years\\PRB $<$ 0} \\",74 r"\midrule"]75 for _, r in d.iterrows():76 lines.append(77 f"{r['muni']} & {r['n']:,.0f} & {_f(r['median_ratio'])} & "78 f"{_f(r['cod'], 1)} & {_f(r['prd'])} & {_f(r['prb'], 4)} & "79 f"{_f(r['share_years_prb_neg'], 2)} \\\\")80 lines += [r"\bottomrule", r"\end{tabular}"]81 (TAB / "iaao_cities.tex").write_text("\n".join(lines))828384def t_vertical():85 v = pd.read_csv(RES / "vertical.csv")86 pf = pd.read_csv(RES / "paglin_fogarty.csv", index_col=0).squeeze()87 pf = {k: (v if k == "estimator" else float(v)) for k, v in pf.items()}88 q = pd.read_csv(RES / "quantile.csv")8990 def row(label, vals):91 return label + " & " + " & ".join(vals) + r" \\"9293 cols = list(v["estimator"])94 lines = [r"\begin{tabular}{l" + "c" * len(cols) + "}", r"\toprule",95 row("", [f"({i+1})" for i in range(len(cols))]),96 row("", [c.replace(" (pooled)", "").replace(" (cell)", "")97 .replace(" (rank instrument)", "") for c in cols]),98 r"\midrule",99 row(r"$\beta$ (ln sale price)",100 [f"{_f(r['beta'])}{_stars(r['beta'] - 1, r['se'])}"101 for _, r in v.iterrows()]),102 row("", [f"({_f(r['se'])})" for _, r in v.iterrows()]),103 row(r"$\gamma = \beta - 1$",104 [f"{r['gamma']:+.3f}" for _, r in v.iterrows()]),105 r"\midrule",106 row("Cell fixed effects",107 ["No", "Yes", "Yes"]),108 row("Error-in-price robust", ["No", "No", "Yes"]),109 row("Observations", [f"{r['n']:,.0f}" for _, r in v.iterrows()]),110 row("$R^2$", [f"{_f(r['r2'])}" for _, r in v.iterrows()]),111 r"\bottomrule", r"\end{tabular}"]112 (TAB / "vertical.tex").write_text("\n".join(lines))113 # stars in this table test H0: beta = 1 (proportionality), noted in caption114115 lines = [r"\begin{tabular}{lcc}", r"\toprule",116 r" & Coefficient & Std.\ error \\", r"\midrule",117 f"Intercept ($\\$$) & {pf['intercept']:,.0f} & "118 f"{pf['intercept_se']:,.0f} \\\\",119 f"Slope on sale price & {_f(pf['slope'])} & "120 f"{_f(pf['slope_se'])} \\\\",121 f"Observations & \\multicolumn{{2}}{{c}}{{{pf['n']:,.0f}}} \\\\",122 r"\bottomrule", r"\end{tabular}"]123 (TAB / "paglin_fogarty.tex").write_text("\n".join(lines))124125 lines = [r"\begin{tabular}{lccccc}", r"\toprule",126 "Quantile $\\tau$ & " + " & ".join(f"{t:.2f}" for t in q["tau"])127 + r" \\", r"\midrule",128 r"$\beta(\tau)$ & " + " & ".join(129 f"{_f(r['beta'])}{_stars(r['beta'] - 1, r['se'])}"130 for _, r in q.iterrows()) + r" \\",131 " & " + " & ".join(f"({_f(r['se'], 4)})" for _, r in q.iterrows())132 + r" \\",133 r"\bottomrule", r"\end{tabular}"]134 (TAB / "quantile.tex").write_text("\n".join(lines))135136137def t_heterogeneity():138 h = pd.read_csv(RES / "heterogeneity.csv")139 panels = {140 "Property class": ["Single-family", "Condominium", "Plex (2–5 units)",141 "Cottage"],142 "Building age": ["Age < 20 y", "Age 20–60 y", "Age > 60 y"],143 "Assessed land share": ["Land share < 0.2", "Land share 0.2–0.4",144 "Land share > 0.4"],145 "Roll lag at sale": ["Roll lag < 24 m", "Roll lag 24–48 m",146 "Roll lag > 48 m"],147 "Municipality size": ["Muni < 1k sales", "Muni 1k–10k sales",148 "Muni > 10k sales"],149 "Sale year": [f"Sales {y}" for y in range(2021, 2027)],150 }151 lines = [r"\begin{tabular}{lccr}", r"\toprule",152 r"Subsample & $\gamma$ & Std.\ error & $n$ \\"]153 for panel, keys in panels.items():154 lines.append(r"\midrule")155 lines.append(r"\multicolumn{4}{l}{\itshape " + panel + r"}\\")156 for k in keys:157 r = h[h["group"] == k]158 if r.empty:159 continue160 r = r.iloc[0]161 label = (k.replace("<", "$<$").replace(">", "$>$")162 .replace("–", "--"))163 lines.append(164 f"\\quad {label} & {r['gamma']:+.3f}"165 f"{_stars(r['gamma'], r['se'])} & ({_f(r['se'])}) & "166 f"{r['n']:,.0f} \\\\")167 lines += [r"\bottomrule", r"\end{tabular}"]168 (TAB / "heterogeneity.tex").write_text("\n".join(lines))169170171def t_taxshift():172 t = pd.read_csv(RES / "taxshift.csv")173 lines = [r"\begin{tabular}{lcccc}", r"\toprule",174 r"Price decile & $n$ & \makecell{Mean excess\\burden (\%)} & "175 r"\makecell{Median excess\\burden (\%)} & Std.\ error \\",176 r"\midrule"]177 for _, r in t.iterrows():178 lines.append(179 f"{int(r['decile'])} & {r['n']:,.0f} & "180 f"{100 * r['mean_rel']:+.1f} & {100 * r['median_rel']:+.1f} & "181 f"({100 * r['se']:.2f}) \\\\")182 lines += [r"\bottomrule", r"\end{tabular}"]183 (TAB / "taxshift.tex").write_text("\n".join(lines))184185186def t_horizontal():187 h = pd.read_csv(RES / "horizontal.csv", index_col=0)188 meta = pd.read_csv(RES / "horizontal_meta.csv", index_col=0).squeeze()189 labels = {"age_dec": "Building age (decades)",190 "land_share": "Assessed land share",191 "is_condo": "Condominium",192 "is_plex": "Plex (2--5 units)",193 "is_cottage": "Cottage"}194 lines = [r"\begin{tabular}{lcc}", r"\toprule",195 r" & Coefficient & Std.\ error \\", r"\midrule"]196 for k, lab in labels.items():197 r = h.loc[k]198 lines.append(f"{lab} & {r['coef']:+.4f}{_stars(r['coef'], r['se'])} & "199 f"({_f(r['se'], 4)}) \\\\")200 lines += [r"\midrule",201 f"Mean of dependent variable & \\multicolumn{{2}}{{c}}"202 f"{{{float(meta['mean_dep']):.3f}}} \\\\",203 f"Observations & \\multicolumn{{2}}{{c}}{{{float(meta['n']):,.0f}}} \\\\",204 r"\bottomrule", r"\end{tabular}"]205 (TAB / "horizontal.tex").write_text("\n".join(lines))206207208def t_robustness():209 r = pd.read_csv(RES / "robustness.csv")210 lines = [r"\begin{tabular}{lccccr}", r"\toprule",211 r"Sample variant & $\gamma_{\text{FE}}$ & Std.\ err. & "212 r"$\gamma_{\text{IV}}$ & Std.\ err. & $n$ \\",213 r"\midrule"]214 for _, x in r.iterrows():215 label = (str(x["variant"]).replace("<=", "$\\le$")216 .replace(">=", "$\\ge$").replace("$100k", "\\$100k"))217 lines.append(218 f"{label} & {x['gamma_fe']:+.3f}{_stars(x['gamma_fe'], x['se_fe'])}"219 f" & ({_f(x['se_fe'])}) & "220 f"{x['gamma_iv']:+.3f}{_stars(x['gamma_iv'], x['se_iv'])} & "221 f"({_f(x['se_iv'])}) & {x['n']:,.0f} \\\\")222 lines += [r"\bottomrule", r"\end{tabular}"]223 (TAB / "robustness.tex").write_text("\n".join(lines))224225226def main() -> None:227 config.ensure_dirs()228 t_summary()229 t_iaao()230 t_cities()231 t_vertical()232 t_heterogeneity()233 t_taxshift()234 t_horizontal()235 t_robustness()236 made = sorted(p.name for p in TAB.glob("*.tex"))237 print(f"{len(made)} tables written:", ", ".join(made))238239240if __name__ == "__main__":241 main()242