ml-models / Data and evaluation / Preprocessing and feature scaling intro

Preprocessing and feature scaling

Models consume numbers, and the numbers you hand them are rarely in a shape they can use well: one column in the tens, another in the hundreds of thousands, a color written as a word. This page covers standardizing and min-max scaling, one-hot and ordinal encoding and log transforms, which models care about scale and which do not, and the one rule that keeps it honest: learn the statistics from training data, then apply them everywhere.

On this page
  1. Problem
  2. Intuition
  3. Mechanics
  4. Scaling numeric features
  5. Who cares about scale
  6. Encoding categories
  7. Skewed features and log transforms
  8. fit versus transform
  9. Worked example
  10. Five people, two features
  11. Who is nearest to the 35-year-old?
  12. A log transform on income
  13. Fit on train, apply to test
  14. One-hot versus ordinal
  15. Implementation
  16. Tradeoffs
  17. Check yourself
  18. Connections
  19. Read first
  20. Read next
  21. References

Problem

A learner sees an array of numbers and treats them as equally meaningful. It cannot know that one column is in years and another in dollars, so a feature measured in large units can dominate anything that adds, compares or takes steps in proportion to raw size: a distance, a dot product, a gradient. And words such as "red" or "blue" are not numbers at all; encoding them badly (as 0, 1, 2) invents an order that is not there.

Preprocessing is the set of simple, mostly invertible transformations that put features into a form a model can use. They are cheap and easy to get subtly wrong, usually by letting information from the test data reach the training step.

Intuition

Imagine measuring how far apart two houses are on a map whose east–west axis is marked in meters and whose north–south axis is marked in kilometers. If you ignore the labels and subtract numbers, the east–west gap, being a number in the thousands, swamps the north–south gap, even when the houses are equally far apart in both directions. Scaling redraws the map so a step of "typical size" along either axis counts the same.

Encoding is the opposite problem: giving a label a position in space without implying a distance that does not exist. Red is not "one less than green"; one-hot encoding puts every category on its own axis, equally far from the others.

Fit statistics on training data, then transform every split The training rows flow into a fit step that learns a mean and a standard deviation per column. Those two numbers flow into transform steps applied to the training rows, the validation rows and the test rows. An arrow from the test rows to the fit step is crossed out because test data must never influence the fitted statistics. train rows fit: mean, std transform train transform val transform test to the model learned from train only test rows ×

The same two numbers, computed once on the training rows, are used for every split. Test rows never contribute to them.

Mechanics

Scaling numeric features

Standardization (z-score) subtracts the column mean μ and divides by the standard deviation σ: z = (x − μ) / σ. The result has mean 0 and spread 1, and keeps the shape of the distribution. Min-max scaling maps the observed minimum to 0 and the maximum to 1: x′ = (x − min) / (max − min). It bounds the range but is moved around entirely by a single extreme value. Robust scaling uses the median and the interquartile range instead of the mean and standard deviation, so outliers have little say. Max-abs scaling divides by the largest absolute value and keeps zeros as zeros, which suits sparse data.

A constant column has zero spread; scikit-learn treats a zero scale as 1 rather than dividing by zero.

Who cares about scale

  • Gradient descent. With features on very different scales the loss surface is a long narrow valley: one learning rate is too large for the steep direction and too small for the shallow one, so training zigzags and crawls. Standardized inputs give rounder bowls and faster convergence.
  • Distance-based models (k-nearest neighbors, k-means, SVMs with an RBF kernel). The distance is a sum of squared differences, so the largest-unit column decides it.
  • Regularized linear models. Ridge and lasso penalize the size of coefficients, and a coefficient's size depends on the feature's units.
  • PCA. It finds the directions of greatest variance, which means the biggest-unit feature unless you standardize.
  • Trees and tree ensembles do not care. A split asks "is x > t?", and any monotone rescaling of x moves t without changing which rows fall on each side.

