Skip to content

factrix.metrics.oos_decay

Out-of-sample (OOS) persistence analysis for any time-indexed series.

This tool is agnostic to what the series represents — it only knows about a single IS/OOS split on a time-indexed numeric sequence.

Notes

Pipeline. Time-series only, single IS/OOS window split on a 1-D series; descriptive decay diagnostic (no formal H_0).

Input. DataFrame with date, value (IC series, CAAR series, spread series).

Output. MetricResult with value = survival ratio + sign-flip / status detail in metadata.

factrix.metrics.oos_decay.oos_decay

oos_decay(series: DataFrame, value_col: str = 'value', is_ratio: float = 0.7, survival_threshold: float = 0.5) -> MetricResult

Single-split out-of-sample (OOS) survival ratio with sign-flip detection.

Splits the sorted series at is_ratio (IS = first is_ratio * n rows, OOS = remainder), computes |mean_OOS| / |mean_IS| (the survival ratio), and checks for an IS/OOS sign flip.

Parameters:

Name Type Description Default
series DataFrame

DataFrame with date and value_col, sorted by date.

required
value_col str

Numeric column to evaluate.

'value'
is_ratio float

Fraction of the series allocated to IS (default 0.7).

0.7
survival_threshold float

Minimum survival ratio for status="PASS" (default 0.5).

0.5

Returns:

Type Description
MetricResult

MetricResult with:

MetricResult
  • value: survival ratio (NaN on short-circuit)
MetricResult
  • stat: None — descriptive only (no hypothesis test
MetricResult

attached; a t-stat at MIN_OOS_PERIODS_HARD = 5 would have power

MetricResult

~ 0 and would invite mis-reading the diagnostic as a

MetricResult

significance test)

MetricResult
  • metadata:

  • sign_flipped (bool)

  • status ("PASS" | "VETOED")
  • is_ratio (float)
  • mean_is (float)
  • mean_oos (float)
  • survival_threshold (float)
  • reason (str, short-circuit only): "insufficient_oos_periods"
Notes

For multi-fraction sweeps, call oos_decay per fraction and aggregate on the caller side::

results = {f: oos_decay(series, is_ratio=f) for f in (0.6, 0.7, 0.8)}
median = statistics.median(r.value for r in results.values())

Descriptive only — no p_value is emitted.

References
  • McLean-Pontiff (2016): post-publication returns ~58% lower than in-sample, with ~32% of that drop attributable to publication itself (the remaining ~26% is the pure out-of-sample decay).
  • Lopez-de-Prado (2018): CPCV for robust train/test split.

Examples:

Survival on a per-date information coefficient (IC) series from :func:~factrix.metrics.ic.compute_ic:

>>> import factrix as fx
>>> from factrix.preprocess import compute_forward_return
>>> from factrix.metrics.ic import compute_ic
>>> from factrix.metrics.oos_decay import oos_decay
>>> panel = compute_forward_return(
...     fx.datasets.make_cs_panel(n_assets=80, n_dates=240, seed=0),
...     forward_periods=5,
... )
>>> series = compute_ic(panel)["factor"].rename({"ic": "value"}).select("date", "value")
>>> result = oos_decay(series)
>>> result.name == ""
True

Descriptive only — no formal \(H_0\)

oos_decay emits a survival ratio + sign-flip detail; no p_value is attached and stat is None. A \(t\)-test at the MIN_OOS_PERIODS_HARD floor would have power \(\approx 0\) and would invite mis-reading the diagnostic as a significance test. Callers routing this output into Benjamini-Hochberg-Yekutieli (BHY) / gate logic must read status ("PASS" / "VETOED") and sign_flipped, not a probability.

Use cases

  • Persistence read on a factor-return series


    oos_decay is a standalone series diagnostic — input is a 1-D (date, value) series, typically information coefficient (IC) from compute_ic, spread from compute_spread_series, or any other factor-mimicking-portfolio return series. Reports \(|\mathrm{mean}_{\text{OOS}}| / |\mathrm{mean}_{\text{IS}}|\) across multiple (IS_fraction, OOS_fraction) splits.

  • Sign-flip veto


    Any split with opposite-signed IS and out-of-sample (OOS) means flips sign_flipped = True and forces status = "VETOED" — IC sign-flip OOS means the factor predicts the wrong direction, not just a weaker one. McLean & Pontiff (2016) report average OOS decay around 32 %; factrix's default survival_threshold = 0.5 sits inside that window.

  • Median across splits, not mean


    Headline is median_f s_f over the default splits [(0.6, 0.4), (0.7, 0.3), (0.8, 0.2)]. A single regime change landing inside one split distorts the mean disproportionately; the median absorbs it.

Choosing a function

Goal Function
Multi-split OOS survival + sign-flip gate on a (date, value) series oos_decay
Typed accessor for an individual split's (is_ratio, mean_is, mean_oos, ...) SplitDetail

Worked example — IC series fed into oos_decay

compute_ic → oos_decay

import factrix as fx
from factrix.metrics.ic import compute_ic
from factrix.metrics.oos_decay import oos_decay
from factrix.preprocess import compute_forward_return

raw   = fx.datasets.make_cs_panel(
    n_assets=100, n_dates=1000, ic_target=0.08, seed=2024,
)
panel = compute_forward_return(raw, forward_periods=5)

# The series diagnostic consumes (date, value); the value column on
# the compute_ic output is named ``ic``.
ic_df = compute_ic(panel)["factor"]
out   = oos_decay(ic_df, value_col="ic")
print(out.value, out.metadata["status"], out.metadata["sign_flipped"])
# 0.94   PASS   False   (approximate)
for split in out.metadata["per_split"]:
    print(split)
# {"is_ratio": 0.6, "mean_is": 0.080, "mean_oos": 0.077,
#  "survival_ratio": 0.96, "sign_flipped": False}
# ...

See also

  • compute_ic / compute_spread_series


    Canonical producers of the (date, value) series this diagnostic consumes.

    api/metrics/ic →

  • positive_rate / trend


    Sibling series diagnostics on the same input shape — sign significance and slope detection. Pair with oos when both in-sample magnitude and out-of-sample persistence matter.

    api/metrics/positive_rate →

  • by_slice


    Per-slice survival summaries (regime / universe / sector).

    api/by-slice →

  • Metric applicability reference


    When this metric applies and the sample-size guards that gate it (MIN_OOS_PERIODS_HARD * 2 floor; per-split MIN_OOS_PERIODS_HARD on each side).

    reference/metric-applicability →

  • Series diagnostics landing


    Adjacent axis-agnostic series diagnostics.

    api/metrics/series-tools →