Условие
В категории «город» 500 уникальных значений. Решили использовать target encoding (средний target по городу). На validation модель отличная, на проде падает. Что не так?
Решение
Подход
Target encoding (TE): заменяем категорию на E[y | category]. Проблемы:
- Leakage в train: для каждой записи использовано её же y → модель «угадывает» training labels.
- Overfit на редких категориях: city с n=2 даёт unstable mean.
- Distribution shift: TE из train применяется к test → новые города получают global mean.
Решения
1. Out-of-fold encoding для train:
from sklearn.model_selection import KFold
import numpy as np
def oof_target_encoding(X, y, cat_col, n_splits=5, smoothing=10, random_state=42):
enc = np.zeros(len(X))
kf = KFold(n_splits, shuffle=True, random_state=random_state)
global_mean = y.mean()
for tr_idx, te_idx in kf.split(X):
# На train fold считаем mean по категории + smoothing
df_tr = X.iloc[tr_idx].copy()
df_tr['_y'] = y.iloc[tr_idx]
agg = df_tr.groupby(cat_col)['_y'].agg(['mean','count'])
agg['smoothed'] = (agg['mean']*agg['count'] + global_mean*smoothing) / (agg['count'] + smoothing)
enc[te_idx] = X.iloc[te_idx][cat_col].map(agg['smoothed']).fillna(global_mean)
return enc
# Для test: encoding с полного train
def te_for_test(X_train, y_train, X_test, cat_col, smoothing=10):
df = X_train.copy()
df['_y'] = y_train
agg = df.groupby(cat_col)['_y'].agg(['mean','count'])
gm = y_train.mean()
agg['s'] = (agg['mean']*agg['count'] + gm*smoothing) / (agg['count'] + smoothing)
return X_test[cat_col].map(agg['s']).fillna(gm)2. Smoothing:
TE_smoothed(c) = (n_c · mean_c + α · global_mean) / (n_c + α)
α — strength of prior. 10-100 типично. Защищает от редких категорий.
3. Noise injection (CatBoost style):
Добавляем шум к TE на train, чтобы модель не зависела от точного значения.
4. Ordered target stats (CatBoost):
Для каждой записи используем только предыдущие в случайной перестановке записи. Никакого leakage.
Smoothing demonstration
# Без smoothing: rare category {y_observed=1} → TE=1.0, на тесте провал
# С smoothing α=10, global_mean=0.2: TE = (1·1 + 10·0.2)/(1+10) = 0.27Подводные камни
- На train без OOF TE = direct label leakage. Score на CV завышен на 5-30%.
- Smoothing α слишком мал → нет регуляризации. Слишком большой → теряем сигнал. Tune через CV.
- Categorical с очень высокой cardinality (user_id) — даже OOF TE даёт leakage потому что одно и то же значение редко повторяется. Лучше hash trick или target encoding на агрегированных уровнях.
- Distribution shift: train города ≠ test города. Global mean fallback должен быть consistent.
- CatBoost решает TE проблему «из коробки», но требует, чтобы данные были shuffled.
Эталонный ответ
Target encoding без защиты — leakage. Решения: out-of-fold encoding на train, smoothing (n·mean + α·global)/(n+α), ordered target stats (CatBoost). На test — TE из полного train. На редких категориях — smoothing α=10-100 обязателен.