Data scientist reviewing time-based model validation, feature availability, and retail return data on multiple office monitors

Build a Backtest That Only Knows What Was Available

Thu, Sep 17, 2026

A synthetic retail-return model has an uncomfortable property: its offline backtest gets better after the warehouse team repairs several months of customer-history data. The repaired values have older event dates, so a conventional chronological join treats them as historical. But the corrections were not actually published until weeks after the original predictions. The model is being credited for information that did not exist online when it supposedly made those decisions.

That is the failure this playbook is designed to find.

A time-ordered split answers one question: did training rows precede validation rows? Point-in-time machine-learning validation asks a stricter one: for every prediction and every model-fit cutoff, what information was genuinely knowable then? That requires separate evidence for event time, feature availability, correction history, prediction time, label maturity and fold-local fitting.

Throughout this article, documented behavior means behavior supported by cited software documentation. Proposed operating model means a validation contract recommended here. Unverified assumption means a statement that still requires evidence from the actual warehouse, feature pipeline, serving path or installed software version.

The objective is not to improve an accuracy score. It is to build a row-level temporal evidence ledger, deliberately attack it with late and corrected records, and reach a reproducible accept, hold or invalidate decision about an evaluation dataset.

Define the decision and the historical knowledge boundary

Start from the prediction decision, not from whatever tables happen to exist today.

Proposed operating model: our synthetic task predicts, at the moment an order is released for fulfillment, whether that order will enter an approved-return state during the next 30 days. The entity is order_id. The prediction timestamp is recorded in UTC. Customer-history and product-history features are retrieved as of that timestamp. Models are hypothetically retrained weekly. No production performance is claimed; every identifier, timestamp and value below is illustrative.

Before querying data, write down the estimand: what future population and decision does the offline score intend to represent? A backtest for repeat customers asks a different question from one that claims to generalize to entirely unseen customers.

Contract field

Synthetic definition

Required evidence

Owner

Prediction target

Return approved within 30 days after prediction

Label specification

Model/data owner

Entity

order_id; customer may repeat

Entity-key contract

Analytics engineering

Prediction time

Fulfillment-release timestamp, UTC

Serving/request record

ML engineering

Feature context

Only information available by prediction time

Historical snapshot or availability timestamp

Data engineering

Training cadence

Weekly synthetic retrain

Fit-cutoff manifest

Model owner

Evaluation purpose

Estimate future-period performance for the stated population

Fold specification

Model-quality reviewer

The key distinction is available information, not merely “rows with old dates.” A customer statistic calculated on May 31 but published on June 20 was not usable for a June 15 prediction, even though its business-effective date is May 31.

A reviewer should refuse to discuss model scores until these semantics are explicit. Otherwise an attractive result can be perfectly reproducible yet answer the wrong historical question.

Separate temporal leakage from ordinary model evaluation

Chronological validation is necessary for many temporal problems, but it does not prove point-in-time correctness. This article therefore starts where the statistical foundations of model validation leave off: after recognizing that split design matters, inspect whether each row itself contains knowledge from the future.

The scikit-learn 1.9.1 API documents TimeSeriesSplit(n_splits=5, ..., gap=0) and explains that gap is the number of samples excluded between training and test portions. It also states that equally spaced samples are required for its documented comparable-duration interpretation. Those defaults are API semantics, not a leakage certificate. See scikit-learn, TimeSeriesSplit, stable 1.9.1 documentation as accessed September 17, 2026.

Four failure classes should be kept separate:

Failure class

Example

Fixed by chronology alone?

Correct control

Future-feature leakage

May event corrected in July and joined into June row

No

Availability-qualified history

Fitting leakage

Scaler or feature selector fit on validation data

No

Fold-local fit

Target leakage

Feature directly encodes return disposition

No

Feature-semantic review

Legitimate later observation

June prediction scored after its 30-day outcome matures

Not an error

Keep features frozen; observe outcome later

The scikit-learn pitfalls guide defines leakage broadly as using information that would not be available at prediction time and recommends learning preprocessing only from training data. Its synthetic feature-selection demonstration uses 200 samples and 10,000 random features to show how preprocessing before splitting can create an optimistic result. That is a teaching fixture, not evidence that leakage occurs at any particular prevalence in real projects. See scikit-learn, Common pitfalls and recommended practices, current stable guide, publication date not stated, accessed September 17, 2026.

