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 |
required |
value_col
|
str
|
Numeric column to evaluate. |
'value'
|
is_ratio
|
float
|
Fraction of the series allocated to IS (default |
0.7
|
survival_threshold
|
float
|
Minimum survival ratio for |
0.5
|
Returns:
| Type | Description |
|---|---|
MetricResult
|
MetricResult with: |
MetricResult
|
|
MetricResult
|
|
MetricResult
|
attached; a t-stat at |
MetricResult
|
~ 0 and would invite mis-reading the diagnostic as a |
MetricResult
|
significance test) |
MetricResult
|
|
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_decayis a standalone series diagnostic — input is a 1-D(date, value)series, typically information coefficient (IC) fromcompute_ic, spread fromcompute_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 = Trueand forcesstatus = "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 defaultsurvival_threshold = 0.5sits inside that window. -
Median across splits, not mean
Headline is
median_f s_fover 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. -
positive_rate/trend
Sibling series diagnostics on the same input shape — sign significance and slope detection. Pair with
ooswhen both in-sample magnitude and out-of-sample persistence matter. -
by_slice
Per-slice survival summaries (regime / universe / sector).
-
Metric applicability reference
When this metric applies and the sample-size guards that gate it (
MIN_OOS_PERIODS_HARD * 2floor; per-splitMIN_OOS_PERIODS_HARDon each side). -
Series diagnostics landing
Adjacent axis-agnostic series diagnostics.