Target (Mean) Encoding
Target encoding replaces a category with a statistic of the target for that category. For a binary target, it can represent a smoothed positive-outcome rate; for regression, a smoothed target mean. It is compact, but it uses labels, so the information boundary is part of the algorithm.
Keep two ideas separate: smoothing controls how much to trust a small category, while cross-fitting prevents a training row’s target from supplying its own feature. Neither replaces a genuinely held-out evaluation.
Start with a category’s observed target rate
Suppose the feature is store, and the target is whether a visit ended in a purchase: 1 for yes, 0 for no. In a constructed reference dataset, store A has 3 purchases in 4 visits, B has 1 in 1 visit, and C has 1 in 5 visits.
The raw rates are 0.75, 1.00, and 0.20. Store B’s perfect rate rests on one observation. A useful estimate can pull such a small group toward the overall rate, which is 5 purchases in 10 visits, or 0.5.
Combine local evidence with a global baseline
For category $c$, let $n_c$ be its number of reference rows and $s_c$ the sum of their targets. Let $\mu$ be the reference dataset’s overall target mean, and $\alpha\geq0$ the smoothing strength. A common encoding is
The extra $\alpha\mu$ acts like a prior contribution with strength $\alpha$. It is a way to blend the category rate with the global rate, not a claim that extra customers were observed. Larger categories receive more influence from their own data.
| Store | Purchases / visits | Raw rate | Encoded |
|---|---|---|---|
| A | 3 / 4 | 0.75 | 0.6250 |
| B | 1 / 1 | 1.00 | 0.6000 |
| C | 1 / 5 | 0.20 | 0.3333 |
At $\alpha=0$, observed categories receive their raw means. A category with no reference rows still needs a fallback; use the reference mean instead of evaluating a zero-over-zero expression. Scikit-learn’s numeric smooth uses this blending form, while smooth="auto" estimates the amount of smoothing.
“Fit on training data” is necessary but not sufficient
Consider an identifier that appears once per training row. With no smoothing, its category mean equals that row’s target. A downstream classifier sees the answer directly in its input. New identifiers receive only the fallback mean, so the apparent training signal does not transfer.
This can happen even if validation labels were never used. The problem is self-inclusion inside the training data. Smoothing weakens the direct dependence but does not remove it. A full-training global mean also includes each training row’s target, so the reference mean must respect the same exclusion boundary.
Build each training feature from other folds
Cross-fitting divides the current training set into inner folds. For each fold, fit the encoder on the other folds and transform the held-out rows. Stitch those transformed rows back into their original order, then train the predictor on that assembled feature matrix.
The example below uses eight training rows and two fixed folds. Each reference half has a global mean of 0.5, and smoothing strength is 2. The labels in the held-out half are not read when constructing that half’s features.
| Row | Store | Target used? | Encoding |
|---|---|---|---|
| 1 | A | No | 0.3333 |
| 2 | B | No | 0.6667 |
| 3 | A | No | 0.3333 |
| 4 | C | No | 0.5000 |
| 5 | A | Yes · 0 | reference |
| 6 | B | Yes · 1 | reference |
| 7 | C | Yes · 1 | reference |
| 8 | C | Yes · 0 | reference |
The same store can receive different training encodings in different folds because the permitted reference data differs. That is expected. After constructing the cross-fitted training matrix, also fit a mapping on all current training rows for transforming genuinely new rows.
The training transform and serving transform have different jobs
Scikit-learn’s TargetEncoder.fit_transform(X, y) returns cross-fitted features and retains a full-data mapping for later transform() calls. Calling fit(X, y).transform(X) instead uses each training row’s own target in the fitted statistics and bypasses cross-fitting.
Python: reproduce both folds and fit a complete pipeline
The explicit splitter argument below uses scikit-learn 1.9 or newer. Fixed folds keep the example traceable; select a splitting scheme appropriate to the real task.
import numpy as np
from sklearn.model_selection import KFold
from sklearn.preprocessing import TargetEncoder
from sklearn.pipeline import make_pipeline
from sklearn.linear_model import LogisticRegression
# Checked with scikit-learn 1.9.1; cv splitter objects require 1.9+.
X_train = np.array([["A"], ["B"], ["A"], ["C"],
["A"], ["B"], ["C"], ["C"]])
y_train = np.array([1, 0, 1, 0, 0, 1, 1, 0])
encoder = TargetEncoder(
target_type="binary", smooth=2,
cv=KFold(n_splits=2, shuffle=False), # Fixed folds match the figure.
)
training_features = encoder.fit_transform(X_train, y_train)
assert np.allclose(training_features.ravel(),
[1/3, 2/3, 1/3, 1/2, 3/4, 1/3, 1/3, 1/3])
# After fit_transform, transform uses mappings fitted on ALL training rows.
new_features = encoder.transform([["A"], ["D"]])
assert np.allclose(new_features, [[0.6], [0.5]])
print(new_features)
# Pipeline.fit uses the encoder's fit_transform for the training features.
model = make_pipeline(
TargetEncoder(target_type="binary", smooth=2,
cv=KFold(n_splits=2, shuffle=False)),
LogisticRegression(),
).fit(X_train, y_train)
probabilities = model.predict_proba([["A"], ["D"]])
assert probabilities.shape == (2, 2)
# This tiny table illustrates information flow, not predictive quality.
[[0.6]
[0.5]]
The new store A row receives 0.6 from all eight training rows; unseen D receives their global rate, 0.5. These differ from some cross-fitted training values. The difference is a consequence of using more reference data at serving time, and its effect should be assessed in the outer validation.
For model selection, cross-validate the whole pipeline on outer splits. Do not target-encode the entire dataset once and then cross-validate a classifier on that matrix: an outer validation label could already have influenced another row’s encoding. See cross-validation for tuning for the outer boundary.
Rows from the same entity or future need stricter exclusions
Random inner folds are inappropriate if rows share information that would not be available for a new entity. Use group-aware inner splits when the evaluation requires unseen groups, and maintain the same outer group boundary. Merely excluding the current row is not enough if a near-duplicate from the same entity reveals its label.
For time-based prediction, target statistics must use outcomes that were available before the prediction cutoff. Random cross-fitting can read future labels. Ordered encodings are another way to restrict reference data, but a random permutation is not a substitute for chronological availability.
Why a time-series splitter may need custom handling
Scikit-learn’s internal cross-fitting requires every input row to appear in a held-out fold exactly once. A usual expanding-window time split leaves its initial training window without out-of-fold encodings. Consequently, it cannot simply be substituted without a compatible construction. A custom historical encoder can reserve a warm-up period, train downstream on rows with valid past-based encodings, and define a permitted cold-start prior.
A useful feature still needs validation
Target encoding compresses category identity into a target-related summary. Distinct categories with the same estimate become indistinguishable through that feature alone. A downstream model may still need other information to represent different interactions.
Tune smoothing and the encoder/model combination inside the evaluation procedure. More folds, less smoothing, or a large category count do not guarantee better results. For multiclass targets, scikit-learn creates a class-specific encoding for each original feature, so the output is wider than one column.
Specify target_type when necessary: integer-valued regression targets can otherwise be inferred as classes. Missing feature values may be modeled as a category; missing target labels cannot be used to calculate a supervised target statistic without a separate policy. Keep the label definition, observation window, and fallback stable in deployment.
C++: the smoothed map for a permitted reference pool
This helper implements the first figure’s statistic for finite targets and a nonnegative smoothing strength. It encodes new query categories from a supplied reference pool. To construct training features, call such a helper separately on each inner fold’s complement, as in the Python workflow; this helper alone does not perform cross-fitting.
Compile the category-statistic map
#include <cmath>
#include <iomanip>
#include <iostream>
#include <stdexcept>
#include <string>
#include <unordered_map>
#include <vector>
class TargetMean {
struct Stats { double sum = 0; std::size_t count = 0; };
std::unordered_map<std::string, Stats> groups;
double globalMean, alpha;
public:
TargetMean(const std::vector<std::string>& categories,
const std::vector<double>& targets, double smoothing)
: globalMean(0), alpha(smoothing) {
if (categories.empty() || categories.size() != targets.size()
|| !std::isfinite(alpha) || alpha < 0)
throw std::invalid_argument("invalid reference data or smoothing");
for (std::size_t i = 0; i < targets.size(); ++i) {
if (!std::isfinite(targets[i])) throw std::invalid_argument("nonfinite target");
auto& stats = groups[categories[i]];
stats.sum += targets[i];
++stats.count;
globalMean += targets[i] / targets.size();
}
}
double encode(const std::string& category) const {
const auto found = groups.find(category);
if (found == groups.end()) return globalMean;
const auto& stats = found->second;
return (stats.sum + alpha * globalMean) / (stats.count + alpha);
}
};
int main() {
// A reference pool for encoding other rows, not their own training targets.
const TargetMean encoder(
{"A","A","A","A","B","C","C","C","C","C"},
{1,1,1,0,1,1,0,0,0,0}, 4);
std::cout << std::fixed << std::setprecision(4);
for (const std::string category : {"A","B","C","D"})
std::cout << category << ": " << encoder.encode(category) << "\n";
}
A: 0.6250
B: 0.6000
C: 0.3333
D: 0.5000
References and further reading
- Scikit-learn: TargetEncoder — smoothing, cross-fitting, target types, and unknown-category behavior.
- Scikit-learn: internal cross-fitting example — why fitting and transforming the same training rows can overfit.
- Scikit-learn: target-encoding guide — the encoding formula and fit/transform distinction.
- Prokhorenkova et al.: CatBoost — target-statistic leakage and ordered estimation as part of a broader boosting method.