SPB Git
4.6 KB · 112 lines python
1"""Panel count models for project-announcement intensity.23DV: n_projects (count of strategic projects firm i announces in year t).4Estimators: pooled Poisson (sector+year FE), Poisson with firm+year FE (PPML),5Negative Binomial (overdispersion). Cluster-robust SE by firm.6"""7from __future__ import annotations89import warnings10import numpy as np11import pandas as pd12import statsmodels.api as sm13import statsmodels.formula.api as smf14from scipy import stats1516from common import DATA, set_style, savefig, write_table, save_results17import matplotlib.pyplot as plt1819warnings.filterwarnings("ignore")20set_style()2122df = pd.read_parquet(DATA / "panel.parquet").copy()23df = df.dropna(subset=["n_projects_lag"]).copy()   # drop first year per firm (lag)24df["projects_lag"] = df["n_projects_lag"]25df["cum_ai_lag"] = df.groupby("cik")["cum_ai"].shift(1).fillna(0)26df["year_c"] = df["year"] - df["year"].min()27df["sector"] = df["sector"].fillna("Unknown")28groups = df["cik"]2930# ---- Model 1: pooled Poisson, sector + year FE ----------------------------31f1 = "n_projects ~ projects_lag + cum_ai_lag + post_covid + year_c + C(sector)"32m1 = smf.poisson(f1, data=df).fit(cov_type="cluster", cov_kwds={"groups": groups}, disp=0, maxiter=200)3334# ---- Model 2: Poisson with firm + year FE (PPML) --------------------------35f2 = "n_projects ~ projects_lag + cum_ai_lag + C(cik) + C(year)"36m2 = smf.poisson(f2, data=df).fit(cov_type="cluster", cov_kwds={"groups": groups}, disp=0, maxiter=300)3738# ---- Model 3: Negative Binomial (sector + year FE) ------------------------39f3 = "n_projects ~ projects_lag + cum_ai_lag + post_covid + year_c + C(sector)"40m3 = smf.negativebinomial(f3, data=df).fit(cov_type="cluster", cov_kwds={"groups": groups}, disp=0, maxiter=300)4142# overdispersion test (Cameron-Trivedi style): regress aux on fitted43mu = m1.predict(df)44aux = ((df["n_projects"] - mu) ** 2 - df["n_projects"]) / mu45od = sm.OLS(aux, mu).fit()46od_t = float(od.tvalues.iloc[0]); od_p = float(od.pvalues.iloc[0])4748KEEP = ["projects_lag", "cum_ai_lag", "post_covid", "year_c"]49LBL = {"projects_lag": "Projects$_{t-1}$", "cum_ai_lag": "Cumulative AI$_{t-1}$",50       "post_covid": "Post-COVID (2020+)", "year_c": "Year trend"}515253def col(m, label):54    out = {"_label": label, "_n": int(m.nobs), "_ll": float(m.llf)}55    for k in KEEP:56        if k in m.params.index:57            b, se, p = m.params[k], m.bse[k], m.pvalues[k]58            star = "***" if p < .01 else "**" if p < .05 else "*" if p < .1 else ""59            out[k] = (f"{b:.3f}{star}", f"({se:.3f})")60    return out616263c1, c2, c3 = col(m1, "Pooled Poisson"), col(m2, "Poisson FE"), col(m3, "Neg. Binomial")64cols = [c1, c2, c3]6566lt = [r"\begin{tabular}{lccc}", r"\toprule",67      r" & (1) & (2) & (3) \\",68      r" & Pooled Poisson & Poisson (firm+yr FE) & Neg. Binomial \\", r"\midrule"]69for k in KEEP:70    line = LBL[k]71    se_line = ""72    for cc in cols:73        if k in cc:74            line += f" & {cc[k][0]}"; se_line += f" & {cc[k][1]}"75        else:76            line += " & "; se_line += " & "77    lt.append(line + r" \\"); lt.append(se_line + r" \\")78lt.append(r"\midrule")79lt.append(r"Sector FE & Yes & -- & Yes \\")80lt.append(r"Firm FE & -- & Yes & -- \\")81lt.append(r"Year FE & Yes & Yes & Yes \\")82lt.append("Observations" + "".join(f" & {cc['_n']:,}" for cc in cols) + r" \\")83lt.append("Log-likelihood" + "".join(f" & {cc['_ll']:,.0f}" for cc in cols) + r" \\")84lt += [r"\bottomrule", r"\end{tabular}"]85write_table("tab_count_models", "\n".join(lt))8687# ---- coefficient plot (Poisson FE incidence-rate effects) -----------------88fig, ax = plt.subplots(figsize=(6.4, 3.4))89ys, labels, los, his, bs = [], [], [], [], []90for i, k in enumerate(["projects_lag", "cum_ai_lag"]):91    b, se = m2.params[k], m2.bse[k]92    bs.append(b); los.append(b - 1.96 * se); his.append(b + 1.96 * se)93    labels.append(LBL[k]); ys.append(i)94ax.errorbar(bs, ys, xerr=[np.array(bs) - np.array(los), np.array(his) - np.array(bs)],95            fmt="o", color="#1b3a5b", capsize=4)96ax.axvline(0, color="grey", lw=0.8, ls="--")97ax.set_yticks(ys); ax.set_yticklabels(labels)98ax.set_xlabel("Poisson coefficient (firm + year FE)")99ax.set_title("Drivers of project-announcement intensity")100savefig(fig, "fig_count_coef.pdf")101102save_results("count_models", {103    "overdispersion_t": od_t, "overdispersion_p": od_p,104    "poisson_fe": {k: [float(m2.params[k]), float(m2.bse[k]), float(m2.pvalues[k])]105                   for k in ["projects_lag", "cum_ai_lag"]},106    "nb_alpha": float(m3.params.get("alpha", np.nan)),107    "persistence_irr": float(np.exp(m2.params["projects_lag"])),108    "ai_irr": float(np.exp(m2.params["cum_ai_lag"])),109    "n_obs": int(m1.nobs),110})111print("count models done | overdispersion t =", round(od_t, 2))112
Files · main
Menu

Theme