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_z — BMP standardized AR test (robust to event-induced variance)
Notes
caar and bmp_z are complementary inferential tests on the
per-event-date abnormal-return series. caar is the parametric
cross-event \(t\)-test; bmp_z is the standardized-AR \(z\)-test that
is robust to event-induced variance.
References
- MacKinlay (1997), "Event Studies in Economics and Finance."
- Boehmer, Musumeci & Poulsen (1991), "Event-study Methodology Under Conditions of Event-induced Variance."
factrix.metrics.caar.compute_caar ¶
compute_caar(data: DataFrame, *, factor_col: str = 'factor', return_col: str = 'forward_return') -> DataFrame
Per-event-date weighted abnormal return series.
Magnitude is preserved — no .sign() coercion.
Output columns
date: event date (one row per date carrying at least one event).
caar: cross-asset mean of the (signed/magnitude-weighted)
abnormal return on that date.
n_events: number of events (non-zero factor rows) collapsed into
this date's caar. The downstream caar() test is an
equal-weight calendar-time portfolio across event dates, so
this count is the per-date portfolio breadth — surfaced for
transparency (a date built on 1 event vs 500 is otherwise
indistinguishable), not used to weight or drop dates.
date_ordinal: 0-based position of the date on the full input
calendar (dense rank over every date in data, including
non-event dates). Consumers that sub-sample for non-overlap
independence measure the gap between kept event dates in
these calendar steps rather than in event-index steps —
the rank is computed before the factor != 0 filter, so a
gap of k means k underlying periods elapsed, not
k events. On an event-only series the two diverge under
sparse or clustered events, so the ordinal is what makes the
forward-return overlap window measurable downstream.
factrix.metrics.caar.caar ¶
caar(caar_df: DataFrame, *, forward_periods: int = 5) -> MetricResult
CAAR significance: is mean CAAR significantly different from zero?
The event floor is dynamic — the minimum event-date count scales with the forward_periods parameter (non-overlapping stride) — so it is declared as a resolver (a callable sample_threshold) rather than a constant. Pre-flight counts non-zero factor rows as a loose upper bound; this in-body short-circuit on event dates stays authoritative.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
caar_df
|
DataFrame
|
Output of |
required |
forward_periods
|
int
|
Sampling interval for non-overlapping dates.
Maps to |
5
|
Returns:
| Type | Description |
|---|---|
MetricResult
|
MetricResult 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 of the per-event-date \(\mathrm{CAAR}\) series; \(H_0: \mathbb{E}[\mathrm{CAAR}] = 0\).
The subsample is drawn calendar-aware: the CAAR series is
event-date-indexed (compute_caar keeps only factor != 0
rows), so its dates are calendar-irregular. Sampling every
forward_periods-th row (index distance) would mis-handle both
regimes — sparse events get further thinned (power loss), clustered
events inside one forward-return window are admitted as independent
(iid violated, \(t\) inflated). Instead a greedy pass over
date_ordinal (each event's position on the full calendar) keeps
an event only when its calendar gap to the previously kept event is
>= forward_periods, so consecutive kept observations no longer
share overlapping forward-return windows. The alternative —
reindexing to a dense calendar with zero-fill before fixed-stride
sampling — was rejected: the zero padding would dominate the
subsample and distort the iid mean estimator this path is built
around; the greedy calendar walk keeps the event-only mean intact.
caar is an equal-weight calendar-time portfolio test: the
inference unit is the event date. Same-date events are collapsed
to one cross-asset mean (which absorbs same-date cross-sectional
correlation by construction), and the t-test runs across those
dates — so n counts event dates (the number of periods with
an event), not events. It uses non-overlap resampling rather than
Newey-West (NW) heteroskedasticity-and-autocorrelation-consistent
(HAC), the same convention as ic.
The across-events siblings are complementary, not redundant:
bmp_z is the across-events standardized-AR z-test with an
optional Kolari-Pynnönen clustering correction — use it when events
are heavily clustered or across-events power is wanted; and
corrado_rank is the non-parametric rank test robust to
heavy-tailed event returns. The per-date portfolio breadth behind
this test is surfaced as n_events (the compute_caar series)
and total_events (this result's metadata).
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 == ""
True
factrix.metrics.caar.bmp_z ¶
bmp_z(data: 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) -> MetricResult
Boehmer-Musumeci-Poulsen Standardized Abnormal Return test.
The static event floor (sample_threshold=SampleThreshold(min_events=MIN_EVENTS_HARD)) gates the standardized-AR z-test on the count of events with a usable estimation-window volatility.
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.
The fallback std is lagged by forward_periods so the estimation
window ends before each event's own forward return (which spans
(t, t+h]) rather than leaking the event AR into its own
standardiser; it remains a coarser, horizon-overlapping vol proxy
than a daily-price std and raises WarningCode.BMP_RETURN_VOL_FALLBACK
(metadata["vol_source"] records which path ran).
Steps
- For each event (\(\text{factor} \neq 0\)), look back
estimation_windowperiods of the same asset's returns to estimate \(\sigma_i\). - Scale \(\sigma_i\) to match the forward_return horizon.
- \(\mathrm{SAR}_i = \mathrm{AR}^{\mathrm{signed}}_i / \sigma^{\text{scaled}}_i\).
- \(z = \mathrm{mean}(\mathrm{SAR}) / (\mathrm{std}(\mathrm{SAR}) / \sqrt{N})\).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
DataFrame
|
Full panel (including non-event rows) with |
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
|
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
|
False
|
include_prediction_error_variance
|
bool
|
When True, inflate the
per-event standardiser by \(\sqrt{1 + 1/T_{\mathrm{est}}}\)
(with \(T_{\mathrm{est}}\) = Caveat: |
False
|
Returns:
| Type | Description |
|---|---|
MetricResult
|
MetricResult(value=mean_SAR, p_value=p_bmp, 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=Trueon this function.
Examples:
>>> import factrix as fx
>>> from factrix.preprocess import compute_forward_return
>>> from factrix.metrics.caar import bmp_z
>>> panel = compute_forward_return(
... fx.datasets.make_event_panel(n_assets=50, n_dates=400, seed=0),
... forward_periods=5,
... )
>>> result = bmp_z(panel, forward_periods=5)
>>> result.name == ""
True
Event-study contracts
signed_car, the estimation_window consumed by bmp_z, 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
The per-event-date weighted abnormal return series from a long-format panel. Consumed by
caarfor the significance test, and (where the magnitude-weighted form is wanted) available for per-slice summaries. -
Mean-CAAR significance, non-overlapping
Test \(H_0: \mathbb{E}[\mathrm{CAAR}] = 0\) on the every-
forward_periodssubsample 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=Truewhen the event-date Herfindahl-Hirschman index (HHI) flags same-date shock sharing. -
Magnitude-weighted CAAR
With a continuous
factorcolumn,compute_caarreturns 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_z |
Event counts¶
compute_caar collapses same-date event rows before the caar test runs.
The event-study path therefore exposes these related counts:
| Field | Where to read it | Meaning |
|---|---|---|
n_events |
compute_caar(...).select("date", "n_events") |
Raw event rows collapsed into each event date |
total_events |
caar(...).metadata["total_events"] |
Sum of raw non-zero event rows behind the study |
n_event_periods |
caar(...).metadata["n_event_periods"] |
Distinct event dates in the CAAR series |
n_event_periods_sampled |
caar(...).metadata["n_event_periods_sampled"] |
Event dates kept by the calendar-aware non-overlap sampler used for the t-test |
MetricResult.n_obs equals n_event_periods_sampled, because that is the
sample entering the headline p_value. A large gap between total_events and
n_event_periods means events cluster on the same dates. A large gap between
n_event_periods and n_event_periods_sampled means the forward-return
windows overlap heavily, so the non-overlap sampler thins the effective test
sample.
For asset-allocation policy events, make sure the sparse factor sign encodes
the expected return direction, not just the raw event type. If +1 means
"central-bank hike" but hikes are bearish for one asset group and bullish for
another, map the raw event into an asset-specific expected-return signal before
calling compute_caar, event_hit_rate, or profit_factor.
Worked example — per-event-date CAAR then mean significance¶
compute_caar → caar on a synthetic event panel
import factrix as fx
import polars as pl
from factrix.metrics.caar import compute_caar, caar, bmp_z
from factrix.preprocess import compute_forward_return
pl.Config.set_tbl_formatting("ASCII_MARKDOWN")
raw = fx.datasets.make_event_panel(
n_assets=200, n_dates=500, event_rate=0.02,
post_event_drift_bps=40.0, 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.p_value)
# 0.0039 6.42 1.4e-09 (approximate)
# Variance-robust alternative when same-date clustering is high:
z_bmp = bmp_z(panel, estimation_window=60, forward_periods=5,
kolari_pynnonen_adjust=True)
See also¶
-
clustering_hhi
Event-date HHI — when to switch on the Kolari-Pynnönen adjustment or read
caar's \(t\) with caution. -
by_slice
Axis-agnostic slice dispatcher for per-slice CAAR summaries.
-
slice_pairwise_test/slice_joint_test
Cross-slice CAAR inference (Wald \(\chi^2\) + Holm / Romano-Wolf adjusted \(p\)).
-
Statistical methods
CAAR cross-event \(t\), BMP standardised AR \(z\), Kolari-Pynnönen clustering adjustment.
-
Metric applicability reference
When this metric applies, sample-size guards, and the event-study contracts that fix
signed_car. -
Individual × Sparse landing
Adjacent event-study metrics in the same cell.