Lesson 11 of 30

Chapter 11 — Encoding Categorical Features

Chapter overview

Categorical variables describe membership, type, status, level, region, device family, product group, or other qualitative concepts. Most machine-learning estimators cannot consume raw strings directly, so these variables must be transformed into numerical representations. The transformation is not merely a technical conversion: it encodes assumptions about similarity, order, rarity, and information content.

This chapter develops a practical framework for choosing among ordinal encoding, one-hot encoding, rare-category grouping, frequency encoding, target encoding, and hashing. Particular attention is given to leakage prevention, unknown categories at prediction time, sparse matrices, high cardinality, and production robustness.

Learning objectives

  • Explain why raw category labels cannot usually be supplied directly to numerical learning algorithms.
  • Distinguish nominal and ordinal categories and preserve their semantic meaning during encoding.
  • Apply ordinal and one-hot encoding safely with scikit-learn.
  • Control dimensionality by grouping infrequent categories and using frequency thresholds.
  • Evaluate strategies for high-cardinality features, including frequency encoding, target encoding, and feature hashing.
  • Prevent target leakage by fitting target-dependent encoders only within training folds.
  • Configure encoders so unseen categories do not break production predictions.
  • Construct separate numerical and categorical preprocessing procedures in a ColumnTransformer pipeline.

Running example

Throughout the chapter, we use a customer-subscription dataset containing numerical variables such as age and monthly_spend, categorical variables such as region and plan_type, an ordered satisfaction_level, and a binary target renewed. The same principles apply to healthcare, manufacturing, education, finance, and other tabular domains.

 

11.1 Why categorical variables require encoding

A categorical variable represents a finite set of labels rather than a physical numerical scale. Examples include payment method, city, product family, subscription plan, blood group, device type, and education level. A learning algorithm needs a numerical representation of these concepts before it can estimate distances, coefficients, split points, or gradients.

Machine learning algorithms expect numerical inputs

Many estimators operate on a numerical feature matrix X. Linear and logistic regression compute weighted sums; K-nearest neighbors and support vector machines depend on distances or inner products; neural networks use numerical tensors. A column containing values such as "Basic", "Premium", and "Enterprise" therefore requires a transformation before training.

Python example 11.1 — Inspect categorical columns

import pandas as pd

df = pd.DataFrame({
    "age": [224135,  29],
    "plan_type": ["Basic", "Premium", "Basic", "Enterprise"],
    "region": ["North", "South", "West", "North"],
    "renewed": [011,  1],
})

print(df.dtypes)
print(df.select_dtypes(include="object").columns.tolist())

 

Category values do not automatically have numerical meaning

Replacing labels by arbitrary integers can accidentally introduce an order or a distance that does not exist. Coding {North=0, South=1, West=2} implies that West is numerically farther from North than South is, even though such geometry has no semantic basis. The encoding method must therefore match the variable’s meaning.

Variable

Semantic type

Unsafe shortcut

Why unsafe

regionNominalNorth=0, South=1, West=2Creates false order and false distances.
satisfaction_levelOrdinalAlphabetical codesAlphabetical order may not match Low < Medium < High.
plan_typeNominalBasic=1, Premium=2, Enterprise=3Numbers imply a quantitative spacing not necessarily justified.
is_studentBinaryNo issue if 0/1 is meaningfulBinary variables already have a natural numerical representation.

 

Core principle

Encoding is part of feature engineering. A good encoding preserves useful structure while avoiding invented structure, leakage, excessive dimensionality, and brittle behavior on future data.

 

11.2 Ordinal encoding

Ordinal encoding is appropriate when categories possess a genuine, domain-defined order. The encoder maps each ordered category to an integer while preserving the ranking. The numerical spacing between consecutive codes should not automatically be interpreted as an equal physical distance.

Ordered categories

Examples include Low < Medium < High, Bronze < Silver < Gold, beginner < intermediate < advanced, or an agreement scale from strongly disagree to strongly agree. The order must be justified by domain semantics, not by alphabetical order or convenience.

Mapping categories to ordered numbers

Python example 11.2 — Explicit ordinal mapping with pandas

order = {
    "Low": 0,
    "Medium": 1,
    "High": 2,
}

