Target encoding без утечки: почему среднее по всей таблице завышает качество
Магазин хочет предсказать повторную покупку и добавляет в таблицу категорию «источник заявки». Для каждого источника удобно посчитать долю прошлых покупателей: модель получает числовой признак вместо длинного списка категорий. Но если долю считать по той же строке, которую затем используют для обучения, признак частично содержит её правильный ответ. На редких категориях это почти прямой канал утечки.
Предельный случай — каждый источник встречается ровно один раз. Среднее целевой переменной внутри категории тогда равно самому таргету. Модель с таким признаком легко покажет идеальный результат на обучении, хотя для нового источника она не знает ничего. Ошибка прячется не в алгоритме классификации, а в моменте, когда создаётся признак.
Разделите вычисление статистики и строку, которой она достанется
Для каждой обучающей строки кодировку следует получать из других строк. Это называют out-of-fold target encoding: обучающие объекты делят на фолды, считают долю события на нескольких фолдах и применяют её к оставшемуся. Затем собирают закодированные строки обратно. Финальная модель видит значение, в которое её собственный ответ не входил.
В scikit-learn это различие уже заложено в TargetEncoder. Его fit_transform использует внутреннее разбиение при подготовке обучающих данных. Простая последовательность fit(...).transform(...) на той же таблице даёт иной результат и может протечь. Ниже намеренно взяты уникальные идентификаторы, чтобы разницу было видно без обучения второй модели.
import numpy as np
from sklearn.preprocessing import TargetEncoder
X = np.array([[f"client_{i}"] for i in range(12)], dtype=object)
y = np.array([0, 1] * 6)
encoder = TargetEncoder(target_type="binary", cv=3, random_state=0, smooth=0)
train_safe = encoder.fit_transform(X, y).ravel()
train_leaky = encoder.transform(X).ravel()
print("out-of-fold:", train_safe.tolist())
print("по всей обучающей таблице:", train_leaky.tolist())
assert np.all(train_safe == 0.5)
assert np.array_equal(train_leaky, y.astype(float))В безопасном результате все значения равны 0,5: неизвестной категории достаётся средняя доля события в тренировочной части фолда. Преобразование всей обучающей таблицы после fit возвращает последовательность нулей и единиц, совпадающую с ответами. Проверки в коде закрепляют именно этот вывод. Такую вторую последовательность нельзя подавать классификатору для оценки качества на этих же строках.
Где должны проходить внешние границы
Внутреннее cross-fitting не заменяет train/test split. Сначала отделите финальный тест, а кодировщик и модель обучайте только на train. При подборе параметров помещайте TargetEncoder в Pipeline, чтобы каждый внешний фолд создавал свои статистики заново. На тестовых и новых строках вызывайте transform у уже обученного кодировщика. Для неизвестной категории он подставляет общую среднюю целевого признака из обучения; это разумная отправная точка, но её качество надо проверить на новых категориях отдельно.
Если данные упорядочены по времени, обычное перемешивание внутренних фолдов может нарушить доступность информации. Например, долю покупок по каналу, накопленную в декабре, нельзя дать модели при прогнозе за сентябрь. Тогда стандартный внутренний cross-fitting из примера не подходит: постройте исторические агрегаты вручную по исходам, доступным к дате прогноза, либо используйте кодировщик с поддержкой нужного временного разбиения. Для повторяющихся клиентов учитывайте и групповую границу; она разобрана в статье о GroupKFold и клиентах.
Сглаживание тоже важно: доля 1/1 не равна по надёжности доле 100/100. Параметр smooth смешивает статистику категории с общей средней; для реального набора проверяйте несколько значений на честной валидации. Учебный код отключил сглаживание, чтобы сделать утечку видимой. В рабочем эксперименте сохраните частоты категорий и отдельно посмотрите качество на редких и новых значениях.
Проверка особенно полезна для категорий, которые фактически играют роль идентификаторов. Если в поле тысячи уникальных значений и большинство встречается по одному разу, кодировщик не узнает устойчивый эффект категории: у него нет повторений для оценки. Оставлять такое поле лишь потому, что обучающий score вырос, опасно. Сравните качество на новых категориях с простой моделью без этого поля и посмотрите, сколько объектов получают общую среднюю. Если почти все новые заявки попадают в этот случай, сложная кодировка мало помогает реальному прогнозу.
Проверяйте также момент появления метки. Для повторной покупки ответ может стать известен через месяц после заявки. Если во время прогноза за 10 марта в кодировку попали исходы заявок, созданных в конце февраля и ещё не дозревших, временная утечка остаётся даже при out-of-fold расчёте по строкам. Стройте обучающие срезы по дате доступности исхода, а не только по дате заявки.
От кодировки к табличной модели
В курсе «ML Engineer / Data Scientist: табличные модели в продакшн» вы проходите подготовку признаков, проверку валидации, обучение CatBoost и LightGBM, а затем собираете воспроизводимый пайплайн инференса. Такой маршрут помогает проверить, что статистический признак одинаково корректен в эксперименте и после выпуска модели. Откройте программу курса и начните с раздела подготовки данных.
Продолжить на практике
- ML Engineer / Data Scientist: табличные модели в продакшн ↗
Практический курс для тех, кто хочет получить навыки ML Engineer / Data Scientist на реальных табличных задачах. В курсе Вы не просто обучите CatBoost/LightGBM, а соберёте полноценный production-style пайплайн: подготовка данных, feature engineering (генерация признаков), корректная валидация, защита от leakage (утечек), тюнинг через Optuna, интерпретация через SHAP, batch-инференс и REST API для предсказаний.
