Результаты обучения

После изучения урока вы сможете:

1. Зачем вообще разделять данные?

Модель должна работать не только на строках, которые уже видела. Нам важно оценить, как она поведёт себя на новых наблюдениях — именно таких, которые появятся после запуска. Это и есть проверка обобщающей способности.

Если измерить качество на обучающих примерах, оценка обычно будет слишком оптимистичной: модель уже подстраивалась под эти данные. Разделение имитирует границу между прошлым, доступным при разработке, и будущим, которое ещё неизвестно.

2. Три выборки — три разных решения

Train (обучающая) — данные, на которых алгоритм подбирает параметры модели и обучаемые преобразования.

Validation (валидационная) — данные для сравнения вариантов: выбора алгоритма, гиперпараметров, признаков, порога классификации и других решений разработчика.

Test (тестовая) — независимая финальная проверка уже выбранного процесса. Её результат нужен для оценки ожидаемого качества, а не для очередного раунда настройки.

Роли train / validation / testТри части отвечают за обучение, выбор решения и независимую финальную оценку.TRAINобучить параметрыVALIDATIONвыбрать настройкиTESTоценить один раз
Не смешивайте функции частей: test не участвует в настройке.

3. Почему нельзя бесконечно смотреть на test

Каждый раз, когда вы меняете модель после просмотра тестовой метрики, тест косвенно участвует в разработке. Даже если градиентный алгоритм не обучался на тестовых строках, человек уже использовал их результат для выбора решения.

Так возникает адаптивная подгонка: из многих вариантов постепенно выбирается тот, которому случайно повезло на конкретном тесте. Итоговая метрика становится смещённой вверх. Если тест уже использовали многократно, честнее собрать новый независимый тестовый набор или явно признать ограничение.

4. Случайное и стратифицированное разделение

Случайный split подходит, если строки независимы и одинаково распределены, а порядок наблюдений не несёт смысла. В классификации с редким положительным классом обычное случайное разделение может дать сильно разные доли классов в частях.

Стратификация сохраняет приблизительные пропорции классов в train, validation и test. Она полезна для редких классов, но не устраняет зависимость между строками, временную утечку или дубликаты. Стратификация — не универсальное средство «сделать выборки честными».

Как выбрать схему разбиенияСтратегия определяется тем, какие наблюдения будут новыми при эксплуатации.RANDOMнезависимые строкиSTRATIFIEDдоли классовGROUPновые объектыTIMEбудущие периоды
Разбиение должно воспроизводить реальный сценарий применения.

5. Групповое разделение: не допустить узнавания объекта

Представим датасет медицинских снимков: у одного пациента может быть десятки снимков. Если часть снимков попадёт в train, а другие снимки того же пациента — в test, модель может узнавать индивидуальные особенности пациента. Метрика будет отвечать на вопрос «узнаём ли знакомых пациентов», а не «работаем ли с новыми людьми».

Group split удерживает все строки одной группы в одной части. Группой может быть пациент, клиент, здание, устройство, партия продукции или исходный документ. Идентификатор группы часто нужен для разбиения, даже если его не следует подавать модели как обычный признак.

6. Временное разделение: прошлое предсказывает будущее

Для прогноза спроса, энергопотребления, отказов или цен случайное перемешивание часто неверно. Оно может поместить более поздние наблюдения в train, а ранние — в test. Тогда модель косвенно учится на будущем.

Используйте хронологические границы: ранний период — train, следующий — validation, самый поздний — test. Если модель регулярно переобучается на скользящем окне, проверяйте несколько последовательных временных срезов. Учитывайте горизонт прогноза, задержку публикации данных и сезонность.

Временная границаПри прогнозе будущего обучение должно использовать только прошлую информацию.PASTtrainLATERvalidationLATESTtest
Хронологический порядок защищает от обучения на будущем.

7. Утечка данных: информация попала не туда

Утечка — это использование при обучении или выборе решения информации, которая недоступна в реальном прогнозе либо должна была оставаться скрытой для независимой оценки. Типичные причины: дубликаты между частями; один объект в train и test при сценарии новых объектов; случайное перемешивание временного ряда; признаки, сформированные после целевого события; нормализация или отбор признаков до split.

Особенно коварна утечка через смысл признака. Например, прогнозируем завтрашнее потребление, но используем фактическую загрузку здания за завтра. Значение существует в таблице, однако в момент прогноза его ещё нельзя знать.