df["satisfaction_code"] = df["satisfaction_level"].map(order)
print(df[["satisfaction_level", "satisfaction_code"]])

 

Python example 11.3 — OrdinalEncoder with a declared order

from sklearn.preprocessing import OrdinalEncoder

encoder = OrdinalEncoder(
    categories=[["Low", "Medium", "High"]],
     handle_unknown="use_encoded_value",
    unknown_value=-1,
)

encoded = encoder.fit_transform(df[["satisfaction_level"]])

 

Appropriate examples

Variable

Valid order

Interpretation

service_qualityPoor < Fair < Good < ExcellentRanked qualitative assessment.
education_stagePrimary < Secondary < Undergraduate < GraduateOrdered stage, although intervals are not equal.
risk_bandLow < Medium < High < CriticalOperational risk ranking.
shirt_sizeXS < S < M < L < XLOrdered size categories.

 

Risks of imposing false order

Ordinal encoding becomes dangerous when used for nominal variables. A model may treat the numerical codes as ordered or distance-bearing. In a linear model, moving from code 0 to code 1 has the same algebraic increment as moving from 1 to 2. In KNN, the codes influence distance directly. Tree models are less sensitive to numeric spacing but still split using thresholds, which can create artificial category groupings.

Do not confuse rank with distance

Ordinal encoding preserves order, not equal spacing. If the difference between "Low" and "Medium" is not known to equal the difference between "Medium" and "High", interpret the codes as ordered labels rather than measurements.

 

11.3 One-hot encoding

One-hot encoding represents each category using a separate binary indicator column. It is the standard default for low- to moderate-cardinality nominal variables because it avoids imposing an artificial order.

Binary columns for each category

Python example 11.4 — One-hot encoding with pandas

encoded = pd.get_dummies(
    df,
    columns=["plan_type", "region"],
    dtype=int,
)

print(encoded.head())

 

Python example 11.5 — OneHotEncoder in scikit-learn

from sklearn.preprocessing import OneHotEncoder

encoder = OneHotEncoder(
    handle_unknown="ignore",
    sparse_output=True,
)

X_cat = encoder.fit_transform(df[["plan_type", "region"]])
print(encoder.get_feature_names_out())
print(X_cat.shape)

 

Dropping a reference category

For some linear models, one category may be dropped to create a reference level. With a variable containing k categories, k−1 indicators are enough to reconstruct category membership when an intercept is present. This can reduce exact multicollinearity, although regularized estimators often work acceptably with all indicators.

Python example 11.6 — Drop a reference level

encoder = OneHotEncoder(
    drop="first",
    handle_unknown="ignore",
    sparse_output=True,
)

X_cat = encoder.fit_transform(df[["plan_type", "region"]])

 

Unknown categories

Training data may contain North, South, and West, while a future request contains East. An encoder configured to raise an error can interrupt a prediction service. For robust inference, configure unknown-category behavior explicitly and test it before deployment.

Sparse matrices

One-hot matrices contain mostly zeros. Sparse representations store only nonzero entries, often reducing memory dramatically. This matters when many categorical variables or categories are present. Not every estimator accepts sparse matrices, so the downstream model and pipeline must be checked.

Python example 11.7 — Inspect sparse output

X_cat = encoder.fit_transform(df[["plan_type", "region"]])

print(type(X_cat))
print("Shape:", X_cat.shape)
print("Stored non-zero values:", X_cat.nnz)

 

High-dimensional output

If a feature contains 10,000 unique categories, ordinary one-hot encoding can create up to 10,000 columns. The resulting feature space may consume memory, slow optimization, increase variance, and make interpretation difficult. Rare-category grouping or specialized high-cardinality encoders may be preferable.

Strength

Limitation

No false ordinal relationship for nominal categories.Dimension grows with the number of categories.
Easy to interpret in linear models.Very rare indicators may overfit.
Supported by most tabular ML workflows.Unknown categories need explicit handling.
Works naturally with sparse matrices.Feature names and model artifacts can become large.

 

11.4 Handling rare categories

A rare category appears only a small number of times or represents only a tiny fraction of the training data. Rare categories are statistically difficult: their estimated effects are uncertain, a model can memorize them, and future datasets may contain new variants that were absent during training.

