a mannequin will get to see info it shouldn’t have entry to but, and that hidden peek makes its rating look higher than it truly is. Kaufman, Rosset, Perlich, and Stitelman describe this clearly of their 2012 paper in ACM Transactions on Information Discovery from Knowledge, “Leakage in Knowledge Mining: Formulation, Detection, and Avoidance.” One instance they provide is the INFORMS 2010 Knowledge Mining Problem. Rivals had been imagined to predict inventory value actions utilizing solely a coaching set, then be scored on a separate take a look at set. A number of rivals discovered which actual shares had been hidden within the take a look at set by matching patterns towards public finance knowledge. That allow them pull in info the take a look at set was supposed to maintain hidden, and their scores regarded higher than their fashions truly deserved.
My very own leak was a lot smaller and far simpler to overlook. It didn’t contain determining hidden inventory identities. It got here from operating two strange strains of preprocessing code within the mistaken order.
I educated a small neural community, utilizing scikit-learn’s MLPRegressor, to foretell automobile costs from a used automobile dataset. The primary model of this undertaking reported a robust take a look at rating: an R squared of 0.887. After I mounted the order of two strains, the trustworthy rating was 0.767. The mannequin didn’t worsen. My first measurement had been quietly studying a part of the reply key.
The quantity that regarded stable
R squared is a standard rating for the way effectively a mannequin predicts a quantity. It runs from 0 to 1. Greater is healthier. An R squared of 0.887 means the mannequin explains about 89 % of the variations in automobile value on knowledge it had by no means educated on.
The unique model of this undertaking reported a take a look at R squared of 0.887 and a take a look at error, measured as imply squared error, of about 6.9 million. I reran the very same code, with the very same settings, to verify whether or not that quantity was actual, and it matched. For a small dataset of below 200 vehicles, that may be a genuinely robust consequence, the type that ends a homework project with out additional questions.
The experiment
This undertaking began from a leak I discovered in a automobile value mannequin I had constructed for a category project. That model used a dataset with no license listed anyplace on the web page it got here from, so as a substitute of reproducing it right here, I rebuilt the identical pipeline on UCI’s Vehicle dataset, donated by Jeffrey Schlimmer in 1987 and sourced from the 1985 Ward’s Automotive Yearbook. It’s launched below a Artistic Commons Attribution 4.0 license, which allows reuse like this with credit score. The 2 datasets describe the identical type of factor: one row per automobile, with its specs and its value.
Every row lists a automobile’s make, physique model, engine specs, gas kind, an assigned insurance coverage threat ranking, and value. After dropping a column with a lot of lacking values and eradicating the rows with any remaining lacking values, 193 vehicles stay. Of the 24 columns left to foretell value from, 16 are numeric measurements, comparable to engine dimension, horsepower, and curb weight, and eight are categorical, comparable to gas kind, drive wheel, and engine location.
The objective is regression: predict value, which is a quantity, somewhat than sorting vehicles into classes. I used a multilayer perceptron, or MLP. An MLP is a kind of neural community: a mannequin manufactured from layers of small linked models, the place every unit combines its inputs and passes the consequence ahead. This one has two hidden layers of 64 models every:
MLPRegressor(
hidden_layer_sizes=(64, 64),
max_iter=1000,
random_state=42,
)
I cut up the 193 rows into three teams, matching the identical proportions as the unique project: 60 % for coaching, 20 % for validation, and 20 % for take a look at, which involves 115 coaching rows, 39 validation rows, and 39 take a look at rows. The coaching set is what the mannequin truly learns from. The validation set is supposed to verify the mannequin whereas it’s nonetheless being developed. The take a look at set is a closing, one time verify, meant to be checked out solely as soon as the mannequin is completed.
Two strains, run within the mistaken order
Earlier than any mannequin can use this knowledge, the uncooked columns must be ready. Numeric columns like horsepower must be placed on a standard scale, since some numbers use small ranges and others use giant ones. Categorical columns like gas kind must be transformed into numbers a mannequin can use, generally by turning every class into its personal 0 or 1 column, a way referred to as one scorching encoding. This preparation step is usually referred to as preprocessing, and in scikit-learn it’s often achieved with a small chain of steps referred to as a pipeline.
Right here is the preprocessing code, within the order it ran to supply the robust wanting consequence above:
# Outlier dealing with by way of IQR capping
for col in numerical_cols + ["price"]:
Q1 = df[col].quantile(0.25)
Q3 = df[col].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
df[col] = df[col].clip(decrease=lower_bound, higher=upper_bound)
# Preprocessing pipeline
preprocessor = ColumnTransformer(transformers=[
("num", StandardScaler(), numerical_cols),
("cat", OneHotEncoder(handle_unknown="ignore"), categorical_cols),
])
# Rework the information
X_processed = preprocessor.fit_transform(X)
# Practice-validation-test cut up
X_train_val, X_test, y_train_val, y_test = train_test_split(
X_processed, y, test_size=0.2, random_state=42
)
Nothing about this code alerts an issue, which is a part of the purpose. It’s the similar form of mistake I initially discovered within the class project this undertaking relies on, simply written towards a in another way sourced dataset right here so the consequence might be shared freely.
Learn the code so as, and watch what occurs earlier than the cut up. Step one caps excessive outlier values in every column, utilizing a standard statistical rule: something greater than 1.5 occasions the interquartile vary (the center 50 % of the information) above or beneath the standard vary will get pulled in to that boundary. That boundary is calculated from each row within the dataset, together with rows that may change into the take a look at set two steps later. StandardScaler, which rescales numeric columns, calculates its scaling numbers from each row too. OneHotEncoder, which builds the 0 and 1 columns for classes, learns its checklist of classes from each row as effectively. Solely in spite of everything three of those steps end does train_test_split divide the information into separate items.
None of those three steps appears to be like like a mistake by itself. Capping outliers is a standard factor to do. Scaling numbers is a standard factor to do. Calling fit_transform, which each learns the transformation and applies it in a single step, is the conventional manner to make use of a scikit-learn transformer. The error is completely about order: each one among these steps was allowed to take a look at the take a look at rows earlier than the take a look at set was imagined to exist.
It is a quieter type of drawback than the extra apparent model of a leak, the place a single line compares validation labels with themselves and produces an unimaginable good rating. A leak like this one doesn’t announce itself. The mannequin nonetheless makes actual predictions. The take a look at rows weren’t copied into coaching, they solely quietly nudged the scaling numbers, the outlier boundaries, and the class checklist. The ensuing rating appears to be like like an strange robust consequence, not a damaged one. That’s precisely what makes it value checking for, even when nothing appears to be like mistaken.
The corrected pipeline
The repair is easy to state: cut up the information first, then match each preprocessing step on the coaching rows solely. The validation and take a look at rows ought to solely ever be remodeled utilizing numbers already realized from coaching, by no means used to assist calculate these numbers.
X_train_val, X_test, y_train_val, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
X_train, X_val, y_train, y_val = train_test_split(
X_train_val, y_train_val, test_size=0.25, random_state=42
)
# IQR bounds computed from the coaching rows solely
bounds = {}
for col in numerical_cols:
Q1 = X_train[col].quantile(0.25)
Q3 = X_train[col].quantile(0.75)
IQR = Q3 - Q1
bounds[col] = (Q1 - 1.5 * IQR, Q3 + 1.5 * IQR)
def apply_bounds(body):
body = body.copy()
for col, (decrease, higher) in bounds.objects():
body[col] = body[col].clip(decrease=decrease, higher=higher)
return body
X_train = apply_bounds(X_train)
X_val = apply_bounds(X_val)
X_test = apply_bounds(X_test)
# Cap outlier costs within the coaching goal solely, utilizing bounds from
# coaching costs. Validation and take a look at costs are left as noticed,
# since scoring towards a clipped goal would cover actual errors.
price_q1, price_q3 = y_train.quantile(0.25), y_train.quantile(0.75)
price_iqr = price_q3 - price_q1
y_train = y_train.clip(
decrease=price_q1 - 1.5 * price_iqr, higher=price_q3 + 1.5 * price_iqr
)
preprocessor = ColumnTransformer(transformers=[
("num", StandardScaler(), numerical_cols),
("cat", OneHotEncoder(handle_unknown="ignore"), categorical_cols),
])
X_train_p = preprocessor.fit_transform(X_train)
X_val_p = preprocessor.remodel(X_val)
X_test_p = preprocessor.remodel(X_test)
mlp = MLPRegressor(hidden_layer_sizes=(64, 64), max_iter=1000, random_state=42)
mlp.match(X_train_p, y_train)
Discover the sample: fit_transform solely ever runs on X_train. Validation and take a look at knowledge solely ever undergo remodel, which applies numbers already realized, with out studying something new from them.
What the leak was value
The corrected take a look at R squared is 0.767, down from 0.887. The corrected take a look at error is about 26.2 million, practically 4 occasions the unique 6.9 million. In phrases which might be simpler to image, the standard prediction error, measured as root imply squared error, grew from about $2,630 to about $5,120 per automobile.

