Quick Reference · categorical → numeric transformers

category_encoders cheat sheet

22 sklearn-compatible encoders, and really only three questions: how many columns come out (cardinality), does it need the target (and therefore leak), and is your downstream model a tree or a linear model? Answer those three and the choice makes itself.

foundations & API unsupervised — no y supervised — needs y contrast coding — linear models pipeline & wrappers trap most common

Distilled & cross-checked against: contrib.scikit-learn.org/category_encoders (official API docs, v2.10) · github.com/scikit-learn-contrib/category_encoders · scikit-learn.org · the source papers (Micci-Barreca 2001 · Prokhorenkova 2018 · Weinberger 2009) · verified 2026-08-28 against category-encoders 2.10.0 (sklearn-native transformers; get_feature_names_out + set_output)

One categorical column, k = 1,000 categories — where does it go?
NO TARGET NEEDED — SAFE ANYWHERE NEEDS y — MUST PASS THE GATE city dtype: object k = 1,000 high cardinality OneHotEncoder one column per category 1000 Binary / BaseN / Gray ⌈log₂ k⌉ digit columns 10 HashingEncoder fixed width — collisions allowed 8 Ordinal / Count integer rank · frequency 1 Helmert / Sum / Poly contrast coding — linear models only 999 Target / CatBoost / WOE … mean of y per category 1 columns out → No y → no leakage fit() on train, transform() anywhere. Nothing to get wrong. THE GATE fit_transform(X, y) on TRAIN — never fit().transform() TREE MODEL? Ordinal · Count · Target CatBoost — 1 column each LINEAR MODEL? OneHot · contrast coders — never raw Ordinal CV-safe encoding Pipeline([enc, model]) or NestedCVWrapper out-of-fold statistics A NUMERIC MATRIX …that your model can actually learn from skip the gate → the target leaks into the feature → CV score is a fantasy

Foundations & contracts

Four things to know before you pick an encoder. Card 03 is the one that inverts your sklearn instincts — and it is the single most common way people silently leak the target.

01Install & the 22 Encoderstwo families
  • pip install category_encoders
    import category_encoders as ce
    Requires numpy, scipy, statsmodels, pandas, scikit-learn. Conda: -c conda-forge.
  • # UNSUPERVISED — 12, never touch y
    OrdinalEncoder · OneHotEncoder · BinaryEncoder
    BaseNEncoder · GrayEncoder · RankHotEncoder
    CountEncoder · HashingEncoder
    Plus the four contrast coders below. Safe to fit() anywhere — no leakage is even possible.
  • # CONTRAST CODING — 4, for linear models
    HelmertEncoder · SumEncoder
    BackwardDifferenceEncoder · PolynomialEncoder
    Technically unsupervised, but a different beast — card 12.
  • # SUPERVISED — 9, all need y
    TargetEncoder · MEstimateEncoder · JamesSteinEncoder
    CatBoostEncoder · LeaveOneOutEncoder · WOEEncoder
    QuantileEncoder · SummaryEncoder · GLMMEncoder
    All collapse a column to 1 number, regardless of cardinality. All can leak. Cards 08–11.
  • # WRAPPERS
    ce.wrapper.PolynomialWrapper · ce.wrapper.NestedCVWrapper
    Multiclass targets, and out-of-fold encoding. Card 13.
02The Shared APIevery encoder, same knobs
All 22 inherit one base class, so these parameters mean the same thing everywhere. Learn them once.
  • ce.TargetEncoder(cols=['city', 'brand'])
    Which columns to encode. If cols=None, it encodes every object / category dtype column — and silently leaves the rest alone.
  • handle_unknown='value' # 'error' | 'return_nan' | 'value' | 'indicator'
    Unseen category at transform time. Default 'value' = prior/global mean (supervised) or 0 (one-hot). 'error' in dev, 'value' in prod.
  • handle_missing='value'
    Same options, for NaN at fit time. Default treats NaN as a real, countable category — which is usually what you want.
  • drop_invariant=True
    Drop output columns with zero variance. Cheap free cleanup.
  • return_df=True # default
    pandas out. Set False for a numpy array.
  • enc.get_feature_names_out() · enc.inverse_transform(X_enc)
    Names of the produced columns; and — for the invertible encoders (Ordinal, OneHot, Binary, BaseN) — the round trip back.
  • # 'indicator' can change your column countcare
    On OneHot/Binary/BaseN, handle_unknown='indicator' adds an extra column only when unknowns appear — so train and serve can disagree on width.
