SPB Git

spb/wp36_uqo

Public
11.9 KB · 145 lines python
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
Files · d0f6445591c09fe14b42fb37035f398825960706
Menu

Theme