Tutorial

How to Plot an Event Study: CAAR with a Confidence Band

Turn the bundled ARC sample data into a cumulative average abnormal return (CAAR) timeline, with a transparent, reproducible pointwise uncertainty band.

← All methodology

In short

Fit a market model for each event in a pre-event estimation window, average abnormal returns by event day, and cumulatively sum those averages into CAAR. For a pointwise 95% band, calculate the cross-sectional standard error of each event’s running CAR at each day. The Python and R examples below read the three files in src/sample-data and reproduce every displayed value.

What a CAAR plot shows

The horizontal axis is event time: day 0 is the first trading day on or after the event date. The vertical axis is the cumulative average abnormal return. Each market-model residual is $AR_{i,t}=R_{i,t}-(\hat\alpha_i+\hat\beta_iR_{m,t})$; averaging it across events gives $AAR_t$, and $CAAR_\tau=\sum_{t=-10}^{\tau}AAR_t$.

A jump at day 0 is the immediate reaction relative to the fitted benchmark. A pre-event slope is useful context, but does not by itself prove information leakage: ordinary variation, an imprecise event date, and other news are also possible.

Result from the bundled sample data

This calculation uses all 17 rows in 01_RequestFile.csv, simple returns from the supplied firm and SP500 prices, the specified 120-trading-day estimation window ending at −11, and the requested [−10,+10] event window. It is recomputed from these files, not copied from a result export.

CAAR from the bundled event-study sample dataA CAAR line from event day minus ten to plus ten with a shaded pointwise 95 percent t interval. The line rises after day zero and ends at 11.85 percent.11.85%0%5%10%15%20%−100+10Event time (trading days)
CAAR (blue) and a shaded pointwise 95% t interval. At day +10, CAAR = 11.8505%, interval [3.5872%, 20.1137%].
Event dayCAARPointwise 95% interval
−101.3428%[0.4166%, 2.2689%]
00.0602%[−4.5697%, 4.6901%]
+15.8346%[0.0729%, 11.5964%]
+510.9892%[2.5941%, 19.3842%]
+1011.8505%[3.5872%, 20.1137%]

The confidence band: scope and assumptions

For each event, let $CAR_{i,\tau}=\sum_{t=-10}^{\tau}AR_{i,t}$. The code calculates $SE_\tau=s(CAR_{i,\tau})/\sqrt N$ across events and plots $CAAR_\tau\pm t_{0.975,N-1}SE_\tau$. Here $N=17$, hence $t_{0.975,16}=2.119905$.

This is a pointwise interval, not a simultaneous band for the whole path and not a replacement for Patell, BMP, rank, or other event-study tests. Its t calibration treats the event-level running CARs as independent for this purpose; the supplied files cannot establish that assumption. Overlapping calendar windows, shared shocks, and clustered announcements can violate it, so report clustering and use a dependence-robust formal test when required.

Python: reproduce the chart

Install numpy pandas matplotlib scipy, save this as plot_caar.py in the repository root, and run python plot_caar.py.

from pathlib import Path
import numpy as np, pandas as pd
import matplotlib.pyplot as plt
from scipy.stats import t

root = Path(".")
req = pd.read_csv(root / "src/sample-data/01_RequestFile.csv", sep=";", header=None,
    names=["event_id","firm","market","event_date","group","pre","post","est_end","est_len"],
    parse_dates=["event_date"], dayfirst=True)
firm = pd.read_csv(root / "src/sample-data/02_FirmData.csv", sep=";", header=None,
    names=["firm","date","price"], parse_dates=["date"], dayfirst=True)
market = pd.read_csv(root / "src/sample-data/03_MarketData.csv", sep=";", header=None,
    names=["market","date","price"], parse_dates=["date"], dayfirst=True)
req.firm = req.firm.str.strip(); firm.firm = firm.firm.str.strip()
firm["ret"] = firm.sort_values(["firm","date"]).groupby("firm").price.pct_change()
market["mkt_ret"] = market.sort_values(["market","date"]).groupby("market").price.pct_change()

rows = []
for e in req.itertuples(index=False):
    g = (firm[firm.firm.eq(e.firm)].merge(market[["market","date","mkt_ret"]], on="date")
         .dropna(subset=["ret","mkt_ret"]).sort_values("date").reset_index(drop=True))
    zero = g.index[g.date.ge(e.event_date)][0]; g["tau"] = g.index - zero
    est = g[g.tau.between(e.est_end - e.est_len + 1, e.est_end)]
    beta, alpha = np.polyfit(est.mkt_ret, est.ret, 1)
    evt = g[g.tau.between(e.pre, e.post)].copy()
    evt["ar"] = evt.ret - (alpha + beta * evt.mkt_ret); rows.append(evt.assign(event_id=e.event_id))

