How do I scale features correctly without leaking test data in scikit-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.