Разделение DataFrame на выборки: ключевые методы для ML-моделей

Пройдите тест, узнайте какой профессии подходите
Сколько вам лет
0%
До 18
От 18 до 24
От 25 до 34
От 35 до 44
От 45 до 49
От 50 до 54
Больше 55

Для кого эта статья:

  • Начинающие дата-сайентисты и аналитики данных
  • Профессионалы, желающие углубить свои знания в области машинного обучения
  • Студенты, обучающиеся по специальностям, связанным с аналитикой данных и программированием

    Разделение DataFrame на обучающую и тестовую выборки – критический момент, отделяющий любителя от профессионала в машинном обучении. Неправильный сплит данных может привести к катастрофическим последствиям: переобучению, смещенным оценкам точности и краху проекта. По данным исследования Stack Overflow, более 63% начинающих дата-сайентистов ошибаются именно на этапе разделения данных. Правильное разделение DataFrame в pandas – это не просто техническая операция, это краеугольный камень вашего ML-пайплайна. Давайте разберемся, как делать это профессионально и избежать распространенных ловушек 🔍.

Хотите стать экспертом в анализе данных и машинном обучении? В Профессии аналитик данных от Skypro вы освоите не только фундаментальные принципы pandas и scikit-learn, но и продвинутые техники разделения данных для реальных проектов. Наши студенты учатся создавать робастные модели машинного обучения с первого же курса, а выпускники успешно проходят технические интервью в топовых компаниях. Вместо теоретизирования – практические навыки, которые можно применить немедленно!

Зачем нужно разделение данных на выборки в pandas

Разделение данных на тренировочную и тестовую выборки – фундаментальный принцип машинного обучения, который необходим для объективной оценки эффективности модели. Это не просто формальность, а ключевой механизм защиты от переобучения (overfitting).

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

Александр Соколов, Lead Data Scientist

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

Результаты были впечатляющими: 95% точности на тестовых данных! Но когда модель запустили на реальных данных, точность едва достигала 65%. Причина? Утечка данных (data leakage). Нормализуя весь датасет перед разделением, я неявно передавал информацию из тестовой выборки в обучающую.

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

Основные причины, почему разделение данных критически важно:

  • Предотвращение переобучения: Тестирование на независимой выборке выявляет модели, которые "запомнили" обучающие данные вместо извлечения паттернов.
  • Объективная оценка: Только независимая тестовая выборка дает реалистичную оценку эффективности модели на новых данных.
  • Настройка гиперпараметров: Для тонкой настройки модели требуется отдельная валидационная выборка, чтобы избежать подгонки под тестовые данные.
  • Проверка надежности: Стабильность показателей на разных подмножествах данных подтверждает устойчивость модели.

Типичное соотношение размеров выборок:

Подход Обучающая выборка Валидационная выборка Тестовая выборка Применимость
Стандартный 70-80% 20-30% Большие датасеты, простые модели
Трехчастный 60-70% 15-20% 15-20% Модели с гиперпараметрами
Малые данные 80-90% 10-20% Ограниченные датасеты
Cross-validation 80-90% K-fold 10-20% Малые/средние датасеты, высокая вариабельность

Разделение данных должно быть выполнено до любых преобразований, зависящих от распределения данных (например, нормализации или стандартизации), чтобы избежать утечки информации и получить надежные оценки точности модели. 🚨

Пошаговый план для смены профессии

Метод train

Функция traintestsplit из библиотеки scikit-learn – наиболее элегантное и широко используемое решение для разделения данных на выборки. Её применение к pandas DataFrame не требует дополнительных преобразований и предлагает богатый функционал для контроля процесса разделения.

Базовый синтаксис использования traintestsplit с DataFrame:

Python
Скопировать код
from sklearn.model_selection import train_test_split

# Разделение признаков и целевой переменной
X = df.drop('target', axis=1) # Признаки
y = df['target'] # Целевая переменная

# Разделение на обучающую и тестовую выборки (80% / 20%)
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)

