Changelog¶
All notable changes to purged-cross-validation are recorded here. The
format is based on Keep a Changelog.
The 0.0.x series is the pre-1.0 development line. The library's feature
work was organised internally as Plans A–D (foundations → splitters →
backtest paths → metrics); the cumulative feature set delivered by each
Plan is listed under the published version it shipped in.
Unreleased¶
Changed¶
- Python 3.11 is now the minimum supported version. Python 3.10 reaches end of life this month. CI tests 3.11 through 3.15, and the package metadata, README badge, and install docs list the same range.
Documentation¶
- Quickstart section 4 shows
reconstruct_pathson a hand-written fit loop,CombinatoriallySymmetricCVwith PBO, andminimum_backtest_length, all checked by the quickstart e2e test. - The backtest overfitting audit notebook now compares the minimum backtest length with the in-sample and held-out spans of its Optuna champion. The README already claimed it did.
- The README lists all fifteen example notebooks, and a new Citation section
gives the Zenodo DOI with BibTeX entries.
CITATION.cffrecords the DOI. - Released entries are grouped under versions 0.1.9 and 0.1.10 instead of
remaining under
Unreleased.
0.1.10 - 2026-10-01¶
Documentation¶
- Add a Docs build badge and complete notebook setup instructions, including cloning the repository, creating an environment, and quoting extras for zsh.
- Document
with_times()inputs and add an executable example showing new label horizons without modifying the original splitter.
Fixed¶
- Include
textstatin the development extra so the documented prose gate works after installing.[dev,docs].
0.1.9 - 2026-10-01¶
Documentation¶
- Related-software descriptions now focus on documented capabilities, with links to project sources. Removed unsupported quality judgments from the README, methodology guide, manuscripts, and an earlier benchmark entry.
- The API reference now has an overview and seven topic pages. Existing
symbol bookmarks on
/api/forward to the new pages when JavaScript is enabled; the overview also lists direct links for readers without it.
Fixed¶
- Package author metadata now includes the contact email listed in
CITATION.cff. - Quickstart examples now pass a timedelta to standalone
purgeand a return series to DSR. The DSR example uses separate synthetic trials, rather than treating CPCV prediction paths as independent strategies. Every numbered section is executed directly from Markdown in an e2e test. - Released entries are grouped under versions 0.1.4 through 0.1.8 instead
of remaining under
Unreleased.
0.1.8 - 2026-09-26¶
Fixed¶
- Publication no longer gets skipped after the PR-only version job is
skipped on
main. Tests and citation checks must still pass. A CI-only fix can recover an untagged prepared version without a second bump, provided its package files and citation metadata have not changed. - Release metadata is now added to the working PR instead of a separate
release PR. Publication uses a tested
maincommit, without pushing directly to protectedmain. Repeated PR updates keep the prepared version, and pending package changes from the old failed release are included. - The API reference now displays descriptions for all five public input and
callback type aliases, and documents the shared splitter parameters under
BaseTemporalSplitter.
0.1.7 - 2026-09-26¶
Fixed¶
- API docstrings now use MkDocs-compatible cross-references and a supported note block, so readers see working links instead of literal Sphinx tags.
0.1.6 - 2026-09-04¶
Added¶
- Public
PBOResultandDSRDiagnosticsresult dataclasses, plus theHorizonLike,PathMetricFn, andPerformanceMetriccallback/input type aliases, are now importable directly frompurgedcvfor typed downstream code without reaching into private modules.
0.1.5 - 2026-08-30¶
Added¶
- New public
audit_splitter(cv, X)diagnostic returns one DataFrame row per fold with candidate and final train sizes, purge/embargo/finalization removal counts, indices added during custom finalization,candidate_overlap_fractionandfinal_overlap_fraction, train/test time envelopes with contiguous-block counts, train non-emptiness, and group-disjointness status. It consumes the splitters' real candidate → purged → embargoed stages instead of inferring removal counts from final indices; leakage is reported rather than raised. Subclasses that overridesplit()are rejected because their returned folds can diverge from that auditable pipeline, and custom final indices are validated for positional correctness and uniqueness.
0.1.4 - 2026-08-22¶
Added¶
- Embargo can now be expressed in three mutually exclusive ways throughout
the splitter, PBO, row-level, and diagnostics APIs: a wall-clock duration
(
embargo), a fixed number of post-test rows (embargo_observations), or a fraction of the full dataset (embargo_fraction). Fractional embargo usesfloor(n_samples * embargo_fraction)observations. Positional modes apply independently after every contiguous test block, including non-adjacent CPCV groups, and preserve the established duration-based behavior.
0.1.3 - 2026-08-01¶
Added¶
- Time inputs are now framework-agnostic.
prediction_timesandevaluation_timeson every splitter, together withpurge,apply_embargo,validate_times, and thediagnosticsfunctions, accept pandasSeries/DatetimeIndex, NumPydatetime64/timedelta64arrays, Python lists of datetime/Timestamp/timedelta values, and polarsSeries. Inputs are coerced to a 1-D NumPy array once at the public boundary; polars is duck-typed through.to_numpy()and never imported, so it stays out of the runtime dependencies. tz-aware pandas input is normalized to UTC.groupsaccepts the same containers but holds arbitrary 1-D labels, not timestamps. The pandas input path is unchanged; the widened public signatures use the new exportedTimesLike(times) andArrayLike1D(labels) type aliases.
0.1.2 - 2026-06-13¶
Documentation¶
- Quickstart now shows how to pass
sample_weightthrough the splitters. Because the splitters stay drop-in to scikit-learn and ship no scorer of their own, weights travel via sklearn metadata routing (enable_metadata_routing,set_fit_request/set_score_request); the section covers both train-time-only weighting and weighting the score through a custom scorer, and theUnsetMetadataPassedErrorit raises if the scorer neither requests nor declines the weight. path_metricsdocstring states themax_drawdownsign convention at the point of use (positive magnitude; worst path isidxmax, notidxmin), matching the note already indefault_backtest_metrics.
0.1.1 - 2026-06-05¶
Added¶
minimum_backtest_length(MinBTL): the backtest length, in years, below which a reported annualised Sharpe is within what selecting the best ofn_trialsindependent backtests produces by chance under the null. It is the inverse of the Deflated Sharpe Ratio deflation, reusing the same Bailey-Lopez de Prado expected-maximum bracket term, and finishes the AFML Chapter 11 selection-bias toolset (PSR, DSR, PBO, MinTRL) in-package. Implements Bailey, Borwein, Lopez de Prado & Zhu (2014), "Pseudo-mathematics and financial charlatanism".
0.1.0 - 2026-06-05¶
First minor release. The version bump from the 0.0.x line signals that
the public API in purgedcv/__init__.py is now considered usable and is
maintained deliberately: the splitters, backtest-path reconstruction, and
the Sharpe-ratio metric family are stable enough to build on. Breaking
changes will still happen before 1.0.0, but they will be noted here
rather than slipped into a patch release.
Added¶
probability_of_backtest_overfitting(PBO): estimates how often the configuration chosen as best in-sample lands below the median out-of-sample, using Combinatorially Symmetric Cross-Validation (CSCV). Returns a frozenPBOResultdataclass (read fields by attribute): the PBO value, the per-combination logits, the IS-versus-OOS performance-degradation slope, and the IS/OOS performance pairs. When prediction and evaluation times are supplied it cleans every IS/OOS boundary with the existing purge and embargo machinery. Implements Bailey, Borwein, Lopez de Prado & Salehipour (2017) and AFML Chapter 11.CombinatoriallySymmetricCV: the CSCV splitter that PBO is built on, exposed directly. It isCombinatorialPurgedCVwithn_test_groups = n_splits // 2(evenn_splitsrequired).deflated_sharpe_ratio_full: returns a frozenDSRDiagnosticsdataclass with the Deflated Sharpe probability alongside the quantities that explain it (observed Sharpe, deflated benchmarksr_star, the standardized expected-maximum multiplierexpected_max_z,var_sharpe,n_trials, track-record length, skew, and kurtosis). The scalardeflated_sharpe_ratiokeeps its scalar return. Both gained an optionalbars_per_year:var_sharpeis per-observation by default, and passingbars_per_yearconverts an annualised Sharpe variance to per-observation internally. This closes the unit trap wherepath_metrics(..., bars_per_year=...)returns an annualised Sharpe whose variance does not match the per-observation contract DSR requires.path_metricsanddefault_backtest_metrics: reduce an(n_paths, n_samples)CPCV path matrix to a per-path DataFrame of Sharpe, Calmar, max drawdown, and total return in one call.CombinatorialPurgedCV.reconstruct_paths(fold_predictions): an instance method that assembles per-fold predictions into backtest paths without the caller restatingn_splits,n_test_groups, orn_samples. Complements the existing freereconstruct_pathsfunction.purgedcv.optuna_integration.TrialSharpeRecorder: an Optuna study callback that collects per-trial Sharpe ratios and reports thevar_sharpeand trial count thatdeflated_sharpe_rationeeds, plusn_effective()for the autocorrelation-adjusted count. Importing the module does not require Optuna; theoptunaoptional extra (pip install purgedcv[optuna]) installs it for the surrounding optimisation loop.effective_n_trials: estimates the number of independent trials behind a correlated search (TPE, CMA-ES) from the integrated autocorrelation time of the trial-performance series. Pass it todeflated_sharpe_ratioso a 6000-trial TPE run is not deflated as if all 6000 were independent, which otherwise crushes DSR to zero. Documented as a heuristic.- New example
examples/optuna_dsr_cookbook.py: the canonical end-to-end Optuna + Deflated Sharpe pattern (record per-trial Sharpe, deflate by the effective trial count, convert annualisedvar_sharpeviabars_per_year) that most users will write. - New example notebook
examples/backtest_overfitting_audit.ipynb: a seeded Optuna TPE search over a Ridge strategy on real BTC/USDT daily data, audited end to end with PBO,effective_n_trials,deflated_sharpe_ratio_full(withbars_per_year), andCombinatorialPurgedCV.backtest_pathspluspath_metrics. The search reaches an in-sample Sharpe of +2.5; the audit shows PBO 0.55, the 400 TPE trials worth about 25 independent bets, and a modest deflated Sharpe, while the per-path spread confirms the model family does carry real structure on this trending window. optunaadded to theexamplesoptional-dependency extra so the gallery runs withpip install purgedcv[examples].- Python 3.13 and 3.14 are now part of the CI test matrix and listed among the supported versions.
- New example notebook
examples/selection_regret_lcl.ipynb: on UK Low Carbon London smart-meter data,PurgedGroupKFoldpicks a model that deploys with 5 to 12% lower MAE on 12 truly unseen households than naive shuffledKFolddoes. Holds across 5 random seeds. Companion tosynthetic_leakage_proof.ipynb: same library, opposite framing (negative proof of fabricated skill there; positive demonstration of selection regret here). - New example notebook
examples/selection_regret_crypto.ipynb: same selection-regret protocol on daily BTC/USDT 2021-2023. The naive selector picks a deep RandomForest with apparent CV MAE 0.057; on 180 truly held-out bars it deploys at R² = -1.64 and Sharpe = -0.77.PurgedKFoldpicks Ridge withalpha=100(R² = +0.01, Sharpe = -0.26 on the same bars). Both strategies lose money over the sideways-down deployment window, but the naive pick loses three to five times more per unit of risk. The library's value on a no-edge market is loss avoidance rather than gain.
Changed¶
min_track_record_lengthnow returnsmath.infinstead of raising whenobserved_sharpe <= target_sharpe: no finite track record can establish a gap that is not there, and infinity is the well-defined answer. The return type is nowfloat(wrap inint(...)for a count when finite). Other input validation (alpharange, non-finite inputs) still raises.validate_timesnow requires a datetime-like or timedelta-like dtype forprediction_timesandevaluation_times. Numeric, string, and object-dtype series are rejected; convert withpd.to_datetimefirst.parse_horizonrejectsNaTand other missing horizons.- The splitter constructors and
assert_groups_disjointreject missing (NaN) group labels instead of grouping onNaN. purgeandapply_embargovalidate the horizon before the empty-index short-circuit, so a bad horizon fails fast even when the train or test set is empty.- The automated release job now runs only when a merge changes the
shipped package (
src/orpyproject.toml); documentation, CI, tooling, test, and example changes no longer publish a release. This generalises the documentation-only skip rule introduced in 0.0.9.
Fixed¶
deflated_sharpe_ratioreturned1.0for every input whenn_trials=1. The single-trial branch set the deflated benchmark SR* to-inf; it now uses SR* = 0, so DSR reduces toprobabilistic_sharpe_ratio(returns, 0.0). A losing strategy is no longer reported as certain skill. Covered by a regression test.WalkForwardSplit(window="sliding", train_size=0)silently used the entire history as the train window, becausearr[-0:]selects the whole array.train_sizeis now validated as a positive integer.purgeandapply_embargosilently accepted negative orNaThorizons; they now raiseValueError.reconstruct_pathsacceptedfold_test_indicesthat did not match the canonical CPCV group layout and produced a wrong path matrix; it now verifies the layout and rejects mismatches.CombinatorialPurgedCVacceptedn_splits > n_samples, which builds empty group blocks; this is now rejected at construction.- The metrics functions accepted non-finite inputs (infinite or
NaNreturns,benchmark_skill,var_sharpe, and the scalar arguments ofmin_track_record_length) and non-integern_trials, producing meaningless probabilities. They now raise.min_track_record_lengthwithalpha >= 0.5no longer returns an inflated length. - The splitter integer parameters (
n_splits,test_size,train_size,n_test_groups) are validated at construction; floats and booleans are rejected withTypeError.
0.0.9 - 2026-05-22¶
No library code changed in this release; the published wheel is identical to v0.0.7. The release carried CI and example tooling only.
Added¶
- The CI workflow detects whether a push touched files outside the
documentation surface and skips the automated release job for
documentation-only merges to
main, so editing documentation no longer publishes a PyPI release or bumps the version.
Fixed¶
- Import ordering in
examples/synthetic_leakage_proof.ipynb(ruffI001).
0.0.8 - 2026-05-21¶
Intermediate empty auto-release patch. No library, CI, or documentation changes.
0.0.7 - 2026-05-20¶
This project publishes a patch release automatically on every push to
main. Versions 0.0.5 and 0.0.6 were intermediate same-day auto-release
patches; this entry records the work delivered across 0.0.5 through
0.0.7.
Added¶
- JOSS submission paper at
docs/paper.md(docs/paper.bib) with the real ORCID baked in. - Hosted documentation site at
https://eslazarev.github.io/purged-cross-validation/ using
MkDocs Material + mkdocstrings, deployed by
.github/workflows/docs.yml. The PR check inci.ymlrunsmkdocs build --strict. - Community files required for JOSS verification:
CITATION.cff,CONTRIBUTING.md,CODE_OF_CONDUCT.md(Contributor Covenant 2.1),.zenodo.json, issue and pull-request templates. - Full-population UK Low Carbon London benchmark
(
tools/lcl_full_benchmark.py) — chunked enumeration over the raw ~8 GB corpus, K seeded subsamples of N households, mean ±95% t-interval. Real result (K=20, N=60, seed=0, 4,284 eligible households): temporal leak 1.60% (95% CI 1.27 – 1.94%), household leak 6.03% (95% CI 4.93 – 7.12%). End-to-end testtest_e2e_lcl_full_benchmark.py. - Controlled cross-validation benchmark (
tools/competitor_benchmark.py) recording model scores and label overlap on a shared synthetic task. Results describe the tested split configurations. Runs that cannot complete record the exception rather than an estimated score; they do not establish compatibility outside that environment. End-to-end testtest_e2e_competitor_benchmark.py. context7.jsonso the docs are indexable by Context7.
Changed¶
BaseTemporalSplitternow requires monotonicprediction_timesat construction (require_monotonic=True). Closes a silent train-from-future failure mode inWalkForwardSplit.purge,apply_embargo, and the diagnostics no longer collapse separated test blocks into one global interval. The newsrc/purgedcv/_intervals.pyprovidesoverlaps_any_half_open_intervalandpoints_in_any_closed_interval; both filters operate on the union of local per-row intervals. Fixes CPCV folds with non-adjacent test groups and the artificial-NaN paths (energy_demand_pjmnotebook now reports 0.0% NaN paths instead of 100%).- Embargo is now applied per test row (
[eval_i, eval_i + embargo]unioned across the fold) rather than only aftermax(eval). For contiguous PurgedKFold this is strictly more conservative; for CPCV with non-adjacent groups it is the correct generalisation.
Fixed¶
- Version desync between
pyproject.tomlandsrc/purgedcv/__init__.py. The release workflow now bumps both files (alpha-aware), and the new install-smoke testtest_packaging_metadata_versions_match_runtimefails CI if they drift again.
0.0.4 - 2026-05-17¶
By v0.0.4 the cumulative feature set of Plans A through D below was delivered.
Plan D — Domain D7: statistical metrics¶
probabilistic_sharpe_ratio(returns, benchmark_skill)— probability that the true Sharpe ratio exceeds a benchmark, corrected for non-normality (Bailey & López de Prado 2012).deflated_sharpe_ratio(returns, n_trials, var_sharpe)— PSR adjusted for multiple-comparison bias across independent strategy evaluations (Bailey & López de Prado 2014).min_track_record_length(observed_sharpe, target_sharpe, alpha, skew, kurtosis)— minimum sample size required for PSR to exceed1 − alphaat the observed Sharpe; the analytical inverse of PSR.
Plan C — Domain D6: CPCV backtest path reconstruction¶
reconstruct_paths(fold_predictions, fold_test_indices, n_splits, n_test_groups, n_samples)— pure function that combines the C(N,K) fold outputs into the C(N−1,K−1) canonical backtest paths described in López de Prado (2018) chapter 12.CombinatorialPurgedCV.backtest_paths(estimator, X, y)— convenience method that runs the full CPCV fit-predict loop and callsreconstruct_paths, returning an(n_paths, n_samples)float array withNaNfor unseen observations.
Plan B — Domains D4 + D5: splitter framework¶
BaseTemporalSplitter(D4) — abstract base class wiringpurge+apply_embargointo every fold produced by concrete subclasses; enforces group disjointness whengroupsare supplied.WalkForwardSplit(D5.1) — sliding-window and expanding-window walk-forward CV; configurabletrain_size,test_size, andstep;with_timesadapter for fluent construction.PurgedKFold(D5.2) — contiguous test folds tiling the index space, with purge and embargo applied; degrades to standardKFold(shuffle=False)at zero purge/embargo.PurgedGroupKFold(D5.3) — group-aware variant ofPurgedKFoldthat assigns whole groups to folds so no entity leaks across the train/test boundary.CombinatorialPurgedCV(D5.4) — exhaustive C(N, K) combinatorial fold enumeration for producing multiple backtest paths from a single dataset.- scikit-learn integration: every splitter satisfies the
sklearn.model_selectionsplitter protocol and works insidecross_val_score,GridSearchCV, andPipeline.
Plan A — Foundations: Domains D1 + D2 + D3 + D8¶
parse_horizon,horizons_overlap,validate_times(D1) — time and horizon utilities; strict validation of monotonicity, NaN-freedom, and chronological ordering.purge(D2) — drops training rows whose half-open label horizon[prediction_time, evaluation_time)overlaps the test horizon; implements AFML Section 7.4.1 Snippet 7.1.apply_embargo(D3) — drops training rows whoseprediction_timefalls in the post-test asymmetric embargo window[test_eval_max, test_eval_max + embargo].purgedcv.diagnosticssubmodule (D8) —assert_no_temporal_leakage,assert_embargo_respected,assert_groups_disjoint,compute_overlap_fractionfor auditing custom splits.- Exception hierarchy —
TemporalCVError,TemporalLeakageError,EmbargoViolationError,GroupLeakageError. - src-layout Python package using hatchling as the build backend.
- TDD test suite with hypothesis property tests; ruff + mypy strict + pre-commit quality gates.
Examples (added across 0.0.x development)¶
- Worked-example notebook gallery on real public data: PhysioNet ICU mortality, NASA C-MAPSS turbofan RUL, NOAA GHCN-Daily rainfall, PJM hourly load, Binance BTC/USDT, UK Low Carbon London smart meters, USGS earthquakes, UCI air quality, Premier League matches, and a controlled synthetic leakage proof.
0.0.3 - 2026-05-16¶
Development patch release.
0.0.2 - 2026-05-16¶
Development patch release.
0.0.1 - 2026-05-16¶
First PyPI release.