UQO Working Paper No. 10 — The assessment gap in Quebec: vertical and horizontal inequity in municipal property assessment.
1#!/usr/bin/env python32# Author: Simon-Pierre Boucher — contact@spboucher.ai3"""Step 05 — LaTeX tables.45Converts the step-02/03 CSV outputs (and the step-06 revision outputs,6``rev_*.csv``) into booktabs tables under ``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 numpy as np15import pandas as pd1617sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))1819from wp10 import config # noqa: E4022021RES = config.REPRODUCED22TAB = config.TABLES232425def _stars(coef: float, se: float) -> str:26 t = abs(coef / se) if se > 0 else 027 return "***" if t > 2.576 else "**" if t > 1.96 else "*" if t > 1.645 else ""282930def _f(x, nd=3):31 return f"{x:,.{nd}f}"323334def t_summary():35 s = pd.read_csv(RES / "summary_stats.csv")36 lines = [r"\begin{tabular}{lrrrrrr}", r"\toprule",37 r"Variable & $n$ & Mean & SD & P10 & Median & P90 \\",38 r"\midrule"]39 for _, r in s.iterrows():40 big = r["mean"] > 100041 nd = 0 if big else 3 if r["mean"] < 10 else 142 label = (str(r["variable"]).replace("($)", "(\\$)")43 .replace("(m2)", "(m$^2$)"))44 lines.append(45 f"{label} & {r['n']:,.0f} & {_f(r['mean'], nd)} & "46 f"{_f(r['sd'], nd)} & {_f(r['p10'], nd)} & {_f(r['p50'], nd)} & "47 f"{_f(r['p90'], nd)} \\\\")48 lines += [r"\bottomrule", r"\end{tabular}"]49 (TAB / "summary_stats.tex").write_text("\n".join(lines))505152def t_iaao():53 d = pd.read_csv(RES / "iaao_overall.csv")54 lines = [r"\begin{tabular}{lrcccc}", r"\toprule",55 r"Sample & $n$ & Median ratio & COD & PRD & PRB \\",56 r"\midrule"]57 for _, r in d.iterrows():58 lines.append(59 f"{r['group']} & {r['n']:,.0f} & {_f(r['median_ratio'])} & "60 f"{_f(r['cod'], 1)} & {_f(r['prd'])} & {_f(r['prb'], 4)} \\\\")61 lines.append(62 f" & & \\footnotesize[{_f(r['median_lo'])}, {_f(r['median_hi'])}]"63 f" & \\footnotesize[{_f(r['cod_lo'], 1)}, {_f(r['cod_hi'], 1)}]"64 f" & \\footnotesize[{_f(r['prd_lo'])}, {_f(r['prd_hi'])}]"65 f" & \\footnotesize[{_f(r['prb_lo'], 4)}, {_f(r['prb_hi'], 4)}] \\\\")66 lines += [r"\bottomrule", r"\end{tabular}"]67 (TAB / "iaao.tex").write_text("\n".join(lines))686970def t_cities():71 d = pd.read_csv(RES / "iaao_cities.csv").sort_values("n", ascending=False)72 lines = [r"\begin{tabular}{lrccccc}", r"\toprule",73 r"Municipality & $n$ & Median ratio & COD & PRD & PRB & "74 r"\makecell{Share of years\\PRB $<$ 0} \\",75 r"\midrule"]76 for _, r in d.iterrows():77 lines.append(78 f"{r['muni']} & {r['n']:,.0f} & {_f(r['median_ratio'])} & "79 f"{_f(r['cod'], 1)} & {_f(r['prd'])} & {_f(r['prb'], 4)} & "80 f"{_f(r['share_years_prb_neg'], 2)} \\\\")81 lines += [r"\bottomrule", r"\end{tabular}"]82 (TAB / "iaao_cities.tex").write_text("\n".join(lines))838485def t_vertical():86 v = pd.read_csv(RES / "vertical.csv")87 pf = pd.read_csv(RES / "paglin_fogarty.csv", index_col=0).squeeze()88 pf = {k: (v if k == "estimator" else float(v)) for k, v in pf.items()}89 q = pd.read_csv(RES / "quantile.csv")9091 def row(label, vals):92 return label + " & " + " & ".join(vals) + r" \\"9394 cols = list(v["estimator"])95 lines = [r"\begin{tabular}{l" + "c" * len(cols) + "}", r"\toprule",96 row("", [f"({i+1})" for i in range(len(cols))]),97 row("", [c.replace(" (pooled)", "").replace(" (cell)", "")98 .replace(" (rank instrument)", "") for c in cols]),99 r"\midrule",100 row(r"$\beta$ (ln sale price)",101 [f"{_f(r['beta'])}{_stars(r['beta'] - 1, r['se'])}"102 for _, r in v.iterrows()]),103 row("", [f"({_f(r['se'])})" for _, r in v.iterrows()]),104 row(r"$\gamma = \beta - 1$",105 [f"{r['gamma']:+.3f}" for _, r in v.iterrows()]),106 r"\midrule",107 row("Cell fixed effects",108 ["No", "Yes", "Yes"]),109 row("Error-in-price robust", ["No", "No", "Yes"]),110 row("Observations", [f"{r['n']:,.0f}" for _, r in v.iterrows()]),111 row("$R^2$", [f"{_f(r['r2'])}" for _, r in v.iterrows()]),112 r"\bottomrule", r"\end{tabular}"]113 (TAB / "vertical.tex").write_text("\n".join(lines))114 # stars in this table test H0: beta = 1 (proportionality), noted in caption115116 lines = [r"\begin{tabular}{lcc}", r"\toprule",117 r" & Coefficient & Std.\ error \\", r"\midrule",118 f"Intercept ($\\$$) & {pf['intercept']:,.0f} & "119 f"{pf['intercept_se']:,.0f} \\\\",120 f"Slope on sale price & {_f(pf['slope'])} & "121 f"{_f(pf['slope_se'])} \\\\",122 f"Observations & \\multicolumn{{2}}{{c}}{{{pf['n']:,.0f}}} \\\\",123 r"\bottomrule", r"\end{tabular}"]124 (TAB / "paglin_fogarty.tex").write_text("\n".join(lines))125126 lines = [r"\begin{tabular}{lccccc}", r"\toprule",127 "Quantile $\\tau$ & " + " & ".join(f"{t:.2f}" for t in q["tau"])128 + r" \\", r"\midrule",129 r"$\beta(\tau)$ & " + " & ".join(130 f"{_f(r['beta'])}{_stars(r['beta'] - 1, r['se'])}"131 for _, r in q.iterrows()) + r" \\",132 " & " + " & ".join(f"({_f(r['se'], 4)})" for _, r in q.iterrows())133 + r" \\",134 r"\bottomrule", r"\end{tabular}"]135 (TAB / "quantile.tex").write_text("\n".join(lines))136137138def t_heterogeneity():139 h = pd.read_csv(RES / "heterogeneity.csv")140 panels = {141 "Property class": ["Single-family", "Condominium", "Plex (2–5 units)",142 "Cottage"],143 "Building age": ["Age < 20 y", "Age 20–60 y", "Age > 60 y"],144 "Assessed land share": ["Land share < 0.2", "Land share 0.2–0.4",145 "Land share > 0.4"],146 "Roll lag at sale": ["Roll lag < 24 m", "Roll lag 24–48 m",147 "Roll lag > 48 m"],148 "Municipality size": ["Muni < 1k sales", "Muni 1k–10k sales",149 "Muni > 10k sales"],150 "Sale year": [f"Sales {y}" for y in range(2021, 2027)],151 }152 lines = [r"\begin{tabular}{lccr}", r"\toprule",153 r"Subsample & $\gamma$ & Std.\ error & $n$ \\"]154 for panel, keys in panels.items():155 lines.append(r"\midrule")156 lines.append(r"\multicolumn{4}{l}{\itshape " + panel + r"}\\")157 for k in keys:158 r = h[h["group"] == k]159 if r.empty:160 continue161 r = r.iloc[0]162 label = (k.replace("<", "$<$").replace(">", "$>$")163 .replace("–", "--"))164 lines.append(165 f"\\quad {label} & {r['gamma']:+.3f}"166 f"{_stars(r['gamma'], r['se'])} & ({_f(r['se'])}) & "167 f"{r['n']:,.0f} \\\\")168 lines += [r"\bottomrule", r"\end{tabular}"]169 (TAB / "heterogeneity.tex").write_text("\n".join(lines))170171172def t_taxshift():173 t = pd.read_csv(RES / "taxshift.csv")174 lines = [r"\begin{tabular}{lcccc}", r"\toprule",175 r"Price decile & $n$ & \makecell{Mean excess\\burden (\%)} & "176 r"\makecell{Median excess\\burden (\%)} & Std.\ error \\",177 r"\midrule"]178 for _, r in t.iterrows():179 lines.append(180 f"{int(r['decile'])} & {r['n']:,.0f} & "181 f"{100 * r['mean_rel']:+.1f} & {100 * r['median_rel']:+.1f} & "182 f"({100 * r['se']:.2f}) \\\\")183 lines += [r"\bottomrule", r"\end{tabular}"]184 (TAB / "taxshift.tex").write_text("\n".join(lines))185186187def t_horizontal():188 h = pd.read_csv(RES / "horizontal.csv", index_col=0)189 meta = pd.read_csv(RES / "horizontal_meta.csv", index_col=0).squeeze()190 labels = {"age_dec": "Building age (decades)",191 "land_share": "Assessed land share",192 "is_condo": "Condominium",193 "is_plex": "Plex (2--5 units)",194 "is_cottage": "Cottage"}195 lines = [r"\begin{tabular}{lcc}", r"\toprule",196 r" & Coefficient & Std.\ error \\", r"\midrule"]197 for k, lab in labels.items():198 r = h.loc[k]199 lines.append(f"{lab} & {r['coef']:+.4f}{_stars(r['coef'], r['se'])} & "200 f"({_f(r['se'], 4)}) \\\\")201 lines += [r"\midrule",202 f"Mean of dependent variable & \\multicolumn{{2}}{{c}}"203 f"{{{float(meta['mean_dep']):.3f}}} \\\\",204 f"Observations & \\multicolumn{{2}}{{c}}{{{float(meta['n']):,.0f}}} \\\\",205 r"\bottomrule", r"\end{tabular}"]206 (TAB / "horizontal.tex").write_text("\n".join(lines))207208209def t_robustness():210 r = pd.read_csv(RES / "robustness.csv")211 lines = [r"\begin{tabular}{lccccr}", r"\toprule",212 r"Sample variant & $\gamma_{\text{FE}}$ & Std.\ err. & "213 r"$\gamma_{\text{IV}}$ & Std.\ err. & $n$ \\",214 r"\midrule"]215 for _, x in r.iterrows():216 label = (str(x["variant"]).replace("<=", "$\\le$")217 .replace(">=", "$\\ge$").replace("$100k", "\\$100k"))218 lines.append(219 f"{label} & {x['gamma_fe']:+.3f}{_stars(x['gamma_fe'], x['se_fe'])}"220 f" & ({_f(x['se_fe'])}) & "221 f"{x['gamma_iv']:+.3f}{_stars(x['gamma_iv'], x['se_iv'])} & "222 f"({_f(x['se_iv'])}) & {x['n']:,.0f} \\\\")223 lines += [r"\bottomrule", r"\end{tabular}"]224 (TAB / "robustness.tex").write_text("\n".join(lines))225226227# ---------------------------------------------------------------- revision tables (2026-10-17)228def _pct(x, nd=1):229 return f"{100 * x:.{nd}f}"230231232def t_rev_trim_grid():233 g = pd.read_csv(RES / "rev_trim_grid.csv")234 short = {"baseline": "Baseline (1/99 pct by vintage)",235 "3iqr": r"3 $\times$ IQR within cell",236 "1.5iqr": r"1.5 $\times$ IQR within cell (IAAO)",237 "2.5pct": "2.5/97.5 pct within cell",238 "5pct": "5/95 pct within cell"}239 lines = [r"\begin{tabular}{lrccccc}", r"\toprule",240 r"\multicolumn{7}{l}{\itshape Panel A. Province-wide statistics and within-cell elasticities}\\",241 r"\midrule",242 r"Outlier rule & \makecell{Dropped\\(\%)} & COD & PRD & PRB & "243 r"$\gamma_{\text{FE}}$ & $\gamma_{\text{IV}}$ \\", r"\midrule"]244 for _, r in g.iterrows():245 lines.append(246 f"{short[r['rule']]} & {_pct(r['share_dropped'])} & {_f(r['cod'], 1)} & {_f(r['prd'])} & "247 f"{r['prb']:+.3f} & {r['gamma_fe']:+.3f}{_stars(r['gamma_fe'], r['se_fe'])} & "248 f"{r['gamma_iv']:+.3f}{_stars(r['gamma_iv'], r['se_iv'])} \\\\")249 lines.append(f" & & & & & ({_f(r['se_fe'])}) & ({_f(r['se_iv'])}) \\\\")250 lines += [r"\midrule",251 r"\multicolumn{7}{l}{\itshape Panel B. Tax shift and municipal compliance}\\",252 r"\midrule",253 r"Outlier rule & \makecell{Decile 1\\(\%)} & \makecell{Decile 10\\(\%)} & "254 r"\makecell{Median\\muni.\ COD} & \makecell{COD $>$ 15\\(\%)} & "255 r"\makecell{PRB $<$ 0\\(\%)} & \makecell{PRB $<-0.05$\\(\%)} \\", r"\midrule"]256 for _, r in g.iterrows():257 lines.append(258 f"{short[r['rule']]} & {100 * r['d1_median']:+.1f} & {100 * r['d10_median']:+.1f} & "259 f"{_f(r['median_muni_cod'], 1)} & {_pct(r['share_muni_cod_gt_15'], 0)} & "260 f"{_pct(r['share_muni_prb_neg'], 0)} & {_pct(r['share_muni_prb_lt_m05'], 0)} \\\\")261 lines += [r"\bottomrule", r"\end{tabular}"]262 (TAB / "rev_trim_grid.tex").write_text("\n".join(lines))263264265def t_rev_spline():266 s = pd.read_csv(RES / "rev_spline.csv")267 lines = [r"\begin{tabular}{lcccr}", r"\toprule",268 r"\makecell[l]{Within-cell price\\quintile} & "269 r"\makecell{Range of demeaned\\$\ln SP$} & $\beta$ & $\gamma = \beta - 1$ & $n$ \\",270 r"\midrule"]271 for _, r in s.iterrows():272 lo = f"$< {r['hi']:.2f}$" if r['segment'] == 1 else (273 f"$\\ge {r['lo']:.2f}$" if r['segment'] == len(s) else274 f"$[{r['lo']:.2f},\\ {r['hi']:.2f})$")275 lines.append(f"{int(r['segment'])} & {lo} & {_f(r['beta'])}{_stars(r['beta'] - 1, r['se'])} "276 f"({_f(r['se'])}) & {r['gamma']:+.3f} & {r['n']:,.0f} \\\\")277 lines += [r"\bottomrule", r"\end{tabular}"]278 (TAB / "rev_spline.tex").write_text("\n".join(lines))279280281def t_rev_taxshift_alt():282 t = pd.read_csv(RES / "rev_taxshift_alt.csv")283 o = pd.read_csv(RES / "rev_outlier_by_decile.csv").set_index("decile")["share_outlier"]284 cols = [("sale price", "cell median ratio"), ("sale price", "revenue-neutral (aggregate ratio)"),285 ("IAAO blended value proxy", "cell median ratio"),286 ("IAAO blended value proxy", "revenue-neutral (aggregate ratio)"),287 ("assessed value", "cell median ratio"), ("assessed value", "revenue-neutral (aggregate ratio)")]288 lines = [r"\begin{tabular}{lcccccc|c}", r"\toprule",289 r" & \multicolumn{2}{c}{Sorted on sale price} & \multicolumn{2}{c}{Sorted on blended proxy} & "290 r"\multicolumn{2}{c|}{Sorted on assessed value} & \\",291 r"\cmidrule(lr){2-3}\cmidrule(lr){4-5}\cmidrule(lr){6-7}",292 r"Decile & Median & Aggregate & Median & Aggregate & Median & Aggregate & "293 r"\makecell{IQR outliers\\(\%)} \\", r"\midrule"]294 for dec in range(1, 11):295 vals = []296 for sort, bench in cols:297 v = t[(t["sort"] == sort) & (t["benchmark"] == bench) & (t["decile"] == dec)]["median_rel"].iloc[0]298 vals.append(f"{100 * v:+.1f}")299 lines.append(f"{dec} & " + " & ".join(vals) + f" & {100 * o.loc[dec]:.1f} \\\\")300 lines += [r"\bottomrule", r"\end{tabular}"]301 (TAB / "rev_taxshift_alt.tex").write_text("\n".join(lines))302303304def t_rev_prb_blocks():305 a = pd.read_csv(RES / "rev_prb_blocks.csv")306 b = pd.read_csv(RES / "rev_prb_blocks_trimmed.csv")307 pw_a = pd.read_csv(RES / "iaao_overall.csv").set_index("group")["prb"]308 pw_b = pd.read_csv(RES / "rev_iaao_year_trimmed.csv").set_index("year")["prb"]309 lines = [r"\begin{tabular}{lrccccc}", r"\toprule",310 r"Blocks & $n$ blocks & \makecell{Median\\ratio} & \makecell{Median\\COD} & "311 r"\makecell{Median\\PRB} & \makecell{Share\\PRB $<$ 0} & \makecell{Province-\\wide PRB} \\"]312 for title, d, pw in (("Panel A. Baseline sample (1/99 trim by roll vintage)", a, pw_a),313 ("Panel B. IAAO 1.5 $\\times$ IQR trimmed sample", b, pw_b)):314 lines += [r"\midrule", r"\multicolumn{7}{l}{\itshape " + title + r"}\\", r"\midrule"]315 for _, r in d.iterrows():316 g = str(r["group"])317 pwv = pw.get(g, pw.get(int(g)) if g.isdigit() else None) if g.isdigit() else None318 pw_txt = f"{pwv:+.3f}" if pwv is not None and pd.notna(pwv) else "---"319 label = g.replace("<", "$<$").replace(">", "$>$").replace("–", "--")320 lines.append(f"{label} & {r['n_blocks']:,.0f} & {_f(r['median_ratio'])} & {_f(r['median_cod'], 1)} & "321 f"{r['median_prb']:+.3f} & {_f(r['share_prb_neg'], 2)} & {pw_txt} \\\\")322 lines += [r"\bottomrule", r"\end{tabular}"]323 (TAB / "rev_prb_blocks.tex").write_text("\n".join(lines))324325326def t_rev_cities():327 a = pd.read_csv(RES / "rev_cities.csv").rename(columns={"city": "muni"})328 b = pd.read_csv(RES / "rev_cities_trimmed.csv")329 m = a.merge(b, on="muni", suffixes=("", "_tr")).sort_values("n", ascending=False)330 lines = [r"\begin{tabular}{lrcccccccc}", r"\toprule",331 r" & & & \multicolumn{2}{c}{Baseline sample} & \multicolumn{5}{c}{IAAO 1.5 $\times$ IQR trimmed} \\",332 r"\cmidrule(lr){4-5}\cmidrule(lr){6-10}",333 r"Municipality & $n$ & \makecell{Condo\\share} & $\gamma_{\text{FE}}$ & $\gamma_{\text{IV}}$ & "334 r"COD & PRD & PRB & $\gamma_{\text{FE}}$ & $\gamma_{\text{IV}}$ \\", r"\midrule"]335 for _, r in m.iterrows():336 lines.append(337 f"{r['muni']} & {r['n']:,.0f} & {_f(r['share_condo'], 2)} & "338 f"{r['gamma_fe']:+.3f} & {r['gamma_iv']:+.3f} & {_f(r['cod'], 1)} & {_f(r['prd'])} & "339 f"{r['prb']:+.3f} & {r['gamma_fe_tr']:+.3f} & {r['gamma_iv_tr']:+.3f} \\\\")340 lines += [r"\bottomrule", r"\end{tabular}"]341 (TAB / "rev_cities.tex").write_text("\n".join(lines))342343344def t_rev_het_trimmed():345 h = pd.read_csv(RES / "heterogeneity.csv").set_index("group")346 t = pd.read_csv(RES / "rev_het_trimmed.csv")347 cl = pd.read_csv(RES / "rev_clusters.csv").set_index("group")["n_clusters"]348 lines = [r"\begin{tabular}{lccccr}", r"\toprule",349 r" & Baseline & \multicolumn{3}{c}{IAAO 1.5 $\times$ IQR trimmed} & \\",350 r"\cmidrule(lr){2-2}\cmidrule(lr){3-5}",351 r"Subsample & $\gamma_{\text{FE}}$ & $\gamma_{\text{FE}}$ & Std.\ error & $\gamma_{\text{IV}}$ & "352 r"\makecell{Clusters\\(munic.)} \\", r"\midrule"]353 for _, r in t.iterrows():354 g = r["group"]355 base = h["gamma"].get(g, np.nan) if g in h.index else np.nan356 base_txt = f"{base:+.3f}" if pd.notna(base) else "---"357 ncl = f"{int(cl[g]):,}" if g in cl.index else "---"358 label = g.replace("<", "$<$").replace(">", "$>$").replace("–", "--")359 lines.append(f"{label} & {base_txt} & {r['gamma']:+.3f}{_stars(r['gamma'], r['se'])} & "360 f"({_f(r['se'])}) & {r['gamma_iv']:+.3f} & {ncl} \\\\")361 lines += [r"\bottomrule", r"\end{tabular}"]362 (TAB / "rev_het_trimmed.tex").write_text("\n".join(lines))363364365def t_rev_bounds():366 b = pd.read_csv(RES / "rev_bounds.csv").set_index("quantity")["value"]367 rows = [368 (r"Within-cell variance of $\ln SP$", _f(b["var_lnP_within"])),369 (r"Within-cell variance of $\ln AV$", _f(b["var_lnAV_within"])),370 (r"Within-cell covariance", _f(b["cov_within"])),371 (r"Within-cell correlation", _f(b["corr_within"])),372 (r"Forward slope $\hat\beta_{F}$ (Cheng FE)", _f(b["beta_forward"])),373 (r"Inverse reverse slope $1/\hat\beta_{R}$ (Kochin--Parks)", _f(b["beta_reverse_inverse"])),374 (r"Noise share $\lambda$ rationalising $\beta = 1$", _f(b["lambda_for_beta_1"])),375 (r"Implied transaction-noise s.d.\ for $\beta = 1$ (log points)", _f(b["noise_sd_for_beta_1"])),376 (r"Clapp IV $\hat\beta_{IV}$", _f(b["beta_iv"])),377 (r"Noise share $\lambda$ implied by $\hat\beta_{IV}$", _f(b["lambda_implied_by_iv"])),378 (r"Implied transaction-noise s.d.\ under $\hat\beta_{IV}$ (log points)", _f(b["noise_sd_implied_by_iv"])),379 (r"First stage: coefficient on $Z$ (s.e.)", f"{_f(b['first_stage_coef'])} ({_f(b['first_stage_se'])})"),380 (r"First stage: cluster-robust $F$", f"{b['first_stage_F_cluster']:,.0f}"),381 (r"First stage: $R^2$", _f(b["first_stage_r2"])),382 (r"Share of sales with $Z = -1$ / $Z = +1$", f"{_f(b['share_z_minus1'])} / {_f(b['share_z_plus1'])}"),383 ]384 lines = [r"\begin{tabular}{lc}", r"\toprule", r"Quantity & Value \\", r"\midrule"]385 lines += [f"{k} & {v} \\\\" for k, v in rows]386 lines += [r"\bottomrule", r"\end{tabular}"]387 (TAB / "rev_bounds.tex").write_text("\n".join(lines))388389390def t_rev_cod():391 c = pd.read_csv(RES / "rev_cod.csv")392 short = {"Municipality x year blocks (baseline)": r"Muni.\ $\times$ year blocks (baseline)",393 "Municipality x quarter blocks": r"Muni.\ $\times$ quarter blocks",394 "Municipality x year, time-adjusted ratios": r"Muni.\ $\times$ year, time-adjusted",395 "Province-wide, time-adjusted ratios": "Province-wide, time-adjusted"}396 lines = [r"\begin{tabular}{lrccccc}", r"\toprule",397 r"Time-control scheme & Munis & \makecell{Median\\COD} & "398 r"\makecell{COD\\IQR} & \makecell{COD $>$ 15\\(\%)} & \makecell{Median\\PRB} & "399 r"\makecell{PRB $<-0.05$\\(\%)} \\", r"\midrule"]400 for _, r in c.iterrows():401 lab = short.get(r["scheme"], r["scheme"])402 if pd.isna(r["n_munis"]):403 lines.append(f"{lab} & --- & {_f(r['median_cod'], 1)} & --- & --- & "404 f"{r['median_prb']:+.3f} & --- \\\\")405 else:406 lines.append(f"{lab} & {r['n_munis']:,.0f} & "407 f"{_f(r['median_cod'], 1)} & [{_f(r['p25_cod'], 1)}, {_f(r['p75_cod'], 1)}] & "408 f"{_pct(r['share_cod_gt_15'], 0)} & {r['median_prb']:+.3f} & "409 f"{_pct(r['share_prb_lt_m05'], 0)} \\\\")410 lines += [r"\bottomrule", r"\end{tabular}"]411 (TAB / "rev_cod.tex").write_text("\n".join(lines))412413414def main() -> None:415 config.ensure_dirs()416 t_summary()417 t_iaao()418 t_cities()419 t_vertical()420 t_heterogeneity()421 t_taxshift()422 t_horizontal()423 t_robustness()424 for fn in (t_rev_trim_grid, t_rev_spline, t_rev_taxshift_alt, t_rev_prb_blocks,425 t_rev_cities, t_rev_het_trimmed, t_rev_bounds, t_rev_cod):426 if (RES / 'rev_trim_grid.csv').exists():427 fn()428 made = sorted(p.name for p in TAB.glob("*.tex"))429 print(f"{len(made)} tables written:", ", ".join(made))430431432if __name__ == "__main__":433 main()434