5 методов разделения данных для точных моделей машинного обучения
Для кого эта статья:
- Начинающие и опытные дата-сайентисты, желающие улучшить свои знания о машинном обучении
- Студенты и слушатели курсов по аналитике данных и машинному обучению
Практикующие специалисты в области технологий и анализа данных, интересующиеся оптимизацией моделей
Создать модель машинного обучения, которая работает только в тестовой среде, но проваливается на реальных данных — классическая ошибка новичков. Правильное разделение данных — фундамент надёжных моделей. Представьте, что вы разработали алгоритм кредитного скоринга, но обучили его только на клиентах с высоким доходом. При запуске в продакшн система будет отклонять 90% заёмщиков без очевидной причины! Ваша карьера data scientist'а может закончиться, не начавшись. Давайте разберём 5 методов разделения данных, которые помогут избежать таких катастроф. 🔍
Строите первую модель машинного обучения и не знаете, как правильно подготовить данные? На курсе Профессия аналитик данных от Skypro вы освоите все этапы аналитического процесса — от сбора и очистки данных до создания предиктивных моделей. Вместо абстрактной теории вы получите практические навыки работы с реальными датасетами и узнаете, как грамотно разделять данные для точного тестирования моделей.
Зачем разделять данные на выборки при машинном обучении
Разделение данных на обучающую и тестовую выборки — не просто формальность, а критически важный этап построения модели машинного обучения. Представьте, что вы учитель, который дал ученикам учебник с ответами и затем проверяет их знания по тем же самым заданиям. Результат будет превосходным, но абсолютно бесполезным для оценки реальных знаний. То же самое происходит с моделью, которую тестируют на тех же данных, на которых обучали.
Существуют три основные причины, почему разделение данных необходимо:
- Предотвращение переобучения (overfitting) — когда модель чрезмерно адаптируется к обучающим данным, запоминая их особенности вместо выявления общих закономерностей
- Честная оценка производительности — только на новых данных можно объективно оценить, насколько хорошо модель будет работать в реальных условиях
- Возможность сравнения разных моделей — используя одинаковое разделение, можно корректно сравнивать разные алгоритмы
Ключевой принцип разделения: тестовые данные должны имитировать реальные условия, в которых будет работать модель. Эти данные должны быть недоступны модели во время обучения, но достаточно репрезентативны, чтобы отражать все возможные сценарии использования.
Александр Петров, Lead Data Scientist
Однажды мой коллега представил руководству систему рекомендаций товаров с впечатляющей точностью 98%. Через месяц после запуска конверсия упала на 15%, а не выросла, как ожидалось. Расследование показало, что он тестировал модель на исторических данных с теми же паттернами, что и в обучающей выборке. Система прекрасно рекомендовала товары прошлого сезона, но не смогла адаптироваться к новым трендам. Мы перестроили модель с правильным временным разделением данных — последние 2 месяца использовали как тестовый набор. Точность на тесте упала до 76%, но в продакшене система показала стабильный рост конверсии на 9%. Это наглядно доказало: лучше иметь честную оценку работы модели, чем иллюзию превосходной точности.
Стандартное соотношение обучающей и тестовой выборки — 80:20 или 70:30, но эти пропорции могут меняться в зависимости от объема данных и специфики задачи. При очень больших датасетах достаточно выделить для теста 10% или даже 5% данных. 📊
| Размер датасета | Рекомендуемое соотношение (обучение:тест) | Примечания |
|---|---|---|
| Малый (< 1000 образцов) | 60:40 | Необходим баланс между объемом обучения и надежностью оценки |
| Средний (1000-10000) | 70:30 | Классическое соотношение, работающее в большинстве случаев |
| Большой (10000-100000) | 80:20 | Можно выделить больше данных на обучение |
| Очень большой (> 100000) | 90:10 или 95:5 | Тестовая выборка все равно будет достаточного размера |

