Machine Learning · Insper

Data Preprocessing

Turn raw columns into numbers an algorithm can use without being fooled

Scaling, shape, encoding and imputation, measured on the Titanic passenger list

The algorithm only sees numbers

It minimizes the cost with the numbers you hand it. If they are badly prepared, it minimizes the wrong thing, with full confidence.

incompatible scales

the column with the biggest numbers dominates distances and penalties

Fare 0 to 512, SibSp 0 to 8

text

there is no arithmetic with "female"

Sex, Embarked, Cabin, Ticket, Name

holes

most implementations refuse NaN

Age missing for 19.9%, Cabin for 77.1%

The plan: scaling → shape → encoding → imputation, each measured on a small example, then on the Titanic (891 passengers of the 1912 voyage). EDA looked at the table; this lesson changes it.

Every step learns something (a mean, a median, a list of categories), and it must learn it from the training rows only.

The path of this lesson

  1. Scaleunits decide distances; four scalers
  2. Fit on train onlyfit once on the training rows, transform the rest
  3. Fix the shapelog, Box-Cox, Yeo-Johnson
  4. Encode categoriesordinal, one-hot, target encoding
  5. Impute the holeswhy it is missing; one value, other columns, an indicator
Each stage answers one question: which column owns the distance? → where may a step learn its numbers? → what can a scaler not fix? → how does a category become a number? → what goes in the holes?
Stage 1 · which column owns the distance?

Three passengers, by hand

passengeragefare
ABraund, Mr. Owen Harris (3rd class)22.0£7.25
BConnors, Mr. Patrick70.5£7.75
CPears, Mrs. Thomas (1st class)22.0£66.60
Predict. Using the Euclidean distance on (age, fare), which passenger is closest to A: the 70-year-old B, or the 22-year-old C?

In raw units, the 70-year-old wins

pairage differencefare differencedistance
A–B48.5 years£0.50\(\sqrt{48.5^2 + 0.5^2} = 48.50\)
A–C0 years£59.35\(\sqrt{0^2 + 59.35^2} = 59.35\)

48.5 years is a smaller number than 59.35 pounds. Nothing says a year and a pound should weigh the same: it is just what happens when you subtract raw numbers.

Predict. Now measure each column in standard deviations, \(z = (x - \mu)/\sigma\), with \(\sigma_{\text{Age}} = 14.52\) and \(\sigma_{\text{Fare}} = 52.88\). Does the answer change?

Standardized, the ranking flips

Raw units: B is nearest to A; standardized: C is nearest

A–B: \(48.5 / 14.52 = \mathbf{3.34}\) std

A–C: \(59.35 / 52.88 = \mathbf{1.12}\) std

\(\mu\) and \(\sigma\) computed over the 714 passengers whose age is known.

Neither answer is "true": the raw one is an accident of the units; the standardized one is a choice that gives each column the same say.

Simulator: the unit decides the neighbors

Predict. Slide the income unit from thousands to cents. Do the 5 nearest neighbors of the red point stay the same? Then press standardize.

Which column owns the distance?

For two random rows, the expected squared difference in a column is twice its variance: each column's share of the distance is its share of the total variance.

share of the squared distance (raw)

Fare92.9%
Age7.0%
SibSp, Parch0.0%

A k-NN on raw columns is, in practice, a k-NN on fare. Standardized: 25% each.

a forward look: gradient descent

With a squared loss, the curvature along each direction is set by the variance of the features. Age and fare:

raw208.62, 2798.59ratio 13.4
standardized0.9, 1.1ratio 1.2

A long narrow valley: a learning rate safe for the steep direction crawls along the flat one.

Standardized, the bowl is nearly round: gradient descent can use one learning rate for every weight (the Gradient Descent lesson).

Which models care about scale

care

  • distance-based: k-NN, k-means, kernel SVMs
  • variance-based: PCA (the largest-scale feature wins by default)
  • gradient-based and penalized: linear and logistic regression, neural networks, Ridge/Lasso

do not care

Trees, random forests, gradient boosting: they split on one feature at a time ("is Age < 12?").

Any increasing transformation moves the threshold but sends the same rows to each side.

