03 · Data and labels

Data splits and leakage

9 min read

Data leakage is when a model learns from information it would not have had at the moment it makes a real prediction. The usual case is an input, called a feature, that was computed after the outcome was known. A leaky model looks superb in testing and fails the day it goes live, so the design decisions it changes are how training examples are built and how the data is split for testing.

An everyday example

A football pundit who writes a prediction for Saturday’s match on Sunday morning, after reading the result, will be right every week. The column looks brilliant and it is worthless, because the information that made it right did not exist when the prediction was supposed to be made. Leakage is this mistake happening inside a data pipeline, where nobody notices because nobody is reading the Sunday paper on purpose.

Everything the pundit knew on Saturday morning is allowed, including every past result, the team sheets and the weather. Everything that happened after kick-off is off limits, however tempting. The dividing line is a moment in time. The discipline of this topic is drawing that line for every training example and checking that nothing from the wrong side crossed it.

The pundit knows what they read, while a pipeline does not know what it joined, and that is why the analogy breaks. A table of customer statistics updated every Friday carries no sign that it includes Thursday’s refund, so the check has to be built into how training rows are constructed rather than left to someone noticing. The running example from here on is the payment fraud system of the earlier topics.

Ask when a value became available

The question to ask of every input is whether its value was known at the moment the prediction would have been made. A payment made on Monday must be described with what the system knew on Monday, and a customer summary such as ‘disputes in the last 90 days’ that was computed on Friday may already include the dispute this very payment caused. Joining Friday’s summary onto Monday’s row hands the model the answer, even though the payments table itself was split cleanly by date.

There is a second, subtler boundary, which is the gap between when something happened and when the system could see it. A card payment at 9.59 am that reaches the feature pipeline at 10.05 am was not available to a prediction made at 10.00 am, so a training row for that prediction must leave it out even though it happened first. Rebuilding history faithfully means using the arrival time, the moment the value became visible, alongside the time the event occurred.

The practical tool is a point-in-time join, which means that for each training row you look up the version of each feature that was current at that row’s prediction time rather than the latest version in the table. It is more expensive than a plain join because the feature tables have to keep their history, and that cost is the reason feature history is a design requirement rather than a nicety.

The prediction time is an information boundary9.55 am, Known account state. 10.00 am, Payment scored. 10.05 am, Late event arrives. Next week, Dispute received. Nothing to the right of 10.00 am may appear in the inputs.The prediction time is an information boundary9.55 amKnown account state10.00 amPayment scored10.05 amLate event arrivesNext weekDispute received
Nothing to the right of 10.00 am may appear in the inputs.On a narrow screen, scroll the figure sideways to see every step.

Make the split match the future use

A split is the division of the dataset into the part the model learns from and the parts used to tune and judge it. For a system that predicts the future, split by time, so the model trains on the oldest months, is tuned on the month after, and is judged on the month after that, because that is the situation the live model will be in. A random shuffle lets Tuesday’s row sit in the training set while Monday’s row is in the test set, so the model is judged on a past it has already partly seen, which is the pundit reading Sunday’s paper by another route.

Leave a gap between the training and test periods when labels take time to arrive or when features are built over windows. With a 60-day dispute window, the last 60 days of the training period contain payments whose labels are still unknown, and a feature counting the last seven days of activity for the first test row reaches back into training data. A gap the width of the longest window keeps the two sides from touching.

The split also has to match who the model will meet. If the live model will serve returning customers, their earlier history in the training set is fair, and a split by time is enough. If it must also serve brand-new customers, keep a set of customers entirely out of training so you can measure how it does with no history, and check for near-duplicates such as the same image under two ids, which cross a split unnoticed.

Split by time, with a gapMonths 1 to 6, Training data. Month 7, Gap, labels maturing. Month 8, Tuning data. Month 9, Final test. The gap in month 7 keeps training and test data from touching.Split by time, with a gapMonths 1 to 6Training dataMonth 7Gap, labels maturingMonth 8Tuning dataMonth 9Final test
The gap in month 7 keeps training and test data from touching.On a narrow screen, scroll the figure sideways to see every step.

Keep fitting inside the training boundary

Preprocessing steps also learn from data, and they leak when they learn from the test set. Scaling a feature to a standard range uses the mean of the column, filling missing values uses an average, and building a vocabulary of categories uses the set of values seen. Each of these must be computed on the training part only and then applied unchanged to the tuning and test parts. Target encoding, which replaces a category by the average label of its rows, is the riskiest, because it writes the label into a feature, and it needs the training set itself to be split further so that no row sees its own label.

