The Jerusalem PostTrump has a chance to unite Saudi Arabia, UAE, and Israel against Iran and the Houthis - opinionDaily MaverickBUSINESS REFLECTION: Crossed Wires: AI smashes the foundations of the global kingdom of mathematicsESPN DeportesArsenal triunfa y sigue a la caza del líder CityPunchEvening recap: Tinubu’s 2027 alliance, Eaglets’ AFCON setback, Tenia Abraham’s death, other top storiesESPNTransfer rumors, news: Could Viktor Gyökeres leave Arsenal in January?RTP DesportoArsenal vence em Nottingham e continua na perseguição ao líder Manchester CityInquirerHope, faith, family support fan breast cancer patients’ path to recoveryBollywood HungamaTiger Shroff safe after firing incident reported under Juhu Police StationZDF heuteAktuelle Pressemitteilungen des ZDFPremium TimesTwo killed, two injured in Lagos road crash – LASTMAالشرقيوم الغضب.. فرنسا تواجه تداعيات أسوأ اضطرابات منذ سنواتWirtualna Polska"Co oni mają w głowach?". Macron nie wytrzymał
The Daily Newsstand · Free, Always
Sunday, October 18, 2026

Может ли плохая модель победить хорошую? Как Data Leakage обманывает метрики

Translate

Представим обычную ситуацию: мы обучили несколько моделей, сравнили их на тестовой выборке и получили довольно странный результат. Простейшее дерево решений показывает Accuracy почти 97%, а Random Forest из 300 деревьев — всего 91,5%.

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

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

Это один из самых неприятных сценариев в машинном обучении — Data Leakage, или утечка данных.

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

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

Что вообще считается утечкой данных?

Обычно процесс обучения ML-модели выглядит примерно так:

  1. Собираем датасет.

  2. Разделяем его на обучающую и тестовую выборки.

  3. Обучаем модель.

  4. Проверяем качество на тестовых данных.

  5. Если результаты устраивают, используем модель на новых объектах.

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

На практике этого недостаточно.

Data Leakage — ситуация, когда при обучении или оценке модели используется информация, которая не должна быть доступна в момент реального предсказания.

Например, мы хотим предсказывать, вернёт ли клиент кредит.

В датасете есть признаки:

  • Возраст клиента.

  • Ежемесячный доход.

  • Кредитная история.

  • Размер кредита.

  • Факт передачи задолженности коллекторам.

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

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

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

При этом никакой ошибки в scikit-learn не будет. Алгоритм честно использует все доступные признаки.

Проблема не в алгоритме, а в том, какие данные мы ему предоставили.

Эксперимент: дерево решений против Random Forest

Попробуем воспроизвести подобную ситуацию.

Возьмём искусственный датасет для бинарной классификации. У нас будет 6000 объектов и 20 обычных признаков.

Далее обучим две модели:

  • Модель A: Decision Tree с максимальной глубиной 1. По сути, один вопрос с ответом «да» или «нет».

  • Модель B: Random Forest из 300 деревьев, который использует полноценный набор признаков.

Но для первой модели добавим один специальный признак, содержащий информацию о целевой переменной.

При этом сделаем признак неидеальным: иногда он будет ошибаться. Это несколько ближе к реальным ситуациям, где утечка не обязательно гарантирует 100% точность.

Подготовка данных

Используем scikit-learn, numpy и pandas.

import numpy as np
import pandas as pd

from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score, roc_auc_score

X, y = make_classification(
    n_samples=6000,
    n_features=20,
    n_informative=10,
    n_redundant=5,
    weights=[0.5, 0.5],
    flip_y=0.04,
    class_sep=1.0,
    random_state=42
)

print(X.shape)

Получаем:

(6000, 20)

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

Таким образом, задача не должна быть совсем тривиальной, хотя Random Forest вполне способен показать хорошее качество.

Добавляем утечку

Предположим, что в нашей системе есть поле post_event_status.

Например, оно заполняется после того, как произошло событие, которое мы пытаемся предсказать.