The important practitioner distinction is that a Pipeline protects the fitting boundary for estimators and transformations placed inside it. It does not inspect whether an input column was backfilled after the historical prediction. If the table presented to the pipeline already contains tomorrow's corrected customer history, a perfectly scoped pipeline faithfully learns from contaminated data.

Later labels are different. A prediction made on June 15 may be evaluated in August after the return window has closed. That is not future-feature leakage because the label is being used to measure a prediction that was already fixed. It becomes leakage only when information about that future outcome influences the model, features, preprocessing, threshold selection or training data before the relevant cutoff.

Give every relevant timestamp a precise meaning

A column called created_at is evidence only after its generating semantics are documented. Database insertion time, source-system creation, warehouse ingestion, batch recomputation and online visibility can all produce different timestamps.

Proposed schema:

Field

Meaning in this playbook

Null policy

event_time

When the underlying business fact was valid or occurred

Required for temporal features

available_time

Earliest time this exact feature value could be consumed by the prediction path

Required for approval; unknown means hold/exclude

prediction_time

Time the model input must have been knowable

Required

model_fit_cutoff

Latest information the training run was permitted to use

Required per fitted model

outcome_window_end

End of the target-observation horizon

Required

label_available_time

Time the completed target value became usable by training/evaluation logic

Required for training eligibility

revision_id

Identity of a correction/version of a historical fact

Required where mutable history exists

Store timestamps in an explicit timezone, preferably normalized to UTC, while retaining any source timezone needed to interpret local business cutoffs. Equality rules must be written down: this playbook uses availability at or before the cutoff as eligible, expressed as available_time <= prediction_time.

Separate event occurrence from feature availability

A fact can belong to the past and still arrive in the future.

Suppose a customer had three prior returns by May 31. The warehouse initially reported two, so customer_return_count_90d=2 was available on June 1. A reconciliation process discovers the third return on June 20 and rewrites the May 31 history to 3.

Both versions describe an event-valid period before a June 15 prediction. Only the first was knowable on June 15.

Version

Event-valid time

Available time

Value

Eligible at June 15?

Original

2026-05-31 23:59Z

2026-06-01 02:00Z

2

Yes

Correction

2026-05-31 23:59Z

2026-06-20 04:00Z

3

No

A latest-state warehouse that retains only 3 has destroyed evidence needed for exact historical reconstruction. An event timestamp cannot recreate the superseded value by itself.

Separate label occurrence from label maturity

Labels need their own knowledge boundary.

For the synthetic task, define the target as “approved return within 30 days following prediction_time.” To avoid treating an apparently negative order as final too early, this operating model declares the target mature only after the full 30-day window closes and the label pipeline publishes the completed state.

Time

Role

2026-06-15 12:00Z

Prediction fixed

2026-07-15 12:00Z

Thirty-day outcome horizon closes

2026-07-16 02:00Z

Synthetic nightly label table becomes available

2026-07-16 02:00Z onward

Label eligible for a later training cutoff

A model fitted July 10 cannot train on this target. A reviewer scoring the already-fixed June 15 prediction on August 1 can use it. Training eligibility and evaluation observability are deliberately different predicates.

Inventory history, snapshots and upstream ownership

Point-in-time validation often fails because nobody can prove what the historical table represents. Before implementing joins, inventory every source that contributes to a model input.

This is where upstream data and database ownership matters: the reviewer who needs an availability guarantee is rarely the person who controls source CDC retention, warehouse restatements or batch-publication timestamps.

Use a source evidence matrix:

Source

Claim needed

Strong evidence

Unknown that triggers hold

Order facts

Original event record preserved

Immutable event log / dated snapshot

Rows overwritten in place

Customer aggregates

Value visible by prediction cutoff

Publication timestamp plus revision history

Only latest aggregate retained

Product attributes

Historical version selected

Valid-from plus availability history

Current dimension joined retrospectively

Return labels

Label mature by fit cutoff

Label build timestamp and horizon logic

Only outcome date retained

Serving features

Offline reconstruction matches served values

Approved serving log/fingerprint

No historical serving evidence

An immutable daily snapshot can sometimes substitute for a native availability timestamp, but its resolution matters. A midnight snapshot cannot prove whether a feature was available at 10:00 or 23:00 unless the operational cadence makes that inference defensible.

