1"""LaTeX table fragments (booktabs tabular only) for model results: tab04 hedonic, tab05 hazards,2tab06 competition/spatial, tab07 AMLI. Tables 1-3 come from descriptives.py, 8 from prediction.py,39 from robustness.py. Every number is read from results/."""4from __future__ import annotations56import json78import numpy as np9import pandas as pd1011from . import config as C1213T = C.TABLES14R = C.RESULTS151617def _f(x, d=3):18 return "" if x is None or (isinstance(x, float) and np.isnan(x)) else f"{x:.{d}f}"192021def _stars(p):22 return "$^{***}$" if p < 0.01 else "$^{**}$" if p < 0.05 else "$^{*}$" if p < 0.10 else ""232425def _n(x):26 return f"{int(x):,}".replace(",", "\\,")272829def _write(name, lines):30 T.mkdir(parents=True, exist_ok=True)31 (T / name).write_text("\n".join(lines) + "\n")323334def tab04_hedonic():35 S = json.load(open(R / "hedonic" / "summary.json")); cv = json.load(open(R / "hedonic" / "cv_grouped_dealer.json"))["summary"]36 specs = [("A1_linear_mm", "A1 linear"), ("A2_spline_mm", "A2 splines"), ("A2t_tensor_mm", "A2t tensor"), ("A3_spline_x_powertrain", "A3 splines $\\times$ powertrain"), ("A4_spline_mmt", "A4 splines, trim FE"), ("A5_spline_dealerFE", "A5 + dealer FE")]37 coefs = {k: pd.read_csv(R / "hedonic" / f"coefs_{k}.csv").set_index("term") for k, _ in specs}38 rows = [("age", "Vehicle age (years)"), ("km", "Mileage (10\\,000 km)"), ("C(powertrain)[T.BEV]", "Battery electric"), ("C(powertrain)[T.HEV]", "Hybrid"), ("C(powertrain)[T.PHEV]", "Plug-in hybrid"), ("C(powertrain)[T.Diesel]", "Diesel"),39 ("C(transmission)[T.Manual]", "Manual transmission"), ("C(drivetrain)[T.FWD]", "Front-wheel drive"), ("C(drivetrain)[T.RWD]", "Rear-wheel drive"), ("C(source_class)[T.full_portal]", "Full-inventory portal"), ("C(source_class)[T.banner]", "Dealer group")]40 L = ["\\begin{tabular}{l" + "c" * len(specs) + "}", "\\toprule", "& " + " & ".join(f"({i+1})" for i in range(len(specs))) + " \\\\", "& " + " & ".join(lab for _, lab in specs) + " \\\\", "\\midrule"]41 for term, lab in rows:42 cells, ses = [], []43 for k, _ in specs:44 c = coefs[k]45 if k == "A5_spline_dealerFE" and term.startswith("C(source_class)"):46 cells.append("absorbed"); ses.append(""); continue47 if term in c.index:48 r = c.loc[term]; cells.append(_f(r["Estimate"]) + _stars(r["Pr(>|t|)"])); ses.append("(" + _f(r["Std. Error"]) + ")")49 else:50 cells.append("--"); ses.append("")51 L.append(lab + " & " + " & ".join(cells) + " \\\\"); L.append(" & " + " & ".join(ses) + " \\\\")52 L += ["\\midrule", "Nonlinear age/km terms & no & yes & yes & yes & yes & yes \\\\", "Tensor age $\\times$ km & no & no & yes & no & no & no \\\\",53 "Quality FE & make-model & make-model & make-model & make-model & make-model-trim & make-model-trim \\\\", "Entry-week FE & yes & yes & yes & yes & yes & yes \\\\", "Region dummies & yes & yes & yes & yes & yes & absorbed \\\\", "Dealer FE & no & no & no & no & no & yes \\\\"]54 L.append("Observations & " + " & ".join(_n(S["fits"][k]["n"]) for k, _ in specs) + " \\\\")55 L.append("$R^2$ & " + " & ".join(_f(S["fits"][k]["r2"]) for k, _ in specs) + " \\\\")56 L.append("Within $R^2$ & " + " & ".join(_f(S["fits"][k]["r2_within"]) for k, _ in specs) + " \\\\")57 L.append("In-sample RMSE (log) & " + " & ".join(_f(S["fits"][k]["rmse"]) for k, _ in specs) + " \\\\")58 L.append("Grouped-dealer CV RMSE & " + " & ".join(_f(cv[k]["rmse_mean"]) if k in cv else "--" for k, _ in specs) + " \\\\")59 L += ["\\bottomrule", "\\end{tabular}"]60 _write("tab04_hedonic.tex", L)61 # tests fragment62 t = S["tests"]63 L2 = ["\\begin{tabular}{lccc}", "\\toprule", "Test & $\\chi^2$ & df & $p$ \\\\", "\\midrule"]64 for k, lab in [("H1_nonlinear_age", "H1: nonlinear age terms = 0 (A2)"), ("H1_nonlinear_km", "H1: nonlinear mileage terms = 0 (A2)"), ("H1_nonlinear_joint", "H1: all nonlinear terms = 0 (A2)"), ("H1_tensor_interaction", "H1: age $\\times$ mileage tensor = 0 (A2t)"), ("H1_age_x_powertrain", "H1: age profile $\\times$ powertrain = 0 (A3)"), ("H4_region_joint", "H4: region effects = 0 (A4)")]:65 v = t[k]; L2.append(f"{lab} & {v['stat']:.1f} & {v['df']} & {v['p']:.3g} \\\\")66 L2 += ["\\bottomrule", "\\end{tabular}"]67 _write("tab04b_hedonic_tests.tex", L2)686970def tab05_hazards():71 H = json.load(open(R / "hazards" / "summary.json"))72 B = pd.read_csv(R / "hazards" / "cloglog_B_first_markdown.csv").set_index("term"); Cx = pd.read_csv(R / "hazards" / "cloglog_C_exit.csv").set_index("term")73 cb = pd.read_csv(R / "hazards" / "cox_first_markdown.csv").set_index("term"); ce = pd.read_csv(R / "hazards" / "cox_exit.csv").set_index("term")74 cs = pd.read_csv(R / "hazards" / "cutsize_ols.csv").set_index("term")75 rows = [("ldens", "ldens_entry", "ldens", "log(1 + competitors within 25 km)"), ("resid_A2", "resid_A2", "resid_A2", "Entry hedonic residual (relative price)"), ("pricepos_entry", "pricepos_entry", None, "Price position vs competitors at entry"),76 ("pricepos_25", None, "pricepos_25", "Current price position vs competitors"), ("prior_cuts", None, None, "Prior sustained cuts (count)"), ("lprice", "lprice", "lprice", "log entry price"), ("lkm", "lkm", "lkm", "log mileage"), ("age_years_entry", "age_years_entry", "age_years_entry", "Vehicle age at entry (years)"), ("own_stock", None, None, "Own-dealer same-model stock")]77 L = ["\\begin{tabular}{lccccc}", "\\toprule", "& \\multicolumn{2}{c}{First markdown} & \\multicolumn{2}{c}{Listing exit} & Cut size \\\\", "\\cmidrule(lr){2-3}\\cmidrule(lr){4-5}\\cmidrule(lr){6-6}",78 "& cloglog & Cox & cloglog & Cox & log(\\%) \\\\", "\\midrule"]79 def cell(df, term, est="coef", se="se", p="p"):80 if term is None or term not in df.index:81 return "--", ""82 r = df.loc[term]; return _f(r[est]) + _stars(r[p]), "(" + _f(r[se]) + ")"83 for tB, tC, tS, lab in rows:84 c1 = cell(B, tB); c2 = cell(cb, tC, "coef", "se(coef)", "p"); c3 = cell(Cx, tB); c4 = cell(ce, tC, "coef", "se(coef)", "p"); c5 = cell(cs, tS, "Estimate", "Std. Error", "Pr(>|t|)")85 L.append(lab + " & " + " & ".join(x[0] for x in (c1, c2, c3, c4, c5)) + " \\\\"); L.append(" & " + " & ".join(x[1] for x in (c1, c2, c3, c4, c5)) + " \\\\")86 L += ["\\midrule", f"Listing-age bins (joint $\\chi^2$, df 8) & {H['B_first_markdown_cloglog']['joint_age_bins']['stat']:.1f} & -- & {H['C_exit_cloglog']['joint_age_bins']['stat']:.1f} & -- & -- \\\\",87 f"Listing-days / listings & {_n(H['B_first_markdown_cloglog']['n_rows'])} & {_n(H['B_cut_cox']['n'])} & {_n(H['C_exit_cloglog']['n_rows'])} & {_n(H['C_exit_cox']['n'])} & {_n(H['B_cut_size']['n'])} \\\\",88 f"Events & {_n(H['B_first_markdown_cloglog']['n_events'])} & {_n(H['B_cut_cox']['events'])} & {_n(H['C_exit_cloglog']['n_events'])} & {_n(H['C_exit_cox']['events'])} & -- \\\\",89 f"Concordance / within $R^2$ & -- & {_f(H['B_cut_cox']['concordance'])} & -- & {_f(H['C_exit_cox']['concordance'])} & {_f(H['B_cut_size']['r2_within'])} \\\\",90 f"PH test: covariates with $p<0.01$ & -- & {H['B_cut_cox']['ph_violations_p<0.01']} & -- & {H['C_exit_cox']['ph_violations_p<0.01']} & -- \\\\",91 "\\bottomrule", "\\end{tabular}"]92 _write("tab05_hazards.tex", L)939495def tab06_competition():96 S = json.load(open(R / "competition" / "summary.json")); S2 = json.load(open(R / "competition" / "summary_dow.json")) if (R / "competition" / "summary_dow.json").exists() else {}97 specs = [("main_25km", "Main, 25 km"), ("main_25km_cluster_region", "Cluster: region"), ("main_10km", "10 km"), ("main_50km", "50 km"), ("coarse_fe", "Coarse FE"), ("placebo_permuted", "Placebo")]98 specs2 = [("dealer_dow_fe", "+ dealer $\\times$ weekday FE"), ("dealer_dow_fe_firstcut", "First cut, + dealer $\\times$ weekday")]99 allspecs = [(k, lab, S[k]) for k, lab in specs] + [(k, lab, S2[k]) for k, lab in specs2 if k in S2]100 L = ["\\begin{tabular}{l" + "c" * len(allspecs) + "}", "\\toprule", "& " + " & ".join(lab for _, lab, _ in allspecs) + " \\\\", "\\midrule"]101 for k, lab in [("cc_m7", "$k=-7$"), ("cc_m3", "$k=-3$"), ("cc_m1", "$k=-1$"), ("cc_p0", "$k=0$"), ("cc_p1", "$k=+1$"), ("cc_p2", "$k=+2$"), ("cc_p3", "$k=+3$"), ("cc_p7", "$k=+7$"), ("cc_p14", "$k=+14$")]:102 L.append(lab + " & " + " & ".join(f"{100*v['coef'][k]['b']:.2f}" for _, _, v in allspecs) + " \\\\")103 L.append(" & " + " & ".join(f"({100*v['coef'][k]['se']:.2f})" for _, _, v in allspecs) + " \\\\")104 L += ["\\midrule", "Sum of leads $k=-7..-1$ & " + " & ".join(f"{100*v['leads_joint']['sum']:.2f}" for _, _, v in allspecs) + " \\\\",105 "\\quad joint $p$ & " + " & ".join(f"{v['leads_joint']['p']:.2g}" for _, _, v in allspecs) + " \\\\",106 "Sum of lags $k=0..7$ & " + " & ".join(f"{100*v['lag0_7_sum']['sum']:.2f}" for _, _, v in allspecs) + " \\\\",107 "\\quad joint $p$ & " + " & ".join(f"{v['lag0_7_sum']['p']:.2g}" for _, _, v in allspecs) + " \\\\",108 "Daily base rate (\\%) & " + " & ".join(f"{100*v['base_rate']:.2f}" for _, _, v in allspecs) + " \\\\",109 "Listing-days & " + " & ".join(_n(v["n"]) for _, _, v in allspecs) + " \\\\", "\\bottomrule", "\\end{tabular}"]110 _write("tab06_competition.tex", L)111 # spatial: region premia + dispersion112 prem = pd.read_csv(R / "hedonic" / "region_premia_A4.csv"); p2 = pd.read_csv(R / "hedonic" / "region_premia_A2.csv").set_index("region"); disp = pd.read_csv(R / "hedonic" / "dispersion_by_region.csv").set_index("region")113 prem = prem[prem["region"] != "(missing)"].sort_values("Estimate", ascending=False)114 L = ["\\begin{tabular}{lcccc}", "\\toprule", "Region & A4 premium (\\%) & 95\\% CI & A2 premium (\\%) & Residual IQR \\\\", "\\midrule"]115 for _, r in prem.iterrows():116 L.append(f"{r['region']} & {100*r['Estimate']:.1f} & [{100*r['2.5%']:.1f}, {100*r['97.5%']:.1f}] & {100*p2.loc[r['region'], 'Estimate']:.1f} & {disp.loc[r['region'], 'iqr']:.3f} \\\\")117 L.append(f"Montréal (reference) & 0 & -- & 0 & {disp.loc['Montréal', 'iqr']:.3f} \\\\")118 L += ["\\bottomrule", "\\end{tabular}"]119 _write("tab06b_region_premia.tex", L)120121122def tab07_amli():123 A = json.load(open(R / "amli" / "summary.json"))124 w = A["region"]["weights"]; comps = ["exit", "mdfreq", "mddepth", "disp", "arrival"]; lab = {"exit": "Exit intensity", "mdfreq": "Markdown frequency", "mddepth": "Markdown depth", "disp": "Residual dispersion (sign reversed)", "arrival": "Arrival rate"}125 L = ["\\begin{tabular}{lcccc}", "\\toprule", "Component & Equal & PCA loading & Supervised & Leave-one-out corr. \\\\", "\\midrule"]126 for c in comps:127 sup = w["supervised"].get(c); L.append(f"{lab[c]} & {w['equal'][c]:.2f} & {w['pca'][c]:+.2f} & {'--' if sup is None else f'{sup:+.4f}'} & {A['leave_one_out'][c]['corr_with_full_equal']:.2f} \\\\")128 L += ["\\midrule", f"PCA first-component share & \\multicolumn{{4}}{{c}}{{{A['region']['weights']['pca_explained']:.2f}}} \\\\", f"Corr(equal, PCA) / Corr(equal, supervised) & \\multicolumn{{4}}{{c}}{{{A['corr_equal_pca']:.2f} / {A['corr_equal_supervised']:.2f}}} \\\\"]129 v = A["region_validation"]130 for k, l in [("noexit_fe", "Leave-exit-out AMLI $\\to$ next-week exit (region FE)"), ("noexit_fe_lagexit", "\\quad + lagged exit"), ("noexit_pooled", "\\quad pooled OLS")]:131 L.append(f"{l} & \\multicolumn{{4}}{{c}}{{{100*v[k]['b']:.2f} pp [{100*v[k]['lo']:.2f}, {100*v[k]['hi']:.2f}], $n$ = {v[k]['n']}}} \\\\")132 for seg, l in [("segment_tier", "Price-tier markets"), ("segment_body_filled", "Body-type markets")]:133 s = A[seg]; L.append(f"{l}: PCA share / held-out slope & \\multicolumn{{4}}{{c}}{{{s['pca_explained']:.2f} / {100*s['validation']['noexit_fe']['b']:.2f} pp [{100*s['validation']['noexit_fe']['lo']:.2f}, {100*s['validation']['noexit_fe']['hi']:.2f}]}} \\\\")134 L += ["\\bottomrule", "\\end{tabular}"]135 _write("tab07_amli.tex", L)136137138def run():139 tab04_hedonic(); tab05_hazards(); tab06_competition(); tab07_amli()140 return sorted(p.name for p in T.glob("*.tex"))141142143if __name__ == "__main__":144 print(run())145