BitWithBite
/
Tier 2 › Module 1 › Lesson 1.2
← Prev Next →
Module 1 — The scikit-learn Workflow

Preprocessing: Scaling,
Normalization & Encoding

30 min
📚 Concept + Code
🎯 Lesson 1.2 of 4
Raw data almost never goes directly into a model. Scaling stops one feature dominating because it has bigger numbers. Encoding lets models understand categories. Done wrong — especially fitting on test data — it corrupts every result you produce. Done right, it's invisible infrastructure that makes every model work better.

Why Preprocessing Matters

Imagine you're predicting house prices. Two features:

Without scaling, a distance-based model like k-NN or SVM will treat a 1-unit difference in area as equivalent to a 1-unit difference in rooms — but 1 m² is nothing while 1 room is huge. The model is dominated by whichever feature has the largest absolute values.

Analogy
Think of scaling like converting all measurements to the same unit before comparison. You wouldn't compare heights in centimeters to weights in kilograms and expect a sensible result. Scaling puts all features on a common scale so the model can compare them fairly.
Which models need scaling?
Need it: k-NN, SVM, logistic regression, linear regression, PCA, neural networks — anything using distances or gradient descent.
Don't need it: Decision trees, random forests, XGBoost, LightGBM — they split on thresholds, which are scale-invariant. Scaling them doesn't hurt, but it doesn't help either.

StandardScaler — Zero Mean, Unit Variance

The most common scaler. For each feature, it subtracts the mean and divides by the standard deviation:

Formula
z = (x − μ) / σ

where μ = mean of the feature (computed on training data)
and σ = standard deviation of the feature (computed on training data)

After scaling: every feature has mean = 0 and standard deviation = 1. Values are now in units of "how many standard deviations from the mean."

Example: before and after StandardScaler
Raw salary values (in thousands):
25k
25
50k
50
75k
75
150k
150
After StandardScaler (mean=75, std=47):
-1.06
-1.06
-0.53
-0.53
0.0
0.0
+1.60
+1.60
python
from sklearn.preprocessing import StandardScaler
import numpy as np

X_train = np.array([[25], [50], [75], [150]])
X_test  = np.array([[40], [100]])

scaler = StandardScaler()
scaler.fit(X_train)              # learn μ and σ from train

X_train_sc = scaler.transform(X_train)
X_test_sc  = scaler.transform(X_test)   # use SAME μ and σ

print(scaler.mean_)    # [75.]
print(scaler.scale_)   # [47.17]
print(X_train_sc)
# [[-1.06] [-0.53] [ 0.  ] [ 1.60]]

When to use it: When you have no strong outliers and your data is roughly bell-shaped. Default choice for most models.

MinMaxScaler — Compress to a Fixed Range

Scales each feature to a specific range, usually [0, 1]:

Formula
x_scaled = (x − x_min) / (x_max − x_min)

x_min and x_max computed from training data only.

Use when: your model needs values in [0, 1] specifically — neural networks with sigmoid outputs, image pixel values (0–255 → 0–1).

Weakness: One extreme outlier changes the scale for every other value. A salary of $10M in a dataset of $40k–$150k salaries compresses everything else into a tiny range near zero.

RobustScaler — Handles Outliers

Uses the median and the interquartile range (IQR) instead of mean and std. Outliers have much less influence:

Formula
x_scaled = (x − median) / IQR

IQR = 75th percentile − 25th percentile

Use when: your data has meaningful outliers you can't (or shouldn't) remove — income data, fraud amounts, medical measurements.

Which Scaler to Choose

Decision guide
Does your data have significant outliers?
├── Yes → RobustScaler
└── No → Does the model need values in [0,1]?
├── Yes → MinMaxScaler
└── No → StandardScaler (default)
ScalerCenters onScale unitOutlier resistantBest for
StandardScalerMeanStd deviationNoMost models (default)
MinMaxScalerMin valueValue rangeNoNeural nets, image data
RobustScalerMedianIQRYesData with real outliers
NoneTree-based models (RF, XGBoost)

Encoding Categorical Variables

Models work with numbers. A column containing "red", "green", "blue" needs to become numbers. There are two main approaches, and choosing wrong causes subtle bugs.

Label Encoding — When Order Exists

Assigns an integer to each category: small=0, medium=1, large=2. Only use this when the order is real and meaningful.

python
from sklearn.preprocessing import OrdinalEncoder
import numpy as np

# ✅ CORRECT: size has a real order
X = np.array([['small'], ['large'], ['medium'], ['small']])

enc = OrdinalEncoder(categories=[['small', 'medium', 'large']])
enc.fit(X)
print(enc.transform(X))
# [[0.] [2.] [1.] [0.]]  ← small=0, medium=1, large=2

# ❌ WRONG: color has no order — encoding 'red'=0, 'green'=1, 'blue'=2
#   tells the model green > red, blue > green. False signal.

One-Hot Encoding — When No Order Exists

Creates one binary column per category. "color" with values red/green/blue becomes three columns: is_red, is_green, is_blue. Each row has exactly one 1 and the rest 0.

python
from sklearn.preprocessing import OneHotEncoder
import numpy as np

X = np.array([['red'], ['green'], ['blue'], ['red']])

enc = OneHotEncoder(sparse_output=False)
enc.fit(X)

