Привет, Хабр!
У вас столбец «город» с 800 уникальными значениями. One‑hot encoding превратит его в 800 бинарных столбцов, разреженную матрицу и модель, которая переобучится на третьей эпохе. Label encoding присвоит числа от 0 до 799, но модель решит, что Москва (0) «меньше» Владивостока (799), хотя никакого порядка между городами нет. Frequency encoding скажет, что Москва и Питер похожи, потому что обе встречаются часто, хотя по целевой переменной могут отличаться кардинально.
Target encoding — подход, при котором каждое значение категории заменяется агрегатом целевой переменной по этой категории. Для задачи регрессии — средним, для классификации — средней вероятностью положительного класса. Город «Москва» с конверсией 12% превращается в число 0.12. «Тюмень» с конверсией 8% — в 0.08.
Идея простая. Но и ловушка тут тоже простая: если считать среднее по всей обучающей выборке и подставлять в неё же — модель увидит утечку из целевой переменной (target leakage), переобучится и покажет нереалистично высокие метрики на трейне. Разберём, как делать правильно.
Как работает: базовая формула
Для каждого значения категории c вычисляем:
encoded(c) = (count(c) * mean_target(c) + m * global_mean) / (count(c) + m)
Где count(c) — сколько раз категория встречается в обучающей выборке, mean_target(c) — среднее значение целевой переменной для этой категории, global_mean — среднее по всей выборке, m — параметр сглаживания (smoothing).
Сглаживание m — ключевой элемент. Без него категория, встретившаяся один раз с target=1, получит encoding=1.0, что конечно странновато, одно наблюдение ничего не говорит о категории. Сглаживание подтягивает редкие категории к глобальному среднему: чем меньше наблюдений, тем ближе encoding к global_mean.
При m=0 сглаживания нет (голое среднее по категории). При m=100 категории с менее чем 100 наблюдениями будут почти неотличимы от глобального среднего. Типичные значения m — от 10 до 300, подбирается на валидации.
Проблема: target leakage
Большинство допускают ошибку именно тут.
# НЕПРАВИЛЬНО — утечка целевой переменной import pandas as pd means = df.groupby('city')['target'].mean() df['city_encoded'] = df['city'].map(means)
Строка из обучающей выборки участвует в подсчёте среднего, которое потом используется как признак этой же строки. Модель фактически подглядывает в ответ. На трейне метрики будут завышены, а на реальных данных модель провалится.
Особенно опасно для категорий с малым количеством наблюдений. Если город встретился 3 раза, а target у всех трёх = 1, encoding = 1.0. Модель запоминает эти три строки через encoding, вместо того чтобы обобщать.
Решение 1: Leave‑One‑Out (LOO)
Для каждой строки i среднее считается по всем строкам категории, кроме самой строки i:
# Leave-One-Out target encoding def loo_target_encode(df, col, target, m=10): global_mean = df[target].mean() agg = df.groupby(col)[target].agg(['sum', 'count']) encoded = df.apply( lambda row: ( (agg.loc[row[col], 'sum'] - row[target] + m * global_mean) / (agg.loc[row[col], 'count'] - 1 + m) ), axis=1 ) return encoded
Строка не участвует в вычислении своего собственного encoding. Утечка существенно снижается, но не исчезает полностью, оставшиеся N-1 строк категории всё ещё коррелируют с таргетом.
Решение 2: K‑Fold Target Encoding (рекомендуемый)
Обучающая выборка делится на K фолдов. Для строк фолда k encoding считается по строкам всех остальных фолдов:
import numpy as np from sklearn.model_selection import KFold def kfold_target_encode(df, col, target, n_splits=5, m=20): global_mean = df[target].mean() encoded = pd.Series(np.nan, index=df.index) kf = KFold(n_splits=n_splits, shuffle=True, random_state=42) for train_idx, val_idx in kf.split(df): # Считаем статистики ТОЛЬКО по train-части фолда train = df.iloc[train_idx] agg = train.groupby(col)[target].agg(['mean', 'count']) # Encoding со сглаживанием mapping = ( (agg['count'] * agg['mean'] + m * global_mean) / (agg['count'] + m) ) # Применяем к val-части фолда encoded.iloc[val_idx] = df.iloc[val_idx][col].map(mapping) # Новые категории (не встретились в фолде) — global_mean encoded.fillna(global_mean, inplace=True) return encoded
Каждая строка кодируется статистиками, вычисленными без её участия. При K=5 утечка практически равна нулю.
Для тестовой выборки encoding считается по всей обучающей:
def target_encode_test(train, test, col, target, m=20): global_mean = train[target].mean() agg = train.groupby(col)[target].agg(['mean', 'count']) mapping = ( (agg['count'] * agg['mean'] + m * global_mean) / (agg['count'] + m) ) encoded = test[col].map(mapping).fillna(global_mean) return encoded
Готовые реализации
В category_encoders (популярная библиотека, совместимая со sklearn):
import category_encoders as ce from sklearn.model_selection import cross_val_score from sklearn.ensemble import GradientBoostingClassifier encoder = ce.TargetEncoder(cols=['city', 'device_type'], smoothing=20) # В пайплайне sklearn from sklearn.pipeline import Pipeline pipe = Pipeline([ ('encoder', ce.TargetEncoder(cols=['city', 'device_type'], smoothing=20)), ('model', GradientBoostingClassifier(n_estimators=200)) ]) # cross_val_score корректно применяет encoder на каждом фолде scores = cross_val_score(pipe, X, y, cv=5, scoring='roc_auc')
cross_val_score с пайплайном автоматически fit‑ит encoder только на train‑фолде. Это правильное поведение, утечки нет. Но если вы вручную вызовете encoder.fit_transform(X_train, y_train) и потом передадите в модель — утечка будет, потому что fit_transform считает статистики по всему X_train.
Когда target encoding лучше альтернатив
Высокая кардинальность. IP‑адреса, почтовые индексы, ID товаров, user agents — тысячи и десятки тысяч уникальных значений. One‑hot encoding физически не справится (миллионы столбцов), label encoding не несёт информации. Target encoding сжимает тысячи категорий в один числовой столбец с высокой предсказательной силой.
Древесные модели. XGBoost, LightGBM, CatBoost — target encoding хорошо работает именно с ними, потому что деревья строят сплиты по порогу. Числовое значение encoding даёт дереву осмысленный порог: «если encoding города > 0.15, идём в левое поддерево», то есть «если конверсия в этом городе выше 15%».
Кстати, CatBoost реализует target encoding внутри (ordered target statistics) и делает это автоматически, с защитой от утечки. Если используете CatBoost,то отдельный target encoding не нужен, передавайте категории напрямую с указанием
cat_features.
Линейные модели. Target encoding работает и здесь, но менее выражен эффект — линейная модель может извлечь из one‑hot encoding столько же информации (при достаточном количестве данных). Преимущество target encoding для линейных моделей — в сокращении размерности при высокой кардинальности.
Когда target encoding не нужен
Низкая кардинальность (< 10–15 уникальных значений). One‑hot encoding справится, не создаст проблем с размерностью, и не добавит риска утечки. Для пола, дня недели, типа устройства — не усложняйте.
Мало данных. Если в категории 5 наблюдений, средний таргет по ним — шум, а не сигнал. Сглаживание поможет (подтянет к глобальному среднему), но при совсем малых выборках лучше сгруппировать редкие категории в «другое» и применить one‑hot.
Целевая переменная — непрерывная с высокой дисперсией. Среднее по категории будет зашумлённым. Рассмотрите медиану вместо среднего или quantile encoding.
Что ещё можно кодировать кроме среднего
Target encoding не ограничивается средним. Можно использовать любую агрегатную функцию, и комбинация нескольких даёт модели больше информации:
# Несколько агрегатов одновременно agg = df.groupby('city')['target'].agg( target_mean='mean', target_std='std', target_median='median', count='count' )
std — насколько стабилен таргет внутри категории. Город с mean=0.1 и std=0.02 — стабильно низкая конверсия. Город с mean=0.1 и std=0.15 — нестабильный, средняя не показательна.
count — сколько наблюдений. Сам по себе полезный признак: частота категории коррелирует с поведением (популярный город отличается от редкого).
Для каждого дополнительного агрегата — тот же K‑Fold подход для защиты от утечки. Дополнительные столбцы не бесплатны: чем больше признаков на основе таргета, тем выше риск переобучения, даже с K‑Fold.
Итог
Считайте encoding по K‑Fold на трейне (K=5 или K=10). Не fit_transform на всём трейне.
Для теста и прода — encoding по статистикам всего трейна. Сохраняйте маппинг (словарь категория‑значение) как артефакт модели.
Новые категории (не встречались в трейне) — заменяйте на
global_mean. Не на 0, не на NaN — на среднее по выборке.Подбирайте
m(smoothing) на валидации. Типичный диапазон — 10–100 для средних датасетов, 100–500 для больших.Мониторьте drift. Если распределение категорий на проде сместилось (появился новый город с 30% трафика), encoding устаревает. Периодическое переобучение — супер важная штука.
Если вам важно не просто знать термины, а собирать признаки без утечек и получать модели, которым можно доверять, загляните в каталог — там можно выбрать обучение под свои задачи.

По ряду курсов можно бесплатно пройти тестирование и оценить свой текущий уровень. До 30 апреля за прохождение теста действует скидка 15% на обучение.
Чтобы лучше понять, как данные влияют на поведение модели и качество предсказаний, начните с этих открытых уроков:
13 апреля, 20:00 «Архитектура доверия: качество данных». Записаться.
29 апреля, 20:00 «Деревья решений для задач классификации и регрессии». Записаться.
