Skip to content

factrix.metrics.caar

CAAR (Cumulative Average Abnormal Return) significance tests.

Tests \(H_0\): event abnormal return = 0, using two complementary methods: compute_caar — per-event-date weighted abnormal return series caar — CAAR t-test (parametric, non-overlapping sampling) bmp_test — BMP standardized AR test (robust to event-induced variance)

Notes

Pipeline. Per-event-date weighted abnormal return (per-event-date step) then non-overlapping cross-event sample; \(t\)-test on CAAR, or BMP standardized AR \(z\)-test for event-induced variance.

References

factrix.metrics.caar.compute_caar

compute_caar(df: DataFrame, *, factor_col: str = 'factor', return_col: str = 'forward_return') -> DataFrame

Per-event-date weighted abnormal return series.

Magnitude is preserved — no .sign() coercion. factrix accepts two input contracts; everything else (including signed \(\{-1, 0, +1\}\)) is just a special case of the second:

Input factor signed_car reduces to Statistic tested
\(\{0, 1\}\) \(\text{return}\) on event rows Average event-day return
\(\{0, R\}\), \(R \in \mathbb{R}\) \(\text{return} \times R\) Magnitude-weighted CAAR

Caveat on \(\{-1, 0, +1\}\): it lands in the second row as a weight-\(\pm 1\) case, which gives a magnitude-weighted CAAR, not the textbook MacKinlay (1997) signed CAAR (the latter averages direction-flipped abnormal returns and is a different estimator at finite samples when the negative-leg vol differs from the positive- leg vol). If literature-standard signed CAAR is what you want, pre-compute it externally; factrix's primitive treats \(\pm 1\) as weights, not as direction labels.

Aggregation

cs-first. For each event date, take the cross-sectional mean of signed_car \(= r \times f\) across event rows where \(f \neq 0\); the resulting n_event_dates-length CAAR series feeds a downstream Newey-West (NW) heteroskedasticity-and-autocorrelation-consistent (HAC) \(t\)-test on the mean.

Scale

CAAR magnitude tracks the units of factor (bps, z-score, unit-less ±1). Hypothesis tests via the downstream caar t-statistic are scale-invariant (numerator and denominator both scale linearly), but cross-factor effect-size comparisons require commensurate units.

Parameters:

Name Type Description Default
df DataFrame

Panel with date, asset_id, factor_col, return_col.

required
factor_col str

Numeric column. Magnitude is preserved as a weight in the per-row product; only zero rows are filtered.

'factor'
return_col str

Column with forward/abnormal return.

'forward_return'

Returns:

Type Description
DataFrame

DataFrame with columns date, caar sorted by date.

Notes

Per event date \(d\), \(\mathrm{CAAR}_d = \mathrm{mean}_{i \in \mathrm{events}(d)} (\mathrm{return}_i \times \mathrm{factor}_i)\). Magnitude of factor is preserved as a weight; only rows with factor == 0 are dropped.

factrix follows the MacKinlay (1997) event-window vocabulary (factor as the event indicator / sign on the announcement date) but generalises signed_car to numeric factor magnitude. With a continuous factor column, the resulting CAAR is the per-event regression-slope statistic in the Sefcik-Thompson (1986) lineage rather than the equal-weighted MacKinlay CAAR.

When factor_col triggers _is_sparse_magnitude_weighted the primitive emits a Python UserWarning directly. The sparse PANEL procedures additionally attach WarningCode.SPARSE_MAGNITUDE_WEIGHTED to FactorProfile.warnings independently — the dual emission is deliberate so batch runs that silence Python warnings still surface the regime-switch through the structured channel.

References
  • MacKinlay (1997). "Event Studies in Economics and Finance." Journal of Economic Literature, 35(1), 13–39. Standardised event-window / estimation-window vocabulary inherited by EventConfig.
  • Sefcik & Thompson (1986). "An Approach to Statistical Inference in Cross-Sectional Models with Security Abnormal Returns as Dependent Variable." Journal of Accounting Research, 24(2), 316–334. Per-event regression- slope ancestor of the magnitude-weighted CAAR produced when factor is continuous.
  • Brown & Warner (1985). "Using Daily Stock Returns: The Case of Event Studies." Journal of Financial Economics, 14(1), 3–31. Daily event-study methodology backing the parametric-test path.

Examples:

>>> import factrix as fx
>>> from factrix.preprocess import compute_forward_return
>>> from factrix.metrics.caar import compute_caar
>>> panel = compute_forward_return(
...     fx.datasets.make_event_panel(n_assets=50, n_dates=400, seed=0),
...     forward_periods=5,
... )
>>> caar_df = compute_caar(panel)
>>> set(caar_df.columns) >= {"date", "caar"}
True

factrix.metrics.caar.caar

caar(caar_df: DataFrame, *, forward_periods: int = 5) -> MetricOutput

CAAR significance: is mean CAAR significantly different from zero?

Parameters:

Name Type Description Default
caar_df DataFrame

Output of compute_caar() with columns date, caar.

required
forward_periods int

