SPB Git

spb/wp36_uqo

Public
6.2 KB · 119 lines python
1"""Model D — competitor price-response event study / distributed lags (H3, association only).23Outcome: sustained own price cut on day t (S2 listing-days). Treatment: indicator of4>= 1 sustained cut among local competitors (same make-model, year +-1, other dealer,5within 25 km; 10/50 km robustness) on day t-k, k = -7..+14. Linear probability6model with market x week and make-model-year x week fixed effects (DECISIONS D-011),7SEs clustered by dealer (primary) and by region (secondary). Placebo: competitor-cut8series permuted across listings within make-model (geography broken).9"""10from __future__ import annotations1112import json13import time1415import numpy as np16import pandas as pd17import polars as pl18import pyfixest as pf1920from . import config as C2122OUT = C.RESULTS / "competition"23LEADS, LAGS = 7, 14242526def load(radius: str = "25") -> pd.DataFrame:27    RS = pl.read_parquet(C.PROCESSED / "risk_set.parquet").filter(pl.col("s2_dynamic"))28    L = pl.read_parquet(C.PROCESSED / "listings.parquet").select(["listing_id", "dealer_id", "make", "model", "year", "region", "powertrain", "body_filled", "source_class"])29    RS = RS.join(L, on="listing_id", how="left").sort(["listing_id", "t"])30    RS = RS.with_columns([(pl.col(f"ccut_{radius}") > 0).cast(pl.Int8).alias("cc"), pl.col("first_cut").fill_null(0)])31    shifts = []32    for k in range(-LEADS, LAGS + 1):33        name = f"cc_m{abs(k)}" if k < 0 else f"cc_p{k}"   # m = lead (future competitor cut), p = lag (past)34        shifts.append(pl.col("cc").shift(k).over("listing_id").fill_null(0).alias(name))35    RS = RS.with_columns(shifts)36    RS = RS.with_columns([37        (pl.col("t") // 7).alias("week"),38        pl.concat_str([pl.col("region").fill_null("(missing)"), pl.lit("|"), (pl.col("t") // 7).cast(pl.Utf8)]).alias("mkt_week"),39        pl.concat_str([pl.col("make"), pl.lit("|"), pl.col("model"), pl.lit("|"), pl.col("year").cast(pl.Utf8), pl.lit("|"), (pl.col("t") // 7).cast(pl.Utf8)]).alias("mmy_week"),40        pl.concat_str([pl.col("make"), pl.lit("|"), pl.col("model")]).alias("mm"),41        pl.col("listing_age").clip(upper_bound=60).alias("age_c"),42        (pl.col(f"dens_{radius}") + 1).log().alias("ldens"),43    ])44    df = RS.to_pandas()45    for c in ["mkt_week", "mmy_week", "mm", "dealer_id", "region"]:46        df[c] = pd.factorize(df[c])[0]47    return df484950def lag_names():51    return [f"cc_m{k}" for k in range(LEADS, 0, -1)] + [f"cc_p{k}" for k in range(0, LAGS + 1)]525354def fit(df: pd.DataFrame, y: str = "cut", fe: str = "mkt_week + mmy_week", cluster: str = "dealer_id", name: str = "main") -> dict:55    t0 = time.time()56    rhs = " + ".join(lag_names()) + " + ldens + C(age_c)"57    m = pf.feols(f"{y} ~ {rhs} | {fe}", data=df, vcov={"CRV1": cluster}, fixef_rm="none")58    t = m.tidy().reset_index(); t = t.rename(columns={t.columns[0]: "term"})59    t = t[t["term"].isin(lag_names())].copy()60    t["k"] = t["term"].map(lambda s: -int(s[4:]) if s.startswith("cc_m") else int(s[4:]))61    t = t.sort_values("k"); t.to_csv(OUT / f"eventstudy_{name}.csv", index=False)62    names = list(m._coefnames)63    def joint(cols):64        idx = [names.index(c) for c in cols]; b = np.asarray(m.coef())[idx]; V = np.asarray(m._vcov)[np.ix_(idx, idx)]65        W = float(b @ np.linalg.solve(V, b)); from scipy.stats import chi266        return {"stat": W, "df": len(idx), "p": float(chi2.sf(W, len(idx))), "sum": float(b.sum())}67    leads = [f"cc_m{k}" for k in range(1, LEADS + 1)]; lags = [f"cc_p{k}" for k in range(0, LAGS + 1)]68    out = {"n": int(m._N), "base_rate": float(df[y].mean()), "fe": fe, "cluster": cluster, "seconds": round(time.time() - t0, 1),69           "leads_joint": joint(leads), "lags_joint": joint(lags), "lag0_7_sum": joint([f"cc_p{k}" for k in range(0, 8)]),70           "coef": {r.term: {"b": float(r.Estimate), "se": float(r["Std. Error"]), "lo": float(r["2.5%"]), "hi": float(r["97.5%"])} for _, r in t.iterrows()}}71    return out727374def placebo(df: pd.DataFrame, seed: int = 36) -> dict:75    """Permute the competitor-cut series across listings within make-model (breaks geography)."""76    rng = np.random.default_rng(seed)77    cols = lag_names()78    first = df.groupby("listing_id")["mm"].first()79    perm = {}80    for mm, ids in first.groupby(first).groups.items():81        ids = np.array(list(ids)); sh = ids.copy(); rng.shuffle(sh); perm.update(dict(zip(ids, sh)))82    # map each listing's series to another listing's series of the same make-model, aligned on listing_age83    src = df[["listing_id", "listing_age"] + cols].copy(); src["listing_id"] = src["listing_id"].map(perm)84    d2 = df.drop(columns=cols).merge(src, on=["listing_id", "listing_age"], how="left")85    d2[cols] = d2[cols].fillna(0)86    return fit(d2, name="placebo_permuted")878889def selection_check(df: pd.DataFrame) -> dict:90    """Do listings that ever receive a competitor cut differ at entry? (selection into treatment)"""91    g = df.groupby("listing_id").agg(any_cc=("cc", "max"), ldens=("ldens", "first"), logp=("logp", "first"), pp=("pricepos_25", "first"))92    out = {}93    for v in ["ldens", "logp", "pp"]:94        a, b = g.loc[g.any_cc == 1, v].dropna(), g.loc[g.any_cc == 0, v].dropna()95        out[v] = {"mean_treated": float(a.mean()), "mean_untreated": float(b.mean()), "std_diff": float((a.mean() - b.mean()) / np.sqrt((a.var() + b.var()) / 2))}96    out["share_ever_treated"] = float(g.any_cc.mean())97    return out9899100def run() -> dict:101    OUT.mkdir(parents=True, exist_ok=True)102    res = {}103    df = load("25")104    print("main", flush=True); res["main_25km"] = fit(df)105    print("cluster region", flush=True); res["main_25km_cluster_region"] = fit(df, cluster="region", name="main_cluster_region")106    print("first cut only", flush=True)107    res["firstcut_25km"] = fit(df[df["in_first_cut_risk"]], y="first_cut", name="firstcut")108    print("no FE / coarse", flush=True); res["coarse_fe"] = fit(df, fe="region + week", name="coarse_fe")109    print("placebo", flush=True); res["placebo_permuted"] = placebo(df)110    res["selection"] = selection_check(df)111    for r in ("10", "50"):112        print("radius", r, flush=True); d = load(r); res[f"main_{r}km"] = fit(d, name=f"main_{r}km")113    json.dump(res, open(OUT / "summary.json", "w"), indent=2, default=float)114    return res115116117if __name__ == "__main__":118    print(json.dumps(run(), indent=2, default=float))119
Files · d0f6445591c09fe14b42fb37035f398825960706
Menu

Theme