Architecture¶
purgedcv is organised as a thin public API on top of a small kernel of
interval and time utilities. The diagram below shows the dependency
direction: user code calls a splitter (which already speaks the
scikit-learn protocol), the splitter delegates to one shared base class
that wires purge and apply_embargo into every fold, and those
primitives share a single interval implementation. The statistical
metrics and the diagnostics module sit alongside the splitters as
independent, leakage-aware tools.
The internal grouping D1–D8 refers to the implementation plan in the
project's CHANGELOG.
flowchart TB
user["User estimator + features + timestamps<br/>(prediction_times, evaluation_times)"]
subgraph public["purgedcv public API"]
direction TB
splitters["Splitters · sklearn protocol<br/>WalkForwardSplit (D5.1)<br/>PurgedKFold (D5.2)<br/>PurgedGroupKFold (D5.3)<br/>CombinatorialPurgedCV (D5.4)"]
paths["reconstruct_paths /<br/>backtest_paths (D6)"]
metrics["Statistical metrics (D7)<br/>probabilistic_sharpe_ratio<br/>deflated_sharpe_ratio<br/>min_track_record_length"]
diagnostics["Diagnostics (D8)<br/>audit_splitter<br/>compute_overlap_fraction<br/>assert_no_temporal_leakage<br/>assert_groups_disjoint<br/>assert_embargo_respected"]
end
subgraph internal["Internal kernel"]
direction TB
base["BaseTemporalSplitter (D4)<br/>shared splitter contract<br/>monotonic check, group disjointness"]
primitives["purge (D2) · apply_embargo (D3)<br/>row-level filters"]
intervals["_intervals<br/>half-open & closed<br/>interval merges (O(n log n))"]
time["_time (D1)<br/>parse_horizon<br/>validate_times<br/>horizons_overlap"]
end
user -->|"cv = SplitterClass(...)"| splitters
splitters --> base
base --> primitives
primitives --> intervals
base --> time
splitters -.via cross_val_score / GridSearchCV.-> user
user --> paths
paths --> metrics
user -.audit any custom split.-> diagnostics
diagnostics -.audit purgedcv fold stages.-> base
diagnostics --> intervals
diagnostics --> time
Design rules¶
Three small invariants keep the surface honest.
-
One implementation per boundary kind. Time-based overlap and embargo checks go through
src/purgedcv/_intervals.py; positional embargo windows are implemented once insrc/purgedcv/_embargo.py. Splitters do not duplicate either boundary rule. A CPCV fold with non-adjacent test groups is filtered by the union of local windows rather than by the convex hull between them. -
Label-aware purge. Splitters take framework-agnostic
prediction_timesandevaluation_timescontainers rather than a single integer gap. pandas, NumPy, Python sequences, and Polars are accepted and coerced to one-dimensional NumPy arrays at the public boundary. The purge follows the real label horizon, so a label that overlaps by twelve half-hours and one that overlaps by twenty cannot share a single hand-tunedgapand still be correct — the user passes the actual horizon and the splitter does the right thing per row. -
Row-level diagnostics are independent; splitter audits share the real pipeline.
compute_overlap_fractionand the threeassert_*functions accept positional indices and never assume their input came frompurgedcv, so they can audit sklearn, tscv, or hand-rolled splits equally.audit_splitterdeliberately targetsBaseTemporalSplitter: it consumes the same candidate → purge → embargo → final stages assplit()so its per-stage counts are observed rather than inferred. Subclasses must express fold customization through the stage hooks; overridingsplit()is rejected because the report could no longer guarantee it audits the folds users actually receive.
Module layout¶
src/purgedcv/
├── __init__.py public re-exports; __version__
├── _base.py BaseTemporalSplitter
├── _walk_forward.py WalkForwardSplit
├── _purged_kfold.py PurgedKFold, PurgedGroupKFold
├── _cpcv.py CombinatorialPurgedCV
├── _paths.py reconstruct_paths
├── _purge.py purge
├── _embargo.py apply_embargo
├── _intervals.py half-open / closed interval merges
├── _time.py HorizonLike, parse_horizon, validate_times, horizons_overlap
├── _metrics.py probabilistic_sharpe_ratio, deflated_sharpe_ratio,
│ min_track_record_length
├── _typing.py ArrayLike1D, NDArrayAny, TimesLike
├── diagnostics.py public assertions and per-fold audit report
├── exceptions.py TemporalCVError tree
└── py.typed PEP 561 marker — the package is fully typed
Wheel contents follow exactly this layout; tools/ and examples/
ride along with the source distribution but are not packaged.