1. Feature Scaling
Many models — anything using distance or a gradient, including the neural networks you'll build starting Week 11 — are sensitive to the scale of each feature. A "years of experience" column (0–40) and a "salary" column (0–200,000) will otherwise let salary dominate purely because its numbers are bigger, not because it's more predictive.
import numpy as np
from sklearn.preprocessing import StandardScaler, MinMaxScaler
X = np.array([[5.0, 50000], [10.0, 62000], [2.0, 45000], [20.0, 90000]])
# Standardization: (x - mean) / std -- centers at 0, unit variance
standardized = StandardScaler().fit_transform(X)
# Min-max: (x - min) / (max - min) -- squashes into [0, 1]
normalized = MinMaxScaler().fit_transform(X)
Standardization is the safer default for most models — it's not
distorted by a single extreme value the way min-max scaling is, since min-max
anchors its entire range to whatever the min and max happen to be. Reach for min-max
scaling specifically when you need values in a bounded range (like [0, 1]
pixel intensities feeding a neural network).
Tree-based models (Week 9) split on one feature at a time and are scale-invariant — scaling won't change their predictions. Anything using distance, dot products or gradients (k-means in Week 7, neural networks from Week 11, SVMs in Week 10) needs it.
2. Encoding Categorical Variables
Models operate on numbers, so a column like city or plan_tier
needs to become numeric before it can be used — but naively converting categories to
arbitrary integers invents a false ordering the model will happily "learn."
import pandas as pd
df = pd.DataFrame({
"plan": ["free", "pro", "enterprise", "pro", "free"],
"size": ["small", "medium", "large", "small", "large"],
})
# One-hot: a new binary column per category -- no false ordering
one_hot = pd.get_dummies(df["plan"], prefix="plan")
# Ordinal: use only when a real order exists
size_order = {"small": 0, "medium": 1, "large": 2}
df["size_ordinal"] = df["size"].map(size_order)
One-hot encoding is the right default for unordered categories
(plan has no natural rank) — it creates one binary column per category so
the model can't infer an ordering that isn't there. Ordinal encoding is
only correct when a genuine order exists, like size here. A third option,
target encoding (replacing a category with the average target value for
that category), is powerful for high-cardinality columns like "zip code" but is also the
single easiest way to leak information from your target — a problem you'll dig into in
Section 5.
3. Handling Outliers & Skewed Distributions
Building on Week 3's descriptive statistics: an extreme outlier doesn't just distort the mean, it can dominate a model that's sensitive to scale — one $10 million transaction in a dataset of $50 purchases can single-handedly determine a linear model's coefficients.
import numpy as np
import pandas as pd
df = pd.DataFrame({"transaction": [45, 52, 48, 61, 55, 10_000_000, 50]})
# Detect outliers with the IQR method
q1, q3 = df["transaction"].quantile([0.25, 0.75])
iqr = q3 - q1
lower, upper = q1 - 1.5 * iqr, q3 + 1.5 * iqr
is_outlier = (df["transaction"] < lower) | (df["transaction"] > upper)
# A common fix for skewed positive data: log-transform
df["transaction_log"] = np.log1p(df["transaction"])
np.log1p (log of 1 + x, safely handling zero) compresses large
values much more than small ones, turning a heavily right-skewed column into something
closer to normal — often enough, on its own, to make a linear model behave far better
without discarding a single row of real data.
4. Pipelines & ColumnTransformer
Applying scaling, encoding and imputation as separate manual steps works in a notebook,
but it's fragile — it's easy to fit a scaler on the wrong data, or forget a step when you
apply the same transformations to new data later. scikit-learn's Pipeline
and ColumnTransformer bundle every step into one object that fits and
transforms consistently.
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.impute import SimpleImputer
numeric_features = ["age", "income"]
categorical_features = ["plan", "city"]
numeric_pipeline = Pipeline([
("impute", SimpleImputer(strategy="median")),
("scale", StandardScaler()),
])
categorical_pipeline = Pipeline([
("impute", SimpleImputer(strategy="most_frequent")),
("encode", OneHotEncoder(handle_unknown="ignore")),
])
preprocessor = ColumnTransformer([
("num", numeric_pipeline, numeric_features),
("cat", categorical_pipeline, categorical_features),
])
# preprocessor.fit_transform(X_train) -- fits ALL steps only on training data
# preprocessor.transform(X_test) -- reuses those exact fitted parameters
The payoff is that fit and transform are now separate,
enforced steps — which is precisely the mechanism that prevents the leakage bug in
Section 5. You'll reuse this exact pipeline pattern as the first step of the Week 8
mini-project and every classical ML model after it.
5. Data Leakage
Data leakage happens when information from outside the training data — often, indirectly, information from the test set or the target itself — sneaks into training and makes a model look far better than it will ever perform in the real world. It is the single most common reason a beginner's model reports 99% accuracy and then fails completely in production.
# BUG: scaler is fit on the full dataset, BEFORE the train/test split
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X) # sees test data's mean/std!
X_train, X_test, y_train, y_test = train_test_split(X_scaled, y)
# The scaler's mean/std now leak information about the test set into training
# Split FIRST, then fit the scaler only on training data
X_train, X_test, y_train, y_test = train_test_split(X, y)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train) # fit only on training data
X_test_scaled = scaler.transform(X_test) # reuse those training statistics
The fix is always the same shape: split before you fit anything, then
only ever call .transform() (never .fit() again) on the test
set. This is exactly what the Pipeline from Section 4 enforces automatically
when combined with cross-validation — another reason to prefer it over manual steps.
Beyond scaling, a feature can leak the target directly — e.g. a "cancellation_processed_date" column when predicting churn is only ever filled in after a customer has already churned. Always ask: "would I actually have this value at prediction time?" You'll apply this same scrutiny to cross-validation setups in Week 6.
6. Hands-on Exercise
Build a leakage-free preprocessing pipeline for a mixed dataset
You're given a small customer dataset with numeric, categorical and missing values, plus one deliberately suspicious column.
import pandas as pd
import numpy as np
df = pd.DataFrame({
"age": [25, 34, np.nan, 45, 29, 52, 38],
"income": [42000, 58000, 51000, 95000, 47000, 250000, 61000],
"plan": ["free", "pro", "pro", "enterprise", "free", "enterprise", np.nan],
"signup_channel": ["ad", "referral", "ad", "organic", "referral", "ad", "organic"],
"refund_issued": [0, 0, 1, 0, 0, 1, 0], # only ever set AFTER a churn decision
"churned": [0, 0, 1, 0, 0, 1, 0],
})
Requirements:
- Split the data into train/test before fitting anything, predicting
churned. - Identify and drop
refund_issuedbefore modeling, and explain in one sentence why it's a target-leakage risk rather than a legitimate feature. - Build a
ColumnTransformerthat imputes and standardizesage/income, and imputes and one-hot encodesplan/signup_channel. - Log-transform
incomebefore scaling, and explain what changes about its distribution. - Fit the full pipeline on the training set only, then transform the test set, and confirm the transformed test set's mean isn't exactly 0 (proving it reused the training set's statistics rather than its own).
If the transformed test set's mean is suspiciously close to 0, you've likely called .fit_transform() on the test set instead of .transform() — that's the leakage bug from Section 5, reproduced on purpose so you can recognize it.
7. Knowledge Check
Four quick questions. Expand each to check your answer.
Q1
Why is one-hot encoding usually safer than ordinal encoding for a column like city?
Why is one-hot encoding usually safer than ordinal encoding for a column like city?
Ordinal encoding assigns an arbitrary integer to each category (e.g. Boston=0, Chicago=1, Denver=2), which implies a false numeric order and distance a model will treat as meaningful ("Denver" isn't twice "Chicago"). One-hot encoding creates an independent binary column per category, so no false ordering is introduced — it's the right default whenever categories have no natural rank.
Q2
Why must you split into train/test before fitting a StandardScaler, not after?
Why must you split into train/test before fitting a StandardScaler, not after?
Fitting the scaler on the full dataset computes a mean and standard deviation that include the test set's values — meaning the "unseen" test data has already influenced how the training data gets transformed. That's data leakage: it makes validation performance look better than the model will actually achieve on truly new data.
Q3
Which models are sensitive to feature scale, and which aren't?
Which models are sensitive to feature scale, and which aren't?
Tree-based models (decision trees, random forests, gradient boosting) split on one feature's threshold at a time and are unaffected by scale. Distance- and gradient-based models — k-means, SVMs, linear/logistic regression, and every neural network — are sensitive to scale, because a larger-magnitude feature can dominate a distance calculation or gradient purely due to its units.
Q4
A column called refund_issued perfectly predicts churn. Why is it dangerous to include as a feature?
A column called refund_issued perfectly predicts churn. Why is it dangerous to include as a feature?
If the refund is only ever issued after a customer has already churned, the column doesn't help predict churn — it's a downstream consequence of it. This is target leakage: the model would appear highly accurate during training but couldn't actually use this feature to predict churn in advance, since the value doesn't exist yet at the time a real prediction would need to be made.