- BrainTools - https://www.braintools.ru -
Сегодня рассмотрим невинный на первый взгляд параметр shuffle=True в train_test_split.
Под «перемешать» подразумевается применение псевдо-рандомного пермутационного алгоритма (обычно Fisher–Yates) к индексам выборки до того, как мы режем её на train/test. Цель — заставить train-и-test быть независимыми и одинаково распределёнными (i.i.d.). В scikit-learn эта логика [1] зашита в параметр shuffle почти всех сплиттеров. В train_test_split он True по умолчанию, что прямо сказано в документации — «shuffle bool, default=True».
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(
features,
target,
test_size=0.2,
random_state=42, # надо для репликабельности
shuffle=True # смело тасуем
)
Когда shuffle=True, функция:
Генерирует случайную перестановку индексов (учитывая random_state).
По ней делит данные.
Если присвоить shuffle=False, она просто берет “с головы” train_size строк, а “хвост” — test. Расклад очевиден из исходников и подтверждён в официальном доке.
Time-series — классика, где порядок — закон. Если мы перемешаем, то модель увидит будущее раньше прошлого и станет гадалкой. С точки зрения [2] статистики это “look-ahead bias”. На том же Cross-Validated прямым текстом: в time-series нужно держать хронологию и юзать TimeSeriesSplit.
Клинические данные, где несколько записей на одного пациента; логи пользователей, где одна сессия раскидана на десятки строк. Если рандомно раскидать строки по split’ам, то в test попадут “следы” тех же юзеров, что и в train, а это утечка через id-коррелированные признаки.
Тут групповая целостность — must-have. Мы шейкаем между группами, но не внутри.
Look-ahead bias — когда модель учится на будущей информации.
Target leakage — признак сформирован на основе целевой переменной или будущих значений.
Temporal leakage — метки пакуются по календарю: например, is_holiday. Если их перетасовать, тест узнает праздники раньше времени.
leakage — в целом сам по себе самый популярный баг ML-систем. Утечка часто выглядит невинно: добавили total_sales_next_month как фичу для модели, предсказывающей спрос — и получили 99 % R².
from sklearn.model_selection import GroupShuffleSplit
gss = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42)
train_idx, test_idx = next(gss.split(X, y, groups=user_id))
X_train, X_test = X.iloc[train_idx], X.iloc[test_idx]
y_train, y_test = y.iloc[train_idx], y.iloc[test_idx]
GroupShuffleSplit гарантирует, что у каждого user_id ровно один сплит: либо train, либо test. Под капотом он рандомно тасует сами группы, а не записи.
from sklearn.model_selection import TimeSeriesSplit
tscv = TimeSeriesSplit(n_splits=5, test_size=24*7) # неделя в часах
for fold, (train_idx, test_idx) in enumerate(tscv.split(X)):
model.fit(X.iloc[train_idx], y.iloc[train_idx])
y_pred = model.predict(X.iloc[test_idx])
...
Сплиттер отдаёт растающий train и скользящее окно test. Шейка нет вовсе — порядок священен.
У нас зоомаркет Purrfect Shop. В базе лежат четыре ключевых таблицы:
|
Таблица |
Что внутри |
Гранулярность |
|---|---|---|
|
|
|
1 строка на владельца |
|
|
|
1 строка на котика |
|
|
|
1 строка на чек |
|
|
|
1 строка на позицию в чеке |
Мы хотим решить две задачи:
Churn-классификация: предсказать, уйдёт ли клиент в течение 30 дней.
Прогноз оборота на следующие 7 дней из тайм-серии.
Плюс обучаем CNN, которая по фото угадывает породу для автозаполнения карточек.
Наивный, но опасный подход:
from sklearn.model_selection import train_test_split
X = features_df # собрали признаки на уровне *клиента*
y = labels_df['will_churn']
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.25, shuffle=True, random_state=42
)
Каждая строка — клиент, а значит зависимостей во времени нет. Shuffle здесь уместен: классы «уйдет/останется» раскиданы равномерно — модель не видит паттерна “первые 75 % клиентов — новые, последние — старые”.
Мы решаем “чуть улучшить” датасет и переходим на строки-чек. Один клиент = десятки чеков:
orders_df['will_churn'] = ...
X = orders_df.drop('will_churn', axis=1)
y = orders_df['will_churn']
# те же 5 строк кода:
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.25, shuffle=True, random_state=42
)
Теперь половина чеков Петя-Котолюб попала в train, половина — в test.
Утечка: признаки, вроде «total_sum_last_3_orders», пересекаются. Результат — AUC = 0.97 на тесте, но в проде падаем до 0.68 и ловим.
Правильно: GroupShuffleSplit
from sklearn.model_selection import GroupShuffleSplit
gss = GroupShuffleSplit(test_size=0.25, n_splits=1, random_state=42)
train_idx, test_idx = next(gss.split(X, y, groups=orders_df['customer_id']))
X_train, X_test = X.iloc[train_idx], X.iloc[test_idx]
y_train, y_test = y.iloc[train_idx], y.iloc[test_idx]
Теперь каждый клиент живёт только в одном сплите. AUC честно падает до 0.79 — зато в проде всё стабильно.
Как ломается time series:
from sklearn.model_selection import train_test_split
# aggregated_df: daily revenue, lag-features, holidays, etc.
X = aggregated_df.drop('revenue', axis=1)
y = aggregated_df['revenue']
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, shuffle=True, random_state=42
)
Модель случайно видит 2025-06-01 в train, а 2025-05-20 в test.
Используем TimeSeriesSplit:
from sklearn.model_selection import TimeSeriesSplit
import lightgbm as lgb
import numpy as np
tscv = TimeSeriesSplit(n_splits=5, test_size=7)
scores = []
for fold, (tr, val) in enumerate(tscv.split(X)):
model = lgb.LGBMRegressor(n_estimators=500, learning_rate=0.03)
model.fit(X.iloc[tr], y.iloc[tr])
preds = model.predict(X.iloc[val])
rmse = np.sqrt(((preds - y.iloc[val])**2).mean())
scores.append(rmse)
print(f'Fold {fold}: RMSE={rmse:.2f}')
print(f'Mean CV RMSE: {np.mean(scores):.2f}')
Без shuffle, с растущим окном.
Когда обучаем сверточку, порядок картинок не важен; наоборот, shuffle помогает стохастическому градиенту быстрее и стабильнее сходиться.
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
train_ds = datasets.ImageFolder(
root='cats/train',
transform=transforms.Compose([
transforms.Resize((224, 224)),
transforms.RandomHorizontalFlip(),
transforms.ToTensor()
])
)
train_loader = DataLoader(
train_ds,
batch_size=32,
shuffle=True, # критично!
num_workers=4,
pin_memory=True
)
val_loader = DataLoader(
val_ds,
batch_size=32,
shuffle=False, # чтоб метрики не «плясали»
num_workers=4
)
На примере нашего котомаркета видим три сценария:
Табличные i.i.d. данные — мешаем, чтобы избежать систематической ошибки [3].
Группы / время — бережём порядок, потому что утечка дороже.
Vision / NLP — мешаем внутри эпохи, но держим валидацию детерминированной.
Если сомневаетесь — не шейкайте.
|
Тип данных / задача |
Шейкать? |
Сплиттер |
Комментарий |
|---|---|---|---|
|
Классический табличный ML |
Да |
|
Базовая практика, избегаем order-bias. |
|
Изображения/тексты без явных групп |
Да |
|
Сохраняем баланс классов. |
|
Логи пользователей, несколько строк на ID |
Нет |
|
Целостность группы важнее. |
|
Временные ряды (прогноз спроса, финансы) |
Нет |
|
Хронология важна. |
|
Подготовка hold-out для AB-теста |
Нет |
|
Сессии не должны пересекаться. |
|
Kaggle с фейк-ID, но явной утечки нет |
Скорее да |
|
Читайте описание соревнования. |
|
Tiny-датасет ≤ 100 строк |
Да, но фиксируйте seed |
Любой |
Варьируйте до 10-кратного CV для стабильности. |
Если чувствуете запах [4] временной или групповой зависимости — уберите руку от shuffle=True и достаньте правильный сплиттер.
Готовите данные для моделей машинного обучения [5]? Тогда знаете: неправильный сплит — и модель учит будущее, «подглядывает» в тест и в итоге проваливается в проде.
Если вам близки такие темы, как предотвращение утечек, GroupShuffle, TimeSeriesSplit, честные A/B‑тесты и грамотная работа с временными рядами — в Otus пройдут скоро открытые уроки, которые рекомендуем посетить:
3 июля в 18:00 — «Как правильно готовить данные для ML‑моделей?» [6]
Разберётесь, какие ошибки подготовки мешают вашим моделям быть стабильными — и как этого избежать.
16 июля в 18:00 — «Random Forest — мощный метод ансамблирования в ML» [7]
На примерах разберётесь, почему даже самые сильные алгоритмы не спасут, если в train утекает test.
Хотите больше? Загляните в каталог курсов [8] — там есть всё: от ML‑специализации до продвинутого Python.
А чтобы ничего не пропустить, добавьте календарь открытых уроков [9] — пусть он напомнит вам, когда стоит подключиться к трансляции.
Автор: badcasedaily1
Источник [10]
Сайт-источник BrainTools: https://www.braintools.ru
Путь до страницы источника: https://www.braintools.ru/article/16832
URLs in this post:
[1] логика: http://www.braintools.ru/article/7640
[2] зрения: http://www.braintools.ru/article/6238
[3] ошибки: http://www.braintools.ru/article/4192
[4] запах: http://www.braintools.ru/article/9870
[5] обучения: http://www.braintools.ru/article/5125
[6] «Как правильно готовить данные для ML‑моделей?»: https://otus.pw/Byqr/
[7] «Random Forest — мощный метод ансамблирования в ML»: https://otus.pw/4xUT/
[8] каталог курсов: https://otus.pw/ztYL/
[9] календарь открытых уроков: https://otus.pw/BK4nX/
[10] Источник: https://habr.com/ru/companies/otus/articles/921526/?utm_campaign=921526&utm_source=habrahabr&utm_medium=rss
Нажмите здесь для печати.