Hold rule: when a materially predictive field has no trustworthy historical availability evidence and no immutable snapshot from which availability can be bounded, do not silently substitute event_time. Record the uncertainty. Exclude the field, narrow the evaluation claim, or keep the dataset on hold.

Unverified assumption: “warehouse created_at equals online availability” is not acceptable merely because the names sound compatible. Establish which process writes it, whether retries preserve or replace it, whether upstream backfills inherit old timestamps, and whether online serving reads the same publication path.

A plausible temporal reconstruction is useful for investigation. It is not evidence that the deployed model actually knew those values.

Build point-in-time joins that enforce the intended boundary

Three joins that look superficially similar can answer different questions.

Join type

Eligibility rule

Main risk

Latest-value join

Latest row visible today for entity

Historical restatements leak backward

Event-time join

feature_event_time <= prediction_time

Late arrivals/backfills may still leak

Availability-qualified join

Event rule and available_time <= prediction_time

Depends on trustworthy availability semantics

The general contract is independent of any feature store:

# Pseudocode: availability-qualified historical lookup
candidate_rows =
    feature_history[
        entity_key matches
        AND feature_event_time <= prediction_time
        AND feature_available_time <= prediction_time
    ]

eligible_row =
    latest candidate under documented event/version ordering

IF no trustworthy feature_available_time exists:
    mark eligibility = UNKNOWN
    do not substitute event_time silently

Current Feast documentation makes this distinction explicitly. By default, its point-in-time historical retrieval constrains feature event timestamps. If a source has a created_timestamp_column, that timestamp is used to deduplicate records sharing an event timestamp, but, by default, is not otherwise an availability cutoff. The documentation describes filter_by_created_timestamp=True, which adds created_timestamp <= entity_timestamp; rows with null created timestamps are excluded, and unsupported offline stores raise an error rather than silently ignoring the option. See Feast, Point-in-time joins, current reference, publication/feature-launch date not inferred here, accessed September 17, 2026.

That is documented behavior, but two qualifications are essential.

First, Feast's point-in-time join documentation says this reproduces availability only if the created timestamp actually represents when the value became available online. A generic source-creation timestamp is not automatically equivalent to serving visibility.

Second, support is offline-store dependent. Do not copy the following documented call shape into a production repository and assume it works merely because the current documentation shows it:

# Version-qualified API shape to verify locally before execution.
# Record Feast SDK version, offline-store implementation and capability test.
training_job = store.get_historical_features(
    entity_df=entity_df,
    features=feature_refs,
    filter_by_created_timestamp=True,
)

As of the research cutoff, PyPI lists Feast 0.66.0 as the latest release, uploaded August 21, 2026. A reproducible project should nevertheless pin the exact installed SDK and store plugin rather than rely on the word “current.”

Feast's point-in-time join documentation treats TTL as a separate rule: the historical lookup scans backward from each entity timestamp by the feature view's TTL. It is not measured backward from the current clock. Therefore TTL limits historical age; it does not prove that a feature had arrived by prediction time.

Construct folds around time, labels and deployment purpose

After row-level eligibility is defined, construct temporal folds. Do not reverse that order.

Write the intervals before viewing locked-test performance. For a weekly synthetic model, one candidate plan could be:

Fold

Training prediction interval

Fit cutoff

Validation prediction interval

Label rule

A

Jan 1–Mar 31

May 1 02:00Z

May 2–May 15

Train labels available by fit cutoff

B

Jan 1–Apr 15

May 16 02:00Z

May 17–May 31

Same

C

Jan 1–Apr 30

Jun 1 02:00Z

Jun 2–Jun 15

Same

Locked test

Predeclared history

Predeclared final fit

Later untouched period

Score only after labels mature

The dates are synthetic. The principle is to reproduce the intended training cadence and the amount of history actually available to each fitted model.

Choose a real temporal gap rather than an accidental sample gap

TimeSeriesSplit.gap is not a timedelta. scikit-learn documents it as a count of samples removed from the end of each training set before the test set, with default 0. The same API notes that equal spacing is needed for test folds to represent comparable durations.

Consequently, gap=48 means “48 rows,” not “48 hours,” unless the dataset has exactly one ordered sample per hour under the intended semantics.

For irregular retail events, define time masks directly:

train:
    prediction_time < validation_start - embargo_duration
    AND label_available_time <= fit_cutoff

