Cross-validation стратегии на собеседовании Data Scientist

Готовься к собесу аналитика как в Duolingo
10 минут в день — SQL, Python, A/B, метрики. 1700+ вопросов в Telegram
Открыть Карьерник в Telegram

Карьерник — Duolingo для аналитиков: 10 минут в день тренируй SQL, Python, A/B, статистику, метрики и ещё 3 темы собеса. 1500+ вопросов в Telegram-боте. Бесплатно.

Зачем разбирать на собесе

CV — основной инструмент честной оценки модели. На собесе DS обязательно: «как валидировать модель на временных рядах», «зачем stratified», «что такое group leakage». Уровень middle/senior — детали nested CV и custom CV для бизнес-задач.

Главная боль без понимания — DS использует случайный k-fold на временных рядах, получает 0.95 AUC на CV и 0.6 в проде.

Базовый k-fold

Делим данные на K частей. На каждой итерации тренируем на K-1, валидируем на одной. Усредняем метрику.

from sklearn.model_selection import KFold, cross_val_score

kf = KFold(n_splits=5, shuffle=True, random_state=42)
scores = cross_val_score(model, X, y, cv=kf, scoring='roc_auc')
print(scores.mean(), scores.std())

Зачем shuffle=True. Если данные в таблице упорядочены (по дате, по классу), без shuffle K folds могут получиться смещёнными.

Сколько K. 5 или 10 — компромисс между bias и variance оценки. K=10 даёт меньше bias (модель тренируется на 90% данных), но дороже.

Stratified k-fold

При классификации с несбалансированными классами обычный k-fold может выдать fold, в котором почти нет minority class. Stratified k-fold сохраняет пропорцию классов в каждом fold.

from sklearn.model_selection import StratifiedKFold

skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
for train_idx, val_idx in skf.split(X, y):
    ...

Дефолт для классификации. cross_val_score с классификатором использует stratified автоматически.

Расширение — StratifiedGroupKFold (sklearn ≥ 1.0): сохраняет пропорцию классов и при этом не разрывает группы.

Time-series CV

В временных рядах нельзя случайно перемешивать — это leakage. Будущие данные используются для тренировки, что в проде невозможно. Все CV-метрики надуты.

Корректная схема — expanding window:

fold 1:  [train: 1-100][val: 101-110]
fold 2:  [train: 1-110][val: 111-120]
fold 3:  [train: 1-120][val: 121-130]
fold 4:  [train: 1-130][val: 131-140]
from sklearn.model_selection import TimeSeriesSplit

tss = TimeSeriesSplit(n_splits=5, gap=0, test_size=10)
for train_idx, val_idx in tss.split(X):
    ...

Sliding window — фиксированная длина train (если хотите не учить на старых паттернах).

gap — буфер между train и val, важен, когда между ними может быть leakage (например, target известен с задержкой 7 дней, а val начинается через 1 день).

Признак-leakage помимо времени. Скользящие средние, lag-фичи на момент t не должны видеть данные > t. Считать фичи покадрово.

Group k-fold

Если в данных есть группы, которые нельзя разрывать между train и val — GroupKFold.

from sklearn.model_selection import GroupKFold

gkf = GroupKFold(n_splits=5)
for train_idx, val_idx in gkf.split(X, y, groups=user_ids):
    ...

Когда нужен:

  • Несколько событий на одного пользователя (предсказываем покупку — пользователь не должен попасть в train и val одновременно).
  • Кросс-сессии (разные сессии одного юзера).
  • Медицинские данные с пациентами.
  • Несколько сэмплов на один документ / товар / адрес.

Без groups → leakage: модель учит «этот пользователь конвертит» вместо общего паттерна → CV-оценка надута.

Готовься к собесу аналитика как в Duolingo
10 минут в день — SQL, Python, A/B, метрики. 1700+ вопросов в Telegram
Открыть Карьерник в Telegram

Leave-one-out и leave-p-out

LOO (leave-one-out) — k = N. На каждой итерации train на N-1, val на 1.

from sklearn.model_selection import LeaveOneOut

loo = LeaveOneOut()

Свойства:

  • Bias оценки минимальный (модель тренируется почти на полных данных).
  • Variance оценки максимальная (один объект — нестабильно).
  • Стоимость огромная (N тренировок).
  • Применяется на маленьких датасетах (< 1k объектов) или когда деление дорогое.

Leave-p-out — на каждой итерации val из p объектов. Комбинаторно дорого, редко применяется.

Nested CV для выбора гиперпараметров

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

outer fold (для оценки):
  inner fold (для подбора гиперпараметров):
    train inner / val inner
  best params -> train на outer.train -> eval на outer.val
from sklearn.model_selection import GridSearchCV, cross_val_score

inner_cv = KFold(n_splits=3)
outer_cv = KFold(n_splits=5)

clf = GridSearchCV(estimator=model, param_grid=grid, cv=inner_cv)
nested_scores = cross_val_score(clf, X, y, cv=outer_cv)
print(nested_scores.mean())

Когда нужен. Маленькие датасеты, серьёзная статья, важна честная оценка обобщения. На больших данных — обычно одно train/val/test разбиение работает не хуже и дешевле.

Частые ошибки

Случайный shuffle на временных рядах. Главный баг с надутой метрикой. Всегда TimeSeriesSplit или ручной expanding window.

Не делать stratified для дисбаланса. Минорный класс может пропасть из fold.

Делить пользователей между train и val. Group leakage. Использовать GroupKFold.

Считать препроцессинг до CV. StandardScaler.fit(X) перед cross_val_score → leakage статистики val в train. Используй Pipelinemake_pipeline(StandardScaler(), model).

Tuning на test. Несколько раз пере-тюнили под test → test перестал быть честным. Финальная оценка только на отложенном hold-out.

Не учитывать gap. В time-series модель из train видит target с лагом, в проде такого не будет.

Считать LOO лучшим CV. На больших данных дорого без выгод. Variance оценки выше, чем у k-fold с k=5/10.

Использовать AUC на k-fold с очень маленьким classroomом. Если в fold лишь 1 положительный объект — AUC нестабильна. Stratified помогает.

Не фиксировать random_state. Каждый запуск даёт разные результаты — невоспроизводимо.

Связанные темы

FAQ

Сколько фолдов выбрать?

Дефолт 5 или 10. На маленьких датасетах (< 1k) — больше фолдов. На больших (> 100k) — 3-5 хватает, экономит время.

Stratified для регрессии существует?

Да, через бинаризацию таргета: разбить y на квантили и стратифицировать по корзинам. Полезно при skewed target.

k-fold можно для time-series?

Только если задача — predict over different users at random time, и время не критично. В предсказании будущего на основе прошлого — никогда.

Что такое purged k-fold?

Расширение time-series CV для финансовых данных: между train и val вводится gap (purge) и embargo, чтобы предсказания не использовали данные «слишком близко». Marcos López de Prado, «Advances in Financial ML».

Почему CV метрика выше прода?

Возможные причины: leakage (group/time), distribution shift между train и prod, target shift, изменение бизнеса. Чек: попробовать adversarial validation, проверить временной leakage.

Это официальная информация?

Нет. Статья основана на документации scikit-learn, Hastie 2009, и стандартных подходах в ML.


Тренируйте Data Science — откройте тренажёр с 1500+ вопросами для собесов.