Skip to content

Diagnostics and exceptions

Inspect what a splitter removed and check temporal, group, and embargo constraints. Start with audit_splitter for a fold report. The assertion helpers below raise the documented exceptions when a constraint is violated.

Diagnostics

purgedcv.audit_splitter

audit_splitter(cv: BaseTemporalSplitter, X: NDArrayAny | DataFrame) -> pd.DataFrame

Return a non-raising, per-fold report for a temporal splitter.

The report consumes the same candidate → purge → embargo → final pipeline as BaseTemporalSplitter.split; purge and embargo counts therefore come from the actual intermediate index arrays rather than being inferred from the final split. Sliding walk-forward truncation is implemented by the finalization stage and therefore appears in rows_removed_by_finalization. If a custom finalization hook introduces indices, rows_added_by_finalization reports them separately.

candidate_overlap_fraction is the fraction of candidate training rows whose label horizons overlap the test horizons after applying the splitter's purge_horizon padding. For non-empty candidates it equals rows_removed_by_purge / candidate_train_size; empty candidates report zero. compute_overlap_fraction reproduces it only when given the same pre-purge candidate indices and purge horizon — indices returned by cv.split() are already final. final_overlap_fraction repeats the measure on final training rows and should be zero for a clean splitter.

For the built-in splitters, temporal_leakage_free is structurally expected to be True: purge removes overlaps and later stages only remove rows. The column is primarily a regression guard for custom BaseTemporalSplitter subclasses whose finalization hook could accidentally reintroduce indices; it is not an independent validation algorithm. For an empty final train set it is vacuously True; inspect train_nonempty before treating a fold as usable. Leakage and group overlap are reported as values instead of raising; malformed inputs and splitter configuration errors still raise.

Time columns are outer envelopes, not continuous windows. CPCV and interleaved group folds can contain several disjoint positional blocks, so train and test envelopes may overlap even when final_overlap_fraction == 0. train_block_count and test_block_count expose that layout; the counts refer to contiguous runs of positional indices.

The audit requires the inherited BaseTemporalSplitter.split implementation. A subclass overriding split() is rejected because its returned folds may diverge from the auditable candidate → purge → embargo → finalization pipeline. Custom subclasses should use _iter_test_indices, _candidate_train_idx, and _finalize_train_idx instead.

Parameters:

Name Type Description Default
cv BaseTemporalSplitter

A BaseTemporalSplitter instance with times already bound.

required
X NDArrayAny | DataFrame

Feature matrix or other sized sample container. Its length must match the times bound to cv; feature values are not inspected.

required

Returns:

Type Description
DataFrame

A DataFrame with one row per fold. Columns contain the zero-based fold

DataFrame

number; candidate, final, and test sizes; train non-emptiness; rows

DataFrame

removed at purge, embargo, and finalization; indices added by custom

DataFrame

finalization; candidate and final temporal-overlap fractions;

DataFrame

contiguous block counts; outer train/test time envelopes; and

DataFrame

groups_disjoint (None when no groups are bound).

Raises:

Type Description
TypeError

if cv is not a BaseTemporalSplitter or its class overrides BaseTemporalSplitter.split.

ValueError

if X has the wrong length or the splitter cannot form its configured folds.

Examples:

>>> import numpy as np
>>> import pandas as pd
>>> from purgedcv import PurgedKFold, audit_splitter
>>> pred = pd.date_range("2024-01-01", periods=20, freq="D")
>>> evalu = pred + pd.Timedelta(days=1)
>>> cv = PurgedKFold(
...     4, prediction_times=pred, evaluation_times=evalu,
...     purge_horizon="1D", embargo_observations=2,
... )
>>> report = audit_splitter(cv, np.zeros((20, 1)))
>>> report.shape[0]
4
>>> bool(report["final_overlap_fraction"].eq(0.0).all())
True

purgedcv.diagnostics.compute_overlap_fraction

compute_overlap_fraction(train_idx: NDArrayAny, test_idx: NDArrayAny, prediction_times: TimesLike, evaluation_times: TimesLike, *, purge_horizon: HorizonLike | None = None) -> float

Return the fraction of training rows whose half-open label horizon overlaps any test horizon, optionally padded by purge_horizon.

Unlike assert_no_temporal_leakage, this does not raise when it finds leakage: it returns 0.0 for clean splits and 1.0 when every training row leaks. Malformed indices, times, and horizons still raise validation errors. Useful for logging splitter health metrics or for debugging a splitter that produces unexpected behavior.