8. Предобработка — часть модели, а не нейтральная подготовка

Заполнение пропусков средним, стандартизация, отбор признаков, PCA, словарь категорий и target encoding получают параметры из данных. Поэтому их нужно обучать внутри train, а затем применять к validation и test без повторного fit.

Правильный конвейер: разделить данные; вызвать fit на обучающей части; вызвать transform на остальных; обучить модель; сравнить на validation. В cross-validation весь pipeline заново обучается внутри каждого фолда. Так статистика из валидационной части не просачивается в обучение.

Pipeline без утечкиВсе обучаемые преобразования fit выполняются только на обучающей части.TRAINfitTRANSFORMбез refitVALIDATIONоценка
Один и тот же обученный pipeline преобразует validation и test без повторного fit.

9. Cross-validation и настройка гиперпараметров

Когда данных немного, один случайный validation split может оказаться нестабильным. K-fold cross-validation делит обучающую часть на K фолдов: каждый фолд по очереди становится проверочным, остальные используются для обучения. Среднее и разброс метрики показывают, насколько результат зависит от разбиения.

Для классификации часто применяют StratifiedKFold, для связанных объектов — GroupKFold, для времени — TimeSeriesSplit. Вложенная cross-validation нужна, когда требуется оценить весь процесс подбора гиперпараметров без дополнительной независимой выборки. Test при этом всё равно остаётся нетронутым.

10. Протокол честной оценки

До обучения письменно зафиксируйте: целевую переменную и горизонт прогноза; единицу независимого наблюдения; группы и временные границы; список признаков, доступных на момент прогноза; метрику и baseline; правила обработки пропусков; процедуру настройки. После выбора решения заморозьте pipeline и один раз оцените его на test.

Сохраняйте random seed, версии данных, код разбиения, метрики по важным сегментам и доверительные интервалы или разброс по фолдам. Одна цифра без контекста не описывает надёжность модели.

Сравнение стратегий

СтратегияКогда применятьГлавный риск
Random splitНезависимые однотипные наблюденияСкрытые группы или временная зависимость
Stratified splitКлассификация с редкими классамиНе устраняет зависимость и утечку
Group splitНовые пациенты, клиенты, здания, устройстваМало независимых групп
Time splitПрогноз будущих периодовНеверная граница доступности признаков

Пример: групповой split и Pipeline

Пример показывает принцип: группировка выполняется до обучения, а заполнение пропусков и масштабирование находятся внутри Pipeline. Код предполагает, что имена столбцов и списки признаков уже подготовлены.

from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.model_selection import GroupShuffleSplit
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.linear_model import LogisticRegression

# Сначала отделяем целевую переменную и группы.
X, y, groups = data[features], data["target"], data["patient_id"]

splitter = GroupShuffleSplit(
    n_splits=1, test_size=0.2, random_state=42
)
train_idx, test_idx = next(splitter.split(X, y, groups))

numeric = Pipeline([
    ("imputer", SimpleImputer(strategy="median")),
    ("scale", StandardScaler()),
])
categorical = Pipeline([
    ("imputer", SimpleImputer(strategy="most_frequent")),
    ("encode", OneHotEncoder(handle_unknown="ignore")),
])
preprocess = ColumnTransformer([
    ("num", numeric, numeric_features),
    ("cat", categorical, categorical_features),
])
model = Pipeline([
    ("preprocess", preprocess),
    ("classifier", LogisticRegression(max_iter=1000)),
])
model.fit(X.iloc[train_idx], y.iloc[train_idx])
# Test используется только после фиксации решения.

Для временной задачи вместо GroupShuffleSplit используйте хронологическую схему; для классификации с дисбалансом проверьте стратификацию, не нарушая групп и времени.

Практическое задание

Практикум: прогноз отказа оборудования. В таблице есть device_id, timestamp, temperature, vibration, maintenance_flag и failure_next_24h. Определите, какие признаки доступны за 24 часа до прогноза. Предложите два сценария: прогноз для уже известных устройств и для новых устройств. Для каждого выберите split, опишите train/validation/test и объясните, где обучаются imputer и scaler.

  1. Запишите единицу независимого наблюдения и сценарий применения.
  2. Отметьте признаки, которые ещё неизвестны на момент прогноза.
  3. Выберите стратегию split и обоснуйте её.
  4. Опишите порядок fit/transform и момент финального test.