The simple Market Model regresses firm returns on a single market index. While widely used, it may miss systematic risk factors that explain return variation. The EventStudy package provides several alternatives:
| Model | Class | Factors |
|---|---|---|
| Fama-French 3-Factor | FamaFrench3FactorModel |
Market, SMB, HML |
| Fama-French 5-Factor | FamaFrench5FactorModel |
Market, SMB, HML, RMW, CMA |
| Carhart 4-Factor | Carhart4FactorModel |
Market, SMB, HML, MOM |
| GARCH(1,1) | GARCHModel |
Market (time-varying volatility) |
| BHAR | BHARModel |
Market (compounded returns) |
Factor models reduce estimation-window residual variance, yielding more powerful tests. GARCH accounts for volatility clustering. BHAR avoids the bias from summing daily abnormal returns over long horizons.
factor_tblFactor models require a factor table passed to
EventStudyTask$new(). This table must contain a
date column (same format as firm/index data) plus the
relevant factor columns.
| Model | Required columns in factor_tbl |
|---|---|
| Fama-French 3-Factor | smb, hml, risk_free_rate |
| Fama-French 5-Factor | smb, hml, rmw,
cma, risk_free_rate |
| Carhart 4-Factor | smb, hml, mom,
risk_free_rate |
The risk_free_rate column is special: when present,
prepare_event_study() automatically computes excess
returns:
excess_return = firm_returns - risk_free_ratemarket_excess = index_returns - risk_free_rateThese are the dependent and independent variables used by the factor model regressions.
Fama-French factor data is publicly available from Kenneth French’s
website. In practice you might use the frenchdata or
tidyquant packages:
# Example: constructing a factor_tbl manually
# In practice, download from Kenneth French's data library
set.seed(42)
n <- 300
dates <- format(seq(as.Date("2014-06-01"), by = "day", length.out = n),
"%d.%m.%Y")
factor_tbl <- tibble(
date = dates,
smb = rnorm(n, 0, 0.005), # Small Minus Big
hml = rnorm(n, 0, 0.005), # High Minus Low
rmw = rnorm(n, 0, 0.004), # Robust Minus Weak
cma = rnorm(n, 0, 0.004), # Conservative Minus Aggressive
mom = rnorm(n, 0, 0.006), # Momentum
risk_free_rate = rep(0.0001, n) # Daily risk-free rate
)The three-factor model (Fama & French, 1993) explains stock returns using market risk, size (SMB), and value (HML) factors:
\[R_i - R_f = \alpha + \beta_m (R_m - R_f) + \beta_s \text{SMB} + \beta_h \text{HML} + \epsilon\]
set.seed(42)
n <- 300
dates <- format(seq(as.Date("2014-06-01"), by = "day", length.out = n),
"%d.%m.%Y")
firm_tbl <- tibble(
symbol = "FIRM_A",
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 = 1L, firm_symbol = "FIRM_A", index_symbol = "INDEX_1",
event_date = dates[200], group = "Test",
event_window_start = -10L, event_window_end = 10L,
shift_estimation_window = -11L, estimation_window_length = 150L
)
# Pass factor_tbl to the task
task <- EventStudyTask$new(firm_tbl, index_tbl, request_tbl,
factor_tbl = factor_tbl)
params <- ParameterSet$new(
return_calculation = LogReturn$new(),
return_model = FamaFrench3FactorModel$new()
)
task <- task |>
prepare_event_study(params) |>
fit_model(params) |>
calculate_statistics(params)After fitting, the model stores all factor coefficients:
# Model statistics for event 1
stats <- task$data_tbl$model[[1]]$statistics
# Factor loadings
stats$alpha # Intercept (Jensen's alpha)
stats$beta # Market beta (same as `market_excess`)
stats$smb # SMB loading
stats$hml # HML loading
stats$r2 # R-squared
stats$sigma # Residual standard error
# Full coefficient table
task$get_model_stats(event_id = 1)The five-factor model (Fama & French, 2015) adds profitability (RMW) and investment (CMA) factors:
\[R_i - R_f = \alpha + \beta_m (R_m - R_f) + \beta_s \text{SMB} + \beta_h \text{HML} + \beta_r \text{RMW} + \beta_c \text{CMA} + \epsilon\]
task_ff5 <- EventStudyTask$new(firm_tbl, index_tbl, request_tbl,
factor_tbl = factor_tbl)
params_ff5 <- ParameterSet$new(
return_calculation = LogReturn$new(),
return_model = FamaFrench5FactorModel$new()
)
task_ff5 <- task_ff5 |>
prepare_event_study(params_ff5) |>
fit_model(params_ff5) |>
calculate_statistics(params_ff5)The Carhart (1997) model adds a momentum factor (MOM) to the three-factor model:
\[R_i - R_f = \alpha + \beta_m (R_m - R_f) + \beta_s \text{SMB} + \beta_h \text{HML} + \beta_{mom} \text{MOM} + \epsilon\]
task_c4 <- EventStudyTask$new(firm_tbl, index_tbl, request_tbl,
factor_tbl = factor_tbl)
params_c4 <- ParameterSet$new(
return_calculation = LogReturn$new(),
return_model = Carhart4FactorModel$new()
)
task_c4 <- task_c4 |>
prepare_event_study(params_c4) |>
fit_model(params_c4) |>
calculate_statistics(params_c4)The GARCH model (Bollerslev, 1986) accounts for volatility clustering—the tendency of large returns to follow large returns. The mean equation includes the market return as a regressor (like the Market Model), but the variance is modeled as time-varying:
\[R_{i,t} = \mu + \beta R_{m,t} + \epsilon_t, \quad \epsilon_t \sim N(0, \sigma_t^2)\] \[\sigma_t^2 = \omega + \alpha_1 \epsilon_{t-1}^2 + \beta_1 \sigma_{t-1}^2\]
The GARCH model stores conditional volatility from the estimation window:
stats <- task_garch$data_tbl$model[[1]]$statistics
stats$alpha # Mean equation intercept
stats$beta # Market beta
stats$sigma # Average conditional sigma
stats$garch_sigma # Time series of conditional sigmasThe time-varying conditional sigma can be particularly useful for standardized test statistics, as it accounts for periods of high and low volatility in the estimation window.
GARCH is most beneficial when:
For short estimation windows in stable markets, the simple Market Model is often sufficient.
Standard CARs sum daily abnormal returns:
\[CAR_i = \sum_{t=1}^{T} AR_{i,t}\]
For long horizons (months or years), this introduces a compounding bias. BHAR instead compounds returns, which better reflects the actual investor experience:
\[BHAR_i = \prod_{t=1}^{T} (1 + R_{i,t}) - \prod_{t=1}^{T} (1 + R_{m,t})\]
task_bhar <- EventStudyTask$new(firm_tbl, index_tbl, request_tbl)
params_bhar <- ParameterSet$new(
return_model = BHARModel$new(),
single_event_statistics = SingleEventStatisticsSet$new(
tests = list(BHARTTest$new())
)
)
task_bhar <- task_bhar |>
prepare_event_study(params_bhar) |>
fit_model(params_bhar) |>
calculate_statistics(params_bhar)Unlike the Market Model where abnormal returns are computed per observation, BHAR computes running compounded differences. The abnormal return at each point represents the cumulative buy-and-hold difference up to that point:
The difference between BHAR and CAR grows with the horizon and the magnitude of returns:
# Short window: CAR and BHAR are nearly identical
task_short <- EventStudyTask$new(firm_tbl, index_tbl, request_tbl)
params_short <- ParameterSet$new(return_model = MarketModel$new())
task_short <- run_event_study(task_short)
car_short <- tidy.EventStudyTask(task_short, type = "car")
bhar_short <- tidy.EventStudyTask(task_bhar, type = "ar")For a 21-day event window (typical), the difference is usually negligible. For horizons of 6–12 months or longer, BHAR is strongly preferred.
A rigorous event study often compares results across multiple models to assess robustness:
models <- list(
"Market Model" = MarketModel$new(),
"FF3" = FamaFrench3FactorModel$new(),
"FF5" = FamaFrench5FactorModel$new(),
"Carhart" = Carhart4FactorModel$new()
)
results <- purrr::imap_dfr(models, function(model, name) {
t <- EventStudyTask$new(firm_tbl, index_tbl, request_tbl,
factor_tbl = factor_tbl)
p <- ParameterSet$new(
return_calculation = LogReturn$new(),
return_model = model
)
t <- t |>
prepare_event_study(p) |>
fit_model(p) |>
calculate_statistics(p)
tidy.EventStudyTask(t, type = "car") |>
mutate(model = name)
})
# Plot CARs across models
ggplot(results, aes(x = relative_index, y = car, color = model)) +
geom_line(linewidth = 0.8) +
geom_hline(yintercept = 0, linetype = "dashed", color = "grey40") +
geom_vline(xintercept = 0, linetype = "dotted", color = "red", alpha = 0.6) +
labs(
title = "CAR Comparison Across Models",
x = "Relative Time to Event",
y = "Cumulative Abnormal Return",
color = "Model"
) +
theme_minimal()If the main result holds across all models, you have strong evidence that it is not an artifact of the return model specification.
| Model | Best for | Extra data needed |
|---|---|---|
| Market Model | Short horizons, standard studies | None |
| FF3 | Controlling for size and value | factor_tbl with SMB, HML |
| FF5 | Adding profitability and investment | factor_tbl with SMB, HML, RMW, CMA |
| Carhart | Adding momentum exposure | factor_tbl with SMB, HML, MOM |
| GARCH | Volatile markets, heteroskedastic returns | None (requires rugarch) |
| BHAR | Long-horizon studies (6+ months) | None |