Sampling interval for non-overlapping dates. Maps to config.forward_periods — the return horizon used in compute_forward_return. Distinct from EventConfig.event_window_post which controls MFE/MAE.

5

Returns:

Type Description
MetricOutput

MetricOutput with value=mean CAAR, stat=t from non-overlapping sampling.

Notes

\(t = \mathrm{mean}(\mathrm{CAAR}) / (\mathrm{std}(\mathrm{CAAR}) / \sqrt{n})\) on a non-overlap subsample (stride forward_periods) of the per-event-date \(\mathrm{CAAR}\) series; \(H_0: \mathbb{E}[\mathrm{CAAR}] = 0\).

factrix uses non-overlap resampling rather than Newey-West (NW) heteroskedasticity-and-autocorrelation-consistent (HAC) for the default CAAR test — the same convention as ic — and exposes bmp_test as the variance-robust sibling for event-induced variance regimes.

References
  • Brown & Warner (1985). "Using Daily Stock Returns: The Case of Event Studies." Journal of Financial Economics, 14(1), 3–31. Daily event-study t-test specification at standard sample sizes.
  • MacKinlay (1997). "Event Studies in Economics and Finance." Journal of Economic Literature, 35(1), 13–39. Event-window vocabulary.

Examples:

Chain from :func:compute_caar output:

>>> import factrix as fx
>>> from factrix.preprocess import compute_forward_return
>>> from factrix.metrics.caar import compute_caar, caar
>>> panel = compute_forward_return(
...     fx.datasets.make_event_panel(n_assets=50, n_dates=400, seed=0),
...     forward_periods=5,
... )
>>> caar_df = compute_caar(panel)
>>> result = caar(caar_df, forward_periods=5)
>>> result.name
'caar'

factrix.metrics.caar.bmp_test

bmp_test(df: DataFrame, *, factor_col: str = 'factor', return_col: str = 'forward_return', estimation_window: int = 60, forward_periods: int = 5, kolari_pynnonen_adjust: bool = False, include_prediction_error_variance: bool = False) -> MetricOutput

Boehmer-Musumeci-Poulsen Standardized Abnormal Return test.

Standardizes each event's abnormal return by the asset's pre-event residual volatility, making the test robust to event-induced variance inflation that biases the ordinary CAAR \(t\)-test.

Uses price column for estimation-window volatility if available; falls back to per-asset historical forward_return std otherwise.

Steps
  1. For each event (\(\text{factor} \neq 0\)), look back estimation_window periods of the same asset's returns to estimate \(\sigma_i\).
  2. Scale \(\sigma_i\) to match the forward_return horizon.
  3. \(\mathrm{SAR}_i = \mathrm{AR}^{\mathrm{signed}}_i / \sigma^{\text{scaled}}_i\).
  4. \(z = \mathrm{mean}(\mathrm{SAR}) / (\mathrm{std}(\mathrm{SAR}) / \sqrt{N})\).

Parameters:

Name Type Description Default
df DataFrame

Full panel (including non-event rows) with date, asset_id, factor, forward_return. Must include enough history for estimation window.

required
estimation_window int

Number of periods before each event for volatility estimation (default 60).

60
forward_periods int

Return horizon for vol scaling (default 5). When using price-derived daily vol, scales by 1/sqrt(forward_periods) to match per-period forward_return.

5
kolari_pynnonen_adjust bool

When True, apply the Kolari-Pynnönen (2010) adjustment for cross-sectional correlation of SAR: \(z_{\mathrm{KP}} = z_{\mathrm{BMP}} \cdot \sqrt{(1 - \hat r) / (1 + (N_{\mathrm{eff}} - 1) \cdot \hat r)}\) where \(\hat r\) is the ICC-style within-date correlation of SAR and N_eff is the average events per event date. Vanilla BMP overstates significance when events cluster on the same date (earnings season, macro release), inflating z by factors of 1.5-2×. Enable this when the event-study clustering_hhi diagnostic is high (≥ 0.3) or when you otherwise expect same-date shock sharing.

False
include_prediction_error_variance bool

When True, inflate the per-event standardiser by \(\sqrt{1 + 1/T_{\mathrm{est}}}\) (with \(T_{\mathrm{est}}\) = estimation_window) to absorb the prediction-error variance of the mean-adjusted residual forecast — the strict Boehmer-Musumeci-Poulsen (1991) denominator. Default is False, preserving the prior factrix denominator (residual std only). Under mean-adjusted residuals + a single estimation_window the correction scales every SAR by the same constant, so mean_SAR and std_SAR shrink by \(1/\sqrt{1 + 1/T_{\mathrm{est}}}\) but the \(z\) statistic is invariant: the flag documents the strict standardiser, it does not move inference in this regime. Per-event \(T_i\) variation (which would move \(z\)) requires a market-model extension and is out of scope here.

Caveat: rolling_std(min_samples=20) accepts events with as few as 20 prior returns, so the effective \(T_i\) for early-history events can be smaller than estimation_window. The constant correction is therefore an approximation in that regime; ensure every event has at least estimation_window prior returns when the strict denominator matters.

