SPB Git forge

spb/wp17_uqo

Public
2commits 1branches 1releases
19.9 MBsize
maindefault branch
58 min agolast push
TeX 74.5% Python 25.4%
6.1 KB · 133 lines python
Raw Blame History
1"""207_event_study.py -- Market-model event study around the public launch of3ChatGPT (30 November 2022). Tests whether firms with HIGHER pre-event AI4disclosure (from their last pre-event 10-K, FY2021) earned higher cumulative5abnormal returns (CAR) after the AI-narrative shock.67Design:8  Event date t0 = 2022-11-30.9  Estimation window: 2022-01-03 .. 2022-10-31 (market model AR = r - a - b*rmkt).10  Event window: 2022-11-01 .. 2024-06-30.11  Treatment: tercile of pre-event AI exposure (composite adoption+infra, FY2021).12Outputs daily CAR paths per tercile + cross-sectional CAR regressions.13"""14import time, warnings15import numpy as np, pandas as pd, yfinance as yf16import statsmodels.api as sm17warnings.filterwarnings("ignore")18D="data/processed"; T="results/tables"1920panel=pd.read_parquet(f"{D}/analysis_panel.parquet")21tickers=sorted(panel.ticker.dropna().unique().tolist())2223# ---- download daily prices (cache) ----24import os25DPATH=f"{D}/prices_daily_event.parquet"2627def extract_close(d, t):28    """Return Close series for ticker t from a yf.download frame (any layout)."""29    try:30        if isinstance(d.columns, pd.MultiIndex):31            if t in d.columns.get_level_values(0):32                return d[t]["Close"].dropna()33            if "Close" in d.columns.get_level_values(0):34                return d["Close"][t].dropna()35        return d["Close"].dropna()36    except Exception:37        return None3839if not os.path.exists(DPATH):40    frames=[]41    for i in range(0,len(tickers),60):42        b=tickers[i:i+60]43        d=yf.download(b,start="2021-09-01",end="2024-07-15",interval="1d",auto_adjust=True,44                      progress=False,group_by="ticker",threads=True)45        for t in b:46            s=extract_close(d,t)47            if s is None or len(s)==0: continue48            tmp=s.reset_index(); tmp.columns=["date","px"]; tmp["ticker"]=t; frames.append(tmp)49        time.sleep(1); print("daily batch",i//60+1, flush=True)50    spy=yf.download("SPY",start="2021-09-01",end="2024-07-15",interval="1d",auto_adjust=True,progress=False)51    sc=extract_close(spy,"SPY")52    sdf=sc.reset_index(); sdf.columns=["date","px"]; sdf["ticker"]="SPY"53    frames.append(sdf)54    dp=pd.concat(frames,ignore_index=True)55    dp.to_parquet(DPATH,index=False)56else:57    dp=pd.read_parquet(DPATH)5859dp["date"]=pd.to_datetime(dp["date"])60dp=dp.sort_values(["ticker","date"])61dp["ret"]=dp.groupby("ticker")["px"].pct_change()62spy=dp[dp.ticker=="SPY"][["date","ret"]].rename(columns={"ret":"mkt"})6364t0=pd.Timestamp("2022-11-30")65est0,est1=pd.Timestamp("2022-01-03"),pd.Timestamp("2022-10-31")66ev0,ev1=pd.Timestamp("2022-11-01"),pd.Timestamp("2024-06-30")6768# pre-event AI exposure from FY2021 filing69exp=panel[panel.fiscal_year==2021].copy()70exp["ai_pre"]=(exp.groupby("fiscal_year")["ai_adoption_wmean"].transform(lambda g:(g-g.mean())/g.std())71              +exp.groupby("fiscal_year")["ai_infrastructure_wmean"].transform(lambda g:(g-g.mean())/g.std()))/272exp=exp[["ticker","ai_pre","log_mktcap","sector"]].dropna(subset=["ai_pre"])73exp["tercile"]=pd.qcut(exp.ai_pre,3,labels=["Low","Mid","High"])7475# compute AR per ticker76car_rows=[]; daily_ar={}77beta_map={}78for t,g in dp[dp.ticker!="SPY"].groupby("ticker"):79    g=g.merge(spy,on="date",how="left").dropna(subset=["ret","mkt"])80    est=g[(g.date>=est0)&(g.date<=est1)]81    if len(est)<60: continue82    X=sm.add_constant(est["mkt"]); m=sm.OLS(est["ret"],X).fit()83    a,b=m.params["const"],m.params["mkt"]; beta_map[t]=b84    ev=g[(g.date>=ev0)&(g.date<=ev1)].copy()85    ev["ar"]=ev["ret"]-(a+b*ev["mkt"])86    ev["car"]=ev["ar"].cumsum()87    daily_ar[t]=ev.set_index("date")["car"]88    for hz,lab in [(60,"car_60d"),(125,"car_6m"),(250,"car_1y"),(395,"car_18m")]:89        sub=ev.iloc[:hz]90        car_rows.append({"ticker":t,"hz":lab,"car":sub["ar"].sum()})91car=pd.DataFrame(car_rows).pivot(index="ticker",columns="hz",values="car").reset_index()92car=car.merge(exp,on="ticker",how="inner"); car["beta"]=car.ticker.map(beta_map)93car.to_parquet(f"{D}/event_car.parquet",index=False)9495# mean CAR path by tercile96allc=pd.DataFrame(daily_ar)  # cols=tickers, index=date97allc=allc.dropna(how="all")98paths={}99for terc in ["Low","Mid","High"]:100    tks=[t for t in exp[exp.tercile==terc].ticker if t in allc.columns]101    paths[terc]=allc[tks].mean(axis=1)102path_df=pd.DataFrame(paths)103path_df["High_minus_Low"]=path_df["High"]-path_df["Low"]104path_df.to_parquet(f"{D}/event_car_paths.parquet")105106# tests107RES={}108print("=== ChatGPT event study: mean CAR by pre-event AI tercile ===")109for hz in ["car_60d","car_6m","car_1y","car_18m"]:110    hi=car[car.tercile=="High"][hz].dropna(); lo=car[car.tercile=="Low"][hz].dropna()111    from scipy import stats112    tt=stats.ttest_ind(hi,lo,equal_var=False)113    RES[hz]={"High":round(hi.mean()*100,2),"Mid":round(car[car.tercile=='Mid'][hz].mean()*100,2),114             "Low":round(lo.mean()*100,2),"H_minus_L":round((hi.mean()-lo.mean())*100,2),115             "t":round(tt.statistic,2),"p":round(tt.pvalue,4)}116    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']})")117118# cross-sectional regression: CAR_1y on ai_pre + controls119reg=car.dropna(subset=["car_1y","ai_pre","log_mktcap","beta"]).copy()120X=sm.add_constant(reg[["ai_pre","log_mktcap","beta"]])121m=sm.OLS(reg["car_1y"],X).fit(cov_type="HC1")122RES["xsec_reg_1y"]={"ai_pre_coef":round(m.params["ai_pre"]*100,3),"ai_pre_t":round(m.tvalues["ai_pre"],2),123                    "n":int(m.nobs),"r2":round(m.rsquared,3)}124# add sector FE version125import statsmodels.formula.api as smf126m2=smf.ols("car_1y ~ ai_pre + log_mktcap + beta + C(sector)",data=reg).fit(cov_type="HC1")127RES["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)}128print(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']})")129print(f"  + sector FE: {RES['xsec_reg_1y_sectfe']['ai_pre_coef']:+.2f}%/SD (t={RES['xsec_reg_1y_sectfe']['ai_pre_t']})")130131import json; json.dump(RES,open(f"{T}/results_event.json","w"),indent=2,default=str)132print("\nSaved event-study results.")133