[ ] AI WORKBENCHLet’s talk
Machine Learning

Data Leakage in Machine Learning: How to Spot and Prevent It Before Deployment

Data leakage makes ML models look better in testing than they perform in real use. Learn the common causes, a worked example, and how to prevent it.

◷ 10 min read · · AI Workbench
Data Leakage in Machine Learning: How to Spot and Prevent It Before Deployment

A machine learning model can score very well in testing and still disappoint in real use. One common cause is data leakage: the model gets information during training or testing that it will not have when it makes real predictions. This article explains how leakage happens, how to find it, and how to design your evaluation so it gives you an honest score.

You do not need advanced maths. If you know that a model is trained on examples and then tested on other examples, you can follow along.

In this article

What data leakage is and why it matters

The scikit-learn documentation defines data leakage as using information to build a model that would not be available when the model makes predictions. The result is an overly optimistic performance estimate and weaker results on genuinely new data (scikit-learn, Common pitfalls). AWS gives a similar definition, framed around the moment of inference, when a deployed model receives a request (AWS Prescriptive Guidance).

Think of an exam. If a student sees the questions in advance, a high mark tells you about their memory, not their understanding. A leaked test set works the same way.

Leakage is not a rare edge case. Kapoor and Narayanan reviewed research fields that use machine learning and report finding errors in 17 fields, affecting 329 papers. They propose a taxonomy of 8 types of leakage (arXiv:2207.07048, submitted July 14, 2022). In a civil war prediction case study, they report that claims of complex models beating logistic regression did not reproduce once leakage was corrected. These are the authors' findings, and your own data may behave differently.

Before going further, a few terms. The training set is the data the model learns from. The validation set guides choices such as settings and features. The test set is the final check, used once the validation results look good (Google Machine Learning Crash Course). A hyperparameter is a setting you choose before training, such as how deep a decision tree may grow.

Leaks caused by preprocessing and feature selection

Preprocessing means preparing data for a model, for example scaling numbers to a common range or filling in missing values. Many people do this on the whole dataset and split afterwards. That is a leak, because the averages and ranges you computed already contain information from the test rows.

The scikit-learn guidance is direct: never call fit on the test data. Learn any transformation from the training subset only, then apply it to held-out data. For a mean, use the training mean, not the mean of all the data (scikit-learn, Common pitfalls).

The same page shows how large the effect can be. It builds 200 samples with 10,000 random features and a random yes/no target, so honest accuracy should be about 0.5. When it selects the best 25 features on all the data before splitting, test accuracy reaches 0.76. When it splits first and selects features on the training data only, accuracy is 0.5. The documented example shows that a leak can create apparent skill from pure noise.

The fix the documentation recommends is a Pipeline, which chains steps so each one is fitted only on the training portion. The cross-validation page shows the same pattern for scaling inside each fold (scikit-learn, Cross-validation). Cross-validation means splitting data into several folds, training on some and testing on the rest, and repeating so every fold gets a turn as the test set.

Duplicates, related rows and time

Even a correct split can leak if the rows are not independent. Three situations come up often.

Duplicate rows

If the same example appears twice, a random split can put one copy in training and the other in testing. Google's course describes a spam example where precision looked nearly identical on training and test data because duplicate emails had leaked into both. It also notes that an unusually low test loss can signal a mistake and should be rechecked (Google Machine Learning Crash Course). AWS adds that heavy duplication can cause overfitting to be mistaken for generalization.

Related rows

Often one person, device or machine produces many rows. The scikit-learn page says to use group-aware splitting when samples come in dependent groups, such as several samples per patient. GroupKFold keeps each group on one side of the split (scikit-learn, Cross-validation).

CrowdStrike, a security vendor, reports the same pattern in its own threat-detection work. Its post (dated August 11, 2025) says random splits overstated performance compared with splits grouped by machine. The data is internal and unpublished, so treat this as a vendor claim from one experiment. The post also says that on its final realistic holdout, a randomly split model scored higher, and the authors said they might still use a random split (CrowdStrike blog). The lesson is to measure the difference on your own data rather than assume it.

Time

For data ordered in time, nearby observations are related. A random split lets the model train on the future and be tested on the past. TimeSeriesSplit instead trains on earlier folds and tests on the next one (scikit-learn, Cross-validation). AWS gives a grocery example: a demand model trained on 2019 and tested on early 2020 would probably score well, yet shopping patterns had changed by deployment. It suggests an extra recent test set as a safeguard.

Features you will not have in production

A feature is an input column the model uses. Leakage also happens when a feature is filled in only after the outcome is known. Historical tables are often enriched after the fact, so a column can look available when it was not at prediction time.

AWS gives a credit example: a feature counting a customer's loans over the last six months is a risk if the model later scores a new customer with no such history (AWS Prescriptive Guidance). AWS also mentions point-in-time ("time travel") queries in its feature store for viewing data as it was at a given moment. That is a vendor product, but the underlying idea works anywhere: rebuild each feature as it would have looked on the prediction date.

A practical check is to ask of every column: "At the exact moment of prediction, would I have this value?" If the answer is "not sure", investigate before trusting the score.

Privacy and security implications