Для эксперимента создадим такой признак на основе целевой переменной, а затем случайным образом изменим около 4% его значений.

rng = np.random.default_rng(42)

post_event_status = y.copy()

flip = rng.random(len(y)) < 0.04

post_event_status[flip] = 1 - post_event_status[flip]

print(
    "Совпадение с target:",
    np.mean(post_event_status == y)
)

Результат:

Совпадение с target: 0.9627

Получается, новый признак совпадает с правильным ответом примерно в 96,3% случаев.

Разумеется, в настоящем проекте так создавать признаки нельзя. Здесь мы делаем это специально, чтобы продемонстрировать последствия утечки.

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

Разделяем данные

Теперь создадим обучающую и тестовую выборки.

(
    X_train,
    X_test,
    y_train,
    y_test,
    leak_train,
    leak_test
) = train_test_split(
    X,
    y,
    post_event_status,
    test_size=0.3,
    stratify=y,
    random_state=42
)

Здесь всё выглядит вполне корректно.

Мы разделили объекты на train и test. Одни и те же строки не попадают в обе выборки.

Но обратите внимание: в тестовых данных у нас уже присутствует признак post_event_status, содержащий информацию о правильном ответе.

Именно поэтому обычный train_test_split не гарантирует отсутствие утечек.

Обучаем модели

Начнём с максимально простого дерева.

weak_model = DecisionTreeClassifier(
    max_depth=1,
    random_state=42
)

weak_model.fit(
    leak_train.reshape(-1, 1),
    y_train
)

Наша первая модель получает всего один признак. При этом он подозрительно хорошо связан с целевой переменной.

Теперь обучим Random Forest на 20 исходных признаках, без утечки.

strong_model = RandomForestClassifier(
    n_estimators=300,
    min_samples_leaf=3,
    random_state=42,
    n_jobs=-1
)

strong_model.fit(X_train, y_train)

У второго алгоритма заметно больше возможностей: сотни деревьев, нелинейные зависимости, комбинации признаков.

Посмотрим на результаты.

Сравнение качества

Для оценки используем Accuracy и ROC-AUC.

models = {
    "Decision Tree + Leakage": (
        weak_model,
        leak_test.reshape(-1, 1)
    ),
    "Random Forest": (
        strong_model,
        X_test
    )
}

results = []

for name, (model, features) in models.items():
    probabilities = model.predict_proba(features)[:, 1]
    predictions = (probabilities >= 0.5).astype(int)

    results.append({
        "Model": name,
        "Accuracy": accuracy_score(y_test, predictions),
        "ROC-AUC": roc_auc_score(y_test, probabilities)
    })

print(pd.DataFrame(results).round(4).to_string(index=False))

Я запустил этот эксперимент и получил следующие результаты:

Модель

Accuracy

ROC-AUC

Decision Tree + Leakage

0.9683

0.9683

Random Forest

0.9150

0.9632

Простейшее дерево победило Random Forest по обеим метрикам.

Причём разница в Accuracy составляет более пяти процентных пунктов.

Самое интересное, что эти цифры настоящие. Здесь не происходит никакой ошибки в вычислении метрик.

На данной тестовой выборке первая модель действительно предсказывает правильный класс чаще второй.

Но означает ли это, что она лучше? Нет.

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

Нужно также понимать, что в этом эксперименте мы специально сконструировали утечку. Результаты не доказывают превосходства Decision Tree над Random Forest в какой-либо реальной задаче.

Почему тестовая выборка нас не спасла?

Здесь находится, на мой взгляд, самая важная часть эксперимента.

Многие воспринимают тестовую выборку как универсальную защиту от переобучения и утечек.

Но test set защищает только от определённых проблем.

В нашем случае модель не обучалась на тестовых объектах. Мы не перемешивали метки между выборками и не передавали ей y_test напрямую во время обучения.

Тем не менее в признаках тестовых объектов оказалась информация, созданная на основе их правильных ответов.

В реальном приложении такой признак будет либо отсутствовать, либо появляться уже после принятия решения.