Encoding categories

  • One-hot creates one 0/1 column per category. No order is implied. Cost: width grows with the number of categories, which hurts for thousands of them.
  • Ordinal maps categories to integers 0, 1, 2…. It is right only when the categories really are ordered (small, medium, large). Otherwise a linear model reads "blue = 0, red = 2" as a distance.
  • Categories are sorted, so the column order is alphabetical or numeric, and a category that appears only at prediction time must be handled explicitly (scikit-learn's handle_unknown).

Skewed features and log transforms

Incomes, prices, counts and durations are often right-skewed: a few huge values stretch the mean away from the typical row. Taking a logarithm compresses the tail, turns multiplicative differences into additive ones, and makes the spread more even. PowerTransformer (Box-Cox and Yeo-Johnson) learns the power that best makes a column look Gaussian; for counts that include zero use log(1 + x).

fit versus transform

Every scikit-learn transformer has two steps. fit(X) learns the parameters from data (the mean and standard deviation, the category list, the min and max) and stores them as attributes ending in an underscore. transform(X) applies those stored parameters to any rows. fit_transform does both on one array. The rule is: fit on the training rows only, then transform train, validation and test with that same fitted object. Refitting on the test rows silently gives the test set a different ruler.

Five people before and after standardizing age and income Two scatter plots of the same five people. On the left, raw age and income: the points form a vertical column because income spans a hundred thousand while age spans under forty. On the right, after standardizing both columns, the points spread across a similar range on both axes. raw: age 22–60 (x), income 30k–250k (y) standardized (both axes about −2 to 2)

Same five people. In raw units the x differences are small compared with the y differences, so a distance sees almost only income.

ComponentShape/ParamsMeaningSource
StandardScaler with_mean=True, with_std=True; learns mean_, var_, scale_ of shape (n_features,) z = (x − μ) / σ per column; the default scaler for gradient and distance methods. _data.py
StandardScaler.transform X: (n_samples, n_features) Subtracts mean_ and divides by scale_; does not refit. _data.py
_handle_zeros_in_scale scale: (n_features,) Replaces a zero spread with 1 so a constant column does not divide by zero. _data.py
MinMaxScaler feature_range=(0, 1), clip=False x′ = (x − min) / (max − min), then mapped into the range. _data.py
RobustScaler quantile_range=(25.0, 75.0) Centers on the median and divides by the interquartile range. _data.py
PowerTransformer method="yeo-johnson", standardize=True Learns a per-column power transform that makes the column more Gaussian. _data.py
OneHotEncoder handle_unknown, drop, sparse_output; learns categories_ One 0/1 column per category, with no implied order. _encoders.py
OrdinalEncoder categories, handle_unknown, unknown_value Maps each category to an integer; correct only for truly ordered categories. _encoders.py

Worked example

Five people, two features

Ages are 22, 35, 47, 51, 60; incomes are 30000, 52000, 61000, 90000, 250000. The age column has mean 43.00 and standard deviation 13.22 (the population version, dividing by n, which is what StandardScaler uses); income has mean 96,600 and standard deviation 79,079. Standardized, the ages become -1.59, -0.61, 0.3, 0.61, 1.29 and the incomes -0.84, -0.56, -0.45, -0.08, 1.94. Min-max scaling gives ages 0.0, 0.34, 0.66, 0.76, 1.0 and incomes 0.0, 0.1, 0.14, 0.27, 1.0; note how the single large income squeezes the other four incomes into 0 to 0.27.

Who is nearest to the 35-year-old?

Person B is 47 and earns 61,000; person C is 22 and earns 30,000; person A is 35 and earns 52,000. On raw columns the Euclidean distance from A to B is 9,000.0 and from A to C is 22,000.0. The twelve-year age gap adds only 144 under the square root next to 81,000,000 for income, so the distance is income alone. After standardizing, A–B is 0.915 and A–C is 1.022. Both features now count: B is still nearer, but the margin falls from 2.44 times to 1.12 times, and with different people the order itself can flip. That change in who counts as near, not the arithmetic, is what scaling is for.

A log transform on income

The mean income is 96,600 but the median is 61,000: one 250,000 earner drags the mean far above the typical row. In base-10 logarithms the incomes are 4.477, 4.716, 4.785, 4.954, 5.398, with mean 4.866 and median 4.785. The gap between mean and median shrinks from 0.58 of the median to 0.02 of it, and the 250,000 row is now 0.44 above its neighbor instead of 160,000 above.

Fit on train, apply to test

Suppose the first four people are the training rows and the 60-year-old is the test row. The training ages have mean 38.75 and standard deviation 11.32, so the test row becomes (60 − 38.75) / 11.32 = 1.88. Had the scaler been fit on all five, that same person would have been 1.29. The two numbers differ because the ruler differs. In real use the test row should be scored with a ruler that existed before it arrived, which is the training one.

One-hot versus ordinal

The colors red, green, blue, green, red have sorted categories blue, green, red. One-hot gives each row a vector with a single 1: 0 0 1; 0 1 0; 1 0 0; 0 1 0; 0 0 1. Ordinal gives 2, 1, 0, 1, 2, which tells a linear model that red is twice as far from blue as green is, a relationship the colors do not have.

Implementation

from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.linear_model import LogisticRegression

pre = ColumnTransformer([
    ("num", StandardScaler(), ["age", "income"]),
    ("cat", OneHotEncoder(handle_unknown="ignore"), ["color"]),
])
model = Pipeline([("pre", pre), ("clf", LogisticRegression())])

model.fit(X_train, y_train)        # scaler and encoder learn from train only
model.score(X_test, y_test)        # test rows use the stored mean_, scale_, categories_

# by hand, the same rule:
scaler = StandardScaler().fit(X_train)       # learn mean_ and scale_
X_train_s = scaler.transform(X_train)
X_test_s = scaler.transform(X_test)          # same ruler, never fit on test

Putting the steps in a Pipeline makes the rule automatic: when you cross-validate it, the scaler is refit inside each training fold and never sees the validation fold.

Tradeoffs

  • Standardization or min-max? Standardization tolerates a few large values better and is the usual default. Min-max is handy when you need a bounded range (pixel intensities, inputs to some neural nets) but one outlier compresses everything else.
  • Outliers. Mean and standard deviation are themselves pulled by outliers; robust scaling or a log transform is safer when the column is heavy-tailed.
  • Interpretability. Scaled coefficients are per standard deviation, not per original unit; keep the fitted scaler to translate back.
  • One-hot width. A column with 10,000 categories becomes 10,000 columns. Sparse output, grouping rare categories or learned embeddings are the usual escapes.
  • Trees do not need scaling. Skipping it for them saves work, but a pipeline with a mix of model types often scales everything for uniformity; it is harmless.
  • Sparse data. Centering turns zeros into non-zeros and destroys sparsity; use with_mean=False or max-abs scaling.
  • Leakage. Scaling is the most common way test information gets into training: compute the statistics on all rows before splitting and the model has quietly seen the test set. See data splits for the splitting side of the same discipline.

Check yourself

Why does a decision tree give the same splits on raw and standardized features?

Standardizing is a monotone, per-column map. A split "x > t" and "z > (t − μ)/σ" partition the rows identically, so the tree's structure and predictions are unchanged.

You standardize the whole dataset, then split into train and test. What is wrong?

The mean and standard deviation used information from the test rows. Split first, fit the scaler on the training rows, and transform the test rows with it.

When is an ordinal encoding of a categorical feature acceptable?

When the categories have a genuine order and roughly meaningful gaps (sizes, grades), or when the model is a tree, which can split the integers into any groups it likes.

Connections

References

  • Hastie, Tibshirani and Friedman. The Elements of Statistical Learning, 2nd ed., section 3.4 (ridge regression and standardized inputs). Springer, 2009.
  • LeCun, Bottou, Orr and Müller. Efficient BackProp. In Neural Networks: Tricks of the Trade, 1998, on centering and normalizing inputs.
  • Box and Cox. An Analysis of Transformations. Journal of the Royal Statistical Society B, 1964; Yeo and Johnson, A New Family of Power Transformations to Improve Normality or Symmetry. Biometrika, 2000.
  • scikit-learn 1.9.1: StandardScaler, MinMaxScaler, OneHotEncoder, OrdinalEncoder.