Grouping infrequent categories

A common strategy replaces infrequent labels with a shared category such as "Other" or "Rare". The grouping rule must be learned from the training data and then applied unchanged to validation, test, and production data.

Python example 11.8 — Group categories using a frequency threshold

counts = df["city"].value_counts()
min_count = 20
frequent = counts[counts >= min_count].index

df["city_grouped"] = df["city"].where(
    df["city"].isin(frequent),
    "Other",
)

 

Minimum frequency thresholds

Thresholds can be expressed as a count (for example, at least 20 rows) or as a proportion (for example, at least 1% of training observations). The threshold is a modeling choice and should be assessed with cross-validation rather than selected solely for convenience.

Python example 11.9 — OneHotEncoder with built-in infrequent-category handling

encoder = OneHotEncoder(
     handle_unknown="infrequent_if_exist",
    min_frequency=0.01,
    sparse_output=True,
)

X_cat = encoder.fit_transform(df[["city", "plan_type"]])

 

The “Other” category

An "Other" group increases support by pooling categories, but it is semantically heterogeneous. Its effect should not be interpreted as a coherent real-world group. The category is a modeling device that trades detailed identity for statistical stability.

Risks of rare-category overfitting

  • A category appearing once can perfectly coincide with one target value by chance.
  • A tree may isolate a tiny category and create an unstable leaf.
  • Target encoding can produce extreme estimates for tiny groups without smoothing.
  • One-hot coefficients for rare levels can have high variance.
  • A category may disappear or change spelling in production.

Training-only rule

Compute rare-category membership from the training partition only. If the full dataset is used to decide which categories are rare, information about validation or test composition enters preprocessing and weakens the honesty of evaluation.

 

11.5 High-cardinality categorical features

High-cardinality features contain hundreds, thousands, or even millions of distinct values. Examples include product IDs, merchant IDs, postal codes, URLs, device models, job titles, and user-generated tags. Ordinary one-hot encoding may be impractical or statistically weak.

Problems with thousands of categories

  • Large sparse matrices and high memory usage.
  • Slow training and hyperparameter search.
  • Rare-level overfitting.
  • Unstable categories between training and production.
  • Large model artifacts and difficult feature inspection.
  • Potential identity leakage when the feature is actually an identifier.

Frequency encoding

Frequency encoding replaces each category with its count or proportion in the training data. It is compact and target-independent, but different categories with the same frequency receive the same value. The encoding expresses prevalence, not category identity.

Python example 11.10 — Frequency encoding

train_freq = X_train["merchant"].value_counts(normalize=True)

X_train["merchant_freq"] = X_train["merchant"].map(train_freq)
X_valid["merchant_freq"] = (
     X_valid["merchant"].map(train_freq).fillna(0.0)
)

 

Target encoding

Target encoding replaces a category with a statistic of the target, commonly the mean target value for that category. For binary classification, this can approximate the historical positive-class rate. It is compact and powerful, especially for high-cardinality variables, but it directly uses labels and therefore has a serious leakage risk.

Python example 11.11 — Naive target encoding (illustrates the risky pattern)

# WARNING: do not compute this on all rows and evaluate on the same rows.
category_mean = train_df.groupby("merchant")["renewed"].mean()
train_df["merchant_te_naive"] = train_df["merchant"].map(category_mean)

 

Leakage risks in target encoding

If a row contributes to the statistic used to encode itself, the encoded feature contains information about its own target. The problem is particularly severe for rare categories: a category appearing once receives exactly that row’s target. Validation scores can become unrealistically high.

Leakage warning

Target-dependent encoders must be trained inside the model-validation process. Never fit a target encoder once on the complete dataset before cross-validation or before the train/test split.

 

Cross-validated encoding

For training rows, out-of-fold target encoding computes each row’s encoded value using only other folds. For validation or test rows, the encoder is fitted on the available training data and applied to unseen rows. Smoothing is often added so that small groups shrink toward the global mean.

Python example 11.12 — Simple out-of-fold target encoding

import numpy as np
from sklearn.model_selection import KFold

X = train_df[["merchant"]].copy()
y = train_df["renewed"].copy()
encoded = pd.Series(index=X.index, dtype=float)