Twelve factors of R squared is just not a rounding error. It’s the distinction between a consequence value highlighting in a report and a consequence you’d name stable however strange. The entire hole comes from which rows had been allowed to affect the scaler, the outlier boundaries, and the class checklist earlier than the mannequin was ever examined.
One trustworthy restrict on this quantity: 193 rows is a small dataset, and the take a look at set is barely 39 vehicles. A spot of precisely twelve factors of R squared is particular to this dataset, this cut up, and this random seed. On a bigger dataset, the identical mistake would seemingly produce a smaller hole, since an even bigger coaching set strikes the scaler’s imply and the outlier boundaries much less when a couple of take a look at rows are faraway from the calculation. The route of the error, an inflated rating, holds no matter dataset dimension. The precise dimension of the inflation doesn’t.
The validation set no one requested for
A validation set solely earns its place if one thing truly will get scored on it. It’s a widespread sufficient shortcut to construct one, in the identical line that builds the take a look at set, after which by no means name predict on it, leaving the mannequin’s match high quality unchecked between coaching and the ultimate take a look at.
Scoring the corrected pipeline on all three splits exhibits why that issues:

A mannequin often suits its coaching knowledge just a little higher than it predicts on new knowledge. That’s regular, up to some extent, and the dimensions of the hole is what tells you whether or not that time has been handed. Right here, coaching R squared and take a look at R squared differ by about 0.10, a average hole for a mannequin with two 64 unit hidden layers educated on solely 115 rows. That hole is just not proof of a damaged mannequin. It’s proof that the validation set, as soon as it’s truly used, does the job it was constructed for.
Studying the corrected consequence