Ключевые параметры traintestsplit:

  • test_size: Доля данных для тестовой выборки (от 0 до 1) или абсолютное количество объектов.
  • train_size: Альтернативно можно указать долю обучающей выборки (обычно используется либо testsize, либо trainsize).
  • random_state: Сид генератора случайных чисел для воспроизводимости результатов.
  • stratify: Параметр для стратификации (обычно передается y для сохранения баланса классов).
  • shuffle: Флаг перемешивания данных перед разделением (True по умолчанию).

Для сохранения целостности данных и работы с полными строками DataFrame можно использовать следующий подход:

Python
Скопировать код
# Разделение целого DataFrame на обучающую и тестовую части
df_train, df_test = train_test_split(df, test_size=0.2, random_state=42)

# Теперь можно разделить признаки и целевую переменную
X_train = df_train.drop('target', axis=1)
y_train = df_train['target']
X_test = df_test.drop('target', axis=1)
y_test = df_test['target']

Преимущества использования traintestsplit:

Преимущество Описание Практическое значение
Интеграция с экосистемой scikit-learn Бесшовная работа с пайплайнами scikit-learn Упрощает построение полноценных ML-пайплайнов
Детерминированные результаты Фиксация random_state обеспечивает воспроизводимость Критично для исследований и промышленной разработки
Стратификация Сохранение распределения целевой переменной Решает проблему несбалансированных классов
Производительность Оптимизированная для больших датасетов Экономит вычислительные ресурсы и время

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

Ручное разделение DataFrame на выборки в pandas

Хотя scikit-learn предоставляет отличный инструментарий для разделения данных, иногда требуется ручной контроль над процессом с использованием нативных возможностей pandas. Это может быть необходимо при работе со специфическими структурами данных или при интеграции в существующие pandas-ориентированные пайплайны.

Существует несколько способов ручного разделения DataFrame:

1. Использование метода sample() для случайного разделения:

Python
Скопировать код
# Размер выборки
train_size = int(0.8 * len(df))

# Создание обучающей выборки (случайные 80% строк)
df_train = df.sample(n=train_size, random_state=42)

# Создание тестовой выборки (оставшиеся 20% строк)
df_test = df.drop(df_train.index)

# Проверка размеров
print(f"Обучающая выборка: {df_train.shape}")
print(f"Тестовая выборка: {df_test.shape}")

2. Использование маски для детерминированного разделения:

Python
Скопировать код
import numpy as np

# Создание маски (True для 80% строк)
mask = np.random.rand(len(df)) < 0.8

# Применение маски для разделения данных
df_train = df[mask]
df_test = df[~mask]

3. Разделение по индексам:

Python
Скопировать код
# Перемешиваем индексы DataFrame
shuffled_indices = df.index.to_list()
np.random.shuffle(shuffled_indices)

# Определяем размер обучающей выборки
train_size = int(0.8 * len(df))

# Получаем индексы для каждой выборки
train_indices = shuffled_indices[:train_size]
test_indices = shuffled_indices[train_size:]

# Создаем выборки
df_train = df.loc[train_indices]
df_test = df.loc[test_indices]

Евгения Петрова, Data Scientist

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

Я разработала кастомную функцию разделения, которая учитывала временные метки:

Python
Скопировать код
def time_based_split(df, cutoff_date, date_column='timestamp'):
"""
Разделяет данные по времени: всё до cutoff_date – обучающая выборка,
всё после – тестовая.
"""
train = df[df[date_column] < cutoff_date]
test = df[df[date_column] >= cutoff_date]
return train, test

Это решение не только обеспечило корректное разделение данных, но и смоделировало реальный сценарий использования нашей модели — предсказание на новых, ранее невиденных данных. Точность выросла на 17% по сравнению с обычным random split.

Преимущества и недостатки ручного разделения:

  • ✅ Гибкость: Полный контроль над процессом разделения.
  • ✅ Прозрачность: Ясное понимание того, как именно происходит разделение.
  • ✅ Интеграция: Легко встраивается в пайплайны, основанные на pandas.
  • ❌ Трудоемкость: Требует больше кода и ручного управления.
  • ❌ Ограниченная функциональность: Стратификация и другие продвинутые техники требуют дополнительной реализации.
  • ❌ Риск ошибок: Выше вероятность программных ошибок по сравнению с готовыми решениями.

