MODELCATALOG

Scaling numbers, encoding categories.

sklearn.preprocessing

preprocessing

Scaling numbers, encoding categories.

1 Get data Something to practise on
2 Split & tune An honest test set
3 Prepare Into numbers a model accepts
4 Compose One object, no leakage
5 Model The algorithm itself
6 Unsupervised No labels available
7 Measure Is it any good, and why
8 Utilities Glue and extension

What it is for

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.

StandardScaler common 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

OneHotEncoder common 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+)
LabelEncoder trap 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
FunctionTransformer common 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.