validation:
    validation_start <= prediction_time < validation_end

Boundary test

Expected result

Feature available one microsecond after prediction

Exclude

Feature available exactly at prediction

Include if contract uses <=

Train label available one second after fit cutoff

Exclude from training

Event stream has 1 row one day and 50,000 next day

Timestamp masks still preserve duration

The gap or embargo should correspond to a documented dependency: label windows, overlapping aggregates, delayed upstream feeds or other contamination paths. Do not add an arbitrary gap and call the dataset safe.

Handle repeated entities and immature labels deliberately

Repeated customers are not automatically leakage. The correct handling depends on the estimand.

If production predicts another order for known customers, allowing earlier customer history can be appropriate. If the claim is performance on previously unseen customers, customer-level isolation is required.

The groups argument does not make TimeSeriesSplit group-aware: the current scikit-learn API documents groups as ignored for split.

Intended claim

Entity treatment

Future orders from existing customer population

Repeated customers may be valid; history must be point-in-time correct

New-customer generalization

Isolate customers across train/evaluation according to design

Unknown deployment population

Hold claim until estimand is specified

Similarly, immature labels are removed from training at that fit cutoff, not automatically from all later scoring. A prediction can remain in evaluation until its target becomes observable, provided the prediction and features are frozen.

Fit transformations only inside the permitted training scope

After temporal eligibility comes estimator hygiene.

The scikit-learn pitfalls guide recommends splitting before learned preprocessing and shows that a Pipeline helps ensure transformations are fit on the appropriate training subset during validation.

A minimal pattern for the already-audited feature matrix is:

# Illustrative structure; execute only after point-in-time eligibility is built.
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler
from sklearn.linear_model import LogisticRegression

numeric = ["price", "customer_return_rate_90d"]
categorical = ["category", "fulfillment_channel"]

preprocess = ColumnTransformer(
    transformers=[
        ("num", Pipeline([
            ("impute", SimpleImputer(strategy="median")),
            ("scale", StandardScaler()),
        ]), numeric),
        ("cat", Pipeline([
            ("impute", SimpleImputer(strategy="most_frequent")),
            ("encode", OneHotEncoder(handle_unknown="ignore")),
        ]), categorical),
    ]
)

model = Pipeline([
    ("preprocess", preprocess),
    ("classifier", LogisticRegression(max_iter=1000)),
])

# X_train and y_train must already satisfy the fold's temporal predicates.
model.fit(X_train, y_train)

The crucial phrase is already satisfy. Pipelines have no concept of warehouse revision history. If customer_return_rate_90d has been reconstructed from a July correction for a June prediction, the pipeline will not detect it.

Separate selection layers:

Stage

Data permitted

Imputer/scaler/encoder fit

Current fold's training rows

Feature selection

Current fold's training scope

Hyperparameter search

Training plus designated inner validation only

Probability threshold selection

Predeclared validation data, never locked test

Final test scoring

Exactly once under the locked policy

Model selection itself can induce leakage when repeated decisions are made against the final test period. A temporal audit therefore records not only rows and timestamps but also which results humans were allowed to inspect before freezing the candidate model.

Falsify the backtest with a synthetic retail timeline

Now build records intended to break the historical reconstruction.

All rows below are synthetic and illustrative. Let the focal prediction be 2026-06-15 12:00:00Z, and let one synthetic model-fit cutoff be 2026-07-10 02:00:00Z.

Record

Type

Event time

Available time

Value / label fact

Expected audit result

R1

Clean customer feature

Jun 14 20:00Z

Jun 15 02:00Z

prior-return rate = 0.08

Eligible

R2

Late-arriving feature

Jun 15 10:00Z

Jun 15 12:05Z

carrier-risk = 0.31

Exclude at prediction

R3a

Original history

May 31 23:59Z

Jun 1 02:00Z

return count = 2

Eligible

R3b

Backfilled correction

May 31 23:59Z

Jun 20 04:00Z

return count = 3

Exclude at prediction

R4

Training label

Window ends Jul 15 12:00Z

Jul 16 02:00Z

final return label

Exclude from Jul 10 fit

R5

Feature with unknown availability

Jun 10 00:00Z

NULL

loyalty tier = Gold

Unknown; hold/exclude

An event-only reconstruction admits R2 and R3b because both have event timestamps no later than the prediction. R2 is only five minutes late, but the magnitude of lateness is irrelevant to correctness: the feature either crossed the knowledge boundary or it did not.