Рекомендации по ручному разделению данных:

  • Всегда фиксируйте random_state или seed для воспроизводимости результатов.
  • Проверяйте распределение ключевых признаков и целевой переменной в обеих выборках.
  • При работе с большими данными учитывайте оптимизацию памяти и используйте эффективные операции pandas.
  • Документируйте логику разделения для будущего использования и обеспечения прозрачности процесса.

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

Стратифицированное разделение данных для сбалансированных выборок

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

Стратифицированное разделение с scikit-learn:

Python
Скопировать код
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
)

# Проверка распределения классов
print("Обучающая выборка:", pd.Series(y_train).value_counts(normalize=True))
print("Тестовая выборка:", pd.Series(y_test).value_counts(normalize=True))

Для стратифицированного разделения по нескольким признакам можно создать составной признак:

Python
Скопировать код
# Создание составного признака для стратификации
df['strat_feature'] = df['class'] + '_' + df['gender']

# Разделение с учетом составного признака
train_df, test_df = train_test_split(
df, test_size=0.2, random_state=42, 
stratify=df['strat_feature']
)

# Удаление временного признака
train_df = train_df.drop('strat_feature', axis=1)
test_df = test_df.drop('strat_feature', axis=1)

Для сложных случаев с множеством категориальных признаков можно использовать технику бинирования (binning) для числовых признаков и последующей стратификации:

Python
Скопировать код
import pandas as pd
import numpy as np

# Бинирование числовых признаков
df['age_bin'] = pd.qcut(df['age'], q=5, labels=False)
df['income_bin'] = pd.qcut(df['income'], q=5, labels=False)

# Создание составного признака
df['strat_var'] = df['age_bin'].astype(str) + '_' + \
df['income_bin'].astype(str) + '_' + \
df['gender']

# Стратифицированное разделение
train, test = train_test_split(
df, test_size=0.2, stratify=df['strat_var'], random_state=42
)

# Очистка временных переменных
for col in ['age_bin', 'income_bin', 'strat_var']:
train = train.drop(col, axis=1)
test = test.drop(col, axis=1)

Кроме traintestsplit, существуют специализированные инструменты для более сложных стратифицированных разделений:

  • StratifiedKFold: Для стратифицированной кросс-валидации.
  • RepeatedStratifiedKFold: Многократное повторение стратифицированной кросс-валидации.
  • StratifiedShuffleSplit: Генерирует несколько стратифицированных обучающих/тестовых наборов.

Пример использования StratifiedKFold:

Python
Скопировать код
from sklearn.model_selection import StratifiedKFold

# Создаем объект для 5-фолдной стратифицированной кросс-валидации
skf = StratifiedKFold(n_splits=5, random_state=42, shuffle=True)

# Используем его для генерации индексов обучающей и тестовой выборок
for train_index, test_index in skf.split(X, y):
X_train, X_test = X.iloc[train_index], X.iloc[test_index]
y_train, y_test = y.iloc[train_index], y.iloc[test_index]

# Здесь можно обучать и тестировать модель
print(f"Размер обучающей выборки: {len(X_train)}, тестовой: {len(X_test)}")
print(f"Распределение классов в обучающей: {pd.Series(y_train).value_counts(normalize=True)}")

Когда следует использовать стратифицированное разделение:

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

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

Временное разделение данных для временных рядов в pandas

Для временных рядов случайное разделение данных недопустимо – оно нарушает временную структуру и приводит к утечке данных из будущего в прошлое. Корректный подход – хронологическое разделение, при котором обучающая выборка содержит более ранние данные, а тестовая – более поздние.

Базовый подход к временному разделению:

Python
Скопировать код
# Сортировка DataFrame по временной метке
df = df.sort_values('timestamp')

# Определение точки разделения (80% данных для обучения)
split_idx = int(len(df) * 0.8)

# Разделение на обучающую и тестовую выборки
train_df = df.iloc[:split_idx]
test_df = df.iloc[split_idx:]