03fit_transform ≠ fit().transform()the contract that bites
For supervised encoders these two are deliberately different, and that violates every sklearn habit you have. Get this backwards and you leak the target.
DataCall
TRAINenc.fit_transform(X_tr, y_tr)
Regularised / out-of-fold values. LeaveOneOut excludes each row's own target; CatBoost uses only preceding rows.
TESTenc.transform(X_te)
Full-training statistics, no y. This is correct — the test set must be encoded with what training knew.
  • enc.fit(X_tr, y_tr).transform(X_tr) # ✗ LEAKSnever
    Applies the unregularised full-data means back onto the rows they were computed from. Every row sees its own target. Your CV score will be beautiful and your model worthless.
  • X_tr_e = enc.fit_transform(X_tr, y_tr)
    X_te_e = enc.transform(X_te)
    The only correct two-liner. Memorise its shape.
  • # unsupervised encoders: both are identicalsafe
    No y, nothing to leak. The distinction only exists for the 9 supervised ones.
04Cardinality → Column Countthe sizing table
The first question is always how wide does this get? For k = 1,000 categories:
EncoderFormulaCols
OneHotk1000
RankHotk (cumulative)1000
Contrast ×4k − 1999
Binary / Gray⌈log₂ k⌉10
BaseN(base=3)⌈log₃ k⌉7
Hashingn_componentsfixed8
Summarylen(quantiles)3
Ordinal / Count11
All supervised11
BaseN is the generalisation: base=1 → OneHot, base=2 → Binary. Everything in between trades width against how much the model must learn to un-mix the digits.

Unsupervised — no target, no leakage

Nothing here can leak, so nothing here needs a wrapper. The only real decision is how much width you can afford and whether the integers you produce imply a false ordering.

05Ordinal & OneHotthe two you already know
  • ce.OrdinalEncoder(cols=['size'])
    Category → integer. Perfect for trees (a tree can split anywhere, so arbitrary integers cost nothing) and actively harmful for linear models, which will read Paris=3 as three times Tokyo=1.
  • ce.OrdinalEncoder(mapping=[{
      'col': 'size',
      'mapping': {'S': 1, 'M': 2, 'L': 3}}])
    Genuinely ordinal data? Supply the order explicitly. This is the one time an Ordinal integer is honest — and the library will otherwise assign order alphabetically or by first appearance.
  • ce.OneHotEncoder(use_cat_names=True)
    One column per category. The safe default below ~15 categories. use_cat_names gives you city_Paris instead of city_1.
  • # OneHot on k=1000 → 1000 sparse columnsexplodes
    Memory, training time, and — for trees especially — a badly weakened model: each binary column carries so little signal that the tree can't find a good split. This is the reason this library exists.
06Binary · BaseN · Graylog-width, no target
Ordinal-encode, then write the integer in base n, one column per digit. You get one-hot's "no false ordering" property at logarithmic width.
  • ce.BinaryEncoder(cols=['city']) # 1000 → 10 cols
    Base 2. The pragmatic middle ground when you can't afford one-hot but won't risk the target.
  • ce.BaseNEncoder(base=3)
    The generalisation. base=1 is OneHot; base=2 is Binary. Higher base → fewer, denser columns.
  • ce.GrayEncoder()
    Binary, but in Gray code — adjacent integers differ in exactly one bit. Slightly kinder to distance-based and neural models, since a ±1 category change is a ±1 bit change.
  • # the honest caveatthink
    The digit columns are arbitrary — bit 3 of a hashed integer means nothing. A tree must recombine several columns to isolate one category, so Binary often underperforms both one-hot (low k) and target encoding (high k). It's a compromise, not a free lunch. Benchmark it, don't assume it.
