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.
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() 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.
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").
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):
The result object includes the merged CAR + characteristics data, which is useful for custom analyses:
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)
car_quantiles() provides a quick summary of the CAR
distribution:
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)
})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.