R5 is equally important. Silently replacing a null availability timestamp with event time manufactures evidence that does not exist. Under this operating model it receives UNKNOWN, not TRUE.

Compare a clean historical row with a late correction

Keep the prediction fixed and compare R3a with R3b.

Record

Event-time rule

Availability rule

Final eligibility

R3a

Pass

Pass

Include

R3b

Pass

Fail

Exclude

This is the adversarial fixture every mutable aggregate should be able to survive. Introduce a correction whose business-valid time precedes the prediction but whose knowledge time follows it. If the corrected value appears in the reconstructed June feature vector, the join is not enforcing the intended boundary.

For Feast, the current point-in-time join documentation says filter_by_created_timestamp=True applies the extra created-timestamp condition where supported and excludes null created timestamps. It also says an unsupported offline store raises an error. Therefore the test plan must record the real Feast SDK, offline-store implementation and created-timestamp semantics before treating the flag as evidence of correctness.

A successful API call is not enough. The test fixture should assert the value: R3a must win at June 15; R3b must not appear.

Test fit-time labels and evaluation-time observation separately

R4 isolates the label question.

The June 15 prediction's 30-day window closes July 15 at 12:00Z, and the synthetic label pipeline publishes its finalized state July 16 at 02:00Z.

Use

Cutoff

R4 eligible?

Reason

Fit model

Jul 10 02:00Z

No

Label unavailable

Fit model

Jul 16 02:00Z

Yes under <= policy

Mature label available

Score frozen June prediction

Aug 1

Yes

Future outcome can now be observed

Add explicit edge cases: a label available exactly at the fit cutoff passes only if the contract uses inclusive <=; a timestamp lacking timezone interpretation fails schema validation; a null label-availability timestamp does not become eligible by assumption.

The expected result follows from the fixture specification; it is not an executed test result.

For the initial candidate dataset, any extract that uses current corrected history and event-time-only filtering should be invalidated, because R3b is known by construction to violate the historical knowledge boundary. A rebuilt availability-qualified extract that still contains R5 should remain on hold until that uncertainty is resolved or the row/feature is excluded under a documented policy.

Compare reconstructed features with serving-time evidence

A reconstruction is stronger when it can be checked against what the serving path actually saw.

Google's Rules of Machine Learning defines training-serving skew as differences between training and serving behavior and, in Rule 29, recommends saving serving-time feature values so they can be compared with training data. Rule 31 separately warns that joined tables can change between training and serving. The page was last updated August 25, 2025; accessed September 17, 2026. See Google for Developers, Rules of Machine Learning: Best Practices for ML Engineering.

For privacy-conscious validation, that principle need not mean indiscriminately logging every raw sensitive field. A controlled evidence record can contain approved feature values, hashes, bucketed values or a canonical fingerprint, depending on what is necessary and permitted.

Evidence field

Example purpose

prediction_id

Join audit record to prediction

prediction_time

Historical cutoff

feature_schema_version

Detect definition mismatch

feature_vector_hash

Detect reconstructed-value mismatch

Approved replay subset

Diagnose which fields differ

Retrieval code/version

Reproduce transformation

Online source revision

Tie value to serving state

For deterministic, non-sensitive features, a canonical serialization plus cryptographic hash can answer a narrow question: “Did the reconstructed vector equal the vector captured at serving?” A mismatch does not immediately tell you why. Possible causes include historical correction, different feature code, stale online materialization, serialization differences or a reconstruction bug.

Do not label every mismatch “model drift.” Drift concerns changes in distributions or relationships over time; a point-in-time mismatch is first an evidence-integrity problem.

This is deliberately narrower than the broader notebook-to-production workflow. The audit here asks whether an offline evaluation faithfully represents past information, not how to operate an entire deployment platform.

Unverified assumption: if no serving-time evidence was retained, offline reconstruction may be the best available approximation, but it cannot prove the exact feature vector the historical service consumed.

Add automated invariants and negative controls

Temporal rules become credible when deliberately bad rows make the build fail.

The strongest test suite mixes positive controls, boundary cases and negative controls:

Invariant

Synthetic adversary

Required outcome

Feature availability

Set available_time = prediction_time + 1s

Row rejected

Label maturity

Set label_available_time = fit_cutoff + 1s

