Data Preprocessing — Encoding & Scaling
OneHotEncoder and LabelEncoder, rescaling numeric columns with StandardScaler and MinMaxScaler, and the single rule that prevents the most common data leakage bug: fit preprocessing on the training set only.
Why Raw Data Usually Isn't ML-Ready
A typical pandas DataFrame straight out of a CSV mixes numbers, text categories, and inconsistent ranges — all in one table. Two problems come up constantly:
city or payment_method holds strings. Scikit-learn's math-based algorithms need numbers, so categories must be converted before fitting.age in the 20–80 range and income in the tens of thousands. Left as-is, some algorithms treat the bigger-magnitude column as automatically more important.| age | income | city | subscribed | |
|---|---|---|---|---|
| 0 | 25 | 42000 | Lahore | yes |
| 1 | 41 | 81000 | Karachi | no |
| 2 | 33 | 55000 | Lahore | yes |
By the end of this lesson, that table needs to become: city converted into numeric columns, subscribed converted into a numeric target, and age/income rescaled onto a comparable range — all without letting any information from the test set influence how those transforms are learned.
Categorical Encoding
There are two encoders you'll reach for constantly, and picking the wrong one for the job silently damages a model.
OneHotEncoder — for unordered (nominal) categories
One-hot encoding creates a separate binary (0/1) column for each category. Use it for features where the categories have no natural order — like city or payment_method — so the model doesn't assume "Karachi" is somehow greater than "Lahore" the way it would if you encoded them as 0 and 1 on a single column.
from sklearn.preprocessing import OneHotEncoder import numpy as np cities = np.array([['Lahore'], ['Karachi'], ['Lahore'], ['Islamabad']]) # sparse_output=False returns a plain NumPy array (sklearn >= 1.2; # use sparse=False on older versions) encoder = OneHotEncoder(sparse_output=False) encoded = encoder.fit_transform(cities) print(encoded) print(encoder.categories_)
| city_Islamabad | city_Karachi | city_Lahore | |
|---|---|---|---|
| 0 | 0 | 0 | 1 |
| 1 | 0 | 1 | 0 |
| 2 | 0 | 0 | 1 |
| 3 | 1 | 0 | 0 |
LabelEncoder — mainly for the target column
LabelEncoder converts categories into a single column of integers (0, 1, 2, …). It's the right tool for encoding a classification target (y), where a single integer column is exactly what's expected. It's generally the WRONG tool for an input feature with unordered categories, because a single numeric column implies an order and a distance between categories that usually isn't real.
from sklearn.preprocessing import LabelEncoder # A typical use: encoding the target column y, not a feature labels = ['no', 'yes', 'yes', 'no'] le = LabelEncoder() y_encoded = le.fit_transform(labels) print(y_encoded) # [0 1 1 0] print(le.classes_) # ['no' 'yes'] — alphabetical order by default
size with values small/medium/large, scikit-learn's OrdinalEncoder lets you specify the correct category order explicitly. Neither OneHotEncoder (which throws away the order) nor a default LabelEncoder (which orders alphabetically, not logically) handles this correctly out of the box.Feature Scaling
Once every column is numeric, the columns still might live on very different scales. Scaling rewrites each numeric column onto a comparable range without changing the relationships within that column.
StandardScaler — zero mean, unit variance
MinMaxScaler — squeeze into a fixed range
from sklearn.preprocessing import StandardScaler, MinMaxScaler standard = StandardScaler() X_train_std = standard.fit_transform(X_train) minmax = MinMaxScaler() X_train_mm = minmax.fit_transform(X_train) print("Standardized mean ≈ 0:", X_train_std.mean(axis=0).round(2)) print("MinMax range is [0, 1]:", X_train_mm.min(), X_train_mm.max())
StandardScaler is the safer default for most algorithms and tolerates outliers a bit better. MinMaxScaler is preferred when you specifically need values bounded in a fixed range (some neural network activation functions expect this), but it's more sensitive to outliers since a single extreme value stretches the whole range.Why scaling matters for some algorithms and not others:
The Fit-on-Train-Only Rule
Every encoder and scaler in scikit-learn has both a .fit() step (learn parameters — the mean/std for a scaler, the category list for an encoder) and a .transform() step (apply those learned parameters). The rule that prevents data leakage: fit only on the training set, then use that same fitted object to transform both the training set and the test set.
from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42 ) scaler = StandardScaler() # CORRECT: fit_transform on train, transform (no fit!) on test X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test) # WRONG — do not do this: # X_test_scaled = scaler.fit_transform(X_test) # refits on test stats!
.fit_transform() on the test set, the scaler learns a DIFFERENT mean and standard deviation than the one it used for training — and worse, that mean/std is computed partly from data the model is supposed to have never seen. The model's reported test performance becomes unrealistically optimistic, and it will behave differently the moment it meets real new data that wasn't part of computing that second fit.This is exactly what scikit-learn's Pipeline class from Lesson 2 protects you from — inside a Pipeline, .fit() is only ever called on training folds, and .transform() is applied everywhere else automatically.
Lesson Summary
.transform() both train and test with that same fitted object.scaler.fit_transform(X_test) after already calling scaler.fit_transform(X_train)?Hands-on practice with the exact functions from this lesson.
Using the pattern from Section 2, one-hot encode this list of categories:
['red', 'blue', 'green', 'blue', 'red']. Print the resulting array and encoder.categories_.
Given
X_train = np.array([[10], [20], [30], [40], [50]]), apply both StandardScaler and MinMaxScaler, and print both results side by side. Confirm the MinMax output stays between 0 and 1.
Below is a snippet with a data leakage bug. Rewrite it correctly.
X_scaled = scaler.fit_transform(X)
X_train, X_test, y_train, y_test = train_test_split(X_scaled, y, test_size=0.2)
💡 Show hints if you're stuck
- Task 1:
encoder.categories_lists the alphabetically-sorted unique categories the encoder learned, e.g. array(['blue', 'green', 'red']). - Task 2: MinMax on this data gives [0, 0.25, 0.5, 0.75, 1.0]. Standard gives values centered around 0, symmetric since the data is evenly spaced.
- Task 3: The bug is scaling BEFORE splitting. Correct order: split first, then
scaler.fit_transform(X_train)andscaler.transform(X_test)separately.