Evaluation leakage and privacy leakage are different problems, but both involve data crossing a boundary it should not cross. Here are the privacy and security points that matter in practice.

  • Models can memorize. Carlini and colleagues showed that an attacker could query GPT-2 and recover hundreds of verbatim training sequences, including names, phone numbers and email addresses. Extraction worked even for text that appeared in only one training document, and the authors report that larger models were more vulnerable (arXiv:2012.07805, submitted December 14, 2020). That paper covers language models, so do not assume the same rates for other model types.
  • Split copies are still personal data. If your raw table contains personal information, so do your training, validation and test files. Apply the same access controls to every copy.
  • Group keys can identify people. A customer or patient ID used for grouping is itself sensitive. Use a pseudonymous ID created for the project, not a real account number. This is a good practice from the author, not a documented requirement of the sources above.
  • Shortcut features can be unfair. A feature that only exists after a decision was made may encode past human decisions, not the underlying outcome.

A worked example

This is an illustrative scenario. I have not run this code or measured any results, so no scores are claimed.

A subscription app wants to predict which customers will cancel next month. The table customer_months.csv has one row per customer per month. A customer who has been around for a year contributes twelve rows. The target column is cancelled_next_month.

Prerequisites: Python 3, pandas, and scikit-learn. The documentation I checked is for scikit-learn 1.9.1; the classes used here (Pipeline, GroupKFold, cross_val_score) are long-standing, but check your installed version with python -c "import sklearn; print(sklearn.__version__)". Nothing here deletes data. It reads the CSV and never writes to it.

  1. Remove exact duplicates and hold out the newest months. Input: the full table. What happens: you drop repeated rows, sort by month, and set aside the last two months as a final test set that nothing touches until the end. Result: a development set from the past and a test set from the future. Why it matters: the test set now mimics real use, where the model predicts months it has never seen.
  2. Choose a group key. Input: the development set. What happens: you use customer_id as the group. Result: every customer's rows will stay on one side of any split. Why it matters: without this, the model can recognize a customer from training rows and look good on that same customer's test rows.
  3. Audit the features. Input: the column list. What happens: for each column you ask whether it is known on the prediction date. A column such as cancellation_reason fails this test because it exists only after someone cancels. Result: a shorter, safer feature list. Why it matters: a post-outcome column can make a model look nearly perfect while being useless in production.
  4. Build one pipeline. Input: the safe features. What happens: scaling and the classifier are chained, so scaling is learned inside each training fold only. Result: one object you can cross-validate. Why it matters: this follows the scikit-learn guidance on fitting transformations to training data only.
  5. Cross-validate with groups. Input: the pipeline and the groups. What happens: GroupKFold trains and scores five times, never splitting a customer across train and test. Result: five scores and their average. Why it matters: this is your honest development estimate, and it is usually lower than a naive random split would give.
  6. Score the held-out months once. Input: the final pipeline and the newest months. What happens: you fit on all development data and score the untouched test set a single time. Result: one number you can compare to step 5. Why it matters: a large gap suggests a distribution change or leftover leak. Do not tweak the model and score again, or the test set stops being independent.
import pandas as pd
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import GroupKFold, cross_val_score
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler

df = pd.read_csv("customer_months.csv").drop_duplicates() df = df.sort_values("month")

dev = df[df["month"] < "2026-09"] # development data test = df[df["month"] >= "2026-09"] # final holdout, use once

features = ["logins_last_30d", "tickets_last_30d", "months_active"] model = make_pipeline(StandardScaler(), LogisticRegression(max_iter=1000))

scores = cross_val_score( model, dev[features], dev["cancelled_next_month"], groups=dev["customer_id"], cv=GroupKFold(n_splits=5), ) print(scores.mean(), scores.std())

How to check it: confirm that no customer ID appears in both a training and a test fold, for example by looping over GroupKFold(n_splits=5).split(...) and testing that the two ID sets do not overlap. If it fails: if the CSV will not load, check the file path and column names. If the month comparison behaves oddly, check that month is stored as text in YYYY-MM form or as a true date. If scores look suspiciously high, return to step 3 and look for a column that depends on the outcome.

Common mistakes

  • Scaling or imputing before the split. Put every learned step inside a Pipeline and fit it on training data only.
  • Selecting features on all the data. The scikit-learn example turned random noise into 0.76 accuracy this way. Select features inside the pipeline.
  • Splitting rows randomly when rows belong to people or machines. Group by the entity and use GroupKFold or StratifiedGroupKFold.
  • Shuffling time-ordered data. Train on the past and test on the future, with TimeSeriesSplit or a date cutoff.
  • Tuning against the test set. Google's course warns that validation and test sets wear out with repeated use. Tune on validation data and keep the test set for the end.
  • Trusting a result that is too good. An unusually strong score deserves a hunt for duplicates and outcome-dependent columns before any celebration.

Recap

  • Data leakage means the model used information it will not have at prediction time, which inflates test scores.
  • Split first, then fit every learned step, including scaling, imputing and feature selection, on training data only. Pipelines make this automatic.
  • Match the split to the data: remove duplicates, group related rows, and respect time order.
  • Audit each feature by asking whether it is known on the prediction date.
  • Treat every copy of your data as sensitive, and remember that models can memorize training text.
  • Use the test set once. A suspiciously high score is a reason to investigate, not to ship.

Research Sources

Keep reading

All articles