Training row rejected

Join cardinality

Duplicate eligible versions

Fail unless ordering resolves uniquely

Correction history

Add late backfill with older event time

Backfill not selected

Missing availability

Set availability null

Unknown/excluded, never silently event-time

Fold-local fit

Pre-fit transformer globally

Test detects fitted state or workflow violation

Entity policy

Insert same entity across forbidden partitions

Isolation check fails

A compact implementation contract might look like:

ASSERT all(feature_available_time <= prediction_time)
       for included features

ASSERT all(label_available_time <= model_fit_cutoff)
       for rows used to fit

ASSERT one resolved feature version
       per (prediction_id, feature_definition)

ASSERT deliberately_future_feature_fixture IS EXCLUDED

ASSERT missing_availability_policy IS EXPLICIT

ASSERT preprocessing objects ARE FIT ONLY INSIDE fold training scope

The negative control matters. A test that merely confirms ordinary rows pass may be testing the wrong field or an always-true predicate. Deliberately future-date a feature and confirm that the evaluation build rejects it.

Likewise, inject two versions with identical event time and different correction times. Confirm that the version available before prediction is selected, not merely whichever row is newest today.

For irregular events, add boundary records immediately before, exactly at and immediately after every fold cutoff. For timezone-sensitive pipelines, test equivalent instants expressed in different offsets and reject naïve timestamps when the contract requires aware UTC values.

These are proposed tests, not executed results from the Refonte Learning environment or any external dataset. The audit artifact should store the test code, fixture revision and result when somebody actually runs it.

A broader data-science learning roadmap can provide surrounding skill context, but this temporal evidence suite should remain an engineering acceptance test, not a generic project checklist.

Interpret score changes without rewarding a contaminated dataset

When leakage is removed, the score may rise, fall or barely move. None of those outcomes determines whether the repair was correct.

Validity is upstream of performance.

scikit-learn's official lagged-feature example uses the Bike Sharing Demand dataset with 17,379 original records and contrasts a random train/test split with time-aware evaluation. In that specific worked example, the random split produces a more optimistic error estimate than the temporal procedure. The numbers belong to that dataset and implementation; they are not a universal “penalty” for chronological validation. See scikit-learn, Lagged features for time series forecasting, current stable worked example, publication date not stated, accessed September 17, 2026.

Report score changes alongside validity changes:

Report field

Why it matters

Original evaluation definition

Shows what was previously claimed

Rows/features removed

Quantifies audit impact

Exclusion reason

Separates leakage from missingness

Temporal cohort

Reveals period-specific behavior

Updated estimate

Measures model only after validity repair

Uncertainty

Avoids overinterpreting small differences

Locked-test status

Shows whether model decisions contaminated final evaluation

Do not praise an evaluation because it scores higher. If a higher score depends on a July correction being inserted into a June prediction, the result is unusable regardless of apparent predictive strength.

Conversely, a lower score after cleaning is not evidence that the model became worse. The estimator may be identical; what changed is the credibility of the measurement.

Reviewers should compare temporal cohorts that correspond to meaningful operational periods and report uncertainty appropriate to the task. This article intentionally does not prescribe a universal classification metric, confidence-interval method or business threshold because the exercise is about the information boundary, not a metric catalog.

Package a reproducible dataset and evaluation audit

A point-in-time backtest should be reconstructible by a reviewer who was not present when the data was built.

The audit manifest is the interface between model evaluation and data provenance:

Manifest field

Example content

Raw snapshot IDs

Immutable object/version IDs or table snapshots

Query revision

Git commit / SQL hash

Timestamp semantics

Definitions, timezone and inclusivity

Availability evidence

Source fields and producing pipelines

Correction policy

How superseded versions are selected

Fold definitions

Exact timestamp intervals

Label maturity rule

Outcome horizon plus availability predicate

Exclusion ledger

Row ID, field, reason, reviewer status

Preprocessing revision

Pipeline/code/package versions

Model revision

Model code and parameters

Environment

Python and dependency lock

Locked-test policy

Who can view results and when

Reviewer

Named accountable role, not merely team alias

The row-level exclusion ledger deserves first-class status. “Removed 1.7% of data” is less useful than knowing which records were late features, which lacked availability timestamps and which labels were immature.