Neither chart is dramatic, and that may be a good signal. A accurately measured consequence ought to appear like an honest mannequin with a standard quantity of error, not a damaged one. Fixing the leak was by no means about making the mannequin look worse. It was about making the rating describe what the mannequin truly does.
A brief guidelines for pipeline order
Here’s a brief guidelines for preprocessing order:
- Break up the information earlier than becoming something that learns from it: scalers, encoders, lacking worth fillers, outlier boundaries.
- If a
ColumnTransformerorPipelinecallsfit_transform, verify precisely what knowledge it was referred to as on. - By no means name
matchorfit_transformon validation or take a look at knowledge. Solely ever nameremodelon it. - If a validation set is constructed, use it. An unused validation set is just not a safeguard, it’s a step somebody meant to take and didn’t.
- Examine practice, validation, and take a look at scores collectively. A single take a look at rating, by itself, can not present you the hole that issues.
What I realized
An analysis bug that compares labels with themselves is unimaginable to defend when you take a look at it: actually 100% accuracy from evaluating an array with itself. A leak in preprocessing order is quieter, as a result of fit_transform on the complete dataset is legitimate, working Python, produces a plausible quantity, and passes an off-the-cuff learn of the code. The one option to catch it’s to ask a particular query about each step: which rows had been used to calculate this imply, this boundary, this checklist of classes, and does that match which rows had been later used to check the mannequin.
The lesson is easy to state and straightforward to skip in follow: a reported rating is the output of a full pipeline, not only a mannequin, and each step in that pipeline, together with the analysis code, is one thing to verify earlier than trusting the quantity it produces. That verify extends to the information itself. The category project that first confirmed me this leak used a dataset with no listed license, which is its personal type of factor to catch earlier than publishing a consequence constructed on it.
Reproduce the experiment
The companion folder accommodates the dataset, each pipeline variations, saved charts, and metrics:
car-price-regression-leakage-bug/
code/
car_price_leakage_comparison.py
knowledge/
car_price_dataset.csv
media/
leaky_vs_corrected_r2.png
corrected_train_val_test_r2.png
corrected_actual_vs_predicted.png
corrected_residuals_vs_predicted.png
outputs/
leaky_vs_corrected_metrics.csv
run_summary.json
From the article folder, set up the packages and run each pipelines:
python3 -m venv .venv
supply .venv/bin/activate
pip set up pandas numpy scikit-learn matplotlib seaborn ucimlrepo
python code/car_price_leakage_comparison.py
The script downloads the UCI Vehicle dataset immediately if an area copy is just not already saved, prints practice, validation, and take a look at scores for each the leaky and corrected pipelines, and saves the comparability desk and all 4 charts.
Chosen sources
- Kaufman, S., Rosset, S., Perlich, C., & Stitelman, O. (2012). Leakage in knowledge mining: Formulation, detection, and avoidance. ACM Transactions on Information Discovery from Knowledge, 6(4), Article 15.
- Schlimmer, J. (1985). Vehicle [Dataset]. UCI Machine Studying Repository. Licensed below CC BY 4.0.
- scikit-learn: ColumnTransformer
- scikit-learn: Pipeline and stopping knowledge leakage
- scikit-learn: MLPRegressor