07Count & Hashingfrequency · fixed width
  • ce.CountEncoder(cols=['city'])
    Category → how often it appears. Surprisingly strong with gradient boosting, and completely leak-free. Often the best first thing to try on high cardinality.
  • ce.CountEncoder(normalize=True, min_group_size=10)
    normalize → relative frequency instead of raw count. min_group_size lumps rare categories into one bucket — a cheap, principled way to kill the long tail.
  • # Count collides by constructioncare
    Two unrelated categories that each appear 47 times get the same value. If frequency isn't actually predictive, you've just thrown the column away.
  • ce.HashingEncoder(n_components=8, hash_method='md5')
    The hashing trick. Width is fixed in advance and independent of k — so unseen categories at serve time need no special handling at all. The go-to for streaming / online learning and truly unbounded vocabularies.
  • # hash collisions are permanent and silenttradeoff
    Two categories can land in the same bucket and become indistinguishable forever. Not invertible, not interpretable. Raise n_components to trade memory for fewer collisions.
08RankHotthermometer coding
For genuinely ordinal data. Category of rank r → the first r columns are 1, the rest 0. So the encoding of "Large" contains the encoding of "Medium".
  • ce.RankHotEncoder(cols=['size'])
    S → [1,0,0] · M → [1,1,0] · L → [1,1,1]. Unlike OneHot it preserves the order; unlike Ordinal it doesn't force equal spacing between ranks.
  • # the sweet spotuse when
    Low-cardinality ordinal features on a linear model, where "L is more than M" matters but "L is exactly 3× S" is a lie you don't want to tell.

Supervised — the target-based family

All nine collapse any cardinality into one highly predictive column. All nine can leak. The entire design space is one question: how do you compute a category's mean without letting a row see its own target? — and the different answers are what separate Target, CatBoost and LeaveOneOut.

09TargetEncodermean of y, smoothed
Replace each category with the mean of y for that category — blended toward the global mean, so rare categories don't overfit on a sample of three.
  • ce.TargetEncoder(
      cols=['city'],
      min_samples_leaf=20, # k — S-curve midpoint
      smoothing=10) # f — S-curve steepness
    The blend weight is a sigmoid in the category count:
    λ(n) = 1 / (1 + exp(−(n − k) / f))
    enc = prior·(1−λ) + category_mean·λ
    λ = 0.5 exactly at n = min_samples_leaf. Bigger smoothing → flatter curve → more shrinkage overall.
  • ce.TargetEncoder(hierarchy={'city': {...}})
    Shrink toward the parent group's mean instead of the global one (city → region → country). Genuinely powerful on geography and product taxonomies, and largely undiscovered.
  • ce.MEstimateEncoder(m=1.0)
    The simpler cousin — one knob instead of two.
    enc = (Σy + m·prior) / (n + m)
    m is literally "how many virtual prior observations to add". Easier to reason about and to tune than TargetEncoder's S-curve. Start here.
  • min_samples_leaf=1, smoothing=1overfits
    Barely any shrinkage — a category seen once gets encoded as its own target. That's not a feature, that's a copy of y.
