knitr::opts_chunk$set( collapse = TRUE, comment = "#>", eval = FALSE )
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 |
library(EventStudy) library(dplyr) library(ggplot2)
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():
validate_task(task) #> Validation passed: no issues found across 4 event(s).
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:
# This would trigger a warning: # [Event 1 / FIRM_A] Event date '32.02.2015' not found in data.
The estimation window must have enough observations for reliable model estimation. The default minimum is 30, but you can adjust it:
# Stricter requirement: at least 100 observations validate_task(task, min_estimation_obs = 100) # Lenient: at least 20 validate_task(task, min_estimation_obs = 20)
Large gaps (> 5 calendar days) in the time series may indicate missing data, delistings, or trading halts:
# Warnings like: # [Event 2 / FIRM_B] 1 gap(s) > 5 days found in time series.
Estimation and event windows must not overlap, as this would contaminate the benchmark model:
# Warnings like: # [Event 3 / FIRM_C] Estimation and event windows overlap by 5 observation(s).
A high proportion of zero returns in the estimation window suggests illiquid securities, which violate the assumptions of most test statistics:
# Warnings like: # [Event 4 / FIRM_D] 15.3% zero returns in estimation window (possible thin trading).
validate_task() returns the task invisibly and issues warning() calls for
each problem. You can capture warnings programmatically:
task <- prepare_event_study(task, params) warnings <- tryCatch( withCallingHandlers( validate_task(task), warning = function(w) { message("Issue: ", conditionMessage(w)) invokeRestart("muffleWarning") } ) )
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)diag_event1 <- model_diagnostics(task, event_id = 1) diag_event1
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:
plot_diagnostics(task, event_id = 1)
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.
# Visual diagnostics for all events for (eid in task$data_tbl$event_id) { plot_diagnostics(task, event_id = eid) }
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.826
pretrend_a <- pretrend_test(task, group = "Group_A") pretrend_b <- pretrend_test(task, group = "Group_B")
Putting 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 | Moderate | Investigate serial dependence | | 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 |
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.