Example: Earnings Surprise Analysis (AAPL, MSFT, GOOGL)
Source:vignettes/articles/example-earnings.Rmd
example-earnings.RmdAbstract
This worked example takes you from the bundled
earnings_surprises dataset – three US mega-caps (AAPL,
MSFT, GOOGL) around their Q1-2023 analyst-beat earnings dates – through
a complete market-model event study: data loading, pipeline execution,
cross-sectional and standardized-residual test statistics (Patell Z and
BMP), and a rendered cumulative-abnormal-return (CAR) plot with written
interpretation.
For the theory behind the market model, see Return Models; for the Patell Z and BMP tests, see Test Statistics.
Research question
Do AAPL, MSFT, and GOOGL earn significantly positive abnormal returns around their quarterly earnings beats? Formally, we test the null
H_0:\ \mathbb{E}[AAR_t] = 0 \quad \text{for all } t \text{ in the event window},
against the alternative that the cumulative average abnormal return (CAAR) differs from zero.
Data
The earnings_surprises dataset ships with the package
(in data/) and requires no network access.
It is a named list of three tibbles – firm (stacked daily
adjusted prices for the three firms), index (the S&P
500 benchmark), and request (the event specification: one
row per firm with event date and estimation/event windows) – plus a
meta provenance block.
library(EventStudy)
data("earnings_surprises", package = "EventStudy")
# The dataset is a list of tibbles; inspect the pieces we consume.
dplyr::glimpse(earnings_surprises$firm)
#> Rows: 813
#> Columns: 3
#> $ symbol <chr> "AAPL", "AAPL", "AAPL", "AAPL", "AAPL", "AAPL", "AAPL", "AAPL…
#> $ date <chr> "01.06.2022", "02.06.2022", "03.06.2022", "06.06.2022", "07.0…
#> $ adjusted <dbl> 145.7, 148.1, 142.4, 143.1, 145.7, 144.9, 139.7, 134.3, 129.2…
earnings_surprises$request[, c(
"event_id", "firm_symbol", "index_symbol", "event_date",
"group", "event_window_start", "event_window_end",
"estimation_window_length"
)]
#> # A tibble: 3 × 8
#> event_id firm_symbol index_symbol event_date group event_window_start
#> <int> <chr> <chr> <chr> <chr> <int>
#> 1 1 AAPL ^GSPC 04.05.2023 Earnings Beat -5
#> 2 2 MSFT ^GSPC 25.04.2023 Earnings Beat -5
#> 3 3 GOOGL ^GSPC 25.04.2023 Earnings Beat -5
#> # ℹ 2 more variables: event_window_end <int>, estimation_window_length <int>All three events belong to a single "Earnings Beat"
group, which lets the multi-event statistics aggregate them into one
AAR/CAAR series.
Setup and pipeline
EventStudyTask$new() takes the three tibbles
positionally – firm prices, reference (index) prices, and the request
table. We fit a MarketModel and attach the multi-event
statistics we care about (CSectTTest,
PatellZTest, BMPTest) via a
MultiEventStatisticsSet.
task <- EventStudyTask$new(
earnings_surprises$firm, # firm daily adjusted prices
earnings_surprises$index, # benchmark index prices
earnings_surprises$request # event / window specification
)
params <- ParameterSet$new(
return_model = MarketModel$new(),
multi_event_statistics = MultiEventStatisticsSet$new(
tests = list(CSectTTest$new(), PatellZTest$new(), BMPTest$new())
)
)
result <- run_event_study(task, params)The pipeline builds the task, fits the market model on each firm’s estimation window, computes abnormal returns over the event window, and evaluates the statistics. See run_event_study(), EventStudyTask, ParameterSet, MarketModel, PatellZTest, and BMPTest.
Results table
The tidy() method returns per-firm cumulative abnormal
returns as a tidy tibble. We call the S3 method explicitly
(tidy.EventStudyTask()), since the broom
generic is not re-exported by the package.
car_tbl <- EventStudy::tidy.EventStudyTask(result, type = "car")
# Show the last (widest) CAR window per firm.
car_tbl |>
dplyr::group_by(firm_symbol) |>
dplyr::slice_tail(n = 1) |>
dplyr::ungroup() |>
dplyr::select(firm_symbol, term, estimate, std.error, statistic, p.value) |>
knitr::kable(
digits = 4,
caption = "Cumulative Abnormal Returns by firm (widest event window)"
)| firm_symbol | term | estimate | std.error | statistic | p.value |
|---|---|---|---|---|---|
| AAPL | [-5,5] | 0.0323 | 0.0353 | 0.9150 | 0.3613 |
| GOOGL | [-5,5] | 0.0130 | 0.0517 | 0.2518 | 0.8014 |
| MSFT | [-5,5] | 0.0678 | 0.0389 | 1.7436 | 0.0828 |
Plot
plot_event_study() returns a ggplot;
wrapping it with plotly::ggplotly() produces an interactive
plot for the article.
gg <- plot_event_study(
result,
type = "car",
event_id = 1L,
title = "AAPL: cumulative abnormal return with 95% confidence band"
)
plotly::ggplotly(gg)Statistical interpretation
The Patell Z and BMP statistics both aggregate the three firms’ standardized abnormal returns, but BMP additionally corrects for event-induced cross-sectional variance.
pz <- result$aar_caar_tbl$PatellZ[[1]]
bmp <- result$aar_caar_tbl$BMP[[1]]
caar_patell <- round(utils::tail(pz$caar, 1L), 4)
z_patell <- round(utils::tail(pz$caar_z, 1L), 3)
t_bmp <- round(utils::tail(bmp$cbmp_t, 1L), 3)
df_bmp <- utils::tail(bmp$n_valid_events, 1L) - 1L
# Two-sided p-values: Patell Z ~ N(0,1); BMP t ~ t(df_bmp)
p_patell <- round(2 * pnorm(abs(z_patell), lower.tail = FALSE), 4)
p_bmp <- round(2 * pt(abs(t_bmp), df = df_bmp, lower.tail = FALSE), 4)At the end of the event window the CAAR across the three firms is 0.0377. The Patell Z on the cumulative series is 1.664 (two-sided p = 0.0961) and the BMP statistic is 2.248 (two-sided p = 0.1536). Where the two disagree in magnitude, the BMP figure is the more conservative – it inflates the standard error when abnormal returns are correlated across firms on the event day, which is exactly the situation a common earnings-season calendar can create (Boehmer et al. 1991). Read together, they tell you whether an apparently significant AAR survives a cross-sectional correlation correction.
Diagnostics note
The market-model OLS residuals underpinning these statistics can be tested for autocorrelation and non-normality with es_diagnostics(); a poor estimation-window fit weakens every downstream statistic. See Diagnostics & Robustness for the full battery.
Further reading
- Return Models – market-model theory and alternatives.
- Test Statistics – Patell Z and BMP derivations.
- run_event_study() – pipeline reference page.
Session info
#> R version 4.6.1 (2026-06-24)
#> Platform: x86_64-pc-linux-gnu
#> Running under: Ubuntu 24.04.4 LTS
#>
#> Matrix products: default
#> BLAS: /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3
#> LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.26.so; LAPACK version 3.12.0
#>
#> locale:
#> [1] LC_CTYPE=C.UTF-8 LC_NUMERIC=C LC_TIME=C.UTF-8
#> [4] LC_COLLATE=C.UTF-8 LC_MONETARY=C.UTF-8 LC_MESSAGES=C.UTF-8
#> [7] LC_PAPER=C.UTF-8 LC_NAME=C LC_ADDRESS=C
#> [10] LC_TELEPHONE=C LC_MEASUREMENT=C.UTF-8 LC_IDENTIFICATION=C
#>
#> time zone: UTC
#> tzcode source: system (glibc)
#>
#> attached base packages:
#> [1] stats graphics grDevices utils datasets methods base
#>
#> other attached packages:
#> [1] EventStudy_0.62.0
#>
#> loaded via a namespace (and not attached):
#> [1] plotly_4.12.1 sass_0.4.10 utf8_1.2.6
#> [4] generics_0.1.4 tidyr_1.3.2 stringi_1.8.9
#> [7] digest_0.6.39 magrittr_2.0.5 evaluate_1.0.5
#> [10] grid_4.6.1 RColorBrewer_1.1-3 fastmap_1.2.0
#> [13] jsonlite_2.0.0 httr_1.4.9 purrr_1.2.2
#> [16] crosstalk_1.2.2 viridisLite_0.4.3 scales_1.4.0
#> [19] textshaping_1.0.5 jquerylib_0.1.4 cli_3.6.6
#> [22] rlang_1.3.0 withr_3.0.3 cachem_1.1.0
#> [25] yaml_2.3.12 otel_0.2.0 tools_4.6.1
#> [28] dplyr_1.2.1 ggplot2_4.0.3 vctrs_0.7.3
#> [31] R6_2.6.1 lifecycle_1.0.5 stringr_1.6.0
#> [34] fs_2.1.0 htmlwidgets_1.6.4 ragg_1.5.2
#> [37] pkgconfig_2.0.3 desc_1.4.3 pkgdown_2.2.1
#> [40] pillar_1.11.1 bslib_0.12.0 gtable_0.3.6
#> [43] glue_1.8.1 data.table_1.18.6.1 systemfonts_1.3.2
#> [46] xfun_0.60 tibble_3.3.1 tidyselect_1.2.1
#> [49] knitr_1.51 farver_2.1.2 htmltools_0.5.9
#> [52] rmarkdown_2.32 labeling_0.4.3 compiler_4.6.1
#> [55] S7_0.2.2 distributional_0.8.1