Как правильно создать отложенную (hold-out) тестовую выборку?

Ответ

Отложенная тестовая выборка (hold-out test set) — это часть данных, которая изолируется на самом начале и не используется ни для обучения, ни для настройки гиперпараметров. Её единственная цель — финальная оценка качества уже обученной модели. Стандартный инструмент — train_test_split из sklearn.model_selection.

Базовый пример:

from sklearn.model_selection import train_test_split

# Исходные данные и целевая переменная
X, y = load_data()

# Разделение: 80% на обучение/валидацию, 20% на финальный тест
X_train_val, X_test, y_train_val, y_test = train_test_split(
    X, y,
    test_size=0.2,
    random_state=42,  # Для воспроизводимости результата
    shuffle=True      # Перемешивать данные перед разбиением
)

Критически важные практики:

  1. Разделение до любых преобразований: Тестовая выборка должна имизировать "новые, невиданные" данные. Любая утечка информации из теста в train (например, через масштабирование) исказит оценку.

    # НЕПРАВИЛЬНО: Сначала масштабировать всю выборку, потом делить.
    # ПРАВИЛЬНО:
    X_train_val, X_test, y_train_val, y_test = train_test_split(X, y, test_size=0.2)
    scaler = StandardScaler().fit(X_train_val) # Обучаем scaler ТОЛЬКО на train
    X_train_val_scaled = scaler.transform(X_train_val)
    X_test_scaled = scaler.transform(X_test) # Трансформируем тест тем же scaler'ом
  2. Стратификация (для классификации): Если классы несбалансированы, используйте stratify=y. Это сохранит пропорции классов в train и test наборах.

    X_train, X_test, y_train, y_test = train_test_split(
        X, y,
        test_size=0.2,
        random_state=42,
        stratify=y  # Ключевой параметр для классификации
    )
  3. Для временных рядов: Простое случайное разбиение неприменимо, так как нарушает временной порядок. Тестовой выборкой должны быть последние наблюдения.

    split_index = int(len(X) * 0.8)
    X_train, X_test = X[:split_index], X[split_index:]
    y_train, y_test = y[:split_index], y[split_index:]

Итог: Отложенная выборка — это "священный грааль" для финальной проверки. Все этапы обучения и валидации (включая кросс-валидацию) должны происходить только на X_train_val/y_train_val. К X_test/y_test обращаемся один раз в самом конце.

Ответ 18+ 🔞

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

Представь себе: ты готовишься к экзамену. У тебя есть учебник (это твои тренировочные данные) и есть тайный список вопросов от преподова (это твоя тестовая выборка). Так вот, если ты заранее подсмотришь в этот список, то на экзамене ты, конечно, блеснёшь, но знаний-то нихуя не прибавится. И когда в реальной жизни тебе зададут похожий, но другой вопрос — ты охуеешь. Вот так же и с моделями.

Отложенная тестовая выборка — это как раз тот самый запечатанный конверт с вопросами. Ты его откладываешь в самый дальний ящик в начале проекта и не трогаешь, пока не закончишь всю свою возню с обучением и настройкой. Её единственная цель — дать тебе честный, непредвзятый ответ на вопрос: "Ну и насколько твоя модель, сука, реально умная?"

Стандартный инструмент для этого дела — train_test_split из sklearn. Выглядит просто, но тут, ёпта, дьявол в деталях.

Базовый пример, с него и начнём:

from sklearn.model_selection import train_test_split

# Допустим, у тебя уже есть данные и таргеты
X, y = load_data()

# Делим: 80% оставляем себе для всех экспериментов, 20% — священный тест
X_train_val, X_test, y_train_val, y_test = train_test_split(
    X, y,
    test_size=0.2,       # 20% в тест — классика жанра
    random_state=42,     # Чтобы при каждом запуске делилось одинаково, а не как бог на душу положит
    shuffle=True         # Перемешиваем данные перед делением, чтобы не было перекоса
)

Вот, вроде бы, всё просто. Но теперь слушай сюда, потому что большинство косяков происходит на следующем этапе.

Критически важные практики, или как не облажаться

  1. Разделение до любых преобразований — это закон! Твоя тестовая выборка должна быть как инопланетянин, который только что с корабля сошёл. Она не должна ничего знать о твоих тренировочных данных. Если ты сначала отмасштабируешь ВСЕ данные, а потом их поделишь — это пиздец, утечка информации. Ты уже дал тестовым данным знание о распределении всего датасета.

    # НЕПРАВИЛЬНО (так делать — себя не уважать):
    # scaler = StandardScaler().fit(X) # Обучаем на ВСЕХ данных
    # X_scaled = scaler.transform(X)   # Трансформируем ВСЁ
    # # А теперь делим... Поздно, брат. Ты уже всё испортил.
    
    # ПРАВИЛЬНО (делай так и будет тебе счастье):
    X_train_val, X_test, y_train_val, y_test = train_test_split(X, y, test_size=0.2)
    scaler = StandardScaler().fit(X_train_val) # Обучаем скалер ТОЛЬКО на тренировочной части!
    X_train_val_scaled = scaler.transform(X_train_val)
    X_test_scaled = scaler.transform(X_test) # А тест трансформируем тем же самым, уже обученным скалером. Никакого нового `.fit()`!
  2. Стратификация (особенно для классификации). Если у тебя в данных, например, 90% котов и 10% собак, и ты сделаешь просто случайное разбиение, то может так выйти, что в тест попадут одни коты. Модель, которую ты учил на котах и собаках, будет оцениваться только на котах — результат будет, мягко говоря, необъективный, доверия ебать ноль. Чтобы этого не было, используй stratify=y. Он сохранит пропорции классов.

    X_train, X_test, y_train, y_test = train_test_split(
        X, y,
        test_size=0.2,
        random_state=42,
        stratify=y  # Вот эта волшебная штука заставит разделиться честно
    )
  3. Для временных рядов — забудь про случайность. С временными рядами вообще отдельная история. Там нельзя просто взять и перемешать данные, потому что в них есть порядок. Тестом должны быть последние наблюдения, как будто ты предсказываешь будущее.

    split_index = int(len(X) * 0.8) # 80% на обучение, 20% на тест
    X_train, X_test = X[:split_index], X[split_index:]
    y_train, y_test = y[:split_index], y[split_index:]
    # Никакого shuffle, никакого random_state. Всё по порядку.

Итог, чтобы запомнить раз и навсегда: Отложенная выборка — это твой неприкосновенный запас, святой грааль финальной проверки. Всё, что ты делаешь: подбор гиперпараметров, кросс-валидацию, выбор фич — делай ТОЛЬКО на X_train_val и y_train_val. К X_test и y_test ты прикасаешься ОДИН РАЗ, в самом конце, когда модель уже полностью готова и ты хочешь узнать её реальную силу. Если будешь на ней что-то подбирать — считай, что обманул сам себя, и твоя оценка качества будет пиздопроебибной, то есть ни на что не годной.