A well-conducted event study requires more than just running the pipeline. You should validate the data before estimation and diagnose the model after fitting. The EventStudy package provides three dedicated functions and a diagnostic plotting tool:
| Function | When to use | What it checks |
|---|---|---|
validate_task() |
Before or after prepare_event_study() |
Data integrity, windows, gaps |
model_diagnostics() |
After fit_model() |
Residual normality, autocorrelation, model fit |
pretrend_test() |
After fit_model() |
Pre-event abnormal returns |
plot_diagnostics() |
After fit_model() |
Visual residual analysis |
set.seed(42)
n <- 300
dates <- format(seq(as.Date("2014-06-01"), by = "day", length.out = n),
"%d.%m.%Y")
firms <- c("FIRM_A", "FIRM_B", "FIRM_C", "FIRM_D")
firm_tbl <- purrr::map_dfr(firms, function(sym) {
tibble(
symbol = sym,
date = dates,
adjusted = 100 * cumprod(1 + rnorm(n, 0.0003, 0.015))
)
})
index_tbl <- tibble(
symbol = "INDEX_1",
date = dates,
adjusted = 1000 * cumprod(1 + rnorm(n, 0.0002, 0.012))
)
request_tbl <- tibble(
event_id = 1:4,
firm_symbol = firms,
index_symbol = rep("INDEX_1", 4),
event_date = rep(dates[200], 4),
group = c("Group_A", "Group_A", "Group_B", "Group_B"),
event_window_start = rep(-10L, 4),
event_window_end = rep(10L, 4),
shift_estimation_window = rep(-11L, 4),
estimation_window_length = rep(150L, 4)
)
task <- EventStudyTask$new(firm_tbl, index_tbl, request_tbl)
params <- ParameterSet$new()validate_task() checks data integrity before estimation.
Run it right after creating the task or after calling
prepare_event_study():
The function performs five checks for each event:
The event date from the request table must appear in the stock data. Missing event dates typically indicate mismatched date formats or non-trading days:
The estimation window must have enough observations for reliable model estimation. The default minimum is 30, but you can adjust it:
Large gaps (> 5 calendar days) in the time series may indicate missing data, delistings, or trading halts:
Estimation and event windows must not overlap, as this would contaminate the benchmark model:
validate_task() returns the task invisibly and issues
warning() calls for each problem. You can capture warnings
programmatically:
After fitting models, model_diagnostics() runs
statistical tests on the estimation-window residuals:
task <- fit_model(task, params)
diag <- model_diagnostics(task)
diag
#> # A tibble: 4 x 9
#> event_id firm_symbol is_fitted shapiro_p dw_stat ljung_box_p acf1 sigma r2
#> <int> <chr> <lgl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 1 FIRM_A TRUE 0.342 2.01 0.654 0.012 0.0149 0.032
#> 2 2 FIRM_B TRUE 0.127 1.95 0.891 0.034 0.0162 0.028
#> ...shapiro_p)Tests whether estimation-window residuals follow a normal distribution. Parametric test statistics (AR T-test, CAR T-test, Patell Z) assume normally distributed residuals.
What to do if normality is rejected:
SignTest,
GeneralizedSignTest, RankTest) instead of or
alongside parametric testsdw_stat)Tests for first-order autocorrelation in residuals. Values near 2 indicate no autocorrelation.
What to do if autocorrelation is detected:
ljung_box_p)A more formal test for autocorrelation at multiple lags (up to 10 or
floor(n/5), whichever is smaller).
acf1)The lag-1 autocorrelation coefficient of residuals. Values close to zero are desirable:
sigma, r2)A quick way to flag problematic events:
diag <- model_diagnostics(task)
# Flag events with potential issues
diag %>%
mutate(
normality_ok = shapiro_p > 0.05,
no_autocorr = ljung_box_p > 0.05,
dw_ok = dw_stat > 1.5 & dw_stat < 2.5,
all_ok = normality_ok & no_autocorr & dw_ok
) %>%
select(event_id, firm_symbol, normality_ok, no_autocorr, dw_ok, all_ok)plot_diagnostics() produces a four-panel residual
analysis plot:
The four panels are:
Residuals vs. Observation: Should show no pattern (random scatter around zero). Trends suggest model misspecification; funneling suggests heteroskedasticity.
Q-Q Plot: Points should fall on the diagonal line. Deviations at the tails indicate heavy-tailed or skewed residuals.
Residual Distribution: Histogram of residuals. Should be roughly bell-shaped and centered at zero.
ACF of Residuals: Autocorrelation at each lag. Bars exceeding the blue dashed lines (95% confidence bands) indicate significant autocorrelation.
The pre-trend test checks whether abnormal returns are already present before the event. Significant pre-event effects may indicate:
task <- calculate_statistics(task, params)
pretrend <- pretrend_test(task)
pretrend
#> # A tibble: 2 x 7
#> group n_pre_periods n_events mean_pre_ar sd_pre_ar t_stat p_value
#> <chr> <int> <int> <dbl> <dbl> <dbl> <dbl>
#> Group_A 10 2 0.00012 0.0142 0.168 0.868
#> Group_B 10 2 -0.00034 0.0155 -0.221 0.826Putting all four tools together in a systematic workflow:
# 1. Create task
task <- EventStudyTask$new(firm_tbl, index_tbl, request_tbl)
# 2. Validate data BEFORE estimation
validate_task(task)
# 3. Prepare and fit
params <- ParameterSet$new(return_model = MarketModel$new())
task <- prepare_event_study(task, params)
# 4. Validate AFTER preparation (now windows are assigned)
validate_task(task)
# 5. Fit models
task <- fit_model(task, params)
# 6. Numerical diagnostics
diag <- model_diagnostics(task)
print(diag)
# 7. Flag problematic events
problems <- diag %>%
filter(shapiro_p < 0.05 | ljung_box_p < 0.05 | dw_stat < 1.5 | dw_stat > 2.5)
if (nrow(problems) > 0) {
cat("Events with diagnostic issues:\n")
print(problems %>% select(event_id, firm_symbol, shapiro_p, ljung_box_p, dw_stat))
} else {
cat("All events pass diagnostic checks.\n")
}
# 8. Visual diagnostics for flagged events (or all events)
events_to_check <- if (nrow(problems) > 0) problems$event_id else diag$event_id[1]
for (eid in events_to_check) {
plot_diagnostics(task, event_id = eid)
}
# 9. Calculate statistics and test pre-trends
task <- calculate_statistics(task, params)
pretrend <- pretrend_test(task)
print(pretrend)| Diagnostic result | Severity | Action |
|---|---|---|
| Shapiro p < 0.05 | Moderate | Add non-parametric tests (Sign, Rank) |
| DW < 1.5 or > 2.5 | Moderate | Consider GARCH; check for thin trading |
| Ljung-Box p < 0.05 | Moderate | Check data quality; consider GARCH |
| acf1 | > 0.2 | |
| R² very low (< 0.01) | Low | Normal for many stocks; consider factor models |
| R² very high (> 0.8) | Low | Verify data; possible look-ahead bias |
| Pre-trend p < 0.05 | High | Investigate leakage, confounders, wrong event date |
| Estimation window < 30 obs | High | Extend window or drop event |
| Window overlap | Critical | Fix request table parameters |
| > 10% zero returns | Moderate | Possible illiquidity; consider non-parametric tests |