Skip to content

Machine Learning Foundations

Feature scaling and encoding

Putting inputs on a comparable footing

A five-minute preprocessing step that turns a slow, zig-zagging descent into a straight sprint to the answer - and turns words into numbers a model can actually learn from.

Suppose one feature is a house's area in square feet (values around 2,000) and another is its number of bedrooms (values around 3). To the learning algorithm those are just two numbers, but one is a thousand times bigger than the other - and that mismatch quietly wrecks training. Gradient descent starts zig-zagging down a narrow canyon instead of walking straight to the answer, and it crawls. The fix is almost embarrassingly cheap: rescale every feature to a comparable range before training, and turn each category into a number in a way that doesn't invent a false order. Do that, and the same optimizer converges in a handful of steps instead of hundreds.

One step size, two very different features

Gradient descent takes one step size and applies it to every weight at once. If area is measured in thousands and bedrooms in single digits, the weight on area feels an enormous slope while the weight on bedrooms feels a gentle one. A step big enough to move the gentle direction overshoots the steep one and bounces; a step small enough to be safe on the steep one barely nudges the gentle one. No single step size is good for both - unless you first make the two features the same size.

Two ways to put features on the same footing

There are two standard rescalings, and you will meet both constantly. Min-max normalization squeezes a feature into the interval by subtracting its minimum and dividing by its range:

Standardization - the z-score from Lesson 59 - recenters a feature to mean and rescales it to standard deviation :

Min-max is handy when you need bounded inputs (pixels in , say); standardization is the default for most models because it is robust to differing spreads and doesn't let a single outlier squash everything else into a corner. Either way the goal is identical: no feature gets to dominate simply because its raw numbers happen to be large.

Why unscaled features make descent zig-zag

Picture the cost as a landscape over the two weights (Lesson 11). For a linear model the landscape is a bowl, and its contour lines - the level sets of equal cost - tell you the shape. When the features share a scale, the bowl is round and the contours are circles: the downhill direction points straight at the minimum, so descent walks there directly. When the features differ wildly in scale, the bowl is a long, narrow canyon and the contours are stretched ellipses. Now the steepest-downhill direction points mostly across the canyon rather than along it, so each step slams into the far wall and the path zig-zags, inching toward the minimum. Flip the toggle below and watch the canyon become a bowl.

Cost contours & the descent pathtoggle · Step · Run
← click to normalize the two features and re-run the descent
weight w₁ →weight w₂ ↑start
cost contours (equal-cost rings)gradient-descent paththe minimum (target)
condition κ12
step size α0.16
steps taken0
dist to min4.37

Raw features - κ = 12, so α is capped tiny. Press Run and watch the path zig-zag slowly toward the minimum.

Condition number: the exact reason it's slow

The bowl's shape is set by the Hessian of the cost - for linear regression that's , and the curvature along each weight is proportional to the scale of that feature. Two features whose scales differ by a factor give curvatures that differ by . The ratio of the largest curvature to the smallest is the condition number . Two facts follow. First, the largest safe step size is capped by the steepest direction, - so one big feature forces a tiny on everyone. Second, the number of steps to converge grows like . Standardizing makes every feature unit-variance, driving : the bowl becomes round, can be large, and descent finishes in a few steps. In the demo above, goes from to and the safe jumps from to - that is the whole speed-up.

The aha: same math, rounder bowl

Scaling doesn't change where the minimum is or what gradient descent does - the update rule is untouched. It only changes the geometry of the bowl the optimizer walks in. A round bowl means the negative-gradient arrow points straight at the bottom from everywhere, so a single learning rate serves every direction at once. That is why one cheap preprocessing pass can turn a hundred zig-zag steps into three straight ones.

Turning categories into numbers

Models do arithmetic, so a categorical column like color ∈ {red, green, blue} has to become numeric. The tempting shortcut - red , green , blue - is a trap: it tells the model that green sits "between" red and blue and that blue is "three times" red, an ordering that doesn't exist. One-hot encoding avoids the fiction by giving each category its own column: red , green , blue . No category is larger than another; they are simply different directions. (Ordinal encoding as is fine only when the categories are genuinely ordered, like small < medium < large.)

Fit the scaler on the training set only - or you leak

The mean, standard deviation, min, and max used to scale must be computed from the training set alone, then applied unchanged to validation and test data. If you compute them over the whole dataset, information about the test examples seeps into training - data leakage - and your reported accuracy is optimistically wrong. Same rule for one-hot: the set of categories is learned from training data, and an unseen category at test time maps to all-zeros, not a brand-new column. In scikit-learn this is exactly why you .fit() on train and only .transform() on test.
import numpy as np

# --- learn the statistics from TRAIN ONLY, then reuse them ---
mu    = X_train.mean(axis=0)      # per-feature mean   (z-score)
sigma = X_train.std(axis=0)       # per-feature std
def zscore(X): return (X - mu) / sigma       # -> mean 0, std 1

lo    = X_train.min(axis=0)       # per-feature min    (min-max)
hi    = X_train.max(axis=0)
def minmax(X): return (X - lo) / (hi - lo)   # -> into [0, 1]

X_train_s = zscore(X_train)             # never call .mean() on X_test!
X_val_s   = zscore(X_val)               # reuse the TRAIN mu, sigma
X_test_s  = zscore(X_test)

# --- one-hot a categorical column: 3 categories -> 3 columns ---
cats   = ["red", "green", "blue"]        # the vocabulary, learned from TRAIN
onehot = np.array([[c == k for k in cats] for c in colors], dtype=float)
# "green" -> [0, 1, 0]   an unseen color -> [0, 0, 0]
Scale and encode - fit on train, apply everywhere

Check yourself

Rescaling both features reshapes the stretched cost contours into:

The statistics used to scale features must be computed from:

Recall min-max vs z-score, and why scaling speeds up gradient descent.

Try to state it, then check.

Lock it in

  • Gradient descent applies one step size to every weight, so features on wildly different scales force a tiny safe and make the path zig-zag down a narrow canyon.
  • Two rescalings: min-max maps a feature into ; standardization (z-score) gives mean , std and is the default because it resists outliers.
  • Scaling never moves the minimum or changes the update rule - it rounds the cost bowl, driving the condition number so one learning rate serves every direction.
  • Categories become numbers by one-hot encoding (each category its own direction), never 1/2/3 unless the order is genuinely ordinal.
  • Fit the scaler statistics and category vocabulary on the training set alone; computing them over the test data leaks information and inflates the score.

Primary source

Andrew Ng's Machine Learning Specialization introduces feature scaling with the exact "house size vs. bedrooms" contour picture used here - including why a mismatched scale makes gradient descent oscillate. For a hands-on companion with interactive widgets on normalization, standardization, and categorical encoding, work through Google's Machine Learning Crash Course.

Ask your teacher

This lesson is the z-score from Lesson 59 doing real work: standardizing a feature to mean , std is exactly why gradient descent converges fast instead of crawling down a canyon. And it is the same idea you'll meet deep inside transformers - LayerNorm (Lesson 35) re-standardizes activations to mean , std at every layer, keeping the optimization landscape well-conditioned all the way through a hundred-layer network. Ask me why standardization usually beats min-max in practice, how one-hot encoding relates to the embedding lookups you saw in Lesson 12, or what "batch norm" changes about when the normalization happens.