Поэтому качество 96,83% описывает совершенно другую задачу: предсказание с доступом к будущей информации.

Если убрать эту информацию при реальном использовании, наша модель останется без единственного признака, на котором обучалась.

То есть у неё просто не будет необходимых входных данных.

Интересно, что при этом ROC-AUC тоже оказался высоким. Иногда кажется, что использование более сложной метрики должно защитить от подобных ошибок.

Но метрики не знают, откуда появились признаки. Если модель умеет хорошо ранжировать объекты благодаря утечке, ROC-AUC будет это отражать.

Ни Accuracy, ни F1, ни ROC-AUC не способны самостоятельно определить, что в данных присутствует информация из будущего.

Не все утечки настолько очевидны

В эксперименте мы специально сделали утечку довольно грубой. В реальных проектах всё может быть намного менее заметно.

Рассмотрим несколько ситуаций, которые достаточно легко встретить на практике.

1. Нормализация данных до train/test split

Допустим, мы используем StandardScaler.

Вот распространённый вариант кода:

from sklearn.preprocessing import StandardScaler

scaler = StandardScaler()

X_scaled = scaler.fit_transform(X)

X_train, X_test, y_train, y_test = train_test_split(
    X_scaled,
    y,
    test_size=0.3,
    random_state=42
)

На первый взгляд всё логично: сначала привели признаки к одному масштабу, затем разделили данные.

Однако StandardScaler рассчитывает средние значения и стандартные отклонения по всему датасету.

В том числе по объектам, которые позже попадут в тестовую выборку.

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

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

Важно: такая ошибка далеко не всегда существенно увеличивает качество. На больших однородных датасетах разница может оказаться минимальной. Но методологически оценка всё равно становится некорректной.

Правильный вариант:

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

scaler = StandardScaler()

X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)

Здесь параметры масштабирования рассчитываются исключительно по обучающим данным.

Ещё удобнее использовать Pipeline:

from sklearn.pipeline import Pipeline
from sklearn.linear_model import LogisticRegression

model = Pipeline([
    ("scaler", StandardScaler()),
    ("classifier", LogisticRegression(max_iter=1000))
])

model.fit(X_train, y_train)

print(model.score(X_test, y_test))

Это особенно полезно при кросс-валидации: на каждом обучающем фолде предобработка будет подгоняться заново, без использования валидационного фолда.

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

2. Один пользователь в train и test

Предположим, мы разрабатываем модель, которая должна предсказывать поведение новых пользователей.

У нас есть таблица с их действиями. Один пользователь может встречаться в ней несколько десятков раз.

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

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

На тесте всё выглядит хорошо.

А затем приходят действительно новые пользователи, и качество падает.

Для такой задачи правильнее разделять данные по идентификаторам пользователей, например с помощью GroupShuffleSplit или GroupKFold.

Но здесь есть важная оговорка: если бизнес-задача предполагает предсказание будущих действий уже известных пользователей, присутствие одного пользователя в обеих выборках не обязательно является ошибкой.

Главное — чтобы способ валидации соответствовал реальному сценарию использования модели.

3. Случайное перемешивание временных данных

Ещё один распространённый пример — временные ряды.

Представим, что мы прогнозируем продажи товаров на следующую неделю.

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

Формально строки разные, но при прогнозировании в октябре информация из декабря ещё недоступна.

Поэтому для временных задач обычно требуется хронологическое разделение.

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

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

Скользящее среднее, лаги, агрегаты по пользователям и товарам — всё это тоже может стать источником утечки при неправильном расчёте.

4. Target Encoding до кросс-валидации

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

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

Особенно серьёзные проблемы могут возникать с редкими категориями.

Поэтому target encoding требует аккуратного разделения данных и подходящей схемы обучения кодировщика. Для обучающей выборки нередко используют out-of-fold encoding или специальные реализации с защитой от утечки.

Обычного принципа «один раз закодировал всю таблицу — потом разбил на train/test» здесь недостаточно.

