Turning raw columns into something a model can consume. Which of these you need depends entirely on the model: tree-based methods need almost none of it, while linear models, SVMs, k-NN and neural networks need all of it.
Scalers — numeric features
Tree models (random forests, gradient boosting) do not need scaling at all. Linear models, SVMs, k-NN and neural networks do.
StandardScalercommon
Default. Mean 0, standard deviation 1
MinMaxScaler
You need a bounded range such as 0 to 1
RobustScaler
Outliers present — uses median and IQR instead
Normalizer
Scales each ROW to unit norm, not each column. Different thing
PowerTransformer, QuantileTransformer
Skewed distributions you want closer to normal
Encoders — categorical features
OneHotEncodercommon
Default for nominal categories. Always set handle_unknown='ignore'
OrdinalEncoder
Genuine order, or tree models where arbitrary integers are harmless
TargetEncoder
High cardinality. Encodes by mean target, with cross-fitting to limit leakage (1.3+)
LabelEncodertrap
TARGETS ONLY. Takes 1-D input. Using it on features is a classic error
LabelBinarizer, MultiLabelBinarizer
Multi-label targets
Other
PolynomialFeatures
Interaction and polynomial terms
KBinsDiscretizer
Continuous into bins — the mechanic behind scorecard binning
SplineTransformer
Smooth non-linear basis without full polynomial blow-up
FunctionTransformercommon
Wrap any function so it fits inside a Pipeline
Worked examples 4
Runnable, with the arguments that change the outcome
called out. Output shapes are shown as comments where they help.
Scalers, and when each one is right
The choice is driven by your distribution, not by preference.
from sklearn.preprocessing import (StandardScaler, MinMaxScaler,
RobustScaler, PowerTransformer)
import numpy as np
# Income: heavily right-skewed with a few enormous values
x = np.array([[25_000], [32_000], [41_000], [38_000], [2_400_000]])
print("standard:", StandardScaler().fit_transform(x).ravel().round(2))
# [-0.5 -0.5 -0.5 -0.5 2. ] the outlier squashes everything else
print("robust: ", RobustScaler().fit_transform(x).ravel().round(2))
# [-1.44 0. 1.29 0.64 e+02] median/IQR based - the normal rows
# keep their spread
print("power: ", PowerTransformer().fit_transform(x).ravel().round(2))
# closest to normally distributed - what a linear model actually wants
# Rule of thumb:
# StandardScaler default, roughly symmetric data
# RobustScaler outliers present (transaction amounts, income)
# MinMaxScaler you need a bounded range
# PowerTransformer skewed data feeding a linear model
# Tree models need NONE of these.
Encoding categories without breaking production
handle_unknown is the argument that separates a notebook from a deployed model. The default is to raise.
from sklearn.preprocessing import OneHotEncoder, OrdinalEncoder
import numpy as np
train = np.array([["current"], ["savings"], ["isa"], ["current"]])
live = np.array([["current"], ["business"]]) # unseen category!
# WRONG - raises ValueError the first time a new product launches
enc = OneHotEncoder()
enc.fit(train)
# enc.transform(live) -> ValueError: Found unknown categories
# RIGHT - unknown becomes an all-zero row
enc = OneHotEncoder(handle_unknown="ignore", sparse_output=False)
enc.fit(train)
print(enc.get_feature_names_out()) # ['x0_current' 'x0_isa' 'x0_savings']
print(enc.transform(live))
# [[1. 0. 0.]
# [0. 0. 0.]] <- 'business' encodes as nothing, but does not crash
# min_frequency groups rare categories together, which stops a
# high-cardinality column exploding into thousands of columns
enc = OneHotEncoder(handle_unknown="infrequent_if_exist",
min_frequency=0.01, sparse_output=False)
Target encoding for high-cardinality columns
Merchant ID or postcode with thousands of values would produce an unusable one-hot matrix. TargetEncoder replaces each category with its mean target, and cross-fits internally to limit the leakage that made this technique dangerous.
from sklearn.preprocessing import TargetEncoder
# 8,000 distinct merchants - one-hot would give 8,000 columns
enc = TargetEncoder(
smooth="auto", # shrinks rare categories toward the global mean
cv=5, # internal cross-fitting: this is what prevents
# a category's own rows leaking into its encoding
random_state=42,
)
X_enc = enc.fit_transform(X_train[["merchant_id"]], y_train)
print(X_enc[:5].round(4))
# Requires sklearn 1.3+. Still put it inside a Pipeline - fitting it
# outside, on all data, reintroduces exactly the leakage it avoids.
Binning a continuous variable, scorecard style
KBinsDiscretizer is the mechanic underneath weight-of-evidence binning. Turning a continuous variable into bands is what makes a credit scorecard readable.
from sklearn.preprocessing import KBinsDiscretizer
import numpy as np
age = np.array([[19], [24], [31], [38], [45], [52], [61], [70]])
binner = KBinsDiscretizer(
n_bins=4,
encode="ordinal", # 'onehot' for a linear model
strategy="quantile", # equal-count bins; 'uniform' = equal-width
)
print(binner.fit_transform(age).ravel()) # [0. 0. 1. 1. 2. 2. 3. 3.]
print(binner.bin_edges_[0].round(1)) # [19. 30.3 43.5 58.7 70. ]
# Why bin at all: it lets a linear model capture a non-monotonic
# relationship (risk high when young, low in middle age, up again later)
# while keeping every coefficient explainable band by band.