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-period 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-period 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.
They weight events differently, so their values are not comparable.
compute_caar preserves factor magnitude — AR x factor, the
Sefcik-Thompson (1986) magnitude-weighted CAAR — while bmp_z,
corrado_rank and every event_quality metric use AR x sign(factor).
On a {1, 10} intensity factor the two differ by a factor of ~4 and can
differ in sign, because caar is dominated by the high-|factor| events
and the siblings are not. Comparing caar.value to bmp_z.value or to
event_hit_rate compares different estimands; comparing their p-values
is fine, since all of them test the same \(H_0\).
Pass a .sign()-coerced factor column to compute_caar to put it on the
siblings' footing. factrix does not coerce by default: magnitude weighting
is the whole point of a graded event signal, and a metric that silently
discarded it would be the more surprising default. compute_caar warns
(SPARSE_MAGNITUDE_WEIGHTED) when the column is mixed-sign and not a clean
ternary; a strictly positive intensity column is unambiguous and does not
warn, but is still magnitude-weighted.
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', estimation_window: int = 60, overlap_periods: int = DEFAULT_FORWARD_PERIODS, expected_warnings: tuple[str, ...] = ()) -> DataFrame
Per-event-period weighted abnormal return series.
Magnitude is preserved — no .sign() coercion.
The abnormal return is a real abnormal return: an abnormal_return
column on the input is used as-is (market-adjusted), and otherwise the
asset's estimation-window mean, lagged by overlap_periods, is
subtracted from return_col — see
:func:~factrix.metrics._helpers._attach_abnormal_return. Events without
enough history for that estimate carry a null abnormal return and are
dropped with the other non-finite rows.
Output columns
date: event period (one row per period 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 periods, so
this count is the per-period 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
grid (dense rank over every date in data, including
non-event periods). Consumers that sub-sample for non-overlap
independence measure the gap between kept event periods in
these grid 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.
n_events_dropped_non_finite: total number of event rows removed
before aggregation because the selected return source or
factor_col was non-finite. Broadcast as a constant on every
row (the count is a whole-frame diagnostic, not a per-period one:
a date
whose events were all non-finite leaves the output entirely,
so a per-period column could not carry it). Consumers surface it
as metadata["n_events_dropped_non_finite"].
sparse_magnitude_weighted: whether the input factor is mixed-sign and
not a clean {-1, 0, +1} ternary. Broadcast as a constant so
downstream event tests can attach the structured warning code.
event_weighting: "sign" when every non-zero factor has unit
magnitude, otherwise "factor_magnitude". Unlike the warning
above, this also identifies an all-positive graded intensity.
return_scale: "per_period_simple" when the abnormal return is
derived from return_col; "supplied_abnormal_return" when
the input's abnormal_return column is used as-is.
Non-finite handling
polars' mean propagates float NaN (it only skips nulls), so a
single NaN return on one event used to poison that whole
date's caar — and a NaN caar then reaches _calc_t_stat
downstream, which returns NaN and makes the metric withhold its test
as degenerate_variance, mislabelling missing data as
degeneracy. Event rows are therefore
filtered to a finite selected return source and finite
factor_col before the group_by (this is the producer boundary
that the project convention makes responsible for dropping non-finite values),
the surviving per-period mean is taken over finite events only, and the
dropped count is both reported on the frame and warned about. Note
factor_col needs its own guard: NaN != 0 evaluates to True
in polars, so a NaN factor survives the event filter and would make
_signed_car NaN.
factrix.metrics.caar.caar ¶
caar(caar_df: DataFrame, *, overlap_periods: int = DEFAULT_FORWARD_PERIODS, expected_warnings: tuple[str, ...] = ()) -> MetricResult
CAAR significance: is mean CAAR significantly different from zero?
The event floor is dynamic — the minimum event-period count scales with the overlap_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 periods stays authoritative.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
caar_df
|
DataFrame
|
Output of |
required |
overlap_periods
|
int
|
Sampling interval for non-overlapping dates.
Maps to |
DEFAULT_FORWARD_PERIODS
|
Returns:
| Type | Description |
|---|---|
MetricResult
|
MetricResult with value=mean CAAR on the non-overlap subsample, |
MetricResult
|
stat=t from that same subsample. |
Notes
\(t = \mathrm{mean}(\mathrm{CAAR}) / (\mathrm{std}(\mathrm{CAAR}) / \sqrt{n})\) on a non-overlap subsample of the per-event-period \(\mathrm{CAAR}\) series; \(H_0: \mathbb{E}[\mathrm{CAAR}] = 0\).
Windows are counted in panel periods. The estimation window
behind the abnormal return is measured on the panel's distinct-date
grid, not on the asset's own rows, so it spans estimation_window
grid periods for every name; periods an asset is missing count as
missing observations inside it and can push the window below
min_samples, dropping the event. A ragged panel raises
ragged_period_grid.
One sample behind every field. value, stat, p_value
and n_obs are all computed on the event-spaced subsample. Earlier
versions reported value as the mean of the full event-period
series while stat/p_value/n_obs came from the subsample,
so the published effect size did not belong to the published test —
a headline CAAR could be positive while the t-stat that "supports"
it was estimated on a different (often much smaller) sample. The
full-series mean is still available as
metadata["mean_caar_full"] with its size
metadata["n_event_periods_full"]; use it as a descriptive
summary only, never as the effect size for the reported \(p\).
Non-finite caar rows are dropped before the spacing pass.
Order matters: the greedy walk keeps the first event of every
admissible grid gap, so a null-caar date filtered afterwards
would still have consumed its slot and blocked the next usable
event — silently shrinking the subsample and shifting which dates
it contains. Dropping first lets a usable neighbour take the slot.
The subsample is drawn grid-aware: the CAAR series is
event-period-indexed (compute_caar keeps only factor != 0
rows), so its dates are irregular on the period grid. Sampling every
overlap_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 panel grid) keeps
an event only when its grid gap to the previously kept event is
>= overlap_periods, so consecutive kept observations no longer
share overlapping forward-return windows. The alternative —
reindexing to a dense grid 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 grid walk keeps the event-only mean intact.
caar is an equal-weight calendar-time portfolio test: the
inference unit is the event period. Same-period events are collapsed
to one cross-asset mean (which absorbs same-period cross-sectional
correlation by construction), and the t-test runs across those
periods — so n counts event periods (the number of periods with
an event), not events, and metadata["n_event_periods_sampled"]
names it precisely. n_obs_axis is "events", the one token the
whole event battery reports on: every member's sample is a set of
non-overlapping event observations, and stacking their to_frame()
rows must not yield two axis labels for one quantity. It uses
non-overlap resampling rather than Newey-West (NW) heteroskedasticity-and-autocorrelation-consistent
(HAC), the same convention as ic.
The abnormal return, and when its model is conservative. The
per-period CAAR is a mean of abnormal returns — the asset's
estimation-window mean subtracted, or a supplied abnormal_return
column (metadata["abnormal_return_model"]; see
:func:~factrix.metrics._helpers._attach_abnormal_return). The
mean-adjusted model is identified when little of each event's window
is other events' realised returns; above
ESTIMATION_WINDOW_EVENT_SHARE_WARN of
metadata["estimation_window_event_share"] the test fires
ESTIMATION_WINDOW_CONTAMINATED and reads conservative. caar
is the least affected member of the battery because the same-period
average is taken before the variance (20 assets, 5% trigger: 4.7 /
2.3 / 5.0% at \(h = 1 / 5 / 21\)), but a single asset at \(h = 21\)
rejects 0.0% of true nulls (nominal 5%).
Scale and weighting are explicit. Standard pipeline output carries
metadata["return_scale"] == "per_period_simple" because
:func:~factrix.preprocess.compute_forward_return divides the simple
horizon return by forward_periods. Recover the cumulative simple
CAR with value * EvaluationResult.forward_periods. Do not use
overlap_periods: on a coarse evaluation grid it can be smaller than
the economic horizon. A supplied abnormal_return column instead
carries "supplied_abnormal_return" because factrix does not know
its scale. metadata["event_weighting"] is "sign" for unit
non-zero factors and "factor_magnitude" for graded factors,
including strictly positive intensity signals.
The across-events siblings are complementary, not redundant:
bmp_z is the across-events standardized-AR z-test with the
Kolari-Pynnönen clustering correction on by default — 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, and — because it tests the event-period
series rather than pooled event rows — to same-period clustering as
well. The per-period 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, rng=0),
... forward_periods=5,
... )
>>> caar_df = compute_caar(panel)
>>> result = caar(caar_df, overlap_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, overlap_periods: int = DEFAULT_FORWARD_PERIODS, kolari_pynnonen_adjust: bool = True, include_prediction_error_variance: bool = False, expected_warnings: tuple[str, ...] = ()) -> MetricResult
Boehmer-Musumeci-Poulsen Standardized Abnormal Return test.
The static event floor (MIN_EVENTS_HARD) gates the standardized-AR z-test on the count of events with a usable estimation-window volatility that survive the event-axis spacing pass; the warn floor scales with the overlap_periods parameter (the spacing stride), so the threshold is declared as a resolver (a callable sample_threshold) rather than a constant.
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. The abnormal return is
mean-adjusted (or taken from a supplied abnormal_return column) — see
Notes.
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 overlap_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 price-derived one-period 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 panel periods before each event for volatility estimation (default 60). Counted on the panel's own date grid, so an asset missing periods estimates on a shorter sample rather than a stretched window. |
60
|
overlap_periods
|
int
|
Return horizon for vol scaling (default 5),
counted in panel periods. When using
price-derived one-period vol, scales by
|
DEFAULT_FORWARD_PERIODS
|
kolari_pynnonen_adjust
|
bool
|
On by default. Apply the
Kolari-Pynnönen (2010) adjustment for
cross-sectional correlation of SAR. BMP assumes events are
cross-sectionally independent; same-period events (earnings
season, index rebalances, macro releases) share a common shock
and break that. Measured on a true null with ρ = 0.5 (nominal
5%, \(h = 1\), 20 assets, 300 draws): 1 event per period 3.7%
either way (the adjustment is the identity when no two events
share a period — |
True
|
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\) = overlap_periods) when a price column is
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
\(1 / \sqrt{1 + (N_{\mathrm{eff}} - 1)\, \hat r}\).
Windows are counted in panel periods. Both the estimation-window
mean and the volatility behind \(\sigma_i\) are measured on the panel's
distinct-date grid rather than on the asset's own rows, so
estimation_window spans that many grid periods for every name and
an asset's missing periods count as missing observations inside the
window instead of stretching it further back. A ragged panel raises
ragged_period_grid.
Scope of the \(1/\sqrt{h}\) vol scale. It converts a one-period vol
to the horizon of a forward return realised over \(h\) periods of the
panel's own grid, so it is exact only on the unsampled full grid: on
an evaluation grid sub-sampled after compute_forward_return the
stamped \(h\) no longer counts the periods the return actually spans.
The p-value is unaffected either way. The factor is common to
every event, so it cancels in
\(z = \mathrm{mean}(\mathrm{SAR}) / (\mathrm{std}(\mathrm{SAR}) /
\sqrt{N})\) — numerator and denominator scale together — and the
Kolari-Pynnönen deflator is a function of the SAR correlation,
itself scale-free. Only the descriptive
metadata["std_sar"] (and value, the mean SAR) shifts, by the
constant \(\sqrt{h_{\text{stamped}} / h_{\text{true}}}\); read those two
as scaled units, not as absolute vol multiples, on a resampled grid.
Which K-P deflator. Kolari-Pynnönen's published statistic
multiplies by \(\sqrt{(1 - \hat r) / (1 + (N_{\mathrm{eff}} - 1)\hat r)}\).
The \((1 - \hat r)\) numerator belongs to their setting, where the
SAR variance is estimated within a single event period: a
one-date cross-sectional variance under equicorrelation estimates
only the idiosyncratic share \((1 - \hat r)\sigma^2\), so the
numerator restores the missing between-date component. factrix
pools SAR across all event periods, so std_sar already contains
that component and re-applying \((1 - \hat r)\) would deflate \(z\)
twice — anti-powered rather than merely conservative. The variant
implemented here is therefore the pure design-effect deflator
\(1/\sqrt{1 + (N_{\mathrm{eff}} - 1)\hat r}\), the standard
clustered-sampling correction for a pooled variance. Callers who
need the literal published constant can recover it from
metadata["stat_uncorrected"] and
metadata["kolari_pynnonen_r"].
Time-axis overlap. Events are first strided per asset so no two
kept events on one name sit inside one forward-return window — the
Brown-Warner (1985) non-overlap sampling
convention, applied on the event axis rather than the period
(:func:~factrix.metrics._helpers._sample_events_non_overlapping), and
the same treatment caar applies to its event-period series. It is
the complement of the K-P adjustment above, not a substitute: K-P
deflates \(z\) for events sharing a period across names, this removes
events sharing bars on one name. On a single-asset panel K-P is the
identity by construction, so this pass is the only overlap discipline
there; EVENT_WINDOW_OVERLAP reports what it removed.
Event validity. An event enters the test only when its
estimation-window vol is finite and above EPSILON and its
signed AR is finite. The two rejection reasons are reported
separately (n_dropped_no_vol / n_dropped_non_finite_return,
summing to n_dropped) because they mean different things: the
first is a short history, the second is a hole in return_col.
Both must be excluded — a NaN SAR propagates through mean/std and
turns \(z\) into a silent 0 with \(p = 1\).
The abnormal return. \(\mathrm{AR}_i\) is a genuine abnormal
return: the asset's estimation-window mean is subtracted from
return_col — taken on the same estimation_window bars of
one-bar returns as \(\sigma_i\) when price is present, on
return_col rows lagged by overlap_periods otherwise — or an
abnormal_return column on the input is used as supplied (see
:func:~factrix.metrics._helpers._attach_abnormal_return for both
models and metadata["abnormal_return_model"] /
["estimation_window_source"] for which ran).
Not robust to heavy skew at \(h > 1\) — use corrado_rank. With
\(\hat\mu_i\) and \(\sigma_i\) estimated on the same window (the
textbook form), a right-tail bar raises both, so under skew
\(E[\hat\mu_i / \sigma_i] \neq 0\) and the standardised abnormal return
inherits a bias of \(-\sqrt{h}\,E[\hat\mu_i / \sigma_i]\): the
\(1/\sqrt{h}\) vol scaling shrinks the standardiser while the bias in
the mean does not shrink. Measured on a standardised lognormal(0.9)
iid null (20 assets, 300 draws, nominal 5%): mean SAR \(+0.04 /
+0.08 / +0.16\) and size 5.5 / 18 / 32% at \(h = 1 / 5 / 21\); the same
panels with the raw return supplied as abnormal_return (no
\(\hat\mu\)) are unbiased (3.5 / 3.5 / 0.5%). Estimating the mean on
the vol window rather than on lagged rows does not change this — it
is the estimator, not the window. Signs that mix cancel it (4.3%
with half the events short). corrado_rank (4.3%) and
event_hit_rate (4.3%) are rank / sign tests and do not carry it.
Earlier versions standardised the raw forward return while
documenting mean-adjusted residuals; on a 20-asset panel drifting
0.08% per period with zero-information event dates that returned
\(z = 5.34\) (\(p = 9\times10^{-8}\)) and rejected 50% of null draws;
with the mean subtracted the drifting panel rejects exactly as
often as the same panel without drift (4.3% at \(h = 1\), 3.7% at
\(h = 5\), 20 assets, 300 draws). Events whose asset has too little
history for the estimate are excluded and counted.
Conservative on dense triggers and long horizons. The mean is
estimated from the asset's own history, which contains the realised
forward returns of its earlier events; once those fill more than a
quarter of the window (metadata["estimation_window_event_share"],
ESTIMATION_WINDOW_CONTAMINATED) the per-event abnormal returns
are negatively correlated and \(z\) under-rejects: 0.3% at \(h = 21\) on
20 assets, 0.0% on a single asset, 0.7% with 20 names firing on the
same 40 periods at \(h = 5\) (nominal 5%). The market-adjusted branch
does not remove it and costs the effect on same-period panels — see
:func:~factrix.metrics._helpers._attach_abnormal_return for the
measurements. Read a flagged p-value as an upper bound.
factrix simplifies the original BMP by omitting the prediction-
error term from the standardiser and by using mean-adjusted rather
than market-model residuals — adequate for the default
Brown-Warner / MacKinlay event-study path; pair with the K-P
adjustment when same-period shock sharing is expected.
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 implements with mean-adjusted residuals and, by default, no prediction-error correction.
- 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, rng=0),
... forward_periods=5,
... )
>>> result = bmp_z(panel, overlap_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.
caar.value is per period, not cumulative
Despite the name, caar.value is CAR / h, not the cumulative abnormal
return of MacKinlay (1997) §4. compute_forward_return divides by
forward_periods to put every horizon on a common per-period scale, and
the event family reads that column, so the normalisation is inherited
rather than undone here. With μ = 0.0008 per period over h = 5, the
reported value is 0.001056 — the per-period average — not 5× that.
The contract is machine-readable as
result.metadata["return_scale"] == "per_period_simple". Multiply by
EvaluationResult.forward_periods to recover the cumulative quantity the
name and the Brown-Warner / MacKinlay citations promise. Do not multiply
by overlap_periods: that field controls inference and can be smaller than
the economic horizon on a coarse evaluation grid. Reading caar.value
directly as a 5-period event CAR is off by 5×. A caller-supplied
abnormal_return instead reports "supplied_abnormal_return"; factrix
does not guess that column's scale.
Inspect the event-weighting estimand
compute_caar multiplies abnormal return by the factor without silently
coercing its magnitude. The producer column and downstream
result.metadata["event_weighting"] therefore report:
| Value | Factor contract | Estimand |
|---|---|---|
"sign" |
Every non-zero factor has magnitude 1 | Equal-event signed CAAR |
"factor_magnitude" |
At least one non-zero factor has non-unit magnitude | Factor-magnitude-weighted CAAR |
Strictly positive graded intensity factors are in the second row even
though they do not raise SPARSE_MAGNITUDE_WEIGHTED; that warning remains
reserved for the ambiguous mixed-sign, non-ternary case.
Use cases¶
-
Per-event-period CAAR series
The per-event-period 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-
overlap_periodssubsample of the per-event-period 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. The Kolari-Pynnönen same-period-correlation adjustment is on by default (identity when no two events share a period; 21.5% → 5.0% size at 4 events per period on a null);
kolari_pynnonen_adjust=Falsegives the unadjusted BMP for matching a source that reports it. -
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-period 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-period 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 period |
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 periods in the CAAR series |
n_event_periods_sampled |
caar(...).metadata["n_event_periods_sampled"] |
Event dates kept by the 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-period 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, rng=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, overlap_periods=5)
print(out.value, out.stat, out.p_value)
# 0.0039 6.42 1.4e-09 (approximate)
# Variance-robust alternative when same-period clustering is high:
z_bmp = bmp_z(panel, estimation_window=60, overlap_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.