For each exclusion, preserve a machine-readable reason such as FEATURE_AVAILABLE_AFTER_PREDICTION, LABEL_IMMATURE_AT_FIT, AVAILABILITY_UNKNOWN, AMBIGUOUS_CORRECTION_ORDER or ENTITY_POLICY_VIOLATION.

This is closely related to documenting a reproducible data-science project, but the audit's purpose is operational review rather than portfolio presentation. A reviewer should be able to reproduce the dataset without relying on screenshots, notebook cell state or undocumented manual fixes.

Recommended package:

Artifact

Acceptance evidence

dataset_manifest.yaml

Inputs and snapshot identities

timestamp_contract.md

Exact semantic definitions

folds.csv

Prediction/fit boundaries

eligibility_ledger.parquet

Row-level decisions

fixtures/

Late, corrected, null and boundary records

tests/

Automated invariants

dependency lock

Exact executable environment

review decision

Accept/hold/invalidate plus rationale

Record unresolved uncertainty rather than editing it out of the narrative. A reproducible audit can legitimately conclude that historical availability is unknowable.

Build the data-science foundations behind the method

Point-in-time validation sits on ordinary foundations used with unusual discipline: Python to manipulate and test timestamped records; exploratory analysis to discover duplicates and anomalous histories; statistics to distinguish an estimand from an observed sample; and predictive modeling to keep fitting, selection and evaluation scopes separate.

Foundation

Application in this audit

Python

Build deterministic eligibility predicates and fixtures

Exploratory analysis

Inspect lateness, nulls and correction multiplicity

Statistical reasoning

Define the population and evaluation estimand

Machine learning

Separate training, validation and locked testing

Model optimization

Tune only inside permitted folds

Project work

Package evidence so another reviewer can reproduce it

The verified Refonte Learning Data Science & AI Program page states a three-month duration with 12–14 hours per week and lists Python Data Science, Statistical Modelling, EDA and Data Visualization, Machine Learning and Predictive Modelling, Model Optimization, Deep Learning Methods and application to industry projects among its competencies.

Those foundations can support an exercise like this, but this article's bitemporal audit should be treated as an independent advanced application. The page inspected for this research does not establish a dedicated Feast module or this specific temporal-validation lab.

For readers strengthening those underlying skills, the Refonte Learning Data Science & AI Program provides the verified Python, statistics, exploratory-analysis and predictive-modeling foundations described above.

Decide whether the backtest is valid, conditional or unusable

The final review is an evidence decision, not a vote on whether the score looks reasonable.

Does a time split eliminate leakage? No. It prevents certain future-to-past training/test arrangements, but a historical row can still contain a feature backfilled after its prediction timestamp. The scikit-learn TimeSeriesSplit documentation defines ordering mechanics; it does not audit feature provenance.

Is event time enough? Not when records can arrive late or be corrected. Feast's point-in-time join documentation distinguishes its default event-time constraint from the optional created-timestamp availability filter.

Does a gap measured in samples mean days? No. TimeSeriesSplit.gap counts samples. A duration interpretation requires the corresponding sampling structure; irregular event data generally need explicit timestamp logic.

May future outcomes be used to score past predictions? Yes, once those predictions and their inputs were already fixed. Later outcome observation is how delayed targets become measurable. What cannot happen is allowing the future outcome, or information derived from it, to cross backward into the model or feature construction.

Use this decision table:

Decision

Evidence standard

ACCEPT

Every material feature has defensible point-in-time availability; fit labels were mature; correction history is resolved; fold-local fitting is verified; required negative controls have actually passed

HOLD

No demonstrated leakage, but important availability, label, entity, version or serving evidence remains unknown or tests have not run

INVALIDATE

Known future information is admitted, immature labels trained the model, locked-test information influenced fitting/selection, or historical reconstruction contradicts serving evidence without resolution

For the synthetic fixture in this article, the expected decision for a latest-state or event-time-only candidate extract is INVALIDATE: R3b is explicitly a correction unavailable until after the June 15 prediction, yet such a join would admit it. That conclusion follows from the fixture definition, not from an executed production test.

After rebuilding with availability filtering, removing or resolving R5, enforcing label maturity and adding fold-local preprocessing, the dataset should remain HOLD until its automated invariants and negative controls are actually executed. Only recorded passing evidence should move it to ACCEPT.

That is the standard worth defending: not that the backtest knows the past as the warehouse represents it today, but that it knows only what the model could have known then.