purge_horizon defaults to no padding. Reproducing an audit_splitter report's candidate_overlap_fraction also requires its pre-purge candidate indices; the indices yielded by cv.split() are final and instead reproduce final_overlap_fraction.

Examples:

>>> import numpy as np
>>> import pandas as pd
>>> from purgedcv.diagnostics import compute_overlap_fraction
>>> pred = pd.Series(pd.date_range("2024-01-01", periods=20, freq="D"))
>>> evalu = pred + pd.Timedelta(days=1)
>>> compute_overlap_fraction(np.arange(5), np.arange(10, 15), pred, evalu)
0.0
>>> compute_overlap_fraction(
...     np.arange(10, 15), np.arange(10, 15), pred, evalu
... )
1.0

purgedcv.diagnostics.assert_no_temporal_leakage

assert_no_temporal_leakage(train_idx: NDArrayAny, test_idx: NDArrayAny, prediction_times: TimesLike, evaluation_times: TimesLike, *, purge_horizon: HorizonLike | None = None) -> None

Raise TemporalLeakageError if any training row's label horizon overlaps any test label horizon, optionally padded on both sides by purge_horizon.

Test horizons are checked as a union of half-open intervals, not as one convex hull. This matters for CPCV folds whose test groups are intentionally non-contiguous.

Parameters:

Name Type Description Default
train_idx NDArrayAny

positional indices of training rows.

required
test_idx NDArrayAny

positional indices of test rows.

required
prediction_times TimesLike

prediction times for all rows.

required
evaluation_times TimesLike

evaluation times for all rows.

required
purge_horizon HorizonLike | None

optional padding (default: None ≡ zero padding).

None

Raises:

Type Description
TemporalLeakageError

with the offending training row index and the two overlapping intervals in the message.

Examples:

>>> import numpy as np
>>> import pandas as pd
>>> from purgedcv.diagnostics import assert_no_temporal_leakage
>>> pred = pd.Series(pd.date_range("2024-01-01", periods=20, freq="D"))
>>> evalu = pred + pd.Timedelta(days=1)
>>> assert_no_temporal_leakage(np.arange(5), np.arange(10, 15), pred, evalu)

purgedcv.diagnostics.assert_groups_disjoint

assert_groups_disjoint(train_idx: NDArrayAny, test_idx: NDArrayAny, groups: ArrayLike1D) -> None

Raise GroupLeakageError if any group identifier appears in both train_idx and test_idx.

Used by group-aware splitters to verify that no entity (patient, asset, user, etc.) is represented in both training and test of the same fold. The error message names a representative overlapping group plus the total count of overlapping groups, so the caller can scope follow-up.

Examples:

>>> import numpy as np
>>> import pandas as pd
>>> from purgedcv.diagnostics import assert_groups_disjoint
>>> groups = pd.Series([0, 0, 1, 1, 2, 2])
>>> assert_groups_disjoint(np.array([0, 1]), np.array([2, 3]), groups)

purgedcv.diagnostics.assert_embargo_respected

assert_embargo_respected(train_idx: NDArrayAny, test_idx: NDArrayAny, prediction_times: TimesLike, evaluation_times: TimesLike, embargo: HorizonLike | None = None, *, embargo_observations: int | None = None, embargo_fraction: float | None = None) -> None

Raise EmbargoViolationError when an embargo is violated.

The duration, observation-count, and fractional modes have the same mutually-exclusive semantics as purgedcv.apply_embargo. Exactly one mode must be supplied; omitting all three is treated as a configuration error rather than a successful audit.

Embargo is asymmetric: rows before test boundaries are never flagged. Zero-width modes are identities.

Examples:

>>> import numpy as np
>>> import pandas as pd
>>> from purgedcv.diagnostics import assert_embargo_respected
>>> pred = pd.Series(pd.date_range("2024-01-01", periods=20, freq="D"))
>>> evalu = pred + pd.Timedelta(days=1)
>>> assert_embargo_respected(
...     np.array([18]), np.arange(5, 10), pred, evalu, embargo="2D"
... )

Exceptions

purgedcv.TemporalCVError

Bases: ValueError

Base class for all purged-cross-validation errors.

purgedcv.TemporalLeakageError

Bases: TemporalCVError

Raised when a training row's label horizon overlaps the test horizon.

purgedcv.EmbargoViolationError

Bases: TemporalCVError

Raised when a training row falls inside the post-test embargo window.

purgedcv.GroupLeakageError

Bases: TemporalCVError

Raised when a group_id appears in both training and test of the same fold.