Почему иногда более сложная модель страдает от утечки сильнее?

Вернёмся к нашему эксперименту.

Мы использовали дерево глубиной всего 1, которому было достаточно одного подозрительного признака.

А теперь представим датасет, в котором есть десятки потенциальных источников утечки: время закрытия заявки, итоговый статус операции, результаты последующих проверок.

Сложный алгоритм может обнаружить множество неочевидных связей между этими полями и целевой переменной.

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

Поэтому рост сложности модели сам по себе не защищает от Data Leakage.

Иногда он даже помогает лучше эксплуатировать утечки.

Именно поэтому подозрительно высокое качество — повод не только порадоваться, но и внимательнее посмотреть на данные.

Если простая baseline-модель внезапно показывает почти идеальные результаты, я бы в первую очередь проверил признаки и процедуру валидации, а уже потом занимался подбором гиперпараметров.

Как проверять модель на утечки?

Универсального метода, который автоматически найдёт любой Data Leakage, нет. Но несколько проверок способны существенно снизить риск.

Первое — определить момент предсказания.

Это, пожалуй, важнее всего остального.

Нужно ответить на вопрос: в какой конкретно момент времени модель должна выдавать результат?

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

Второе — разделять данные до обучения предобработки.

Масштабирование, заполнение пропусков, отбор признаков и другие операции с fit обычно должны обучаться только на соответствующей обучающей части данных.

Для этого удобно использовать Pipeline.

Третье — выбирать правильную стратегию валидации.

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

Для временных данных — хронологическая валидация.

Для данных с группами связанных объектов — групповая.

Одна и та же стратегия не подходит абсолютно для всех задач.

Четвёртое — проверять подозрительно сильные признаки.

Если один столбец обеспечивает почти идеальное качество, полезно понять, откуда он появился.

Иногда это действительно хороший признак. Иногда — завуалированный правильный ответ.

Пятое — проверять качество на данных, максимально похожих на реальные.

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

Небольшой чек-лист перед обучением

Перед тем как доверять результатам ML-эксперимента, полезно задать себе несколько вопросов:

  • Не используются ли признаки, которые появляются после прогнозируемого события?

  • Не рассчитывалась ли предобработка по всему датасету до разделения?

  • Не попали ли связанные объекты одновременно в train и test?

  • Соответствует ли способ валидации реальному сценарию?

  • Нет ли признаков, которые практически напрямую кодируют target?

  • Не участвовали ли тестовые метки в выборе признаков или гиперпараметров?

  • Будут ли все использованные признаки доступны при реальном предсказании?

Последний вопрос особенно важен. На практике именно он может выявить проблему, которую невозможно заметить по одним только метрикам.

Итоги

В нашем эксперименте дерево решений глубиной 1 показало Accuracy 96,83%, а Random Forest из 300 деревьев — 91,50%.

Если смотреть исключительно на метрики, победило простое дерево.

Но оно получило признак, созданный на основе целевой переменной. Random Forest же решал задачу по обычным исходным данным.

Поэтому сравнивать их качество как показатель пригодности к реальному использованию нельзя.

Из этого эксперимента можно сделать несколько выводов.

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

Во-вторых, наличие отдельной тестовой выборки ещё не означает, что оценка получилась честной.

И наконец, самое важное: правильная организация данных и валидации зачастую важнее выбора конкретного ML-алгоритма.

Можно потратить часы на подбор гиперпараметров, обучить сложный ансамбль и получить впечатляющий ROC-AUC. Но если в признаках присутствует информация из будущего, все эти усилия могут оказаться бесполезными.

Мне кажется, именно поэтому при работе с новой ML-задачей стоит начинать не с поиска самой мощной модели, а с двух простых вопросов: что мы пытаемся предсказать и какая информация действительно будет нам доступна?

А уже потом сравнивать алгоритмы.

Полезные материалы:

Весь эксперимент выше воспроизводится на Python с использованием NumPy, pandas и scikit-learn. В генерации данных и обучении моделей зафиксирован random_state=42.

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.