Feature Engineering
Feature engineering transforms raw columns into representations models can learn from. Encoding categoricals, scaling numerics, and composing transforms with ColumnTransformer are the core patterns for tabular ML.
Recipe
Quick-reference recipe card - copy-paste ready.
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import OneHotEncoder, StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.ensemble import RandomForestClassifier
preprocessor = ColumnTransformer([
("num", StandardScaler(), numeric_cols),
("cat", OneHotEncoder(handle_unknown="ignore"), categorical_cols),
])
pipe = Pipeline([("prep", preprocessor), ("clf", RandomForestClassifier())])When to reach for this:
- Mixed numeric and categorical columns in the same dataset.
- Models sensitive to feature scale (SVM, logistic regression, neural nets on tabular data).
- High-cardinality categoricals that need hashing or target encoding.
- Reproducible preprocessing bundled with the model for deployment.
Working Example
"""feature_engineering.py - mixed-type preprocessing with ColumnTransformer."""
from __future__ import annotations
import pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.metrics import classification_report
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler
df = pd.DataFrame({
"age": [25, 34, 45, 29, 52],
"income": [45000, 62000, 88000, 51000, 95000],
"city": ["nyc", "la", "nyc", "chicago", "la"],
"plan": ["basic", "pro", "pro", "basic", "enterprise"],
"churned": [0, 0, 1, 0, 1],
})
X = df.drop(columns=["churned"])
y = df["churned"]
numeric_cols = ["age", "income"]
categorical_cols = ["city", "plan"]
preprocessor = ColumnTransformer(
transformers=[
("num", StandardScaler(), numeric_cols),
("cat", OneHotEncoder(handle_unknown="ignore", sparse_output=False), categorical_cols),
],
remainder="drop",
)
pipe = Pipeline([
("prep", preprocessor),
("clf", GradientBoostingClassifier(random_state=42)),
])
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.4, random_state=42)
pipe.fit(X_train, y_train)
print(classification_report(y_test, pipe.predict(X_test)))What this demonstrates:
- Different transforms applied to numeric and categorical column groups.
OneHotEncoderwithhandle_unknown="ignore"for production safety.sparse_output=Falsefor dense output compatible with tree and linear models.- Full pipeline serialization - preprocessing travels with the classifier.
Deep Dive
How It Works
- Scaling centers and scales numeric columns so gradient-based models converge faster.
- One-hot encoding expands categoricals into binary indicator columns per category.
- ColumnTransformer runs each sub-transformer on its assigned columns and concatenates results.
- Pipelines call
fiton training data per CV fold, preventing statistic leakage. - Feature names can be recovered with
get_feature_names_out()after fitting.
Common Transforms
| Transform | Column Type | sklearn Class |
|---|---|---|
| Standard scaling | Numeric | StandardScaler |
| Min-max scaling | Bounded numeric | MinMaxScaler |
| Log transform | Skewed numeric | FunctionTransformer(np.log1p) |
| One-hot | Low-cardinality categorical | OneHotEncoder |
| Ordinal | Ordered categories | OrdinalEncoder |
| Imputation | Missing values | SimpleImputer |
Python Notes
from sklearn.preprocessing import FunctionTransformer
import numpy as np
log_transform = FunctionTransformer(np.log1p, validate=False)
# Chain imputation before scaling in the numeric branch:
from sklearn.impute import SimpleImputer
from sklearn.pipeline import Pipeline as SkPipeline
numeric_pipe = SkPipeline([
("impute", SimpleImputer(strategy="median")),
("scale", StandardScaler()),
])Gotchas
- Fitting on full data - computing mean/std or category lists from test rows leaks information. Fix: fit inside a pipeline on training folds only.
- Unseen categories at inference - new category values crash a plain
OneHotEncoder. Fix:handle_unknown="ignore"orhandle_unknown="infrequent_if_exist". - Target encoding leakage - encoding categories with the target mean using all data inflates CV scores. Fix: use
TargetEncoderinside a pipeline with proper CV. - Scaling tree models unnecessarily - random forests and gradient boosting are scale-invariant. Fix: skip scaling for tree ensembles; still encode categoricals.
- High-cardinality one-hot - thousands of dummy columns slow training and overfit. Fix: hashing (
FeatureHasher), frequency encoding, or embeddings. - Polynomial explosion -
PolynomialFeatures(degree=3)on 20 columns creates thousands of features. Fix: limit degree, use feature selection, or interaction-only mode.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
ColumnTransformer | Mixed column types in one model | All columns are the same type (use a single transformer) |
pandas.get_dummies | Quick EDA in notebooks | Production pipelines (not serializable, no unknown handling) |
| Target encoding | High-cardinality categoricals with enough data | Small datasets (severe overfitting risk) |
| Feature hashing | Very high cardinality, memory constraints | You need interpretable feature names |
FAQs
Why use ColumnTransformer instead of manual pandas operations?
- Serializable with
joblibfor deployment. - Integrates with
Pipelineand cross-validation without leakage. - Handles unknown categories and missing values consistently at inference.
Should I scale features for RandomForest?
- Tree models split on rank order - scaling does not change splits.
- Still encode categoricals and impute missing values.
- Scale for logistic regression, SVM, and k-NN on the same dataset.
How do I get feature names after one-hot encoding?
pipe.fit(X_train, y_train)
names = pipe.named_steps["prep"].get_feature_names_out()
print(list(names))- Names follow the pattern
cat__city_nyc,num__age.
What is remainder="drop"?
- Columns not listed in any transformer are dropped.
- Use
remainder="passthrough"to keep unlisted columns as-is. - Explicit column lists prevent accidental inclusion of ID columns.
How do I handle missing values?
from sklearn.impute import SimpleImputer
num_pipe = Pipeline([("impute", SimpleImputer(strategy="median")), ("scale", StandardScaler())])- Impute before scaling - scaling on NaN columns fails.
strategy="most_frequent"works for categorical imputation withSimpleImputer+p string dtype.
When should I use OrdinalEncoder vs OneHotEncoder?
- Ordinal for categories with natural order (low/medium/high).
- One-hot for nominal categories with no ordering (city names).
- Tree models handle label-encoded ordinals; linear models prefer one-hot.
Can I apply custom pandas functions?
- Use
FunctionTransformerto wrap any numpy-compatible function. - Avoid closures that capture external state - they break serialization.
- Prefer sklearn built-ins when available.
How do I engineer date features?
df["order_year"] = df["order_date"].dt.year
df["order_dow"] = df["order_date"].dt.dayofweek- Extract year, month, day-of-week, is_weekend as numeric columns.
- Drop the raw datetime column from model inputs.
What about text columns?
- Classical ML:
TfidfVectorizerin aColumnTransformerbranch. - LLM-era: precompute embeddings and store as numeric columns.
- See Embeddings & Similarity for embedding-based features.
How do I prevent data leakage with target encoding?
- Never compute target means using the full dataset before splitting.
- Use
category_encoders.TargetEncoderinside aPipelinewith CV. - Or compute encodings within each CV fold only.
Should I normalize or standardize?
- Standardize (
StandardScaler) when features have different units and scales. - Normalize (
MinMaxScaler) when you need bounded [0,1] inputs. - RobustScaler for datasets with outliers.
How do I inspect transformed output shape?
Xt = preprocessor.fit_transform(X_train)
print(Xt.shape) # rows x total encoded features- One-hot expands columns: 3 categories become 3 binary columns.
Related
- Pipelines - chaining transforms and models
- Datasets & Splits - fit transforms on train only
- Supervised Learning - model-specific preprocessing needs
- Imbalanced Data - resampling inside pipelines
- Gradient Boosting - tree models and categoricals
Stack versions: This page was written for Python 3.14.0 (stable 3.14, maintenance 3.13), FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, and uv 0.6+.