Skip to content

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 date, asset_id, factor and either abnormal_return or return_col. Must include non-event rows for ranking.

required
overlap_periods int

Forward-return horizon, injected by evaluate from the panel metadata; standalone calls may pass it directly. Sets the minimum calendar gap between two kept events on one asset, so the tested events no longer share overlapping return windows.

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.

    api/metrics/caar →

  • 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.

    api/metrics/clustering →

  • Statistical methods


    Corrado nonparametric rank test specification and the deviation from eq. (5) factrix takes.

    reference/statistical-methods →

  • Metric applicability reference


    When this metric applies, sample-size guards, and the per-asset full-sample ranking contract.

    reference/metric-applicability →

  • Individual × Sparse landing


    Adjacent event-study metrics in the same cell.

    api/metrics/individual-sparse →