False

Returns:

Type Description
MetricOutput

MetricOutput(name="bmp_test", value=mean_SAR, stat=z_bmp, ...).

Notes

For each event \(i\): estimate pre-event vol \(\sigma_i\) over the estimation_window, scaled to the forward horizon by \(1/\sqrt{h}\) (with \(h\) = forward_periods) when daily prices are available; \(\mathrm{SAR}_i = \mathrm{AR}^{\mathrm{signed}}_i / \sigma_i\); aggregate to \(z = \mathrm{mean}(\mathrm{SAR}) / (\mathrm{std}(\mathrm{SAR}) / \sqrt{N})\). With kolari_pynnonen_adjust=True, scale \(z\) by \(\sqrt{(1 - \hat r) / (1 + (N_{\mathrm{eff}} - 1)\, \hat r)}\).

factrix simplifies the original BMP by omitting the prediction- error term from the standardiser (using mean-adjusted residuals rather than market-model residuals) — adequate for the default Brown-Warner / MacKinlay event-study path; pair with the K-P adjustment when clustering_hhi flags same-date shock sharing. Pass include_prediction_error_variance=True for the strict BMP denominator \(\sigma_i \cdot \sqrt{1 + 1/T_{\mathrm{est}}}\).

References
  • Boehmer, Musumeci & Poulsen (1991). "Event-study Methodology Under Conditions of Event-induced Variance." Journal of Financial Economics, 30(2), 253–272. The BMP standardised AR test factrix simplifies (mean-adjusted residuals, no prediction-error correction by default).
  • Kolari & Pynnönen (2010). "Event Study Testing with Cross-sectional Correlation of Abnormal Returns." Review of Financial Studies, 23(11), 3996–4025. Clustering- adjusted BMP variant; enabled via kolari_pynnonen_adjust=True on this function.

Examples:

>>> import factrix as fx
>>> from factrix.preprocess import compute_forward_return
>>> from factrix.metrics.caar import bmp_test
>>> panel = compute_forward_return(
...     fx.datasets.make_event_panel(n_assets=50, n_dates=400, seed=0),
...     forward_periods=5,
... )
>>> result = bmp_test(panel, forward_periods=5)
>>> result.name
'bmp_test'

Event-study contracts

signed_car, the estimation_window consumed by bmp_test, and factrix's confounded-event handling are documented in Metric applicability § Event-study contracts. factrix computes CAR (sum of per-period abnormal returns), not BHAR; see the same section for the distinction.

Use cases

  • Per-event-date CAAR series


    Build the per-event-date weighted abnormal return series from a long-format panel before any inferential test. Pre-step for caar and (where the magnitude-weighted form is wanted) for downstream slicing.

  • Mean-CAAR significance, non-overlapping


    Test \(H_0: \mathbb{E}[\mathrm{CAAR}] = 0\) on the every-forward_periods subsample of the per-event-date CAAR series to avoid the autocorrelation induced by overlapping forward returns. Default parametric test for the event-sparse cell.

  • Event-induced variance, BMP \(z\)-test


    Standardise each event's abnormal return by the asset's pre-event residual volatility before pooling. Robust to event-induced variance inflation that biases the ordinary CAAR \(t\)-test; pair with kolari_pynnonen_adjust=True when the event-date Herfindahl-Hirschman index (HHI) flags same-date shock sharing.

  • Magnitude-weighted CAAR


    With a continuous factor column, compute_caar returns the per-event regression-slope statistic in the Sefcik-Thompson (1986) lineage rather than the textbook equal-weighted MacKinlay CAAR — see the docstring for the input-contract table.

Choosing a function

Goal Function
Per-event-date CAAR table for downstream inspection / slicing compute_caar
Mean-CAAR significance, deterministic non-overlap subsample caar
Variance-robust event-induced significance (BMP standardised \(z\)) bmp_test

Worked example — per-event-date CAAR then mean significance

compute_caar → caar on a synthetic event panel

import factrix as fx
from factrix.metrics.caar import compute_caar, caar, bmp_test
from factrix.preprocess import compute_forward_return

raw   = fx.datasets.make_event_panel(
    n_assets=200, n_dates=500, event_rate=0.02,
    post_event_drift=0.004, seed=2024,
)
panel = compute_forward_return(raw, forward_periods=5)

caar_df = compute_caar(panel)
print(caar_df.head())
# ┌────────────┬───────────┐
# │ date       ┆ caar      │
# ├────────────┼───────────┤
# │ 2024-01-04 ┆  0.0041   │
# │ 2024-01-11 ┆  0.0037   │
# │ ...        ┆ ...       │
# └────────────┴───────────┘

out = caar(caar_df, forward_periods=5)
print(out.value, out.stat, out.metadata["p_value"])
# 0.0039  6.42  1.4e-09   (approximate)

# Variance-robust alternative when same-date clustering is high:
z_bmp = bmp_test(panel, estimation_window=60, forward_periods=5,
                 kolari_pynnonen_adjust=True)

See also