Что такое train/test split

Основная идея

Train/test split — это разделение данных на две части. На train set модель обучается. На test set — проверяется. Test set играет роль «неизвестных» данных, которых модель не видела.

Без этого разделения оценка модели будет лгать. Модель может запомнить обучающие данные и показывать 99% accuracy на них, но проваливаться на новых. Test set даёт честную оценку.

Стандартные пропорции

Обычно 70-80% данных идёт в train, 20-30% в test. Для больших датасетов (миллионы строк) достаточно 90/10 или даже 95/5. Для маленьких (до 1000 строк) лучше 70/30 — чтобы test был репрезентативным.

Иногда выделяют третью часть — validation set. Train — обучение, validation — выбор гиперпараметров, test — финальная оценка. Это 60/20/20 типично.

Пример в Python

from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(
    X, y,
    test_size=0.2,
    random_state=42,
    stratify=y
)

random_state фиксирует разделение — для воспроизводимости. stratify сохраняет пропорции классов в обоих частях — критично для несбалансированных задач.

Зачем stratify

Если у вас задача с 5% positive class и вы делаете обычный random split, может случиться так, что в test set попадёт 2% positive. Оценка будет нерепрезентативной.

Stratify гарантирует одинаковые пропорции. В train 5% positive → в test тоже 5%.

Для сбалансированных классов stratify не так важен. Для любых реальных задач с дисбалансом — must-have.

Прокачать тему на реальных задачах удобно в боте @kariernik_bot — база вопросов собрана с собеседований в Яндексе, Авито, Ozon, Тинькофф.

Random state и воспроизводимость

random_state = 42 (или любое число) фиксирует seed для random shuffle. При одинаковом seed разделение будет identical, что критично для воспроизводимых экспериментов.

Без random_state каждый запуск даст разный split и разные результаты. Отладка становится невозможной.

На production пайплайнах random_state обязательно фиксировать.

Time series — особый случай

Для time series классический train/test split не работает. Shuffle данных нарушает временную структуру — модель обучается на будущем и предсказывает прошлое.

Правильно — разделить по времени. Train — первые 80% по дате, test — последние 20%.

df_sorted = df.sort_values('date')
split_idx = int(len(df_sorted) * 0.8)
train = df_sorted.iloc[:split_idx]
test = df_sorted.iloc[split_idx:]

Это имитирует реальную production ситуацию: мы всегда предсказываем future на основе past.

Group split

Для данных, сгруппированных по пользователям или клиентам, нельзя случайно разделять строки. Одна и та же сущность может попасть в train и test — data leakage.

Пример: предсказание LTV пользователей. Если транзакции user_id = 1234 попадут в train и test, модель «видит» частично того же user. Результаты оптимистичны.

Правильно — группировать по user_id и разделять пользователей, а не строки.

from sklearn.model_selection import GroupShuffleSplit

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

Data leakage — главная опасность

Data leakage — это когда информация из test попадает в train непредвиденно. Модель показывает отличные результаты на validation, но проваливается в production.

Типичные источники leakage:

Scaling на всех данных. Если fit StandardScaler на X всех, train знает mean и std test'a. Scaling должен быть внутри pipeline, после split.

Feature engineering на всех. Считаем target encoding по всему dataset, потом делим. Train знает paterns из test.

Time leakage. Используем будущие данные в train.

Group leakage. Тот же user в train и test.

Правильная практика — делать всё внутри pipeline, с split в начале.

from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler

pipe = Pipeline([
    ('scaler', StandardScaler()),
    ('model', model_class())
])

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
pipe.fit(X_train, y_train)
score = pipe.score(X_test, y_test)

Scaling fit только на train, apply на test. Всё корректно.

Тестовый set и evaluation

Test set должен использоваться один раз — в самом конце, для финальной оценки. Если вы много раз тюнили модель на test и выбрали лучшую, вы overfitting на test.

Для iterative tuning используйте validation set. Test — только для финального report.

На Kaggle есть private leaderboard именно для этого: ваши эксперименты не должны влиять на final metric.

На собесе такие штуки часто спрашивают. Быстрый способ довести до автоматизма — тренажёр в Telegram с задачами из реальных интервью.

Cross-validation вместо single split

Single train/test split даёт одну метрику, зависящую от конкретного разделения. Для стабильной оценки лучше cross-validation — k раз повторять split и усреднять.

Но CV дороже вычислительно. Для больших моделей (deep learning, huge gradient boosting) часто используют просто train/test split.

Типичные ошибки

Первая — малый test set на маленьком датасете. 20% от 100 примеров — 20 строк. Оценка очень variable.

Вторая — не stratify при дисбалансе классов. Получаете нерепрезентативные splits.

Третья — leakage через preprocessing. Normalization, imputation, encoding делаются после split.

Четвёртая — многократное использование test set. Превращается в validation по факту, overfitting на него.

Пятая — игнорировать time или group structure. Random split для time series — catastrophic.

Читайте также

FAQ

70/30 или 80/20?

Для стандартных размеров данных (10k-100k) 80/20. Для маленьких 70/30. Для огромных — 95/5.

Validation set обязателен?

Если делаете hyperparameter tuning — да. Если просто обучаете одну модель — можно без.

Test set должен быть в том же распределении, что train?

Да. Если нет — это уже другая задача (domain adaptation).

Сколько раз можно использовать test set?

В идеале — один. Каждое использование даёт информацию, которая может повлиять на последующие решения.