In the previous lesson we left one branch of the ColumnTransformer pending: the categorical columns. The customer's city, the product category and the MercaFresh subscription plan are text, and Machine Learning models only know how to operate on numbers: they multiply, add up distances, compute gradients. You can't multiply "Barcelona". Encoding means translating categories into numbers, and it's no mechanical formality: each encoding method tells the model a different story about the data, and choosing badly (for example, imposing an order that doesn't exist) introduces false patterns the model will obediently learn. In this lesson you'll see the main methods, their traps and a clear guide on when to use each one.
Contents
- Why models need numbers
- Nominal vs ordinal: the distinction that governs everything
- Label and ordinal encoding
- One-hot encoding
- The high-cardinality problem
- Frequency and target encoding
- Comparison table and decision
- Full MercaFresh case: city, category and plan
Why models need numbers
Internally, almost every algorithm in module 4 boils its work down to arithmetic on matrices: regression computes weighted sums (weight1 * x1 + weight2 * x2 + ...), K-NN measures distances between rows, neural networks multiply matrices. The moment a column contains "Barcelona", that machinery grinds to a halt: there is no such thing as 0.7 * "Barcelona".
The naive temptation is to assign numbers by hand: Barcelona = 1, Madrid = 2, Valencia = 3. It works syntactically... and that's exactly the danger: the model will believe the numbers. It will interpret that Valencia is "three times" Barcelona, that Madrid sits "between" the two, that the average of Barcelona and Valencia is Madrid. We've invented an order and distances that don't exist in reality. This whole lesson revolves around avoiding that deception.
Nominal vs ordinal: the distinction that governs everything
From module 2 (lesson 02-01) you'll remember the classification of variables. For encoding, the crucial distinction is this:
| Type | Is there a real order? | MercaFresh examples | Natural encoding |
|---|---|---|---|
| Nominal | No: just labels | city, product category, payment method | One-hot |
| Ordinal | Yes: order with meaning | plan (basic < standard < premium), satisfaction (low < medium < high) | Ordinal (ordered integers) |
The control question: does it make sense to say one category is "greater" than another? Premium > basic, yes; Madrid > Sevilla, no. Answering this question before touching the keyboard prevents 90% of encoding mistakes.
Label and ordinal encoding
Both assign an integer to each category; the difference lies in who controls the order.
Ordinal encoding: when the order is real
For the MercaFresh subscription plan, the order is information: a premium customer pays more and behaves differently. Here ordered integers are the right encoding, and we must fix the order explicitly:
import pandas as pd
from sklearn.preprocessing import OrdinalEncoder
df = pd.DataFrame({
"customer_id": [101, 104, 105, 106, 107],
"plan": ["basic", "premium", "standard", "basic", "premium"],
})
# categories fixes the order: basic=0 < standard=1 < premium=2
enc = OrdinalEncoder(categories=[["basic", "standard", "premium"]])
df["plan_code"] = enc.fit_transform(df[["plan"]])
print(df)
# customer_id plan plan_code
# 0 101 basic 0.0
# 1 104 premium 2.0
# 2 105 standard 1.0
# ...Without the categories parameter, OrdinalEncoder sorts alphabetically — which here would produce basic=0, premium=1, standard=2, and with ["low", "high", "medium"] would produce high=0, low=1, medium=2: a silent disaster either way. With ordinals, always dictate the order yourself.
In pure pandas, the equivalent is an explicit mapping, just as valid:
Label encoding: almost exclusively for the target variable
scikit-learn's LabelEncoder does the same thing (category → integer) but is meant for the target variable (for example, churn "yes"/"no" → 1/0), not for features: it works on a single column and assigns the alphabetical order without letting you control it.
from sklearn.preprocessing import LabelEncoder
le = LabelEncoder()
y = le.fit_transform(["no", "yes", "no", "no", "yes"]) # [0 1 0 0 1]
print(le.classes_) # ['no' 'yes']The danger: false order on nominals
What happens if we encode city with integers? Barcelona=0, Madrid=1, Sevilla=2, Valencia=3. A linear model will learn a single coefficient for "city" and conclude things like "each step of city increases churn by 3%" — an absurd rule that depends on alphabetical order. Some tree models (04-03) survive this because they only make splits, but as a general rule: integers on nominals = false order = invented patterns. For nominals, the tool is the next one.
One-hot encoding
The idea: instead of one number per category, one binary column per category. Each row has a 1 in its own column and 0 in the rest — hence the name, "one hot".
df2 = pd.DataFrame({
"customer_id": [101, 104, 105, 108, 110],
"city": ["Barcelona", "Barcelona", "Valencia", "Madrid", "Sevilla"],
})
# With pandas: quick for exploring
dummies = pd.get_dummies(df2["city"], prefix="city", dtype=int)
print(pd.concat([df2, dummies], axis=1))
# customer_id city city_Barcelona city_Madrid city_Sevilla city_Valencia
# 0 101 Barcelona 1 0 0 0
# 1 104 Barcelona 1 0 0 0
# 2 105 Valencia 0 0 0 1
# ...No more invented order or distances: each city is an independent dimension and the model learns its own effect for each one. It's the default encoding for nominals with few categories.
get_dummies vs OneHotEncoder
get_dummies is convenient, but it has a serious flaw in an ML workflow: it doesn't memorize the categories. If next month's batch happens to contain no customer from Sevilla, it will generate one column fewer and the model will receive a matrix with a different shape. OneHotEncoder follows the fit/transform pattern you already master: it learns the categories in fit and always guarantees the same columns.
from sklearn.preprocessing import OneHotEncoder
ohe = OneHotEncoder(sparse_output=False, handle_unknown="ignore")
X_city = ohe.fit_transform(df2[["city"]])
print(ohe.get_feature_names_out())
# ['city_Barcelona' 'city_Madrid' 'city_Sevilla' 'city_Valencia']handle_unknown="ignore": if a customer from Bilbao shows up in production (a never-seen category), it encodes them as all zeros instead of raising an error. Essential in the real world.sparse_output=Falsereturns a regular matrix; with many columns it's better to keep the output sparse to save memory.- It slots directly in as the categorical branch of the
ColumnTransformerfrom the previous lesson.
The multicollinearity trap, in passing
If you know a customer isn't from Barcelona, nor Madrid, nor Sevilla... you already know they're from Valencia: the last column is redundant (it always sums to 1 with the others). That redundancy, called multicollinearity, bothers some classic linear models. The drop="first" parameter (or drop_first=True in get_dummies) removes one column to avoid it. Keep the idea and the parameter name in mind; the deeper why belongs to linear regression (04-01) and regularization (07-01).
The high-cardinality problem
One-hot scales poorly. The city column with 4 values generates 4 columns; but what about MercaFresh's delivery postal code, with 800 distinct values? Or the product id with 5,000?
- 800 new columns, almost all zeros: memory and runtime through the roof.
- Categories with 2 or 3 examples: the model can't learn anything reliable from them (raw material for overfitting, which we'll formalize in 06-05).
Strategies against high cardinality:
- Group by business knowledge: postal codes → province or delivery zone.
- Group the rare tail: keep the frequent categories and merge the rest into
"other"(OneHotEncoder(min_frequency=...)does it for you). - Switch encodings: frequency or target encoding, which generate a single column regardless of cardinality.
# Group the tail: cities with fewer than 20 customers become "other"
counts = df_large["city"].value_counts()
rare = counts[counts < 20].index
df_large["city_grouped"] = df_large["city"].replace(rare, "other")Frequency and target encoding
Two techniques that replace the category with a single informative number, designed for high cardinality. We present them at an introductory level: understand the idea and, above all, the risk.
Frequency encoding
Replaces each category with its frequency (how many times it appears, or its proportion):
freq = df_large["city"].value_counts(normalize=True)
df_large["city_freq"] = df_large["city"].map(freq)Barcelona (40% of customers) becomes 0.40; a village with 2 customers, 0.0002. It's simple, doesn't explode dimensionality, and sometimes the frequency itself is predictive (big cities have better delivery times and less churn). Limitation: two categories with equal frequency become indistinguishable.
Target encoding
Replaces each category with the mean of the target variable within that category: each city becomes its own churn rate.
# Conceptual idea (NOT how you should do it in a real project — see the risk below)
churn_rate_by_city = df_large.groupby("city")["churn"].mean()
df_large["city_target"] = df_large["city"].map(churn_rate_by_city)It's extremely powerful — it condenses exactly the category-target relationship into one number — and for that very reason it's the technique with the highest data leakage risk in all of preprocessing: we're putting the answer (churn) inside a feature. For a city with 3 customers of which 2 churned, the encoding 0.67 is practically whispering those customers' labels to the model. Symptoms and precautions:
- Suspiciously perfect training evaluation and poor performance on new data.
- Compute the means with training data only (the same principle from 03-02, here elevated to critical).
- Use smoothing (blending the category mean with the global mean, weighting the global one more heavily the less data the category has) and the cross-validation schemes you'll see in 06-03. scikit-learn's
TargetEncoderbuilds in these defenses.
A prudent rule for this course: one-hot as the default option; target encoding only when cardinality demands it and with its safeguards in place.
Comparison table and decision
| Method | Columns generated | Imposes order? | High cardinality | Main risk | When to use it |
|---|---|---|---|---|---|
| Ordinal encoding | 1 | Yes (you define it) | Fine | Badly defined order | Real ordinals (plan, level) |
| Label encoding | 1 | Yes (alphabetical) | — | False order | Target variable only |
| One-hot | 1 per category | No | Poor | Column explosion | Nominals with few categories (default) |
| Frequency | 1 | No | Very good | Frequency collisions | High cardinality, quick fix |
| Target | 1 | No | Very good | Data leakage | High cardinality, with smoothing and care |
flowchart TD
A{"Does the variable have<br/>a real order?"} -->|Yes| B["Ordinal encoding<br/>with explicit order"]
A -->|No| C{"Few categories?<br/>(rule of thumb: < 15)"}
C -->|Yes| D["One-hot encoding"]
C -->|No| E{"Can I group with<br/>business criteria?"}
E -->|Yes| F["Group and one-hot"]
E -->|No| G["Frequency or target encoding<br/>(with safeguards)"]
Full MercaFresh case: city, category and plan
Let's apply the decision to the three categoricals of the churn dataset:
import pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import OneHotEncoder, OrdinalEncoder
customers = pd.DataFrame({
"customer_id": [101, 104, 105, 106, 107, 108],
"city": ["Barcelona", "Barcelona", "Valencia", "Madrid", "Madrid", "Sevilla"],
"favorite_category": ["fresh", "pantry", "fresh", "drinks", "fresh", "cleaning"],
"plan": ["basic", "premium", "standard", "basic", "premium", "basic"],
"total_spend": [1250.5, 890.0, 2100.75, 310.2, 15400.0, 670.4],
})
# Reasoned decisions:
# - city: nominal, 4 values -> one-hot
# - favorite_category: nominal, 4 values -> one-hot
# - plan: real ordinal -> ordinal with explicit order
encoder = ColumnTransformer(transformers=[
("nominal", OneHotEncoder(sparse_output=False, handle_unknown="ignore"),
["city", "favorite_category"]),
("ordinal", OrdinalEncoder(categories=[["basic", "standard", "premium"]]),
["plan"]),
("numeric", "passthrough", ["total_spend"]),
])
X = encoder.fit_transform(customers)
print(encoder.get_feature_names_out())
print(pd.DataFrame(X, columns=encoder.get_feature_names_out()).round(2))The result is a fully numeric matrix: 4 city columns + 4 category columns + 1 plan column + the spend. Notice how the ColumnTransformer introduced in 03-03 has absorbed the encoding as just another branch: the preprocessing is still a single object with fit and transform. Note also the city cleanup from 03-01: if we hadn't unified "BCN" and " barcelona ", the one-hot would have created ghost columns for each variant.
Common Mistakes and Tips
- Arbitrary integers on nominals. City = 1, 2, 3 invents order and distances. If you can't say which one is "greater", don't use ordinal.
- Letting
OrdinalEncodersort a real ordinal alphabetically. Always passcategories=[...]with the business order. - Using
get_dummiesin production. With no memory of the categories, each batch can generate different columns. In ML workflows,OneHotEncoderwithhandle_unknown="ignore". - One-hot on 800 postal codes. Column explosion and categories with no data: group first or switch techniques.
- Target encoding computed on the whole dataset. It's data leakage in its purest form: per-category means are learned from training data only, with smoothing.
- Encoding before cleaning the text. "Madrid" and "madrid " generate two columns. The normalization from 03-01 always goes before encoding.
- Tip: after encoding, print
get_feature_names_out()and check that the number and names of the columns are what you expected. Encoding errors are silent; this 5-second check catches nearly all of them.
Exercises
Exercise 1
Classify each MercaFresh variable as nominal or ordinal and state the appropriate encoding method: (a) payment method (card, PayPal, cash on delivery); (b) rating of the last delivery (bad, fair, good, excellent); (c) delivery neighborhood in Barcelona (73 neighborhoods); (d) customer type (individual, business).
Exercise 2
Encode with OrdinalEncoder the column rating = ["good", "bad", "excellent", "fair", "good"] respecting the real order. Show which (incorrect) encoding the default alphabetical order would have produced.
Exercise 3
Apply frequency encoding to the column neighborhood = ["Gracia", "Eixample", "Gracia", "Sants", "Eixample", "Gracia", "Raval", "Eixample"] using proportions. Which two neighborhoods become indistinguishable after encoding, and why is that a limitation of the method?
Solutions
Exercise 1
- (a) Nominal (no payment method is "greater") → one-hot (3 categories, no problem).
- (b) Real ordinal (bad < fair < good < excellent) → ordinal encoding with explicit order.
- (c) High-cardinality nominal (73 values) → group by district if the business allows it; otherwise, frequency or target encoding with safeguards.
- (d) Binary nominal → one-hot; with two categories a single 0/1 column suffices (it's exactly the case where
drop="first"comes naturally).
Exercise 2
from sklearn.preprocessing import OrdinalEncoder
import pandas as pd
df = pd.DataFrame({"rating": ["good", "bad", "excellent", "fair", "good"]})
# Correct: explicit business order
enc = OrdinalEncoder(categories=[["bad", "fair", "good", "excellent"]])
print(enc.fit_transform(df)) # good=2, bad=0, excellent=3, fair=1
# Incorrect: default alphabetical order
enc_bad = OrdinalEncoder()
print(enc_bad.fit_transform(df)) # bad=0, excellent=1, fair=2, good=3With the alphabetical order, "fair" (2) and "good" (3) would rank above "excellent" (1): the model would learn that middling ratings are "greater" than the best one.
Exercise 3
s = pd.Series(["Gracia", "Eixample", "Gracia", "Sants",
"Eixample", "Gracia", "Raval", "Eixample"])
freq = s.value_counts(normalize=True)
print(s.map(freq))
# Gracia -> 0.375, Eixample -> 0.375, Sants -> 0.125, Raval -> 0.125Gracia and Eixample collide at 0.375, and Sants and Raval at 0.125: different neighborhoods become identical to the model. Frequency encoding only preserves "how common the category is", not its identity — the price of compressing everything into one number.
Conclusion
You now know how to translate categories into numbers without lying to the model: ordinal encoding with explicit order for real ordinals, one-hot as the default for nominals (with OneHotEncoder and handle_unknown="ignore" in serious workflows), grouping strategies or frequency/target encoding for high cardinality — with target encoding flagged as the highest-leakage-risk zone — and everything integrated into the ColumnTransformer we've been building. The MercaFresh churn matrix is now fully numeric.
But take a look at it: the one-hot columns are 0 or 1, the plan runs from 0 to 2... and the total spend reaches 15,400. For algorithms that measure distances or descend gradients, that disparity of scales lets a single column drown out all the others. That is exactly the problem of the next lesson: normalization and standardization.
Machine Learning Course
Module 1: Introduction to Machine Learning
- What is Machine Learning?
- History and evolution of Machine Learning
- Types of Machine Learning
- Applications of Machine Learning
- The Machine Learning project workflow
Module 2: Foundations of Statistics and Probability
- Basic statistics concepts
- Probability distributions
- Correlation and covariance
- Statistical inference
- Bayes' theorem
Module 3: Data Preprocessing
- Data cleaning
- Handling missing data
- Data transformation
- Encoding categorical variables
- Normalization and standardization
- Feature engineering
Module 4: Supervised Machine Learning Algorithms
- Linear regression
- Logistic regression
- Decision trees
- Support Vector Machines (SVM)
- K-Nearest Neighbors (K-NN)
- Naive Bayes
- Neural networks
Module 5: Unsupervised Machine Learning Algorithms
- Clustering: K-means
- Hierarchical clustering
- Principal Component Analysis (PCA)
- DBSCAN clustering
- Data visualization with t-SNE and UMAP
Module 6: Model Evaluation and Validation
- Data splitting: training, validation and test
- Evaluation metrics
- Cross-validation
- ROC curve and AUC
- Overfitting and underfitting
Module 7: Advanced Techniques and Optimization
- Regularization: Ridge, Lasso and Elastic Net
- Ensemble Learning
- Gradient Boosting
- Deep neural networks (Deep Learning)
- Hyperparameter optimization
Module 8: Model Implementation and Deployment
- Popular frameworks and libraries
- Deploying models to production
- Model maintenance and monitoring
- Ethical and privacy considerations
Module 9: Hands-On Projects
- Project 1: Housing price prediction
- Project 2: Image classification
- Project 3: Sentiment analysis on social media
- Project 4: Fraud detection
- Project 5: Customer segmentation
