Data Preparation For ML
Encoding Categorical Variables
The Data Science Notes' feature engineering chapter introduced encoding briefly; this chapter goes deeper, since choosing the wrong encoding for a given algorit
Jr Codex ML Notes
Level: Intermediate Prerequisites: Chapter 1 Time to complete: ~25 minutes
Table of Contents
- Why Encoding Is Necessary
- One-Hot Encoding
- Ordinal Encoding
- The Dummy Variable Trap
- High-Cardinality Categoricals
- Target Encoding
- Encoding the Target Variable Itself
- Choosing the Right Encoding
- Summary & Next Steps
1. Why Encoding Is Necessary
The Data Science Notes' feature engineering chapter introduced encoding briefly; this chapter goes deeper, since choosing the wrong encoding for a given algorithm (Modules 3-6) can silently hurt model performance in ways that are easy to miss.
import pandas as pd
df = pd.DataFrame({"color": ["red", "blue", "green", "red", "blue"]})
# Feeding raw text directly into most ML algorithms simply doesn't work —
# they need NUMBERS. But the CHOICE of how to convert matters enormously.2. One-Hot Encoding
Creates one binary (0/1) column per category — the standard choice when categories have no inherent order.
import pandas as pd
df = pd.DataFrame({"color": ["red", "blue", "green", "red"]})
one_hot = pd.get_dummies(df["color"], prefix="color")
print(one_hot)
# color_blue color_green color_red
# 0 False False True
# 1 True False False
# 2 False True False
# 3 False False Truefrom sklearn.preprocessing import OneHotEncoder
encoder = OneHotEncoder(sparse_output=False, handle_unknown="ignore")
encoded = encoder.fit_transform(df[["color"]])
print(encoder.get_feature_names_out()) # ['color_blue', 'color_green', 'color_red']
print(encoded)Why "No False Ordering" Matters
─────────────────────────────────────────
If "red"=0, "blue"=1, "green"=2 were used directly as a
SINGLE numeric column instead, many algorithms (especially
Module 3's linear/logistic regression) would incorrectly
interpret this as "green is somehow MORE than blue is more
than red" — a meaningless, damaging assumption for a
genuinely unordered category.
─────────────────────────────────────────
handle_unknown="ignore" is essential for production use — it prevents a crash if the deployed model later encounters a category (e.g. a new city) that never appeared in training data (a preview of Module 9's production concerns).
3. Ordinal Encoding
For categories with a genuine, meaningful order, ordinal encoding preserves that relationship as a single numeric column — directly appropriate where one-hot would throw away real information.
from sklearn.preprocessing import OrdinalEncoder
df = pd.DataFrame({"size": ["medium", "small", "large", "medium"]})
# Specify the TRUE order explicitly — never let the encoder guess alphabetically!
encoder = OrdinalEncoder(categories=[["small", "medium", "large"]])
df["size_encoded"] = encoder.fit_transform(df[["size"]])
print(df)
# size size_encoded
# 0 medium 1.0
# 1 small 0.0
# 2 large 2.0
# 3 medium 1.0One-Hot vs Ordinal — the Rule from the Data Science Notes, Reapplied
─────────────────────────────────────────
One-hot: NO natural order (color, city, product category)
Ordinal: a REAL, meaningful order exists (size, education
level, satisfaction rating)
Getting this wrong in EITHER direction hurts model quality:
ordinal-encoding an unordered category implies a FALSE ranking;
one-hot-encoding an ordered category THROWS AWAY the order
information a model could have used directly.
─────────────────────────────────────────
4. The Dummy Variable Trap
A subtle issue specific to one-hot encoding, particularly relevant for Module 3's linear/logistic regression: including all one-hot columns creates perfect multicollinearity — one column is always fully predictable from the others.
import pandas as pd
df = pd.DataFrame({"color": ["red", "blue", "green"]})
one_hot_full = pd.get_dummies(df["color"])
print(one_hot_full)
# blue green red
# 0 False False True
# 1 True False False
# 2 False True False
# If you know blue=0 AND green=0, you ALREADY know red=1 — the
# "red" column is REDUNDANT, fully determined by the other two
one_hot_dropped = pd.get_dummies(df["color"], drop_first=True) # drops ONE column
print(one_hot_dropped)
# blue green
# 0 False False ← this row IS "red" — implied by BOTH being False
# 1 True False
# 2 False TrueWhy This Matters Specifically for Regression (Module 3)
─────────────────────────────────────────
Linear/logistic regression's underlying math (Module 3, Ch.1)
can become UNSTABLE or ill-defined when features are perfectly
redundant with each other (multicollinearity) — dropping one
dummy column removes this redundancy WITHOUT losing any
actual information (it's still fully recoverable from the
remaining columns).
Tree-based models (Module 4's decision trees, Module 5's
ensembles) are generally UNAFFECTED by this issue and don't
strictly require dropping a column — but doing so anyway is
a harmless, common convention.
─────────────────────────────────────────
5. High-Cardinality Categoricals
A categorical column with hundreds or thousands of unique values (e.g. "zip code," "product SKU") creates a serious problem for one-hot encoding: it explodes into an enormous number of columns, most containing very few examples each.
import pandas as pd
import numpy as np
# A column with 5,000 unique zip codes would create 5,000 one-hot columns!
n_zip_codes = 5000
print(f"One-hot encoding would create {n_zip_codes} new columns")Problems With Naive One-Hot Encoding at High Cardinality
─────────────────────────────────────────
- Massively increases the FEATURE COUNT — can worsen the
curse of dimensionality (a concept Module 6's dimensionality
reduction chapters address more fully)
- Most resulting columns are EXTREMELY sparse (mostly zeros),
with too few examples per category to learn anything reliable
- Dramatically increases memory usage and training time
─────────────────────────────────────────
Common fixes: group rare categories into a single "Other" bucket before encoding, use ordinal/hash encoding as a more compact alternative, or — the most statistically principled option — target encoding (Section 6).
6. Target Encoding
Replaces each category with a number derived from the target variable's average value for that category — dramatically more compact than one-hot for high-cardinality features, at the cost of requiring careful handling to avoid leakage.
import pandas as pd
df = pd.DataFrame({
"city": ["Boston", "NYC", "Boston", "LA", "NYC", "Boston"],
"purchased": [1, 0, 1, 1, 1, 0], # the TARGET variable (Module 4's classification)
})
target_means = df.groupby("city")["purchased"].mean()
print(target_means)
# city
# Boston 0.667
# LA 1.000
# NYC 0.500
df["city_target_encoded"] = df["city"].map(target_means)
print(df)The Serious Leakage Risk Here
─────────────────────────────────────────
Computing target_means using the FULL dataset (including
what will become test data) leaks the TARGET's information
directly into a FEATURE — a severe violation of Module 1,
Chapter 5's anti-leakage principle, since the encoding for
each row implicitly "knows" its own (or nearby rows') true label.
CORRECT practice: compute target means using ONLY training
data (within each cross-validation fold, ideally), then apply
those FIXED values to validation/test data — never
recomputed using data outside the current training fold.
─────────────────────────────────────────
from sklearn.model_selection import train_test_split
# Correct approach — compute encoding ONLY from training data
X_train, X_test, y_train, y_test = train_test_split(
df[["city"]], df["purchased"], test_size=0.33, random_state=42
)
train_means = y_train.groupby(X_train["city"]).mean() # ONLY from training data
X_train_encoded = X_train["city"].map(train_means)
X_test_encoded = X_test["city"].map(train_means) # REUSE training-derived values, never recompute7. Encoding the Target Variable Itself
For classification (Module 4), the label itself often needs encoding too — text categories like "spam"/"ham" become 0/1 (or 0/1/2/... for multi-class problems).
from sklearn.preprocessing import LabelEncoder
y = ["spam", "ham", "spam", "ham", "ham"]
label_encoder = LabelEncoder()
y_encoded = label_encoder.fit_transform(y)
print(y_encoded) # [1, 0, 1, 0, 0]
print(label_encoder.classes_) # ['ham', 'spam'] — alphabetical by default
# Converting back to original labels after prediction
predictions_encoded = [1, 0, 1]
print(label_encoder.inverse_transform(predictions_encoded)) # ['spam', 'ham', 'spam']LabelEncoder is specifically for target/label encoding, not for input features — for input features with no order, OneHotEncoder (Section 2) remains the correct choice, since LabelEncoder's single-column numeric output would introduce the same false-ordering problem Section 2 warned against.
8. Choosing the Right Encoding
Decision Guide
─────────────────────────────────────────
Category has NO natural order, LOW cardinality (< ~15 values)
→ One-hot encoding (Section 2)
Category HAS a genuine, meaningful order
→ Ordinal encoding, with the order specified EXPLICITLY (Section 3)
Category has HIGH cardinality (hundreds/thousands of values)
→ Target encoding (Section 6, with careful leakage
prevention) or group rare categories into "Other" first
Encoding the TARGET/label itself (classification)
→ LabelEncoder (Section 7)
─────────────────────────────────────────
9. Summary & Next Steps
Key Takeaways
- One-hot encoding creates a binary column per category — the correct default for unordered, low-cardinality categorical features.
- Ordinal encoding preserves a genuine order as a single numeric column — always specify the true order explicitly, never let an encoder guess alphabetically.
- The dummy variable trap (perfect multicollinearity from including all one-hot columns) matters specifically for regression models; drop one column as a harmless convention.
- High-cardinality categoricals overwhelm one-hot encoding — target encoding is more compact but requires strict leakage prevention, computing encodings only from training data.
LabelEncoderis for encoding the target/label in classification, not for input features, where it would introduce a false ordering.
Concept Check
- Why is one-hot encoding preferred over ordinal encoding for a "city" column, but not for a "shirt size" column?
- Why does target encoding carry a serious data leakage risk, and how is it correctly prevented?
- Why shouldn't
LabelEncoderbe used to encode an unordered input feature like "color"?
Next Chapter
→ Chapter 3: Feature Scaling & Normalization
Jr Codex — 1-on-1 Personalized Coaching | Back to Module Index