factrix.metrics.corrado_rank ¶
Corrado nonparametric rank test on event abnormal returns.
Standalone metric in cell (*, SPARSE, *, PANEL) — not part of the
default profile. Available via
from factrix.metrics import corrado_rank.
factrix.metrics.corrado_rank.corrado_rank ¶
corrado_rank(data: DataFrame, *, factor_col: str = 'factor', return_col: str = 'forward_return', overlap_periods: int = DEFAULT_FORWARD_PERIODS, estimation_window: int = 60, expected_warnings: tuple[str, ...] = ()) -> MetricResult
Corrado nonparametric rank test for event abnormal returns.
The static event floor (MIN_EVENTS_HARD) gates the rank test on the count of non-zero (event) observations 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.
A non-parametric alternative to the CAAR t-test. Robust to extreme
returns, non-normal distributions, cross-asset heteroscedasticity,
and same-period event clustering — the regime where the parametric
caar t-test is unreliable and this metric is the recommended
fallback. Direction-adjusted for two-sided signals (extension of the
original one-directional test).
Formula
For each asset \(i\), rank return across the full sample
(event + non-event, finite observations only), transform to
\(U_{i,t} = \mathrm{rank} / (T+1) - 0.5\), and on event rows
form \(U_{\text{event,signed}} = U_{\text{event}} \cdot \mathrm{sign}(\text{factor})\).
Test statistic
Same-period events are then averaged into one observation per event
period, \(\bar{U}_d\), and the test runs on that period series:
\(z = \mathrm{mean}(\bar{U}_d) / (\mathrm{std}(\bar{U}_d) / \sqrt{D})\), \(D\) = number of event periods.
p_value is one-sided (H0: mean_u <= 0): the direction
adjustment already folds sign into \(z\), so a factor that
anti-predicts returns a negative \(z\) rather than a small two-sided
\(p\) — mirroring :func:~factrix.metrics.directional_hit_rate.directional_hit_rate.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
DataFrame
|
Full panel with |
required |
overlap_periods
|
int
|
Forward-return horizon, injected by |
DEFAULT_FORWARD_PERIODS
|
Returns:
| Type | Description |
|---|---|
MetricResult
|
MetricResult with value=mean rank deviation, stat=z. |
Notes
Windows are counted in panel periods. The estimation window behind
the ranked 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; an asset's missing periods count as
missing observations inside it, and a ragged panel raises
ragged_period_grid.
The event period is the unit of inference. Events sharing a period
share whatever moved the market then, so they are not
independent draws. Collapsing each period's cross-section to
\(\bar{U}_d\) before taking the time-series SD folds that
within-period correlation into the denominator; n_obs therefore
counts event periods — reported on the battery's shared axis token
"events" (every member's sample is a set of non-overlapping event
observations) with the count itself in
metadata["n_event_periods"] — and the raw event count is
kept in metadata["n_events"] alongside
events_per_period_mean / events_per_period_max.
An earlier version divided by the pooled std of U_all over
every (asset, date) cell and scaled by \(\sqrt{N_{events}}\).
That ignores clustering entirely: piling k correlated events onto
one period grew \(N_{events}\) without growing the denominator, so \(z\)
scaled with \(\sqrt{k}\) for no new information. Under a simulated
null with 6 events per period it rejected at 22% against a nominal
one-sided 5%; the period-clustered denominator gives 3%. The metric
was liberal in precisely the regime it is recommended for.
This is the intent of Corrado (1989) eq. (5) — a time-series SD of a cross-sectional mean — but not its literal form. Corrado's design is event-time aligned, so every period's cross-section is the full set of N names and the SD may be taken over the whole sample period. In a calendar-time sparse panel the event periods carry a handful of names while a non-event period carries the full universe, so an SD taken over all periods is the SD of a much better-averaged quantity: on the demo panel (50 names per period, 1.57 events per period) it understates the relevant dispersion ~5.7x, i.e. it would be more liberal than the pooled std it replaced. The SD is therefore taken over the event-period series itself, whose scale matches the numerator by construction.
Non-finite returns. Ranks are formed over the finite selected
abnormal-return values only (per asset), and \(T\) in the
\(\mathrm{rank}/(T+1)\) normalisation is the count of those finite
values — so a gap in the return series shifts neither the ranks of
its neighbours nor the uniform scaling. Non-finite event rows (and
rows with a non-finite factor, which survive the != 0 filter
because NaN != 0 is True) are excluded from
\(U_{\text{event}}\) and from the pooled std(U_all), and the
excluded event count is reported as
metadata["n_events_dropped_non_finite"];
metadata["n_pairs_total"] counts the finite cells actually behind
the denominator. This matters because polars ranks a float NaN
as the largest value rather than treating it as missing: left
unmasked, every NaN return would enter the test as a maximal
positive rank deviation.
Missing data. \(T_i\) is the count of non-missing returns for asset \(i\), so the \(\mathrm{rank} / (T_i + 1)\) scaling is the missing-data denominator of Corrado & Zivney (1992) rather than a fixed window length: a gap in one asset's series shifts neither its neighbours' ranks nor the uniform scaling, and the excluded event count is reported.
Time-axis overlap. Before the per-period collapse, events are
strided per asset so no two kept events on one name sit inside one
forward-return window — Brown-Warner (1985)
non-overlap sampling on the event axis, the same treatment caar
applies to its event-period series. The collapse and the stride are
complements: the collapse removes same-period dependence across names,
the stride removes same-asset dependence across time. Without it the
rank test over-rejected a bursty single-asset trigger at 21% against a
nominal 5%; EVENT_WINDOW_OVERLAP reports what was removed.
Short-circuits to MetricResult with
metadata["reason"]="insufficient_events" when
N_events < MIN_EVENTS_HARD;
"insufficient_event_periods" when fewer than
MIN_EVENTS_HARD distinct event periods survive (the time-series
SD cannot be estimated from a handful of periods, however many events
sit on them) — the same floor caar applies to its own event-period
series, and WarningCode.FEW_EVENTS fires in
[MIN_EVENTS_HARD, MIN_EVENTS_WARN); and
a result with DEGENERATE_VARIANCE and no hypothesis test when
std(U_bar_d) < EPSILON.
References
- Corrado (1989). "A Nonparametric Test for Abnormal Security-price Performance in Event Studies." Journal of Financial Economics 23(2), 385–395. The The nonparametric rank test factrix implements, with the denominator taken over the event-period series rather than the full sample period (see Notes).
- Corrado & Zivney (1992). "The Specification and Power of the Sign Test in Event Study Hypothesis Tests Using Daily Stock Returns." Journal of Financial and Quantitative Analysis 27(3), 465–478. Source of the direction-adjustment idea applied to two-sided signals.
Examples:
>>> import factrix as fx
>>> from factrix.preprocess import compute_forward_return
>>> from factrix.metrics.corrado_rank import corrado_rank
>>> panel = compute_forward_return(
... fx.datasets.make_event_panel(n_assets=50, n_dates=400, rng=0),
... forward_periods=5,
... )
>>> result = corrado_rank(panel)
>>> result.name == ""
True
Event-study contracts
Corrado's primitive is
\(\text{signed\_rank} = \text{uniform\_rank}(\text{forward\_return}) \times \text{sign}(\text{factor})\)
— note that the direction adjustment is on the rank, not on
the return itself. The
Event-study contracts table
contrasts this with caar (magnitude-weighted) and event_quality
(sign-only on the raw return). The
estimation_window
section specifies the per-asset baseline this test ranks against.
Use cases¶
-
Robustness screen against parametric CAAR / BMP
Rank-based two-sided \(z\) on event-window abnormal returns. Robust to extreme returns, non-normal distributions, and cross-asset heteroscedasticity — flips the conclusion when parametric CAAR is being driven by a handful of outliers.
-
Direction-adjusted nonparametric inference
Ranks are sign-flipped via
sign(factor)before pooling, so the test handles two-sided signals (long / short events) directly — the Corrado-Zivney (1992) extension to the original one-directional test.
Worked example — rank test alongside parametric CAAR¶
corrado_rank on a synthetic event panel
import factrix as fx
from factrix.metrics.corrado_rank import corrado_rank
from factrix.metrics.caar import compute_caar, caar
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_bps=40.0, rng=2024,
)
panel = compute_forward_return(raw, forward_periods=5)
rank_out = corrado_rank(panel)
print(rank_out.value, rank_out.stat, rank_out.p_value)
# 0.041 5.18 2.2e-07 (approximate)
# Compare to parametric CAAR — divergence flags outlier-driven CAAR:
caar_out = caar(compute_caar(panel), overlap_periods=5)
print(caar_out.stat, rank_out.stat)
# 6.42 5.18
See also¶
-
caar/bmp_z
Parametric counterparts; read alongside Corrado for an outlier-robustness cross-check.
-
clustering_hhi
The pooled-std denominator factrix uses diverges from the Corrado (1989) eq. (5) form when event-date clustering is present — inspect Herfindahl-Hirschman index (HHI) before reading \(p\) at the size boundary.
-
Statistical methods
Corrado nonparametric rank test specification and the deviation from eq. (5) factrix takes.
-
Metric applicability reference
When this metric applies, sample-size guards, and the per-asset full-sample ranking contract.
-
Individual × Sparse landing
Adjacent event-study metrics in the same cell.