Four scalers: \(x' = (x - \text{center}) / \text{spread}\)

scalerformulacenterspreadresult
StandardScaler\((x - \mu) / \sigma\)meanstandard deviationmean 0, std 1
MinMaxScaler\((x - x_{\min}) / (x_{\max} - x_{\min})\)minimumrangevalues in \([0, 1]\)
RobustScaler\((x - \text{median}) / \text{IQR}\)medianIQR \(= Q_3 - Q_1\)median 0, IQR 1
MaxAbsScaler\(x / \max \lvert x \rvert\)none (0 stays 0)largest \(\lvert x \rvert\)values in \([-1, 1]\)
A scaler is a linear map applied to each column separately. The four differ only in which statistics they use. IQR: interquartile range.

By hand: a column with an outlier

Five households, monthly income in R$ thousand: 1, 2, 3, 4 and 40.

  • mean \(\mu = 10\); \(\sigma = \sqrt{(81+64+49+36+900)/5} = \sqrt{226} = 15.03\) (scikit-learn divides by \(n\))
  • minimum 1, maximum 40, range 39
  • median 3; \(Q_1 = 2\), \(Q_3 = 4\), so IQR = 2

First household (income 1): standard \((1-10)/15.03 = -0.599\) · min-max \(0\) · robust \((1-3)/2 = -1\) · max-abs \(1/40 = 0.025\)

Predict. Which scaler keeps the four ordinary households most spread out, and which one squeezes them together?

The outlier sets the scale

rawStandardMinMaxRobustMaxAbs
1−0.5990.000−1.00.025
2−0.5320.026−0.50.050
3−0.4660.0510.00.075
4−0.3990.0770.50.100
401.9961.00018.51.000

Width of the four ordinary values:

  • Standard: 0.2 (the outlier inflates \(\sigma\): 15.03 against 1.12 without it)
  • MinMax, MaxAbs: < 0.08 (the outlier is the maximum)
  • Robust: 1.5, and the outlier is left far away, at 18.5

Learned statistics live in attributes ending with an underscore: mean_, scale_, center_.

Same geometry, different weight next to other columns

On a common axis, the four ordinary incomes span 0.2, 0.077, 0.075 and 1.5; the robust outlier lands at 18.5

Inside the column the ordinary households always occupy the same 8% of the range: a linear map cannot change proportions. What changes is how big their differences are compared with the other columns, and that is what a distance adds up.

Under MinMaxScaler, next to an age column with std 1, two ordinary households look almost identical in income, and age decides everything.

Choosing a scaler

scalereffect of outlierstypical use
StandardScalermoderate: they inflate \(\sigma\)the default: linear models, SVM, PCA, neural networks
MinMaxScalerstrong: one extreme value compresses the resta bounded range is required (pixel intensities)
RobustScalerweak: they are simply left far awaycolumns with outliers you decided to keep
MaxAbsScalerstrong, like min-maxsparse data: zeros stay zeros
  • Normalization is ambiguous (min-max? "make it normal"? scikit-learn's Normalizer rescales each row). Write the formula.
  • A new value outside the training range leaves the interval: an income of 80 becomes \((80-1)/39 = 2.03\) under min-max.
Stage 2 · where may a step learn its numbers?

Fit on the training set, transform everything else

A mean or a standard deviation is a parameter of your model, just like a weight. fit learns and stores, transform applies, fit_transform does both on the same data.

training ages

20, 25, 30, 35, 40

mean 30, std 7.07

new passengers

60 and 70

Predict. Someone calls StandardScaler().fit_transform(test) on the two new rows. What values do 60 and 70 receive?

Four situations

what the code does60 and 70 becomewhat went wrong
right: transform(test)\((60-30)/7.07 = 4.24\), 5.66nothing: "far older than anyone in training"
refit on the 2 test rows−1, +1 (their mean 65, spread 5)−1 meant 22.9 years: the 60-year-old is now 23
refit on a single row0every passenger is "perfectly average"
fit before the splitlearned: mean 40, std 17.11it leaks: the evaluation becomes optimistic
Right: test ages 60 and 70 land at 4.24 and 5.66; wrong: refit on the test rows they land at −1 and +1
fit once, on the training rows. Validation, test and production only ever see transform.
Stage 3 · what can a scaler not fix?

Skewed features: a scaler cannot change the shape

Titanic fares: half cost less than £14.46, three cost £512, and 15 tickets were free. Skewness \(= \frac{1}{n}\sum z_i^3\): 0 when symmetric, positive with a long right tail.

Predict. The raw fare has skewness 4.79. What is it after StandardScaler?
fareskewness
raw4.79
standardized4.79
square root2.09
\(\log(1 + x)\), np.log1p0.39

A nonlinear map is needed. Log turns multiplications into additions (£8 → £80 and £50 → £500 become equal steps); log1p because \(\log 0 = -\infty\).

Let the data choose the power

Fare skewness: 4.79 raw, 0.39 after log1p, −0.04 after Yeo-Johnson, −0.94 after QuantileTransformer

Box-Cox

\(x' = (x^\lambda - 1)/\lambda\), \(\lambda = 0\) means \(\log x\); \(\lambda\) by maximum likelihood. Needs \(x > 0\): refuses the free tickets.

Yeo-Johnson

accepts zero and negatives. \(\lambda = -0.098\), close to a log → skewness −0.04.

QuantileTransformer

replaces each value by its rank → −0.94; the 15 free tickets pile up at −5.20.

Check after transforming. \(\lambda\) and the quantiles are learned: fit on train. Order: shape, then scale. Trees do not care.
Stage 4 · how does a category become a number?

Encoding: three kinds of categories

nominal

no order: port, color, city

→ one-hot

ordinal

a real order: ticket class, education, size S < M < L

→ integers in that order

binary

two categories: Sex

→ one 0/1 column

Predict. OrdinalEncoder numbers the ports alphabetically: Cherbourg = 0, Queenstown = 1, Southampton = 2. What does a distance now believe about them?

Integer codes invent distances

Integer codes put the ports on a line with distances 1, 1 and 2; one-hot puts them at the corners of a triangle, all at 1.414

Codes: Q sits "between" C and S, and C–S is "twice as far" as C–Q. One-hot: every pair at \(\sqrt{1^2+1^2} = 1.414\).

Survival by port: C 0.554, Q 0.390, S 0.337. Steps of 16.4 and 5.3 points: one slope on the codes cannot represent both.

One-hot gives each port its own coefficient. Trees are the exception: cuts at 0.5 and 1.5 isolate each code, so integer codes are fine for them.

Ordinal: you declare the order


OrdinalEncoder().fit(levels).categories_
# ['bachelor', 'high school', 'master']   alphabetical!
OrdinalEncoder(categories=[['high school', 'bachelor', 'master']])
# codes now follow the real order: 0, 1, 2
  

The alphabet puts high school between bachelor and master.

Pclass is already 1, 2, 3. Ordinal (one column) or nominal (three one-hot columns) is a modeling choice: ordinal assumes 1st → 2nd matters as much as 2nd → 3rd.

One-hot: two practical options

unseen categories

Trained on S, C, Q; a passenger from Belfast (B) arrives.

Default: ValueError, the prediction crashes.

handle_unknown='ignore': B becomes 0 0 0, "none of the categories I know".

drop='first'

C + Q + S = 1 on every row: the intercept's column. Rank 3 for 4 columns, so the weights are not unique.

Dropping C (the reference) restores full rank.

drop='first' for linear models with an intercept and no penalty. Keep every column for trees, k-NN and regularized models.

High cardinality, and target encoding

Ticket has 681 distinct values for 891 rows: one-hot would create 681 columns of almost only zeros, a recipe for overfitting.

  • First look for the structure inside: the deck letter of Cabin, the title in Name (Mr, Miss, Mrs, Master...).
  • Then group rare levels: min_frequency=10 keeps 4 titles and merges the other 13.
  • Target encoding: replace each category by the mean of the target in it. One column, whatever the cardinality.
Predict. A column with no information: 2,000 passengers, 800 random tickets, survival by coin flip. Encode it with the per-ticket mean, then apply "survived if > 0.5". What training accuracy do you expect?

Target encoding leaks

encoding of pure noisetraintest
naive per-category mean0.7750.505
TargetEncoder (cross-fitted)0.4960.505
  • Most tickets appear once or twice, so a row's encoded value is mostly its own label: the model learns to read the answer, not to predict it.
  • Cross-fitting: fit_transform encodes each fold with means from the other folds, so no row sees its own label; rare categories are shrunk toward the global mean.
Never compute a target encoding on rows you evaluate on: fit_transform on train, transform on test. Target-free options: frequency encoding, hashing.
Stage 5 · what goes in the holes?

Missing values: why is it missing?

mechanismwhat it meansTitanic example
MCAR, missing completely at randomdepends on nothingthe 2 missing Embarked, plausibly
MAR, missing at randomdepends on other observed columnsAge, missing more in 3rd class
MNAR, missing not at randomdepends on the missing value itselfCabin: no cabin of their own

Age missing, by class

1st 13.9% · 2nd 6.0% · 3rd 27.7%

survival

age known 40.6% · age missing 29.4%

dropna() would cost 177 rows (19.9%), mostly 3rd class: the passengers who died most would be under-represented.

Filling with one value shrinks the variance

Six values, two missing: 2, 4, ?, 6, ?, 8. Observed mean 5, variance \((9+1+1+9)/4 = 5\).

Predict. Fill both holes with the mean (5). What is the new variance?

The imputed 5s add nothing to the squared deviations but add to the count: \(20/6 = 3.33\), the old variance times \(4/6\), the fraction observed.

Titanic ages

std 14.53 → 13.00, exactly the predicted \(\times\sqrt{713/890}\)

the cost

177 passengers share one invented age, 29.7

Imputing from the other columns

Std of Age: 14.53 observed, 13.00 mean imputation, 13.29 KNN, 13.60 iterative with 7 negative ages

KNNImputer

average of the \(k\) most similar rows (needs comparable scales). Mr. James Moran, fare £8.46, no age: nearest fares £7.925 (26 y) and £7.25 (22 y) → 24. The mean says 30.25.

IterativeImputer

a regression of each column on the others, cycled (MICE). It produced 7 negative ages: it does not know that age is positive.

Both spread the guesses (13.29, 13.60 against 13.00), but none recovers 14.53. Check the imputed values.

Keep the fact that it was missing


imp = SimpleImputer(strategy='median', add_indicator=True)
imp.get_feature_names_out()   # ['Age' 'missingindicator_Age']
# passenger 1: [22, 0]    passenger 6: [28, 1]
  

Missing age goes with lower survival (29.4% against 40.6%). The flag costs one column and lets the model decide whether missingness is informative. For categories: a category of its own, 'unknown'.

Never impute the target. Rows with a missing label leave supervised training; inventing labels manufactures signal.

The preprocessing map

Per column: missing values → shape → scale, every step fitted on the training rows. Click the answers to build a recipe.

Which families care about which step

model familyscalingshapeencodingmissing values
k-NN, k-means, SVM, PCArequiredhelpsone-hotmust be imputed
linear/logistic regression, neural networksrequiredhelpsone-hot, drop='first' if no penaltymust be imputed
trees, random forests, gradient boostingnot needednot neededordinal codes are finescikit-learn's trees accept NaN
A log applied after scaling would undo the scaling (and fail on negative values): shape before scale.

Lab: preparing the Titanic for a model

  1. Inventory: Survived is the target, PassengerId goes out, Pclass is a category stored as a number.
  2. Split first: 712 train, 179 test, stratified.
  3. Impute Age (median), encode Sex, Embarked, Pclass (one-hot), scale the numbers.
  4. All in one ColumnTransformer: 12 columns, no NaN, no text.
Predict. The baseline "always did not survive" gets 61.5%. A black-box k-NN with and without scaling, same data: how far apart?

Lab notebook (Jupyter or Colab, File → Upload notebook).

Same model, same data, different scaler

numeric columnstest accuracy
baseline (always "did not survive")0.615
k-NN, no scaling0.676
k-NN, StandardScaler0.821
k-NN, MinMaxScaler0.804
k-NN, RobustScaler0.771

Without scaling, neighbors are chosen almost only by fare (93.1% of the distance).

179 test passengers: each is worth 0.56 points, so the ranking between scalers is not reliable.

Scaling vs not scaling: 14.5 points, and the gap repeats with other random splits.

Summary

  1. Raw units decide distances by accident: scale what distances, variances and gradients use.
  2. Scalers differ in center and spread; outliers set the scale except for RobustScaler.
  3. A scaler cannot fix shape: log1p, Yeo-Johnson, then scale.
  4. One-hot for nominal, declared order for ordinal; target encoding only cross-fitted.
  5. Ask why it is missing; imputation shrinks spread; keep a flag.
  6. Fit on train, transform everything else.
Next: Pipelines, which make that rule impossible to break by accident.