knitr::opts_chunk$set( collapse = TRUE, comment = "#>", eval = FALSE )
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.
library(EventStudy) library(dplyr) library(ggplot2)
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.2912
The 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):
result_3day <- cross_sectional_regression( task, formula = ~ log_market_cap + leverage, data = firm_chars, car_window = c(-1, 1) # relative indices )
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)
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$test
The function automatically selects the appropriate test: - 2 groups: Welch two-sample t-test - 3+ groups: One-way ANOVA (Welch)
group_3day <- car_by_group(task, car_window = c(-1, 1)) group_3day$summary
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))
plot_car_distribution(task)
plot_car_distribution(task, by_group = TRUE, title = "CAR Distribution by Firm Size")
plot_car_distribution(task, car_window = c(-1, 1), title = "3-Day 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.
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.