Every few months I hear the same story. A model looks excellent in testing, goes to production, and quietly underperforms. Nobody changed the code. Nobody changed the data source. What changed is that in production, the model finally had to work with the information it would actually have had. In testing, it had been reading the future.
This is lookahead bias, and in my experience it is less a modeling mistake than a data engineering one. The model did exactly what it was trained to do. The pipeline handed it answers it should never have seen.
What Point-in-Time Actually Means
A dataset is point-in-time correct when every feature attached to an example is exactly what you could have known at the moment the prediction would have been made. Not what is true now. Not what the source system says after corrections. What was knowable then.
That sounds obvious. The trouble is that almost every default in a modern data stack works against it. Tables are updated in place. Dimensions get overwritten. Late records get backfilled under their original dates. Joins pick up the current value of a field because the current value is the only one stored. None of these is a bug on its own. Together, they turn a training set into a time machine.
Where the Future Leaks In
I have run into leakage in five forms often enough that I now check for each one before I trust any evaluation.
Restated data. Source systems correct themselves. Financial figures get restated, lab results get amended, quality records get fixed after review. If the corrected value replaces the original, your model trains on a number that did not exist when the decision was made.
Late-arriving records. A transaction that happened on Monday but landed in the warehouse on Thursday is usually stored under Monday. A model predicting on Tuesday could not have seen it, but a training set built from that table will hand it over anyway.
Overwritten dimensions. A customer segment, a product category, a supplier rating. If the dimension table only holds the current value, every historical event gets joined to today's answer.
Survivorship. Building a training universe from the entities that exist today quietly removes the ones that failed, churned, or were delisted. The model learns from a history where nothing ever goes wrong.
Global preprocessing. Normalizing or imputing with statistics computed over the full dataset, test period included, leaks the future through the back door. So does a random train and test split on time-ordered data.
The Pattern That Fixes It: Two Clocks
The most useful idea I know here is to give every record two timestamps. Event time is when something happened in the world. Knowledge time is when your platform learned about it. Data modelers call this bitemporal data, and it turns a vague principle into a rule you can enforce.
The rule is simple. For a prediction made at time T, a feature value is valid only if its knowledge time is at or before T. Building a training set then becomes an as-of join: for each example, take the latest version of each feature whose knowledge time does not pass the prediction time. In pandas that is merge_asof. In Spark it is a window over knowledge time, partitioned by key. Feature stores sell the same thing as a point-in-time join.
In a medallion lakehouse, this history belongs in the silver layer. Append rather than overwrite, and keep dimensions as Type 2 slowly changing dimensions with valid-from and valid-to columns. Gold tables can serve current values to dashboards, but training should read from history. One caution: table time travel in Delta Lake or Snowflake is not a substitute. It is bounded by retention settings, and it records when the table changed, not when the facts became known.
Evaluation Has to Respect Time Too
Perfect features can still be undone by the split. A random split on time-ordered data lets a model train on Tuesday and get tested on Monday. Walk-forward evaluation fixes this: train on a window, test on the next one, roll forward, and only ever use data available before each test window. I built a walk-forward harness for a market data pipeline I run, and the discipline that matters is that every feature computed for a given date uses data from that date or earlier, enforced in code rather than by convention.
The same idea shows up outside time series. In my research on drug-target interaction, I split molecules by Murcko scaffold instead of at random, because a random split puts near-identical chemical structures on both sides and rewards the model for recognizing rather than generalizing. Different domain, same failure. The test set has to look like the future, not the past in a disguise.
Make It a Test, Not a Habit
Conventions erode. What lasts is an assertion that fails the build. These are the three checks I put into pipelines that feed models:
First, no feature joined to an example may have a knowledge timestamp later than that example's prediction timestamp. If one does, the job fails.
Second, rebuild a training snapshot for a past date twice, a week apart. If the features changed, history is being rewritten somewhere upstream.
Third, treat a sudden jump in offline accuracy after a pipeline change as an alarm, not a win. Investigate before you celebrate.
If a model looks brilliant offline, the first question is not what it learned. It is what it was allowed to see.
Point-in-time correctness is not glamorous, and nobody demos it. But it is the difference between an evaluation you can trust with a real decision and one that falls apart in the first week of production. It is a data engineering problem, which means data engineers are the ones who can fix it.