print(f"Диапазон дат обучающей выборки: {train_df['timestamp'].min()} – {train_df['timestamp'].max()}")
print(f"Диапазон дат тестовой выборки: {test_df['timestamp'].min()} – {test_df['timestamp'].max()}")

Альтернативный подход – разделение по конкретной дате:

Python
Скопировать код
import pandas as pd

# Преобразование столбца с датами в datetime, если нужно
df['timestamp'] = pd.to_datetime(df['timestamp'])

# Разделение по определенной дате
split_date = pd.to_datetime('2023-01-01')

train_df = df[df['timestamp'] < split_date]
test_df = df[df['timestamp'] >= split_date]

Для более продвинутой работы с временными рядами часто используют скользящее окно (rolling window) и расширяющееся окно (expanding window):

Python
Скопировать код
# Пример скользящего окна
def rolling_window_split(df, window_size, test_size, step=1):
"""
Создает наборы данных для обучения и тестирования 
с использованием скользящего окна.

window_size: размер окна для обучения
test_size: размер окна для тестирования
step: шаг скольжения окна
"""
results = []

for i in range(0, len(df) – (window_size + test_size), step):
train = df.iloc[i:i+window_size]
test = df.iloc[i+window_size:i+window_size+test_size]
results.append((train, test))

return results

# Использование функции
splits = rolling_window_split(df, window_size=180, test_size=30, step=30)
print(f"Создано {len(splits)} наборов данных для обучения/тестирования")

TimeSeriesSplit из scikit-learn предоставляет удобный API для кросс-валидации временных рядов:

Python
Скопировать код
from sklearn.model_selection import TimeSeriesSplit

# Создание объекта TimeSeriesSplit с 5 разбиениями
tscv = TimeSeriesSplit(n_splits=5)

# Применение к данным
for train_index, test_index in tscv.split(df):
train_df = df.iloc[train_index]
test_df = df.iloc[test_index]

# Здесь можно обучать и тестировать модель
print(f"Обучающая выборка: {train_df.shape}, Тестовая выборка: {test_df.shape}")

Особенности разделения временных рядов:

Метод Описание Когда использовать Преимущества Недостатки
Разделение по времени Строгое хронологическое разделение Классический прогноз Предотвращает утечку данных Чувствительность к изменению паттернов во времени
Скользящее окно Множественные тренировочные/тестовые наборы Многошаговое прогнозирование Оценка стабильности модели Вычислительная сложность
Расширяющееся окно Постепенное увеличение обучающей выборки Стабильные, длинные ряды Использует максимум исторических данных Может включать устаревшие паттерны
Purged k-fold CV Кросс-валидация с удалением перекрытий Финансовые данные Избегает утечек и перекрытий Сложная реализация

Рекомендации для разделения временных рядов:

  • Строго соблюдайте хронологический порядок – никогда не допускайте попадания данных из будущего в обучающую выборку.
  • Учитывайте сезонные колебания при выборе размера обучающей и тестовой выборок.
  • При наличии длительных трендов рассмотрите использование дополнительной валидационной выборки для промежуточной оценки.
  • Для финансовых данных и данных с эффектом "заглядывания в будущее" используйте специализированные подходы, такие как Purged Cross-Validation.

Правильное разделение временных рядов – не просто техническая деталь, а фундаментальный принцип, определяющий валидность всего прогноза. Использование традиционных методов разделения к временным рядам – гарантированный путь к нереалистично завышенным оценкам точности и разочарованию при внедрении в production. ⏱️

Корректное разделение DataFrame на обучающую и тестовую выборки – это своего рода "иммунная система" вашей модели машинного обучения. Независимо от метода, который вы выберете – scikit-learn traintestsplit, ручное разделение, стратифицированный сплит или временное разделение для рядов – принципиально важно соблюдать основные правила: разделяйте данные до трансформаций, избегайте утечек данных, сохраняйте репрезентативность выборок. Только так ваша модель получит честную оценку и будет готова к столкновению с реальным миром. Помните: потратив дополнительные 15 минут на правильное разделение данных сейчас, вы можете сэкономить недели на отладке неработающей модели в будущем.

Загрузка...