kf = KFold(n_splits=5, shuffle=True, random_state=42)

for fit_idx, hold_idx in kf.split(X):
    fit_x = X.iloc[fit_idx]
    fit_y = y.iloc[fit_idx]
    means = fit_y.groupby(fit_x["merchant"]).mean()
    global_mean = fit_y.mean()
    encoded.iloc[hold_idx] = (
         X.iloc[hold_idx]["merchant"].map(means).fillna(global_mean)
    )

train_df["merchant_te_oof"] = encoded

 

Hashing

Feature hashing maps categories into a fixed number of columns using a hash function. The dimensionality is controlled regardless of the number of unique values, and no category vocabulary needs to be stored. The trade-off is collisions: different categories may map to the same column, and direct interpretability is reduced.

Python example 11.13 — Feature hashing

from sklearn.feature_extraction import FeatureHasher

records = [{"merchant=" + value: 1for value in df["merchant"]]

hasher = FeatureHasher(
    n_features=32,
    input_type="dict",
    alternate_sign=False,
)

X_hash = hasher.transform(records)
print(X_hash.shape)

 

Method

Uses target?

Output size

Main strength

Main caution

One-hotNoNumber of categoriesTransparent and widely supportedCan explode in dimension.
Rare grouping + one-hotNoReduced categoriesSimple and stableGroups heterogeneous labels.
Frequency encodingNo1 column/featureCompact and easyLoses category identity.
Target encodingYes1 column/featureOften strong for high cardinalityLeakage and overfitting risk.
HashingNoFixedBounded memory; no vocabularyCollisions and lower interpretability.

 

11.6 Unknown categories at prediction time

An unknown category is a label observed during transformation that was absent when the encoder was fitted. This is normal in real systems: new cities, products, merchants, devices, campaigns, or job titles appear continuously. Production-safe preprocessing treats unknown categories as an expected condition rather than an exceptional failure.

Configuring encoders safely

Python example 11.14 — Safe one-hot handling of unknown categories

from sklearn.preprocessing import OneHotEncoder

encoder = OneHotEncoder(
    handle_unknown="ignore",
    sparse_output=True,
)

encoder.fit(train_df[["region"]])
new_rows = pd.DataFrame({"region": ["North", "East"]})
X_new = encoder.transform(new_rows)

 

Python example 11.15 — Safe ordinal handling with an explicit unknown code

from sklearn.preprocessing import OrdinalEncoder

encoder = OrdinalEncoder(
    categories=[["Low", "Medium", "High"]],
     handle_unknown="use_encoded_value",
    unknown_value=-1,
)

encoder.fit(train_df[["satisfaction_level"]])

 

Ensuring production robustness

  • Fit encoders only on training data and persist the fitted preprocessing pipeline.
  • Define behavior for unknown and missing categories explicitly.
  • Validate input schemas before prediction.
  • Log the frequency of unseen categories in production.
  • Monitor category drift and the growth of the "Other" group.
  • Use stable category definitions or controlled vocabularies when the domain allows them.
  • Include unknown-category test cases in automated tests.

Python example 11.16 — Test production behavior explicitly

test_cases = pd.DataFrame({
    "region": ["North", "NeverSeenBefore"],
    "plan_type": ["Basic", "NewPlan"],
})

try:
    transformed = preprocessor.transform(test_cases)
    print("Transformation succeeded:", transformed.shape)
except Exception as exc:
    print("Preprocessing failed:", exc)

 

Encoding decision guide

Question

If yes

Recommended direction

Does the category have a real order?YesOrdinal encoding with explicit category order.
Is the feature nominal with modest cardinality?YesOne-hot encoding.
Are many categories very rare?YesGroup infrequent categories, then encode.
Is cardinality very high?YesConsider frequency, leakage-safe target encoding, or hashing.
Will new labels appear in production?YesConfigure unknown handling and monitor drift.
Does the encoding use y?YesFit inside folds/pipelines; prevent leakage.
Is the column actually an identifier?YesUsually remove it or use entity-aware strategy rather than encode blindly.

 

Recommended default

For ordinary tabular supervised learning: start with one-hot encoding for nominal variables and ordinal encoding only for genuinely ordered variables. Add rare-category handling when needed. Move to specialized high-cardinality techniques only when the simple baseline is inadequate or operationally too expensive.

 

Practical lab — Build separate numerical and categorical preprocessing procedures

Scenario

A subscription company wants to predict whether a customer will renew. The dataset contains numerical features, nominal categories, an ordered satisfaction variable, and a high-cardinality city field. Students must construct a leakage-safe preprocessing pipeline and verify that unseen categories can be transformed successfully.

Lab objectives

  • Identify numerical, nominal, and ordinal features.
  • Impute missing values separately by feature type.
  • Scale numerical features.
  • One-hot encode nominal categories with safe unknown handling.
  • Ordinally encode the satisfaction level with a declared order.
  • Group infrequent city labels automatically.
  • Combine all transformations in ColumnTransformer.
  • Fit preprocessing only on the training split.
  • Inspect generated feature names and transformed dimensions.
  • Verify an unseen production row can be processed.

Step 1 — Create the example dataset

Lab code 11.1 — Dataset

import numpy as np
import pandas as pd

rng = np.random.default_rng(42)
n = 600

df = pd.DataFrame({
    "age": rng.integers(1875, n),
    "monthly_spend": rng.normal(6520, n).round(2),
    "plan_type": rng.choice(["Basic", "Plus", "Premium"], n),
    "region": rng.choice(["North", "South", "East", "West"], n),
    "satisfaction_level": rng.choice(["Low", "Medium", "High"], n),
    "city": rng.choice([f"City_{i}" forin range(45)], n),
})

df["renewed"] = (
    (df["monthly_spend"] > 55).astype(int)
    + (df["satisfaction_level"] == "High").astype(int)
    + rng.integers(0,  2, n)
    >= 2
).astype(int)

# Introduce a few missing values for preprocessing practice.
df.loc[rng.choice(df.index,  20, replace=False), "monthly_spend"] = np.nan
df.loc[rng.choice(df.index,  15, replace=False), "region"] = np.nan

 

Step 2 — Separate X and y, then split

Lab code 11.2 — Train/test split before fitting encoders

from sklearn.model_selection import train_test_split

X = df.drop(columns="renewed")
y = df["renewed"]

X_train, X_test, y_train, y_test = train_test_split(
    X,
    y,
    test_size=0.20,
    random_state=42,
    stratify=y,
)

 

Step 3 — Define feature groups

Lab code 11.3 — Feature groups

numeric_features = ["age", "monthly_spend"]
nominal_features = ["plan_type", "region", "city"]
ordinal_features = ["satisfaction_level"]

satisfaction_order = [["Low", "Medium", "High"]]

 

Step 4 — Build separate pipelines

Lab code 11.4 — Numerical, nominal, and ordinal pipelines

from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import OneHotEncoder, OrdinalEncoder, StandardScaler

numeric_pipe = Pipeline([
    ("imputer", SimpleImputer(strategy="median")),
    ("scaler", StandardScaler()),
])

nominal_pipe = Pipeline([
    ("imputer", SimpleImputer(strategy="most_frequent")),
    ("onehot", OneHotEncoder(
         handle_unknown="infrequent_if_exist",
        min_frequency=0.02,
        sparse_output=True,
    )),
])

ordinal_pipe = Pipeline([
    ("imputer", SimpleImputer(strategy="most_frequent")),
    ("ordinal", OrdinalEncoder(
        categories=satisfaction_order,
         handle_unknown="use_encoded_value",
        unknown_value=-1,
    )),
])

 

Step 5 — Combine transformations

Lab code 11.5 — ColumnTransformer

from sklearn.compose import ColumnTransformer

preprocessor = ColumnTransformer([
    ("num", numeric_pipe, numeric_features),
    ("nom", nominal_pipe, nominal_features),
    ("ord", ordinal_pipe, ordinal_features),
])

X_train_ready = preprocessor.fit_transform(X_train)
X_test_ready = preprocessor.transform(X_test)

print("Train shape:", X_train_ready.shape)
print("Test shape:", X_test_ready.shape)

 

Step 6 — Inspect generated feature names

Lab code 11.6 — Feature names

feature_names = preprocessor.get_feature_names_out()

print("Number of output features:", len(feature_names))
print(feature_names[:20])

 

Step 7 — Add the model to the pipeline

Lab code 11.7 — End-to-end classification pipeline

from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, classification_report

model = Pipeline([
    ("preprocess", preprocessor),
    ("classifier", LogisticRegression(max_iter=1000)),
])

model.fit(X_train, y_train)
pred = model.predict(X_test)

print("Accuracy:", accuracy_score(y_test, pred))
print(classification_report(y_test, pred))

 

Step 8 — Test an unseen production category

Lab code 11.8 — Robustness test

new_customer = pd.DataFrame({
    "age": [34],
    "monthly_spend": [72.50],
    "plan_type": ["Ultra"],           # unseen plan
    "region": ["Central"],          # unseen region
    "satisfaction_level": ["High"],
    "city": ["Brand_New_City"],     # unseen city
})

prediction = model.predict(new_customer)
probability = model.predict_proba(new_customer)[:, 1]

print("Predicted renewal:", prediction[0])
print("Renewal probability:", probability[0])

 

Student deliverable

  • A notebook containing the complete preprocessing and model pipeline.
  • A table classifying each input feature as numerical, nominal, ordinal, or high-cardinality.
  • An explanation of why each categorical encoding method was selected.
  • The transformed training and test dimensions.
  • A list of generated output feature names.
  • A demonstration that unseen categories do not crash preprocessing.
  • A short note explaining where leakage could occur if target encoding were introduced.

Evidence-based questions

  1. Why is satisfaction_level encoded ordinally rather than one-hot encoded in this lab?
  2. Why is city allowed to use an infrequent-category mechanism?
  3. What happens to an unseen region when handle_unknown is configured safely?
  4. Why is preprocessor.fit_transform used only on X_train while X_test uses transform?
  5. Would target encoding city be automatically better? What validation would be required before choosing it?

Common mistakes and corrections

Mistake

Why it is a problem

Correction

Integer-code nominal labelsInvents order and distance.Use one-hot or another nominal encoding.
Fit encoder on full datasetLeaks test-set category information.Fit preprocessing on training data only.
Target encode before CVTarget information leaks into validation folds.Use out-of-fold or pipeline-aware encoding.
Ignore unknown categoriesProduction predictions may fail.Configure and test unknown handling.
One-hot encode a huge ID columnCreates massive, unstable feature space.Question whether ID belongs in model; consider grouping/hashing if meaningful.
Treat missing as ordinary string accidentallyMissingness semantics become inconsistent.Impute or encode missingness intentionally.
Drop every rare categoryMay remove important but real signals.Assess rarity policy with domain knowledge and validation.

 

Chapter summary

  • Categorical encoding converts qualitative labels into numerical representations while preserving useful semantics.
  • Ordinal encoding is appropriate only when the categories have a genuine order.
  • One-hot encoding is a strong default for low- and moderate-cardinality nominal variables.
  • Rare-category grouping can improve stability and control dimensionality.
  • High-cardinality features may require frequency encoding, target encoding, or hashing.
  • Target encoding must be cross-validated or otherwise fitted without allowing each row to reveal its own target.
  • Unknown-category behavior must be deliberately configured for robust production systems.
  • ColumnTransformer and Pipeline provide a clean way to apply different preprocessing strategies to different feature groups without leakage.

Knowledge check

1. A feature contains Red, Green, and Blue with no natural ranking. Which encoding is the safest default?

Answer: One-hot encoding.

2. Why is coding Red=0, Green=1, Blue=2 risky?

Answer: It creates an artificial order and numerical distance.

3. What is the main benefit of sparse one-hot output?

Answer: It stores mostly-zero matrices efficiently.

4. What is the central danger of naive target encoding?

Answer: Target leakage and severe overfitting, especially for rare categories.

5. How should a fitted encoder behave when a new category appears in production?

Answer: Use an explicit safe strategy such as ignore, infrequent grouping, or an unknown code, depending on the encoder.

6. Should rare-category thresholds be determined from the test set?

Answer: No. They must be learned from training data only.

Expected outcome

Students should be able to classify categorical variables by semantic type, choose an appropriate encoding strategy, build separate numerical and categorical preprocessing pipelines, prevent target-encoding leakage, and ensure that future unseen categories are handled safely.