NoFutureData

A zero-mandatory-dependency temporal leakage guard for Python and Jupyter. Make the historical information boundary executable in CI.

Point-in-time

Separate event time from known_at and eligible_from, then use the optional pandas helper to join only vintages that were actually available.

Static guard

Scan Python and Jupyter for negative shifts/diffs, backward fills/interpolation, centered windows, forward/nearest as-of joins, same-dataframe whole-series feature aggregates, and negative absolute iloc.

Runtime proof

Mutate or delete only future rows and verify that already-produced historical outputs stay invariant.

Catch the leak before the metric looks amazing

Leaking
# tomorrow's price in today's feature
lead = price.shift(-1)

# future rows in a centered window
smooth = price.rolling(7, center=True).mean()
Guarded in CI
$ nofuture scan src notebooks
FAIL: 2 finding(s)
[SRC001] negative shift can read future rows
[SRC003] centered rolling windows can include future rows

Public planted-leak corpus

31/31reproducible cases pass

Eighteen planted leak cases and thirteen safe controls exercise every shipped semantic static rule (SRC001+), including the opt-in temporal-context rules and targeted suppression. The benchmark reports exact-case accuracy, finding precision/recall/F1, leak detection, specificity, and per-rule coverage. The corpus and runner live in benchmarks/ and run in CI on every push.

Notebook locations are executable evidence

24/24 · 12/12notebooks · paired rules

Each semantic source rule has one tiny leaking notebook and one paired safe notebook. CI verifies exact rule IDs, notebook cell numbers, canonical fixture contents, and source-only cleanliness for the context-gated rules.

Method ablation

0.50 → 1.00leak recall on the 6-case ablation

Static AST rules catch explicit known patterns. Prefix and future-mutation invariance also catch indirect future dependencies. The experiment keeps two safe controls clean and makes the boundary between the methods measurable.

Leakage can manufacture a great holdout

0.630 → 0.991causal → leaked holdout R²

A fixed-seed forecasting experiment keeps the OLS model and chronological split unchanged, then adds one unavailable future feature. R² jumps by 0.361 and RMSE falls to 0.158× the causal baseline. The experiment is synthetic and explicitly not a real-world effect-size estimate.

Context-dependent rules stay explicit

12/21 → 21/21source-only → declared temporal context

The external corpus now spans nine projects/backends: Freqtrade, pandas, scikit-learn, Polars, Xarray, NumPy, Dask, PySpark pandas, and Snowpark pandas. Existing negative-shift/diff semantics transfer across those dataframe implementations without backend-specific detector code; explicit context recovers single-level, nested, grouped/panel, and metadata-routing-capable scikit-learn CV helpers while paired TimeSeriesSplit controls stay clean. The NumPy negative-roll case remains a narrow context-only rule with a paired lag control.

Real reports stay falsifiable

2/2 · 1 FPreported leaks · known boundary

A separate GitHub-issue corpus turns real user reports into regression evidence without pretending every report is maintainer-validated. Layered static + availability checks surface both reported leaks, including one source-only miss. A Freqtrade maintainer-confirmed callback case remains deliberately visible as a static SRC009 false-positive boundary.

Interventions must be valid

13/13mutation-validity checks pass

Future-mutation tests reject counterfactuals that alter history, change row count, or violate a caller-supplied input contract. Bounded-probability, OHLC, normalized-vector, and monotonic cumulative experiments show domain-valid mutations keeping causal transforms clean while exposing planted future dependencies.

Conclusions must survive intervention choice

48/48sensitivity checks pass

Across four domains, three historical cut points, and two valid mutation strengths, every causal control stays clean and every planted future dependency remains detectable. No best-case intervention is selected after the fact.

Behavioral checks transfer to real pandas

18/18executable pandas cases match

Prefix and future-mutation invariance now cover ordinary, grouped/stateful, irregular missing-data, as-of alignment, resampling-boundary, and multi-column pandas transforms: nine causal controls stay clean and nine future-dependent transforms are detected under explicit future-only mutation contracts.

Revision order must not matter

120/120generated vintage checks pass

A fixed-seed generator creates 24 append-only vintage histories with explicit eligibility delays. Valid histories stay valid after row shuffling, early decisions cannot see not-yet-eligible vintages, post-eligibility decisions select the same latest vintage after reordering, and duplicate known_at values consistently produce REV001.

Shrink the counterexample

768/768property invariant evaluations pass

Hypothesis searches 64 generated revision histories across 12 invariants, including multi-column revision keys, null reasons, missing-key rejection, and null-to-value revisions. If one fails, the strategy shrinks the history toward a smaller counterexample while the fixed-seed suite remains available for exact reproduction.

Read the research brief → · Full evaluation, falsification criteria, provenance, and reproduction commands →

Make the dataset contract executable

$ nofuture audit-manifest temporal-contract.json
PASS: 0 finding(s), 3 row/line(s) scanned

Commit availability columns and revision keys as a small JSON contract, then fail CI when a dataset becomes temporally ambiguous. Manifest reference.

Use it in one workflow step

- uses: actions/checkout@v7
- uses: ORANGINGS/nofuturedata@v0.4.0
  with:
    path: src

Also available as a pre-commit hook, a Python API, a CLI, and SARIF output for GitHub Code Scanning.