Загружаем научный разбор
Подготавливаем текст, источники и редакционные примечания без изменения разметки страницы.
Подготавливаем текст, источники и редакционные примечания без изменения разметки страницы.
Автор: Казачкин Даниил Михайлович · Обновлено
Разбираем data leakage, границы train/test, preprocessing внутри Pipeline, групповые и временные разбиения и nested cross-validation без завышения выводов.
Высокая метрика на test-выборке ещё не означает, что модель будет так же работать после выпуска. Оценка становится оптимистичной, если при построении признаков, выборе гиперпараметров или разбиении наблюдений алгоритм получил информацию, которой не будет в момент реального прогноза. Такое нарушение называют data leakage. Оно опасно тем, что код может быть корректным, обучение — воспроизводимым, а результат — систематически неверным.
Работы Кауфмана и соавторов, Коули и Талбота, а также Вармы и Саймона рассматривают разные стороны одной проблемы: нельзя оценивать процедуру на данных, которые прямо или косвенно участвовали в её выборе. Ни один конкретный train/test split сам по себе не гарантирует честность. Нужно определить единицу независимости, момент прогноза и полный вычислительный граф до того, как делить строки.
Пусть обучающая процедура A получает выборку D_train и возвращает модель. Нас интересует не точность на известных строках, а ожидаемая потеря на новых наблюдениях из целевой среды. Test-выборка служит конечным приближением этой неизвестной величины. Чтобы приближение имело смысл, она не должна влиять ни на параметры модели, ни на выбор признаков, порогов, архитектуры или правила остановки.
Поэтому объект оценки шире вызова estimator.fit. В него входят:
Если один из этих шагов подглядел в test, итоговая цифра оценивает уже адаптированное к test решение. Она больше не является независимой проверкой.
Утечка цели. В признаки попадает значение, созданное после события или непосредственно связанное с target. Например, признак refund_issued нельзя использовать для прогноза возврата в момент покупки, если он появляется только после решения о возврате.
Загрязнение preprocessing. Среднее для стандартизации, словарь категорий или список отобранных признаков вычисляют по всей таблице до разбиения. Конкретная метка test может не передаваться в модель, но распределение test уже изменило преобразование.
Неверная единица разбиения. Случайно делят строки, хотя независимыми объектами являются пациенты, пользователи, устройства или документы. Несколько почти одинаковых наблюдений одного объекта оказываются по обе стороны. Модель частично узнаёт объект вместо обобщения на новый объект.
Утечка из будущего. Для временной задачи случайный split переносит будущие состояния в train. Это особенно незаметно в агрегатах: «число покупок пользователя за всё время» включает события после момента, для которого строится прогноз.
Эти случаи нельзя устранить одним параметром random_state. Первый вопрос звучит не «какую долю оставить на test?», а «какую ситуацию использования должен имитировать test?».
Если после выпуска модель прогнозирует для новых строк уже известных пользователей, допустимо сохранять пользователей в обеих частях, но нельзя использовать будущие события. Если она должна работать для совершенно новых пользователей, нужен group split по пользователю. Если прогноз запускается в понедельник для следующей недели, test должен находиться позже train во времени.
Полезно записать контракт одной фразой:
> В момент T модель получает только поля, существовавшие не позднее T, и прогнозирует результат для объекта, который относится к такой-то популяции.
Из контракта выводятся ключ разбиения, временной срез и допустимые признаки. Например, снимки одного пациента следует держать в одной части, если целевая популяция — новые пациенты. Повторные транзакции клиента можно делить по времени, если цель — следующая транзакция известного клиента. Универсального splitter для обеих задач нет.
Дубликаты заслуживают отдельной проверки. Точная копия строки в train и test почти гарантированно делает задачу проще. Near-duplicates — разные кадры одного видео, соседние фрагменты текста, версии документа — требуют предметного идентификатора группы, а не только удаления точных совпадений.
Ниже показана типичная ошибка: медиана и параметры масштабирования обучаются до cross-validation. После этого каждый validation fold уже повлиял на преобразованные признаки.
# Неверно: fit_transform видит все строки до оценки.
X_scaled = scaler.fit_transform(X)
scores = cross_val_score(model, X_scaled, y, cv=5)Корректная схема помещает обучаемые преобразования в Pipeline. Тогда на каждом fold методы fit вызываются только на его train-части, а validation проходит через уже зафиксированные преобразования.
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import StratifiedGroupKFold, cross_validate
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler
numeric = make_pipeline(SimpleImputer(strategy="median"), StandardScaler())
categorical = make_pipeline(
SimpleImputer(strategy="most_frequent"),
OneHotEncoder(handle_unknown="ignore"),
)
preprocess = ColumnTransformer(
[("num", numeric, numeric_columns), ("cat", categorical, category_columns)]
)
pipeline = make_pipeline(preprocess, LogisticRegression(max_iter=2_000))
cv = StratifiedGroupKFold(n_splits=5, shuffle=True, random_state=42)
result = cross_validate(
pipeline,
X,
y,
groups=patient_id,
cv=cv,
scoring=["roc_auc", "average_precision"],
)Пример не является готовым рецептом для любой медицинской или бизнес-задачи. Он демонстрирует две границы: обучаемый preprocessing находится внутри pipeline, а наблюдения одного пациента не расходятся по folds. Для временных данных понадобился бы другой способ разбиения.
Не всякое преобразование обязано обучаться. Детерминированное преобразование единиц измерения обычно не извлекает статистику из выборки. Но как только шаг вычисляет словарь, среднее, частоты, компоненты PCA, top-k признаков или target encoding, его нужно обучать внутри соответствующей train-части.
Cross-validation часто используют одновременно для двух задач: выбрать гиперпараметры и сообщить качество. Если перебрать множество конфигураций и опубликовать максимальный score на тех же folds, выбирается не только сильная модель, но и конфигурация, которой случайный шум оценки оказался выгоден.
Коули и Талбот показали, что дисперсия критерия выбора может приводить к переобучению самой процедуры model selection, а возникающее ухудшение бывает сопоставимо с различиями между алгоритмами в исследованных ими условиях. Это не утверждение о фиксированном размере эффекта для каждого датасета. Оно объясняет механизм: чем больше решений принято по одной конечной оценке, тем меньше эта оценка похожа на независимую.
Варма и Саймон исследовали nested cross-validation в задачах классификации по экспрессии генов. Во внешнем цикле откладывается fold для оценки, а во внутреннем на оставшихся данных выполняется весь подбор. Внешняя оценка относится к процедуре «подобрать и обучить», а не к уже выбранной конфигурации.
Nested CV полезна, когда независимого test-набора мало или нужно сравнить процедуры. Но после неё всё равно важно отдельно описать deployment shift. Перекрёстная проверка на исторических данных не моделирует автоматически смену продукта, сезонность или новую политику сбора данных.
Test-набор не обязан быть огромным, но его размер определяет точность вывода. Различие 0,002 между двумя ROC AUC не становится значимым только потому, что библиотека напечатала три знака после запятой. При зависимых наблюдениях обычная формула для независимых строк также может недооценить неопределённость.
Для каждого сильного признака полезно хранить provenance:
| Вопрос | Безопасный ответ |
|---|---|
| Когда поле физически возникло? | Не позже времени прогноза |
| Какая система его записала? | Источник доступен и после выпуска |
| Использует ли расчёт target? | Только внутри train fold, если это обучаемый encoder |
| Есть ли агрегат «за всё время»? | Ограничен моментом прогноза |
| Может ли идентификатор запоминать объект? | Проверено разбиением по объектам |
| Пересчитывался ли словарь по test? | Нет, fit выполнялся только на train |
Особенно подозрительны признаки, которые неожиданно почти идеально разделяют классы, появились после бизнес-процесса принятия решения или имеют прямой административный смысл. Высокая predictive power — повод проверить происхождение, а не автоматическое доказательство утечки.
Исследования и документация не доказывают, что любой pipeline автоматически свободен от leakage. Pipeline защищает границу вызовов fit и transform, но не знает предметного времени появления колонок и не распознаёт, что две строки принадлежат одному человеку.
Nested cross-validation не делает оценку универсальной. Она снижает selection bias в конкретной схеме подбора, но не исправляет смещение выборки, неверные labels, temporal drift или отличие production-популяции.
Нельзя также заключить, что всякая корреляция с будущим является ошибкой: определяющим служит момент, когда должен быть сделан прогноз. Поле может быть законным для ретроспективного анализа и недоступным для онлайн-решения. Контракт задачи важнее названия признака.
Наконец, отсутствие обнаруженной утечки не доказывает полезность модели. Честная низкая метрика — корректный результат эксперимента. Она позволяет остановить слабую идею или изменить постановку до того, как оптимистичная цифра превратится в production-инцидент.
Практический вывод прост: сначала отделяют мир, на котором принимают решения, от мира, на котором эти решения проверяют; затем следят, чтобы ни один обучаемый шаг не пересёк эту границу. Train/test split — не строка кода, а экспериментальный контракт.
Самостоятельный редакционный разбор ЯдроКода по работам Kaufman et al., Cawley и Talbot, Varma и Simon и официальной документации scikit-learn. Формулировки и примеры созданы редакцией.
Таблица, программные примеры и структура аудита созданы редакцией; текст, рисунки и таблицы первоисточников не копируются.