X_encoded = enc.transform(X)
print(enc.categories_)     # [['blue', 'green', 'red']]
print(X_encoded)
# [[0. 0. 1.]    ← red
#  [0. 1. 0.]    ← green
#  [1. 0. 0.]    ← blue
#  [0. 0. 1.]]   ← red

# drop='first' avoids dummy variable trap (multicollinearity)
enc2 = OneHotEncoder(drop='first', sparse_output=False)
enc2.fit(X)
print(enc2.transform(X))
# [[0. 1.]   # blue is the dropped reference category
#  [1. 0.]
#  [0. 0.]
#  [0. 1.]]
The dummy variable trap
With 3 categories and 3 one-hot columns, the columns are perfectly collinear — knowing any two tells you the third. Linear models can't handle this (singular matrix). Use drop='first' or drop='if_binary' to drop one column. Tree-based models don't need this.

The Silent Bug — Fitting on Test Data

This is the single most common mistake in ML preprocessing. It produces results that look good during development but fail in production.

python — ❌ WRONG (data leakage)
from sklearn.preprocessing import StandardScaler

scaler = StandardScaler()

# ❌ Bug: fitting on all data before splitting
X_scaled = scaler.fit_transform(X_all)   # sees test data!
X_train, X_test = train_test_split(X_scaled, ...)

# ❌ Bug: fitting scaler on test set
scaler.fit(X_test)                        # overwrites train stats!
X_test_scaled = scaler.transform(X_test)
python — ✅ CORRECT
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)

scaler = StandardScaler()
scaler.fit(X_train)               # ✅ learn stats from TRAIN ONLY

X_train_sc = scaler.transform(X_train)  # ✅ apply to train
X_test_sc  = scaler.transform(X_test)   # ✅ apply to test with SAME stats

# Or even better: use a Pipeline — it handles this automatically

Putting Scaling + Encoding in a Pipeline

In real data you have both numeric and categorical columns. ColumnTransformer applies different preprocessing to each column type, then a Pipeline wraps everything:

python — real-world pattern
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.ensemble import RandomForestClassifier
import pandas as pd

# Example dataframe
data = pd.DataFrame({
    'age':    [25, 32, 47, 19],
    'salary': [40000, 75000, 95000, 30000],
    'city':   ['London', 'Paris', 'London', 'Berlin'],
    'dept':   ['Eng', 'HR', 'Eng', 'Finance'],
})
y = [1, 0, 1, 0]

# Define which columns get which treatment
numeric_features     = ['age', 'salary']
categorical_features = ['city', 'dept']

# Build a transformer for each type
numeric_transformer     = StandardScaler()
categorical_transformer = OneHotEncoder(handle_unknown='ignore')

# ColumnTransformer applies each transformer to its columns
preprocessor = ColumnTransformer(transformers=[
    ('num', numeric_transformer,     numeric_features),
    ('cat', categorical_transformer, categorical_features),
])

# Wrap preprocessor + model in a single Pipeline
pipe = Pipeline([
    ('preprocessor', preprocessor),
    ('model',        RandomForestClassifier(random_state=42))
])

pipe.fit(data, y)
print(pipe.predict(data))  # [1 0 1 0]
handle_unknown='ignore'
In production, the test data (or live requests) may contain category values that didn't exist in training — a new city, a new department. handle_unknown='ignore' makes the encoder output all zeros for unknown categories instead of crashing.
✓ Check your understanding
1. You have a city column: London, Paris, Berlin (no natural order). Which encoder should you use?
ALabelEncoder / OrdinalEncoder — assign integers 0, 1, 2
BOneHotEncoder — create a binary column per city
CStandardScaler — scale the city values to zero mean
DNo encoding needed — models handle strings natively
Correct. City has no natural order — London is not "greater than" Berlin. Using OrdinalEncoder would create false ordinal relationships. OneHotEncoder creates a separate binary feature per city, which correctly tells the model these are distinct, unordered categories.
2. You have salary data with a few billionaires mixed into a mostly $30k–$120k dataset. Which scaler is most appropriate?
AStandardScaler
BMinMaxScaler
CRobustScaler
DNo scaling needed
Correct. A few billionaires are extreme outliers that would pull the mean far from where most data lives, making StandardScaler compress most values into a tiny range. RobustScaler uses the median and IQR, which are not affected by extreme values, so the majority of the data is still well-scaled.
3. Which of these is the correct order of operations when preprocessing?
Afit_transform on all data → train_test_split
Btrain_test_split → fit on train → transform train and test separately
Ctrain_test_split → fit on test → transform train using test stats
Dfit on train → fit on test → average the parameters
Correct. Split first, then fit the scaler on training data only. The statistics learned from the training set (mean, std, etc.) are then applied to both train and test. This ensures the test set is a genuine held-out sample the preprocessing pipeline has never seen.

Summary

What you learned
StandardScaler: zero mean, unit variance. Default for most models.
MinMaxScaler: compresses to [0,1]. Good for neural nets, bad with outliers.
RobustScaler: uses median + IQR. Use when outliers are real and meaningful.
OrdinalEncoder: integers with order. Only when order is genuinely meaningful.
OneHotEncoder: binary columns per category. Use for unordered categories.
→ Always fit() on training data only. transform() on everything. A Pipeline makes this automatic.
← 1.1 Estimators, Transformers & Pipelines Next: 1.3 Building a Full Pipeline →
🗒 Cheat Sheet 📝 Worksheet