Простое случайное разделение данных с train
Самый распространенный и простой подход к разделению данных — случайная выборка с использованием функции train_test_split из библиотеки scikit-learn. Этот метод случайным образом распределяет записи между обучающим и тестовым наборами в заданной пропорции, что делает его универсальным решением для большинства задач машинного обучения. 🎲
Принцип работы метода прост: каждая запись в исходном наборе данных имеет равную вероятность попасть в тестовую выборку. Например, при соотношении 80:20 вероятность для каждой записи составляет 20%.
Основные преимущества простого случайного разделения:
- Простота реализации — всего одна строка кода на Python
- Универсальность — работает с большинством типов данных и задач
- Сохранение случайности — минимизирует риск систематического смещения при оценке модели
Вот пример кода для реализации этого метода:
from sklearn.model_selection import train_test_split
# Предположим, X содержит признаки, а y – целевые значения
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
Параметр random_state особенно важен — он обеспечивает воспроизводимость результатов. При установке определенного значения (например, 42) вы получите одинаковое разделение при каждом запуске кода, что критично для сравнения различных моделей и отладки.
Однако у простого случайного разделения есть существенные ограничения:
- Не учитывает распределение классов — в задачах классификации с несбалансированными классами это может привести к искажению результатов
- Игнорирует временную структуру — для временных рядов такой подход некорректен, так как нарушает хронологический порядок
- Не справляется с группированными данными — например, если у вас есть повторяющиеся измерения для одних и тех же объектов
Несмотря на эти ограничения, для однородных данных и сбалансированных задач классификации random split остается золотым стандартом благодаря своей простоте и эффективности.
Стратифицированная выборка: сохраняем распределение классов
Представьте, что вы разрабатываете модель для диагностики редкого заболевания, которым страдает только 5% пациентов в вашем датасете. При обычном случайном разделении существует риск, что в тестовую выборку попадет непропорционально мало или много случаев заболевания, что исказит оценку модели. Стратифицированная выборка решает эту проблему, сохраняя распределение целевой переменной одинаковым во всех частях разделенного набора данных. 📊
Стратифицированное разделение особенно важно в следующих случаях:
- Несбалансированные классы — когда один класс значительно преобладает над другими
- Малые выборки — где случайные колебания могут сильнее влиять на распределение
- Мультиклассовая классификация — для сохранения пропорций всех классов
Реализация в scikit-learn практически не отличается от обычного разделения — достаточно добавить параметр stratify:
from sklearn.model_selection import train_test_split
# y содержит метки классов
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
Мария Соколова, Data Science консультант
Столкнулась с классическим случаем, когда правильное разделение данных буквально спасло проект. Работала с финтех-стартапом, разрабатывающим систему обнаружения мошенничества. В датасете было всего 3% мошеннических транзакций — типичный несбалансированный случай. Разработчик, который начинал проект, использовал обычное случайное разделение, и модель показывала точность 97%, что выглядело впечатляюще. Однако при детальном анализе оказалось, что эта "высокая точность" была результатом того, что модель просто классифицировала все транзакции как легитимные!
После применения стратифицированной выборки картина кардинально изменилась. Тестовый набор теперь содержал те же 3% мошеннических транзакций, что и исходные данные. Мы смогли корректно измерить чувствительность модели (recall) к мошенническим операциям, которая оказалась близкой к нулю. Это привело к полной переработке подхода: мы внедрили техники балансировки классов и изменили метрики оценки с accuracy на F1-score и AUC-ROC. В итоге новая модель стала обнаруживать 82% мошеннических операций при приемлемом уровне ложных срабатываний, что принесло компании экономию в несколько миллионов рублей.
Стратифицированная выборка может применяться не только для задач классификации. В регрессионных задачах можно дискретизировать непрерывный целевой признак на несколько категорий (например, низкий/средний/высокий доход) и использовать эти категории для стратификации:
# Для регрессионной задачи
import pandas as pd
import numpy as np
# Создаем категории из непрерывного целевого признака
y_binned = pd.qcut(y, q=5, labels=False) # Разделение на 5 равных групп
# Используем созданные категории для стратификации
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y_binned
)
| Метод разделения | Преимущества | Недостатки | Когда применять |
|---|---|---|---|
| Случайное разделение | Простота, универсальность | Не сохраняет распределение классов | Сбалансированные данные, большие выборки |
| Стратифицированная выборка | Сохраняет распределение целевой переменной | Сложнее реализовать для многомерной стратификации | Несбалансированные классы, малые выборки |
| Многомерная стратификация | Учитывает несколько факторов одновременно | Требует дополнительных библиотек, сложнее в реализации | Когда важно сохранить распределение нескольких переменных |
Стоит отметить, что для сложных случаев, когда требуется сохранить распределение нескольких переменных одновременно, можно использовать специализированные библиотеки, такие как iterative-stratification или scikit-multilearn. 🧩
Временное разделение для последовательных и временных рядов
Для данных с временной структурой — будь то прогнозирование продаж, анализ биржевых котировок или предсказание погоды — случайное разделение данных становится категорически неверным подходом. Причина очевидна: информация из будущего не должна использоваться для предсказаний в прошлом. 📅
Временное разделение данных соблюдает хронологический порядок, обеспечивая, что модель обучается на исторических данных и тестируется на более поздних периодах. Этот подход имитирует реальное применение модели, когда мы используем известную историю для прогнозирования неизвестного будущего.
Основные стратегии временного разделения:
- Простое разделение по дате — определяется граничная дата, все данные до неё используются для обучения, после — для тестирования
- Скользящее окно (rolling window) — модель обучается на фиксированном интервале времени и тестируется на следующем периоде, затем окно "сдвигается" вперед
- Расширяющееся окно (expanding window) — начальный тренировочный период постепенно расширяется, включая новые данные, а тестирование всегда проводится на следующем периоде
Пример реализации простого временного разделения:
import pandas as pd
# Предположим, df это DataFrame с колонкой 'date'
df = df.sort_values('date') # Сортируем по дате
# Определяем точку разделения
split_date = '2022-01-01'
# Разделяем данные
train = df[df['date'] < split_date]
test = df[df['date'] >= split_date]
X_train, y_train = train.drop('target', axis=1), train['target']
X_test, y_test = test.drop('target', axis=1), test['target']
Для более сложных схем разделения, таких как скользящее окно, можно использовать специализированные функции из scikit-learn:
from sklearn.model_selection import TimeSeriesSplit
tscv = TimeSeriesSplit(n_splits=5)
for train_index, test_index in tscv.split(X):
X_train, X_test = X[train_index], X[test_index]
y_train, y_test = y[train_index], y[test_index]
# Обучение и оценка модели для текущего сплита
Особенности, которые следует учитывать при временном разделении:
- Сезонность данных — обучающая выборка должна содержать полные циклы сезонности (например, все месяцы года для данных с годовой сезонностью)
- Изменение распределения со временем (drift) — более старые данные могут быть менее релевантны для текущих прогнозов
- Размер тестового периода — должен соответствовать горизонту прогнозирования, с которым модель будет работать в реальных условиях
Важно помнить, что при временном разделении данных тестовая выборка часто не является случайной подвыборкой генеральной совокупности, а представляет конкретный временной период со своими особенностями. Это может привести к тому, что производительность модели на тестовом наборе будет существенно отличаться от её реальной производительности в будущем. ⏳
Кросс-валидация: комплексная оценка модели на разных срезах
Простое разделение данных на обучающую и тестовую выборки может приводить к высокой вариативности оценки качества модели, особенно при ограниченном объеме данных. Кросс-валидация решает эту проблему, предоставляя более надежную оценку производительности модели, используя многократное разделение данных и усреднение результатов. 🔄
Принцип кросс-валидации заключается в разделении данных на k непересекающихся блоков (folds). Модель обучается k раз, каждый раз используя k-1 блоков для обучения и оставшийся блок для проверки. Итоговая оценка модели рассчитывается как среднее по всем k итерациям.
Наиболее распространенные виды кросс-валидации:
- k-fold cross-validation — данные разделяются на k равных частей, каждая из которых по очереди выступает в роли тестовой выборки
- Stratified k-fold — то же, что и k-fold, но с сохранением распределения классов в каждом fold
- Leave-one-out (LOO) — экстремальный случай, когда k равно числу образцов; для каждой итерации только один образец используется для тестирования
- Group k-fold — учитывает группировку данных, обеспечивая, что образцы из одной группы не попадают одновременно в обучающую и тестовую выборки
Реализация стандартной 5-fold кросс-валидации с помощью scikit-learn:
from sklearn.model_selection import cross_val_score
from sklearn.linear_model import LogisticRegression
# Создаем модель
model = LogisticRegression()
# Выполняем 5-fold кросс-валидацию
scores = cross_val_score(model, X, y, cv=5)
# Выводим результаты
print(f"Scores for each fold: {scores}")
print(f"Mean accuracy: {scores.mean():.3f} ± {scores.std():.3f}")
Для стратифицированной кросс-валидации используется аналогичный подход:
from sklearn.model_selection import StratifiedKFold
# Создаем стратифицированный разделитель
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
# Выполняем кросс-валидацию
scores = cross_val_score(model, X, y, cv=skf)
Кросс-валидация предоставляет ряд существенных преимуществ:
- Более надежная оценка производительности модели
- Снижение риска переобучения при выборе гиперпараметров
- Эффективное использование ограниченных данных
- Возможность оценки вариативности результатов
Однако есть и недостатки:
- Повышенные вычислительные затраты (модель обучается k раз)
- Неприменимость стандартной кросс-валидации для временных рядов (существуют специальные модификации)
- Сложность интерпретации результатов при высокой вариативности между фолдами
Для временных рядов используются специализированные варианты кросс-валидации:
from sklearn.model_selection import TimeSeriesSplit
# Создаем разделитель для временных рядов
tscv = TimeSeriesSplit(n_splits=5)
# Выполняем кросс-валидацию
scores = cross_val_score(model, X, y, cv=tscv)
Выбор оптимального количества фолдов зависит от размера датасета и вычислительных ресурсов. Типичные значения — от 5 до 10. При маленьких выборках может применяться leave-one-out кросс-валидация, а при очень больших наборах данных иногда достаточно 3 фолдов. 📈
Практическая реализация методов разделения в Python
Теория хороша, но для ежедневной работы data scientist нужны конкретные инструменты. Рассмотрим комплексный пример, который демонстрирует различные методы разделения данных на одном датасете и сравнение их влияния на производительность модели. 🐍
Для начала подготовим данные и импортируем необходимые библиотеки:
import numpy as np
import pandas as pd
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split, StratifiedKFold, KFold, TimeSeriesSplit, cross_val_score
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score, f1_score
# Загрузим данные о раке груди
data = load_breast_cancer()
X = data.data
y = data.target
# Добавим временную составляющую для демонстрации временного разделения
# (это искусственный пример)
np.random.seed(42)
timestamps = pd.date_range(start='2020-01-01', periods=len(X), freq='D')
df = pd.DataFrame(X, columns=data.feature_names)
df['target'] = y
df['timestamp'] = timestamps
Теперь применим различные методы разделения и оценим их влияние:
# 1. Простое случайное разделение
X_train_simple, X_test_simple, y_train_simple, y_test_simple = train_test_split(
X, y, test_size=0.2, random_state=42
)
# Обучаем модель
model_simple = RandomForestClassifier(random_state=42)
model_simple.fit(X_train_simple, y_train_simple)
y_pred_simple = model_simple.predict(X_test_simple)
accuracy_simple = accuracy_score(y_test_simple, y_pred_simple)
# 2. Стратифицированное разделение
X_train_strat, X_test_strat, y_train_strat, y_test_strat = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
model_strat = RandomForestClassifier(random_state=42)
model_strat.fit(X_train_strat, y_train_strat)
y_pred_strat = model_strat.predict(X_test_strat)
accuracy_strat = accuracy_score(y_test_strat, y_pred_strat)
# 3. Временное разделение
df_sorted = df.sort_values('timestamp')
split_point = int(0.8 * len(df_sorted))
train_temporal = df_sorted.iloc[:split_point]
test_temporal = df_sorted.iloc[split_point:]
X_train_temporal = train_temporal.drop(['target', 'timestamp'], axis=1).values
y_train_temporal = train_temporal['target'].values
X_test_temporal = test_temporal.drop(['target', 'timestamp'], axis=1).values
y_test_temporal = test_temporal['target'].values
model_temporal = RandomForestClassifier(random_state=42)
model_temporal.fit(X_train_temporal, y_train_temporal)
y_pred_temporal = model_temporal.predict(X_test_temporal)
accuracy_temporal = accuracy_score(y_test_temporal, y_pred_temporal)
# 4. K-fold кросс-валидация
model_kfold = RandomForestClassifier(random_state=42)
kf = KFold(n_splits=5, shuffle=True, random_state=42)
scores_kfold = cross_val_score(model_kfold, X, y, cv=kf)
# 5. Стратифицированная кросс-валидация
model_strat_kfold = RandomForestClassifier(random_state=42)
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
scores_strat_kfold = cross_val_score(model_strat_kfold, X, y, cv=skf)
Сравним результаты разных методов:
print(f"Простое разделение, точность: {accuracy_simple:.4f}")
print(f"Стратифицированное разделение, точность: {accuracy_strat:.4f}")
print(f"Временное разделение, точность: {accuracy_temporal:.4f}")
print(f"K-fold кросс-валидация, средняя точность: {scores_kfold.mean():.4f} ± {scores_kfold.std():.4f}")
print(f"Стратифицированная кросс-валидация, средняя точность: {scores_strat_kfold.mean():.4f} ± {scores_strat_kfold.std():.4f}")
Выбор оптимального метода разделения данных зависит от многих факторов. Вот практические рекомендации:
- Для стандартных задач с достаточным объемом данных — используйте стратифицированное разделение (
stratify=y) - Для временных рядов — всегда применяйте временное разделение или
TimeSeriesSplit - При настройке гиперпараметров — используйте кросс-валидацию в сочетании с
GridSearchCVилиRandomizedSearchCV - Для оценки финальной модели — выделите отдельную часть данных (holdout set), которая не использовалась при разработке
- При несбалансированных классах — предпочтительна стратифицированная выборка или
StratifiedKFold
Независимо от выбранного метода, всегда устанавливайте random_state для воспроизводимости результатов и более корректного сравнения разных моделей. 🔐
Применение правильного метода разделения данных существенно повышает надежность моделей машинного обучения и точность оценки их производительности в реальных условиях. Это один из тех фундаментальных навыков, которые отличают опытного data scientist от новичка.
Разделение данных на выборки – не просто технический этап подготовки модели, а стратегическое решение, определяющее надёжность всех ваших прогнозов. Помните: не существует универсального метода, который подойдёт для всех задач. Подбирайте технику разделения, исходя из природы данных и специфики вашей задачи. Применяя правильный подход к разделению, вы гарантируете, что ваша модель будет работать так же хорошо в боевых условиях, как и в лаборатории. В конечном счёте, профессиональный аналитик данных отличается не только умением создавать сложные модели, но и способностью корректно оценивать их надёжность.