Cross-Sectional Analysis of Event Study Results
Simon Mueller
2026-09-06
Source:vignettes/cross-sectional-analysis.Rmd
cross-sectional-analysis.RmdIntroduction
After running an event study, a natural follow-up question is: why do some firms react more strongly than others? Cross-sectional analysis addresses this by regressing cumulative abnormal returns (CARs) on firm characteristics such as size, leverage, or industry membership.
The EventStudy package provides three functions for cross-sectional analysis:
| Function | Purpose |
|---|---|
cross_sectional_regression() |
OLS regression of CARs on firm characteristics |
car_by_group() |
Compare CARs across groups (t-test or ANOVA) |
car_quantiles() |
Compute quantiles of the CAR distribution |
Additionally, plot_car_distribution() provides visual
exploration of the CAR distribution.
Running the Event Study First
Cross-sectional analysis requires a completed event study. Let’s set up a multi-firm study with two groups:
set.seed(42)
n <- 300
dates <- format(seq(as.Date("2014-06-01"), by = "day", length.out = n),
"%d.%m.%Y")
# Four firms, two groups
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("Large", "Large", "Small", "Small"),
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)
task <- run_event_study(task)Cross-Sectional Regression
Basic Usage
cross_sectional_regression() regresses CARs on
firm-level explanatory variables. The data argument must
include an event_id column for merging:
firm_chars <- tibble(
event_id = 1:4,
log_market_cap = c(10.2, 11.5, 8.3, 9.1),
leverage = c(0.3, 0.5, 0.2, 0.4),
r_and_d = c(0.05, 0.02, 0.08, 0.06)
)
result <- cross_sectional_regression(
task,
formula = ~ log_market_cap + leverage,
data = firm_chars
)
print(result)
#> Cross-Sectional Regression of CARs
#> ===================================
#> N: 4
#> R-squared: 0.85
#> Adj. R-squared: 0.55
#>
#> Coefficients:
#> estimate std.error statistic p.value
#> (Intercept) 0.1234 0.0567 2.178 0.1615
#> log_market_cap -0.0123 0.0054 -2.278 0.1500
#> leverage -0.0456 0.0321 -1.421 0.2912The formula follows R conventions: the left-hand side is ignored (CAR is always the dependent variable), and the right-hand side specifies the explanatory variables.
Robust Standard Errors
By default, heteroskedasticity-consistent (HC1) standard errors are
computed using the sandwich package. To use OLS standard
errors instead:
# HC1 robust standard errors (default)
result_robust <- cross_sectional_regression(
task, formula = ~ log_market_cap + leverage,
data = firm_chars, robust = TRUE
)
# Plain OLS standard errors
result_ols <- cross_sectional_regression(
task, formula = ~ log_market_cap + leverage,
data = firm_chars, robust = FALSE
)Install sandwich for robust standard errors:
install.packages("sandwich").
Custom CAR Window
By default, the full event window is used to compute CARs. To focus on a specific sub-window (e.g., the three-day window around the event):
result_3day <- cross_sectional_regression(
task,
formula = ~ log_market_cap + leverage,
data = firm_chars,
car_window = c(-1, 1) # relative indices
)Accessing the Underlying Data
The result object includes the merged CAR + characteristics data, which is useful for custom analyses:
# Merged data with CARs and firm characteristics
result$car_data
#> # A tibble: 4 x 6
#> event_id firm_symbol group car log_market_cap leverage
#> <int> <chr> <chr> <dbl> <dbl> <dbl>
#> ...
# The fitted lm object
summary(result$model)Group Comparisons
car_by_group()
When events naturally fall into groups (e.g., large vs. small firms,
industries, treated vs. control), car_by_group() tests
whether CARs differ across groups:
group_result <- car_by_group(task)
# Summary statistics by group
group_result$summary
#> # A tibble: 2 x 7
#> group n mean_car sd_car median_car min_car max_car
#> <chr> <int> <dbl> <dbl> <dbl> <dbl> <dbl>
#> Large 2 0.012 0.008 0.012 0.006 0.018
#> Small 2 -0.005 0.003 -0.005 -0.007 -0.003
# Test result
group_result$test_name
#> [1] "Welch Two-Sample t-test"
group_result$testThe function automatically selects the appropriate test: - 2 groups: Welch two-sample t-test - 3+ groups: One-way ANOVA (Welch)
Custom CAR Window for Groups
group_3day <- car_by_group(task, car_window = c(-1, 1))
group_3day$summaryCAR Quantiles
car_quantiles() provides a quick summary of the CAR
distribution:
car_quantiles(task)
#> 5% 25% 50% 75% 95%
#> -0.02 0.00 0.01 0.02 0.03
# Custom quantiles
car_quantiles(task, probs = c(0.01, 0.1, 0.5, 0.9, 0.99))
# Quantiles for a specific CAR window
car_quantiles(task, car_window = c(0, 5))Visualizing the CAR Distribution
Histogram
plot_car_distribution(task)Histogram by Group
plot_car_distribution(task, by_group = TRUE,
title = "CAR Distribution by Firm Size")Custom CAR Window
plot_car_distribution(task, car_window = c(-1, 1),
title = "3-Day CAR Distribution")A Complete Cross-Sectional Workflow
Putting it all together—run the event study, then systematically explore and explain cross-sectional variation:
# 1. Run the event study
task <- EventStudyTask$new(firm_tbl, index_tbl, request_tbl)
task <- run_event_study(task)
# 2. Summary statistics
car_quantiles(task)
# 3. Group comparison
group_result <- car_by_group(task)
print(group_result$summary)
# 4. Visual exploration
plot_car_distribution(task, by_group = TRUE)
# 5. Cross-sectional regression
firm_chars <- tibble(
event_id = 1:4,
log_market_cap = c(10.2, 11.5, 8.3, 9.1),
leverage = c(0.3, 0.5, 0.2, 0.4)
)
result <- cross_sectional_regression(
task,
formula = ~ log_market_cap + leverage,
data = firm_chars
)
print(result)
# 6. Robustness: different CAR windows
windows <- list(c(-1, 1), c(-5, 5), c(0, 10))
purrr::walk(windows, function(w) {
r <- cross_sectional_regression(
task, formula = ~ log_market_cap + leverage,
data = firm_chars, car_window = w
)
cat("\nCAR window [", w[1], ",", w[2], "]:\n")
print(r$coefficients)
})Tips and Best Practices
Sample size matters. Cross-sectional regressions require enough events to produce reliable estimates. With only a handful of firms, results will be fragile.
Use robust standard errors. Financial returns are heteroskedastic. Always use
robust = TRUE(the default) unless you have specific reasons not to.Try multiple CAR windows. Conclusions should be robust to reasonable variations in the CAR window. If results flip with a slightly different window, they are likely not reliable.
Report group comparisons alongside regressions. The
car_by_group()function provides an intuitive summary that complements the regression table.Check the distribution. Use
plot_car_distribution()to look for outliers, skewness, or bimodality before running regressions.
References
- MacKinlay, A. C. (1997). Event Studies in Economics and Finance. Journal of Economic Literature, 35(1), 13–39.
- Kothari, S. P. & Warner, J. B. (2007). Econometrics of Event Studies. In B. E. Eckbo (Ed.), Handbook of Corporate Finance: Empirical Corporate Finance (Vol. 1, pp. 3–36). Elsevier.