Q&A library Python

How do I scale features correctly without leaking test data in scikit-learn?

Python Advanced data-leakagescalingscikit-learn

Short answer

Fit the scaler on the training data only, then use transform on both training and test sets. Fitting on all the data before splitting leaks test information and inflates your scores.

The short version

Split first, then fit the scaler on the training data alone with fit, and apply it to both sets with transform. Never call fit (or fit_transform) on the test data, and never scale before splitting. Doing so lets information about the test set influence the model, which is called data leakage and gives falsely optimistic results.

The correct order

from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler

X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)

scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)   # learn from train only
X_test_scaled = scaler.transform(X_test)          # apply to test

The scaler learns the mean and standard deviation from the training data, then applies those same numbers to the test data. The test set never influences what the scaler learned.

The mistake that leaks

# WRONG: scales using information from the whole dataset
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)   # sees test rows too
X_train, X_test = train_test_split(X_scaled, ...)

Here the scaler's mean and standard deviation were computed from every row, including the test rows. The model has therefore been given a peek at the test distribution, so its measured performance is better than it will be on genuinely unseen data. This is subtle because the code runs fine and the scores look great; the problem only appears as disappointing results in production.

Why even scaling counts as leakage

It feels harmless, because scaling does not use the target. But the test set's spread has quietly shaped the transformation applied to the training set, so the two are no longer independent. The same argument applies to imputing missing values, selecting features and reducing dimensions: any step that learns something from the data must learn it from the training set only.

The robust fix: a pipeline

Doing this by hand is easy to get wrong, especially inside cross-validation. A Pipeline binds the scaler to the model so the scaler is refitted on the training portion of every split automatically.

from sklearn.pipeline import make_pipeline
from sklearn.linear_model import LogisticRegression

model = make_pipeline(StandardScaler(), LogisticRegression())
model.fit(X_train, y_train)
model.score(X_test, y_test)

Now fit scales using the training data and score applies that same scaling to the test data, with no way to leak by accident. This is the recommended approach, and it becomes essential once cross-validation is involved.

The rule to remember

Fit on train, transform on both. If a preprocessing step ever sees the test data during fitting, your evaluation is compromised. Wrapping preprocessing and model in a pipeline makes the correct behaviour the default and removes the chance of a silent leak.

Need this solved on your own data?

The library is free and always will be. When the problem is bigger than an answer, we take on the work.

Start a project