SPB Git

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%
9.7 KB · 242 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 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