Example: M&A Deal Announcements -- Power Analysis on Synthetic Data
Source:vignettes/articles/example-ma.Rmd
example-ma.RmdSYNTHETIC DATA NOTICE. Every observation in this example is generated by
simulate_event_study()with a fixed seed. No real M&A transactions or stock prices are used. The example exists to teach event-study methodology and to demonstrate statistical power – not to describe any actual deal.
Abstract
Merger and acquisition announcements are a classic event-study application: target shareholders typically earn large positive abnormal returns on the announcement day. But how large must the true effect be, and how many deals must you observe, before a test can reliably detect it? This example uses simulate_event_study() to answer that question directly, sweeping the true abnormal-return magnitude and reading off the statistical power of the cross-sectional test.
For the underlying model and test, see Return Models and Test Statistics.
Research question
For a synthetic M&A deal sample, what minimum true abnormal return is needed for the cross-sectional test to achieve roughly 80% power at the 5% significance level, holding the number of events fixed?
Simulated data
simulate_event_study() draws firm returns from a market
model with known parameters, embeds a known event effect of a
chosen magnitude, and runs the event study n_simulations
times, returning an es_simulation object. Its components
are the empirical power, a per-day
rejection_by_day tibble, the vector of realised
test_stats, and the params used.
library(EventStudy)
sim_result <- simulate_event_study(
n_events = 15,
event_window = c(-5, 5),
abnormal_return = 0.01, # true event-day effect embedded in the DGP
n_simulations = 200,
seed = 42
)
str(sim_result, max.level = 1)
#> List of 4
#> $ power : num 0.96
#> $ rejection_by_day: tibble [11 × 2] (S3: tbl_df/tbl/data.frame)
#> $ test_stats : num [1:200] 5.13 3.55 5.35 3.33 5.29 ...
#> $ params :List of 8
#> - attr(*, "class")= chr "es_simulation"
cat("Empirical power at abnormal_return = 0.01:", sim_result$power, "\n")
#> Empirical power at abnormal_return = 0.01: 0.96Power sweep
The primary result is a sweep over the true abnormal-return
magnitude. For each value we simulate afresh (fixed seed for
reproducibility) and record the empirical power – the
fraction of the n_simulations runs in which the test
correctly rejected the null on the event day.
# Extraction approach: simulate_event_study() returns an `es_simulation`
# object whose `$power` field is exactly the detection rate we want. We map
# over a grid of true effects and read $power from each run.
ar_grid <- c(0.001, 0.002, 0.004, 0.006, 0.010)
power_tbl <- purrr::map_dfr(ar_grid, function(ar) {
sim <- simulate_event_study(
n_events = 15,
event_window = c(-5, 5),
abnormal_return = ar,
n_simulations = 200,
seed = 42
)
tibble::tibble(true_abnormal_return = ar, power = sim$power)
})
knitr::kable(
power_tbl, digits = 4,
caption = "Detection power by true abnormal return (N = 15, cross-sectional test, alpha = 0.05)"
)| true_abnormal_return | power |
|---|---|
| 0.001 | 0.035 |
| 0.002 | 0.100 |
| 0.004 | 0.270 |
| 0.006 | 0.570 |
| 0.010 | 0.960 |
Results plot
The es_simulation object has no built-in event-study
plot (it is a simulation summary, not a fitted task), so we render the
power curve directly with plotly. The dashed line marks the
conventional 80% power target.
plotly::plot_ly(
power_tbl,
x = ~true_abnormal_return, y = ~power,
type = "scatter", mode = "lines+markers",
line = list(color = "#20c997", width = 3),
marker = list(color = "#20c997", size = 8)
) |>
plotly::add_segments(
x = min(ar_grid), xend = max(ar_grid), y = 0.8, yend = 0.8,
line = list(color = "grey", dash = "dash"), showlegend = FALSE,
inherit = FALSE
) |>
plotly::layout(
title = "Power curve: detection rate vs true abnormal return (synthetic)",
xaxis = list(title = "True abnormal return"),
yaxis = list(title = "Power", range = c(0, 1))
)Power analysis interpretation
The power curve rises monotonically with the true effect size. At a true abnormal return of 0.1% the test is essentially blind (power near the nominal 5% false-positive floor); by 1% the test detects the effect in the large majority of samples. The practical reading: if a researcher expects M&A deal announcements to move target prices by the customary several percent, a sample of even fifteen deals gives ample power at conventional significance levels. Weak or diffuse effects, by contrast, demand far larger samples – the familiar power-vs-effect-size tradeoff (MacKinlay 1997).
Note: power estimates above are Monte Carlo averages from 200 simulation runs. At p ≈ 0.50 the sampling error is roughly ±3–5 percentage points (95% interval), so small differences between adjacent grid points are within simulation noise and should not be over-interpreted.
Design implications
Power is jointly determined by the true effect size, the number of events, the event-window width, and the estimation-window length (which sharpens the market-model fit and shrinks the abnormal-return standard error). Widening the event window dilutes a concentrated announcement effect; lengthening the estimation window tightens the benchmark. For a full treatment see the companion Monte Carlo Power Analysis vignette and simulate_event_study().
Diagnostics note
On real (non-simulated) M&A samples, always check model-fit quality with es_diagnostics(): thin trading around small targets and confounding news in the window are common threats that this synthetic ideal-conditions example deliberately excludes.
Further reading
- Monte Carlo Power Analysis – full power-simulation vignette.
- Test Statistics – the cross-sectional test.
- simulate_event_study() – reference page.
- Bootstrap Inference & Multiple Testing – robust alternatives.
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] gtable_0.3.6 jsonlite_2.0.0 dplyr_1.2.1
#> [4] compiler_4.6.1 tidyselect_1.2.1 stringr_1.6.0
#> [7] tidyr_1.3.2 jquerylib_0.1.4 systemfonts_1.3.2
#> [10] scales_1.4.0 textshaping_1.0.5 yaml_2.3.12
#> [13] fastmap_1.2.0 ggplot2_4.0.3 R6_2.6.1
#> [16] generics_0.1.4 distributional_0.8.1 knitr_1.51
#> [19] htmlwidgets_1.6.4 tibble_3.3.1 desc_1.4.3
#> [22] RColorBrewer_1.1-3 bslib_0.12.0 pillar_1.11.1
#> [25] rlang_1.3.0 stringi_1.8.9 cachem_1.1.0
#> [28] xfun_0.60 S7_0.2.2 fs_2.1.0
#> [31] sass_0.4.10 otel_0.2.0 viridisLite_0.4.3
#> [34] plotly_4.12.1 cli_3.6.6 withr_3.0.3
#> [37] pkgdown_2.2.1 magrittr_2.0.5 crosstalk_1.2.2
#> [40] digest_0.6.39 grid_4.6.1 lifecycle_1.0.5
#> [43] vctrs_0.7.3 data.table_1.18.6.1 evaluate_1.0.5
#> [46] glue_1.8.1 farver_2.1.2 ragg_1.5.2
#> [49] purrr_1.2.2 httr_1.4.9 rmarkdown_2.32
#> [52] tools_4.6.1 pkgconfig_2.0.3 htmltools_0.5.9