Feature Scaling (Normalization & Standardization)
What you’ll learn
Section titled “What you’ll learn”- the three scaler formulas, each computed by hand on the same five numbers
- which models are destroyed by unscaled features, which are indifferent, and why — measured
- what a single outlier does to each scaler, quantified
- why
fitbelongs to the training set andtransformto everything - when to log-transform instead of scale
- that scaling never changes the shape of a distribution
Intuition
Section titled “Intuition”The housing data has median_income spanning 0.5 to 15 and population spanning 3 to 35,682.
Nothing is wrong with either column. The problem is that some algorithms treat “a difference of 1”
as meaning the same thing in both.
Two model families care intensely:
- Distance-based — KNN, SVM with RBF, k-means. Euclidean distance sums squared differences, so a feature with a range in the thousands drowns one with a range of 10.
- Gradient-based — linear and logistic regression via SGD, neural networks. Unequal feature scales stretch the cost surface into a ravine, and descent zig-zags instead of descending.
Two families do not:
- Tree-based — splits depend on the ordering of values, and scaling is monotone.
- Closed-form solvers — the Normal Equation solves exactly regardless of conditioning, within numerical limits.
flowchart TD A["Feature ranges differ
by orders of magnitude"] --> B{"How does the model
use the numbers?"} B -->|"distances"| C["KNN, SVM, k-means
SCALING REQUIRED"] B -->|"iterative gradients"| D["SGD, neural networks
SCALING REQUIRED"] B -->|"value ordering"| E["Trees, forests, boosting
no effect"] B -->|"closed form"| F["Normal Equation
no effect"]
The math
Section titled “The math”| Scaler | Centre | Spread | Output range | Outlier behaviour |
|---|---|---|---|---|
MinMaxScaler | min | range | exactly | Terrible — one point sets the range |
StandardScaler | mean | std | unbounded, mean 0, sd 1 | Poor — both statistics move |
RobustScaler | median | IQR | unbounded | Good — both statistics resist |
StandardScaler uses the population standard deviation (, not ), which is
why hand calculations must use rather than pandas’ default .
Worked example by hand
Section titled “Worked example by hand”Five values: .
Step 1 — the statistics.
Step 2 — apply each formula.
| min-max | standard | robust | |
|---|---|---|---|
| 1 | 0.00 | −1.4142 | −1.0 |
| 2 | 0.25 | −0.7071 | −0.5 |
| 3 | 0.50 | 0.0000 | 0.0 |
| 4 | 0.75 | 0.7071 | 0.5 |
| 5 | 1.00 | 1.4142 | 1.0 |
Step 3 — check the properties. Min-max lands exactly on . Standardised values have mean 0 and population standard deviation 1. Robust values are centred on the median with the interquartile range as the unit.
Step 4 — note what did not change. All three columns are perfectly evenly spaced, exactly as the original was. Scaling is an affine map: it slides and stretches the axis and can do nothing else.
What one outlier does
Section titled “What one outlier does”Reading the plot
Section titled “Reading the plot”| Scaler | Span of the ten normal values | Where the outlier lands |
|---|---|---|
MinMaxScaler | 0.0090 | 1.000 |
StandardScaler | 0.0315 | 3.162 |
RobustScaler | 1.8000 | 198.800 |
- Min-max is catastrophic. The single value of 1000 defines the range, so the ten real observations occupy 0.9% of the output interval. Any distance computed between them is now effectively zero.
- Standardisation is bad but less so. The outlier inflates , compressing everything else into a span of 0.03.
- RobustScaler keeps the real data readable. Median and IQR ignore the extreme value entirely, so the ten normal points keep a usable spread — and the outlier is left at 198.8, visibly flagged rather than silently absorbed.
RobustScaler is the right default whenever outliers are present and cannot be removed.
See it move
Section titled “See it move”Two features, wildly different ranges. Watch the cloud transform as the scalers are applied — and watch the shape stay exactly where it was.
Which models actually care
Section titled “Which models actually care”import numpy as np
import pandas as pd
from sklearn.ensemble import RandomForestRegressor
from sklearn.linear_model import LinearRegression, SGDRegressor
from sklearn.model_selection import cross_val_score
from sklearn.neighbors import KNeighborsRegressor
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.svm import SVR
housing = pd.read_csv(URL).dropna()
X = housing.select_dtypes("number").drop(columns=["median_house_value"]).values
y = housing["median_house_value"].values
sample = np.random.default_rng(0).choice(len(y), 3000, replace=False)
X, y = X[sample], y[sample]
models = {
"linear": LinearRegression(),
"sgd": SGDRegressor(max_iter=2000, tol=1e-4, random_state=0),
"knn": KNeighborsRegressor(10),
"svr": SVR(),
"forest": RandomForestRegressor(n_estimators=60, random_state=0, n_jobs=-1),
}
for name, model in models.items():
raw = -cross_val_score(model, X, y, cv=3,
scoring="neg_root_mean_squared_error").mean()
scaled = -cross_val_score(make_pipeline(StandardScaler(), model), X, y, cv=3,
scoring="neg_root_mean_squared_error").mean()
print(f"{name:<7} raw {raw:>22,.0f} scaled {scaled:>9,.0f} "
f"{100 * (scaled - raw) / raw:+7.1f}%")
# linear raw 69,721 scaled 69,721 +0.0%
# sgd raw 3,795,293,450,055,030 scaled 70,035 -100.0%
# knn raw 100,607 scaled 69,128 -31.3%
# svr raw 117,412 scaled 117,374 -0.0%
# forest raw 58,226 scaled 58,263 +0.1%Reading the table
Section titled “Reading the table”- SGD is the headline. Raw features produce an RMSE of — the optimiser diverged. Scaling turns a broken model into a working one, matching the closed-form solution to within 0.5%. Not an improvement; a repair.
- KNN gains 31%. Distance was being dominated by
population, which spans 35,000, whilemedian_incomespanning 15 contributed almost nothing. - Linear regression is byte-identical.
LinearRegressionuseslstsq, and least squares is invariant to affine feature transformations. - The forest is unchanged to within noise, exactly as the theory says.
- SVR barely moves here only because this sample happens to be dominated by one large-range feature either way; on the full dataset with an RBF kernel, scaling matters a great deal.
Fit on train, transform everything
Section titled “Fit on train, transform everything”from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
X_train, X_test = train_test_split(X, test_size=0.2, random_state=42)
scaler = StandardScaler()
scaler.fit(X_train) # learn mu and sigma from training data only
X_train_scaled = scaler.transform(X_train)
X_test_scaled = scaler.transform(X_test) # NOT fit_transform
print(X_train_scaled.mean(axis=0).round(6)) # ~0 by construction
print(X_test_scaled.mean(axis=0).round(4)) # near 0, but not exactly — correctThe test set’s scaled mean is not exactly zero, and that is the point. It was scaled by
parameters learned elsewhere, exactly as a live request will be. If your test data scales to a
perfect mean of zero, you called fit_transform on it and leaked.
Scaling is not transforming
Section titled “Scaling is not transforming”Scaling cannot fix skew, because it is affine. Six of the nine housing columns are heavily right-skewed, and no scaler touches that. A logarithm does:
import numpy as np
from scipy import stats
col = housing["population"]
print("raw skew:", round(float(stats.skew(col)), 3)) # 4.935
print("scaled skew:", round(float(stats.skew(
(col - col.mean()) / col.std())), 3)) # 4.935 — unchanged
print("log skew:", round(float(stats.skew(np.log1p(col))), 3)) # -1.044Standardising leaves the skew at 4.94. log1p brings it to −1.04 — overshooting into a mild
left skew, which is a reminder that a log is a fixed transform, not a tuned one. Different tools for different
problems: scalers equalise ranges, transforms reshape distributions.
| Situation | Reach for |
|---|---|
| Features on different scales, no outliers | StandardScaler |
| Outliers present | RobustScaler |
| Bounded input required (image pixels, some neural nets) | MinMaxScaler |
| Heavy right skew | np.log1p, then a scaler |
| Skew of unknown form | PowerTransformer(method="yeo-johnson") |
| Only the rank matters | QuantileTransformer |
Pitfalls
Section titled “Pitfalls”-
Why does scaling not affect a decision tree?
Scaling is monotone, so it maps every threshold to a corresponding threshold. The partition of the data — and therefore the tree — is identical.
pch.quizShowAnswer
B — A split tests whether a value is above or below a threshold, and any monotone rescaling preserves that ordering — Scaling is monotone, so it maps every threshold to a corresponding threshold. The partition of the data — and therefore the tree — is identical.
-
MinMaxScaler is applied to values 1 to 10 plus one reading of 1000. What happens to the ten normal values?
Min-max divides by max minus min, which the outlier sets to 999. The real data occupies 0.9% of the output interval and becomes numerically indistinguishable.
pch.quizShowAnswer
B — They are compressed into a span of about 0.009, because the outlier defines the entire range — Min-max divides by max minus min, which the outlier sets to 999. The real data occupies 0.9% of the output interval and becomes numerically indistinguishable.
-
Why must you call transform, not fit_transform, on the test set?
A single live request has no mean or standard deviation of its own. The scaler must replay the parameters it learned during training, and the test set has to be treated the same way.
pch.quizShowAnswer
B — Fitting on test data uses statistics that will not exist at prediction time, so the reported score cannot be reproduced in production — A single live request has no mean or standard deviation of its own. The scaler must replay the parameters it learned during training, and the test set has to be treated the same way.
-
Your feature has a skew of 4.94. You standardise it. What is the new skew?
Standardisation is an affine map: subtract a constant, divide by a constant. Skewness is invariant to both. Use a log or a power transform to change the shape.
pch.quizShowAnswer
C — 4.94 — unchanged — Standardisation is an affine map: subtract a constant, divide by a constant. Skewness is invariant to both. Use a log or a power transform to change the shape.
🧪 Try It Yourself
Section titled “🧪 Try It Yourself”Exercise 1 – Min-max scale by hand
Section titled “Exercise 1 – Min-max scale by hand”Exercise 2 – Standardise, and match scikit-learn
Section titled “Exercise 2 – Standardise, and match scikit-learn”Exercise 3 – Watch one outlier crush the rest
Section titled “Exercise 3 – Watch one outlier crush the rest”Exercise 4 – Fit on train, transform both
Section titled “Exercise 4 – Fit on train, transform both”Exercise 5 – Scaling cannot fix skew
Section titled “Exercise 5 – Scaling cannot fix skew”- Min-max maps to , standardisation to mean 0 and sd 1, robust to median 0 and IQR 1.
- Worked by hand on : all three produce evenly spaced output, because scaling is affine.
- Distance-based and gradient-based models need scaling; trees and closed-form solvers do not. SGD on raw housing features reaches an RMSE of ; scaled, it matches the exact solution.
- One outlier crushes the ten real values into a span of 0.009 under min-max and 0.032 under
standardisation, against 1.8 under
RobustScaler. - Fit on train, transform on everything. A test set that scales to exactly zero mean is evidence of a leak.
- Scaling cannot change skewness — 4.935 before and after. Use
log1porPowerTransformerfor that.
Exercise 6 – Which scaler survives one outlier?
Section titled “Exercise 6 – Which scaler survives one outlier?”Continue to Transformation Pipelines & Custom Transformers — assemble imputing, encoding and scaling into one object that cannot leak.
pch.coffeeTagline
pch.coffeeCtapch.feedbackHeading
pch.feedbackSubheading