panel = pd.concat(rows).pivot(index="tau", columns="event_id", values="ar").sort_index()
caar = panel.mean(axis=1).cumsum(); running_car = panel.cumsum(); n = running_car.shape[1]
se = running_car.std(axis=1, ddof=1) / np.sqrt(n); q = t.ppf(.975, n - 1)
lower, upper = caar - q * se, caar + q * se
print(f"N={n}; day +10 CAAR={caar.loc[10]:.4%}; interval=[{lower.loc[10]:.4%}, {upper.loc[10]:.4%}]")
fig, ax = plt.subplots(figsize=(8,4.5)); ax.fill_between(caar.index, lower, upper, color="#93c5fd", alpha=.45, label="Pointwise 95% t interval")
ax.plot(caar.index, caar, color="#075985", marker="o", label="CAAR"); ax.axvline(0, color="0.35", ls="--"); ax.axhline(0, color="0.5", lw=.8)
ax.set(xlabel="Event time (trading days)", ylabel="CAAR", xlim=(-10,10)); ax.yaxis.set_major_formatter(lambda y, _: f"{y:.0%}"); ax.legend(frameon=False)
fig.tight_layout(); fig.savefig("caar_sample.png", dpi=180)

R: reproduce the same chart

This uses base R plus ggplot2. Save it as plot_caar.R in the repository root and run Rscript plot_caar.R.

library(ggplot2)
req <- read.csv("src/sample-data/01_RequestFile.csv", sep=";", header=FALSE, col.names=c("event_id","firm","market","event_date","group","pre","post","est_end","est_len"), stringsAsFactors=FALSE)
req$event_date <- as.Date(req$event_date, "%d.%m.%Y"); req$firm <- trimws(req$firm)
firm <- read.csv("src/sample-data/02_FirmData.csv", sep=";", header=FALSE, col.names=c("firm","date","price")); market <- read.csv("src/sample-data/03_MarketData.csv", sep=";", header=FALSE, col.names=c("market","date","price"))
firm$date <- as.Date(firm$date, "%d.%m.%Y"); market$date <- as.Date(market$date, "%d.%m.%Y"); firm$firm <- trimws(firm$firm)
firm <- firm[order(firm$firm,firm$date),]; market <- market[order(market$market,market$date),]
firm$ret <- ave(firm$price,firm$firm,FUN=function(x)c(NA,x[-1]/x[-length(x)]-1)); market$mkt_ret <- ave(market$price,market$market,FUN=function(x)c(NA,x[-1]/x[-length(x)]-1))
rows <- lapply(seq_len(nrow(req)), function(j) { e <- req[j,]; g <- merge(firm[firm$firm==e$firm,],market[c("market","date","mkt_ret")],by="date"); g <- g[complete.cases(g$ret,g$mkt_ret),]; g <- g[order(g$date),]; zero <- which(g$date>=e$event_date)[1]; g$tau <- seq_len(nrow(g))-zero; est <- g[g$tau>=e$est_end-e$est_len+1 & g$tau<=e$est_end,]; fit <- lm(ret~mkt_ret,data=est); evt <- g[g$tau>=e$pre & g$tau<=e$post,]; evt$ar <- evt$ret-predict(fit,evt); evt$event_id <- e$event_id; evt })
ar <- do.call(rbind,rows); wide <- reshape(ar[c("tau","event_id","ar")],idvar="tau",timevar="event_id",direction="wide"); wide <- wide[order(wide$tau),]; x <- as.matrix(wide[grep("^ar\\.",names(wide))])
caar <- cumsum(rowMeans(x)); running_car <- apply(x,2,cumsum); n <- ncol(x); se <- apply(running_car,1,sd)/sqrt(n); q <- qt(.975,n-1); d <- data.frame(tau=wide$tau,caar=caar,lower=caar-q*se,upper=caar+q*se)
z <- d[d$tau==10,]; cat(sprintf("N=%d; day +10 CAAR=%.4f%%; interval=[%.4f%%, %.4f%%]\\n",n,100*z$caar,100*z$lower,100*z$upper))
ggplot(d,aes(tau,caar))+geom_ribbon(aes(ymin=lower,ymax=upper),fill="#93c5fd",alpha=.45)+geom_line(colour="#075985")+geom_point(colour="#075985")+geom_vline(xintercept=0,linetype="dashed")+geom_hline(yintercept=0,colour="grey50")+labs(x="Event time (trading days)",y="CAAR")+theme_minimal(); ggsave("caar_sample_r.png",width=8,height=4.5,dpi=180)

What to report beside the figure

State the return convention, expected-return model, estimation window and gap, event-time rule, event window, number of events, and calendar overlap. Describe this band as a pointwise cross-sectional t interval for running CAR, not a simultaneous band or a significance test. For formal inference, use the test appropriate to the sample; see Significance tests.

Plot your own results. ARC returns AR, AAR, CAR and CAAR from your request and price files.

Run it free in ARC →

Frequently asked questions

Is this a simultaneous confidence band?

No. It is calculated separately at each event day and does not provide 95% coverage for the entire path at once.

Why use running CARs for the band?

CAAR averages cumulative firm-level abnormal returns, so the corresponding uncertainty at each day is estimated from the cross-sectional variation of running CARs.

Does pre-event movement prove leakage?

No. It can motivate a review of timestamps and other news, but the plot alone cannot distinguish leakage from ordinary variation, date error, or confounding information.