spb/uqo-research-db
Public
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