10CatBoost & LeaveOneOuttwo ways to hide a row's own y
  • ce.CatBoostEncoder(a=1, sigma=None)
    Ordered target statistics. Walks the rows top to bottom; row k is encoded using only rows before it:
    enc_k = (Σ_{j<k} y_j·[x_j=x_k] + a·prior) / (count_{j<k} + a)
    No future information can flow backwards. Usually the strongest of the family.
  • # CatBoostEncoder is ORDER-SENSITIVEshuffle
    This implementation is time-aware (no random permutations, like CatBoost's has_time=True). If your rows arrive sorted by anything correlated with y, you get garbage. Shuffle first — unless it's genuinely a time series, in which case that ordering is exactly what you want.
  • ce.LeaveOneOutEncoder(sigma=0.05)
    Encode each row with the category mean computed excluding that row:
    enc_i = (Σ_{j≠i} y_j·[x_j=x_i]) / (n_i − 1)
    sigma injects Gaussian noise on training data only to blunt the remaining overfit.
  • # LOO still leaks, subtlycare
    Excluding your own row isn't enough: in a 2-row category, your encoding is the other row's target, and the model can invert it. This is exactly why the docs say only LeaveOneOut benefits greatly from NestedCVWrapper.
11The SpecialistsWOE · Quantile · GLMM · JamesStein
  • ce.WOEEncoder(regularization=1.0)
    Weight of Evidenceln(%events / %non-events). Binary target only. The standard in credit scoring, because it's monotonic with log-odds, so it drops straight into a logistic regression and stays interpretable. regularization exists mainly to prevent division by zero.
  • ce.QuantileEncoder(quantile=0.5, m=1.0)
    Uses a quantile of y instead of the mean. Default is the median. Built for regression with skewed targets or outliers, where one billionaire in a postcode ruins the mean forever.
  • ce.SummaryEncoder(quantiles=[0.25, 0.5, 0.75])
    Several QuantileEncoders stacked → one column per quantile. Captures the shape of y's distribution within a category, not just its centre.
  • ce.JamesSteinEncoder(model='independent')
    Shrinks the category mean toward the global mean by a variance-derived factor — the shrinkage weight is estimated, not tuned. Elegant, but formally justified only for normally distributed targets.
  • ce.GLMMEncoder()
    Fits a generalized linear mixed model with the category as a random effect. Its selling point: no hyperparameters to tune — the shrinkage falls out of the model. Its cost: slow (statsmodels under the hood), so it doesn't scale to huge k.
12Contrast Codinglinear models only
Four encoders straight out of classical statistics. They produce k−1 columns whose regression coefficients answer a specific pre-planned question. If you're fitting a tree, skip this entire card — it's pointless there.
  • ce.SumEncoder() # deviation coding
    Each level compared to the grand mean. The intercept becomes the overall mean rather than one arbitrary reference level — which is what you usually wanted from one-hot anyway.
  • ce.HelmertEncoder()
    Each level compared to the mean of all subsequent levels. Useful for ordered factors when you want "does this step up matter?"
  • ce.BackwardDifferenceEncoder()
    Each level compared to the level immediately before it. The natural choice for ordinal data — the coefficients read as "the jump from S to M".
  • ce.PolynomialEncoder()
    Orthogonal polynomial trends (linear, quadratic, cubic…). Assumes the levels are equally spaced — which is a real assumption, and usually a false one.
  • # contrast coding on a tree = wasted columnsno-op
    Trees don't have coefficients to interpret, and these dense k−1 columns are strictly harder to split on than plain integers. The only reason to use them is that you intend to read the coefficients.

How the target-based encoders avoid cheating

Every supervised encoder is one answer to the same question: how do I give this row its category's mean, without letting it see its own target? These are the three answers — and the naive non-answer that ruins models.

✗ naive — fit().transform() on train

Every row sees its own target. A category with one row gets encoded as exactly that row's y. The model just reads the answer off the feature.

row city y enc (mean of ALL rows) 1Paris1 2Paris0 3Paris1 4Oslo1 0.670.670.67 1.00 ← this IS row 4's y Oslo appears once, so its "category mean" is just its own label. The feature has become a copy of the target.

✓ LeaveOneOut — exclude the row itself

Row i's encoding is the category mean computed over every other row. Row 1 sees only rows 2 and 3.

row city y enc (mean of OTHERS) 1Paris1 2Paris0 3Paris1 0.501.000.50 = mean(y₂, y₃) = mean(y₁, y₃) = mean(y₁, y₂) No row contributes to its own encoding. But: in a 2-row category your encoding IS the other row's y — which is why LOO is the one encoder that needs NestedCVWrapper.

✓ CatBoost — only look backwards

Walk the rows in order. Row k uses only the rows above it. Nothing from the future can flow back — the same trick as time-series validation.

row city y enc (running mean, prior a=1) 1Paris1 2Paris0 3Paris1 4Paris1 prior 0.75 0.50 0.60 nothing above it yet sees row 1 sees rows 1–2 sees rows 1–3 enc_k = (Σ_{j<k} y_j·[x_j=x_k] + a·prior) / (count_{j<k} + a) Row order matters → SHUFFLE first, unless it's a real time series.

Smoothing — the trust dial

TargetEncoder blends the category mean toward the global prior. The weight λ is a sigmoid in the category count, crossing 0.5 exactly at min_samples_leaf.

1.0 0.5 0.0 λ (trust) min_samples_leaf λ = 0.5 exactly here rare category → trust the prior common category → trust its own mean dashed = higher `smoothing` (flatter → more shrinkage) n (rows in category) →

Pipeline, wrappers & choosing

The library is sklearn-native — but it works internally in pandas, which is where the ColumnTransformer friction comes from. And since scikit-learn 1.3 ships its own TargetEncoder, card 15 is the honest "do you even need this library?" answer.

13The Two Wrappersmulticlass · out-of-fold
  • from category_encoders.wrapper import PolynomialWrapper
    PolynomialWrapper(ce.WOEEncoder())
    Multiclass targets for encoders that only support binary (WOE) or binomial/continuous (Target, JamesStein…). It one-vs-rest's the target and encodes each class separately, so you get one column per class per feature.
  • from category_encoders.wrapper import NestedCVWrapper
    NestedCVWrapper(ce.LeaveOneOutEncoder(), cv=5)
    Out-of-fold encodings. Splits the training data into folds, fits the encoder on k−1 and encodes the held-out fold — so no row's encoding was ever informed by its own target.
  • # don't wrap everything reflexively
    The docs are explicit: only LeaveOneOutEncoder benefits greatly from NestedCV. Target/CatBoost/MEstimate already regularise internally — wrapping them mostly buys you a 5× slowdown.
14Pipelines & ColumnTransformerthe pandas friction
These encoders work internally with DataFrames, while sklearn historically passed numpy arrays. That mismatch is the source of nearly every integration bug.
  • Pipeline([
      ("enc", ce.TargetEncoder(cols=['city'])),
      ("clf", HistGradientBoostingClassifier()),
    ])
    The clean fix for leakage. Inside cross_val_score, the pipeline calls fit_transform on each training fold and transform on the validation fold — automatically, every time.
  • SomePreprocessor().set_output(transform="pandas")
    Required for clean ColumnTransformer interop. The official docs recommend sklearn ≥ 1.2 plus pandas output, so cols= and get_feature_names_out keep working through the stack.
  • # cols=None inside a ColumnTransformersurprise
    If upstream steps hand you a numpy array, dtype information is gone — so "encode all object columns" can't find any, and your encoder becomes a silent no-op. Name your columns explicitly, or set pandas output.
15Do You Even Need This Library?an honest answer
Since scikit-learn 1.3, sklearn ships its own TargetEncoder with built-in cross-fitting — and it covers the most common case natively.
Use sklearn's built-inReach for category_encoders
OneHotEncoder, OrdinalEncoder — mature, sparse-aware, fast. Binary / BaseN / Gray / Hashing / Count — sklearn has no equivalent.
TargetEncoder(smooth="auto", cv=5)cross-fits by default, multiclass-native. CatBoost, WOE, GLMM, Quantile, JamesStein — and the contrast coders.
You want zero extra dependencies. You want hierarchical target encoding, or min_group_size rare-category lumping.
Same trap, both libraries: sklearn's TargetEncoder.fit_transform() also differs from fit().transform() — it cross-fits internally. Card 03 applies no matter which one you import.

Decision map

Nearly every real choice is settled by three questions: how many categories, tree or linear, and can I afford the leakage risk.

yes
no
k < 15
linear
tree
k = 15 … 1000
no — unsupervised
or leakage-averse
yes
strongest default
simple, one knob
binary target,
credit scoring
skewed / outlier y
regression
k > 1000
or unbounded
Categorical
column
Genuinely
ordered?
OrdinalEncoder
with explicit mapping
· or RankHot
Cardinality k?
Model type?
OneHotEncoder
· or contrast coders
if you'll read coefficients
OrdinalEncoder
trees split anywhere
Can you use y?
CountEncoder
· or BinaryEncoder
Which regulariser?
CatBoostEncoder
shuffle rows first!
MEstimateEncoder
WOEEncoder
QuantileEncoder
HashingEncoder
fixed width · streaming-safe
Put it in a Pipeline.
fit_transform on train,
transform on test.
Ship it

Traps

The first group produces wrong numbers, silently — a beautiful CV score and a worthless model. The second just wastes your afternoon.

16Silently Wrongthese invalidate results
  • enc.fit(X, y).transform(X) # on training dataleak
    The cardinal sin. Use fit_transform(X_tr, y_tr). Card 03.
  • enc.fit_transform(X_all, y_all) train_test_split(…)leak
    Encode after splitting, never before. Or just put the encoder in a Pipeline and stop thinking about it.
  • CatBoostEncoder() # on data sorted by ygarbage
    It's time-aware by design — no random permutation. Sorted input means the running mean is systematically biased. Shuffle first.
  • ce.OrdinalEncoder() LogisticRegression()
    You've told a linear model that Paris(3) is three times Tokyo(1). It will dutifully fit a slope through nonsense. Trees are fine with this; linear models are not.
  • min_samples_leaf=1, smoothing=1
    Effectively no shrinkage. Singleton categories get encoded as their own target. Card 09.
  • # int-coded categories are silently skipped
    With cols=None, only object/category dtypes get encoded. A store_id stored as int64 passes through untouched and gets treated as a magnitude. Always pass cols= explicitly.
17Friction & Surpriseswhat the traceback means
  • # encoder did nothing at all
    cols=None found no object/category columns — usually because an upstream ColumnTransformer handed you a numpy array. Set set_output(transform="pandas"). Card 14.
  • WOEEncoder on a 3-class targetraises
    WOE is binary only. Wrap it: PolynomialWrapper(WOEEncoder()).
  • # column count differs between train and serve
    handle_unknown='indicator' adds a column only when unknowns appear. Use 'value' for a stable width in production.
  • # GLMMEncoder is taking forever
    It fits a mixed model per column via statsmodels. That's the price of "no hyperparameters". Don't point it at k = 50,000.
  • HashingEncoder — no inverse_transform
    Hashing is lossy and one-way by construction. If you need to recover the original category, use Binary or BaseN instead.
  • # target encoding didn't help
    Perfectly normal. If the category has no relationship with y, you've compressed it into a noisy constant. Always benchmark against CountEncoder and plain OneHot — the fancy encoder is not automatically the better one.
18The Meta-Pointread this before optimising
Encoder choice is far less important than people think, and the ranking flips between datasets. Three things that reliably matter more:
  • # 1. leakage disciplinefirst
    A leaky CatBoostEncoder is strictly worse than a clean CountEncoder — and it will look better in CV, which is exactly what makes it dangerous.
  • # 2. the rare-category tail
    Most of the damage lives in categories seen 1–3 times. min_group_size, smoothing, and an explicit handle_unknown policy do more for you than swapping encoder families.
  • # 3. native categorical support
    LightGBM, CatBoost and HistGradientBoosting handle categoricals natively — often better than any pre-encoding, because the split is optimised jointly with the tree. Before reaching for this library, check whether your model already solved it.
  • # the pragmatic shortliststart here
    k < 15 → OneHot (linear) / Ordinal (tree). k high, tree → Count, then CatBoost. k unbounded → Hashing. Benchmark those four and you'll be within noise of the best possible choice, 90% of the time.

Worth memorizing

fit_transform ≠ fit().transform()for every supervised encoder — train uses the first, test the second
import as cepip install category_encoders · import category_encoders as ce
cols=None is a traponly encodes object/category dtypes — int-coded IDs pass through untouched
OneHot = k colsBinary = ⌈log₂k⌉ · Hashing = fixed · every supervised encoder = 1
BaseN generalisesbase=1 is OneHot · base=2 is Binary
Ordinal → trees onlya linear model reads Paris=3 as 3× Tokyo=1
contrast coders → linear onlyHelmert/Sum/BackDiff/Poly are pointless unless you read the coefficients
λ = 0.5 at min_samples_leafTargetEncoder's sigmoid midpoint · higher smoothing = flatter = more shrinkage
MEstimate = simpler Target(Σy + m·prior) / (n + m) — one knob, easier to tune
SHUFFLE before CatBoostit's time-aware by design — sorted rows give a biased running mean
only LOO needs NestedCVTarget/CatBoost/MEstimate already regularise internally
WOE is binary-onlymulticlass? wrap it in PolynomialWrapper
set_output("pandas")required for clean ColumnTransformer interop — the library thinks in DataFrames
Pipeline solves leakageit calls fit_transform on the train fold and transform on the val fold, free
CountEncoder is underratedleak-free, one column, strong with GBMs — try it before target encoding
check your model firstLightGBM/CatBoost/HistGBM handle categoricals natively, often better