""" 07_event_study.py -- Market-model event study around the public launch of ChatGPT (30 November 2022). Tests whether firms with HIGHER pre-event AI disclosure (from their last pre-event 10-K, FY2021) earned higher cumulative abnormal returns (CAR) after the AI-narrative shock. Design: Event date t0 = 2022-11-30. Estimation window: 2022-01-03 .. 2022-10-31 (market model AR = r - a - b*rmkt). Event window: 2022-11-01 .. 2024-06-30. Treatment: tercile of pre-event AI exposure (composite adoption+infra, FY2021). Outputs daily CAR paths per tercile + cross-sectional CAR regressions. """ import time, warnings import numpy as np, pandas as pd, yfinance as yf import statsmodels.api as sm warnings.filterwarnings("ignore") D="data/processed"; T="results/tables" panel=pd.read_parquet(f"{D}/analysis_panel.parquet") tickers=sorted(panel.ticker.dropna().unique().tolist()) # ---- download daily prices (cache) ---- import os DPATH=f"{D}/prices_daily_event.parquet" def extract_close(d, t): """Return Close series for ticker t from a yf.download frame (any layout).""" try: if isinstance(d.columns, pd.MultiIndex): if t in d.columns.get_level_values(0): return d[t]["Close"].dropna() if "Close" in d.columns.get_level_values(0): return d["Close"][t].dropna() return d["Close"].dropna() except Exception: return None if not os.path.exists(DPATH): frames=[] for i in range(0,len(tickers),60): b=tickers[i:i+60] d=yf.download(b,start="2021-09-01",end="2024-07-15",interval="1d",auto_adjust=True, progress=False,group_by="ticker",threads=True) for t in b: s=extract_close(d,t) if s is None or len(s)==0: continue tmp=s.reset_index(); tmp.columns=["date","px"]; tmp["ticker"]=t; frames.append(tmp) time.sleep(1); print("daily batch",i//60+1, flush=True) spy=yf.download("SPY",start="2021-09-01",end="2024-07-15",interval="1d",auto_adjust=True,progress=False) sc=extract_close(spy,"SPY") sdf=sc.reset_index(); sdf.columns=["date","px"]; sdf["ticker"]="SPY" frames.append(sdf) dp=pd.concat(frames,ignore_index=True) dp.to_parquet(DPATH,index=False) else: dp=pd.read_parquet(DPATH) dp["date"]=pd.to_datetime(dp["date"]) dp=dp.sort_values(["ticker","date"]) dp["ret"]=dp.groupby("ticker")["px"].pct_change() spy=dp[dp.ticker=="SPY"][["date","ret"]].rename(columns={"ret":"mkt"}) t0=pd.Timestamp("2022-11-30") est0,est1=pd.Timestamp("2022-01-03"),pd.Timestamp("2022-10-31") ev0,ev1=pd.Timestamp("2022-11-01"),pd.Timestamp("2024-06-30") # pre-event AI exposure from FY2021 filing exp=panel[panel.fiscal_year==2021].copy() exp["ai_pre"]=(exp.groupby("fiscal_year")["ai_adoption_wmean"].transform(lambda g:(g-g.mean())/g.std()) +exp.groupby("fiscal_year")["ai_infrastructure_wmean"].transform(lambda g:(g-g.mean())/g.std()))/2 exp=exp[["ticker","ai_pre","log_mktcap","sector"]].dropna(subset=["ai_pre"]) exp["tercile"]=pd.qcut(exp.ai_pre,3,labels=["Low","Mid","High"]) # compute AR per ticker car_rows=[]; daily_ar={} beta_map={} for t,g in dp[dp.ticker!="SPY"].groupby("ticker"): g=g.merge(spy,on="date",how="left").dropna(subset=["ret","mkt"]) est=g[(g.date>=est0)&(g.date<=est1)] if len(est)<60: continue X=sm.add_constant(est["mkt"]); m=sm.OLS(est["ret"],X).fit() a,b=m.params["const"],m.params["mkt"]; beta_map[t]=b ev=g[(g.date>=ev0)&(g.date<=ev1)].copy() ev["ar"]=ev["ret"]-(a+b*ev["mkt"]) ev["car"]=ev["ar"].cumsum() daily_ar[t]=ev.set_index("date")["car"] for hz,lab in [(60,"car_60d"),(125,"car_6m"),(250,"car_1y"),(395,"car_18m")]: sub=ev.iloc[:hz] car_rows.append({"ticker":t,"hz":lab,"car":sub["ar"].sum()}) car=pd.DataFrame(car_rows).pivot(index="ticker",columns="hz",values="car").reset_index() car=car.merge(exp,on="ticker",how="inner"); car["beta"]=car.ticker.map(beta_map) car.to_parquet(f"{D}/event_car.parquet",index=False) # mean CAR path by tercile allc=pd.DataFrame(daily_ar) # cols=tickers, index=date allc=allc.dropna(how="all") paths={} for terc in ["Low","Mid","High"]: tks=[t for t in exp[exp.tercile==terc].ticker if t in allc.columns] paths[terc]=allc[tks].mean(axis=1) path_df=pd.DataFrame(paths) path_df["High_minus_Low"]=path_df["High"]-path_df["Low"] path_df.to_parquet(f"{D}/event_car_paths.parquet") # tests RES={} print("=== ChatGPT event study: mean CAR by pre-event AI tercile ===") for hz in ["car_60d","car_6m","car_1y","car_18m"]: hi=car[car.tercile=="High"][hz].dropna(); lo=car[car.tercile=="Low"][hz].dropna() from scipy import stats tt=stats.ttest_ind(hi,lo,equal_var=False) RES[hz]={"High":round(hi.mean()*100,2),"Mid":round(car[car.tercile=='Mid'][hz].mean()*100,2), "Low":round(lo.mean()*100,2),"H_minus_L":round((hi.mean()-lo.mean())*100,2), "t":round(tt.statistic,2),"p":round(tt.pvalue,4)} print(f" {hz:8s} High={RES[hz]['High']:+6.2f}% Low={RES[hz]['Low']:+6.2f}% H-L={RES[hz]['H_minus_L']:+6.2f}% (t={RES[hz]['t']}, p={RES[hz]['p']})") # cross-sectional regression: CAR_1y on ai_pre + controls reg=car.dropna(subset=["car_1y","ai_pre","log_mktcap","beta"]).copy() X=sm.add_constant(reg[["ai_pre","log_mktcap","beta"]]) m=sm.OLS(reg["car_1y"],X).fit(cov_type="HC1") RES["xsec_reg_1y"]={"ai_pre_coef":round(m.params["ai_pre"]*100,3),"ai_pre_t":round(m.tvalues["ai_pre"],2), "n":int(m.nobs),"r2":round(m.rsquared,3)} # add sector FE version import statsmodels.formula.api as smf m2=smf.ols("car_1y ~ ai_pre + log_mktcap + beta + C(sector)",data=reg).fit(cov_type="HC1") RES["xsec_reg_1y_sectfe"]={"ai_pre_coef":round(m2.params["ai_pre"]*100,3),"ai_pre_t":round(m2.tvalues["ai_pre"],2),"n":int(m2.nobs)} print(f"\n Cross-sec: CAR_1y on AI_pre: {RES['xsec_reg_1y']['ai_pre_coef']:+.2f}%/SD (t={RES['xsec_reg_1y']['ai_pre_t']}, n={RES['xsec_reg_1y']['n']})") print(f" + sector FE: {RES['xsec_reg_1y_sectfe']['ai_pre_coef']:+.2f}%/SD (t={RES['xsec_reg_1y_sectfe']['ai_pre_t']})") import json; json.dump(RES,open(f"{T}/results_event.json","w"),indent=2,default=str) print("\nSaved event-study results.")