The test set leaks in a second way, through the people using it. Every time you try a change and keep it because the test score went up, the test set has shaped the model a little, and after fifty such decisions it is part of the training process. Use the tuning set for choosing cutoffs, settings and calibration, and open the test set once at the end, or better still, judge the final model on a period that arrived after all the decisions were made.

Investigate gains that are too convenient

A single new feature that lifts a metric from 0.80 to 0.97 is more likely a leak than a discovery. Look at where the feature comes from and when its value is written, remove it and measure on a later period, and trace a handful of rows back to the raw events to see what the model was shown. A field that is only filled in after a human review, such as a review decision code, is a classic case, and so is a missing value that is itself a signal, because the field stays empty until someone has investigated.

The explanation an interviewer wants names the information that could cross the line and the check that stops it, which for a historical join is a point-in-time lookup plus a few sample rows inspected by hand against the raw events. With that boundary in place, the next question is which of the inputs that are available in time are worth computing, which is the subject of the features topic.

Checking a suspicious gainBig jump, One feature, large gain. Find the source, When is it written. Remove it, Retest on a later period. Decision, Keep or drop. Most leaks are found at the second box.Checking a suspicious gainBig jumpOne feature, largegainFind the sourceWhen is it writtenRemove itRetest on a laterperiodDecisionKeep or drop
Most leaks are found at the second box.On a narrow screen, scroll the figure sideways to see every step.

Where people get confused

The two confusions about timing are both about where the boundary sits. A time-based split is believed to remove leakage, when a split separates rows from each other and most leaks live inside a row, in a feature that was computed with knowledge of that row’s own future, so the split and the point-in-time join solve different problems and both are needed. The time an event happened also gets treated as the time it was available. The pipeline that aggregates events runs on a schedule, so a value that happened at 9.59 am may not be visible until 10.05 am, and a training row that uses it at 10.00 am is using the future.

The remaining confusion is about results rather than timing, and it is celebrating a jump in offline numbers. Honest improvements from one feature are usually small, and a large jump should start an investigation rather than a deployment.

In the interview

Whenever you describe a feature built from history, interviewers test it with a feature that sounds plausible and is computed too late, and that exchange takes two or three minutes. Say the rule once, out loud, when you first mention features. For example, ‘every feature is computed with a point-in-time join so it only uses what was known at the prediction moment, and I would split by time with a gap for the label delay’. From then on, each feature you name can be checked against that rule.

Interview questions

A model that estimates a trip’s arrival time uses the average speed of a trip segment, computed after the trip ends. Is splitting the data by time enough to stop the leak?

No, because the leak is inside each row rather than between rows. The average speed of the segment was computed after the trip finished, so every training example already contains information from its own future, and splitting by time does nothing about that.

I would replace it with what was known at departure, such as the historical average speed for that segment at that hour and the live traffic estimate at the moment of the request. I would also pin down which prediction we are making, the whole trip before departure or the remaining time during the trip, because each has a different moment at which the information boundary falls.

Why not shuffle the data randomly into train and test, like in a textbook?

The live model will only ever see the future, and a random shuffle judges it on a past it has partly seen. If Tuesday’s payment is in training and Monday’s is in test, the model has learned from a day after the one it is being tested on, which never happens in production, so the test score is flattering.

I would split by time instead, training on the oldest months, tuning on the next, and testing on the one after, with a gap the width of the longest label or feature window, which for a 60-day dispute window is two months. The cost is that I have less recent data to train on, and I accept that because the score I get is one I can trust.

A feature counting a customer’s disputes in the last 90 days doubles your fraud recall offline. Do you ship it?

I would not ship it yet, because a doubling from one feature is the signature of a leak. I would check when the dispute count is written, since a dispute arrives weeks after the payment, and a count computed from today’s table includes disputes about the very payments in the training set.

The fix is a point-in-time version of the feature that only counts disputes filed before each payment’s prediction time, and I would expect a much smaller gain from it. I would trace five or ten rows by hand against the raw events before trusting even that version, and I would measure on a later period than the one used to build it.

What to practice next

Design these systems on the canvas and get your design graded.

These problems have you build the index sets for a split by hand, which is the place to notice that a shuffled split needs a time cutoff and a gap before it is safe for data like this.