Машинное обучение: обучение и тестирование в Python
Разделение данных на обучающую и тестовую выборки в Python с scikit-learn. Параметры test_size, random_state, stratify и метрики оценки.
Разделение на обучающую и тестовую выборки — это самый фундаментальный шаг в построении модели машинного обучения. Идея проста: скройте часть данных от модели во время обучения, а затем измерьте, насколько хорошо модель справляется с этой скрытой частью. Без такого разделения у вас нет честного способа узнать, действительно ли модель выучила закономерность или просто запомнила обучающие примеры.
В этой главе рассматривается, как работает train_test_split из scikit-learn, что делает каждый параметр и как оценивать полученную модель — для задач регрессии и классификации.
Зачем разделять обучающие и тестовые данные?
Модель, которая обучается и оценивается на одних и тех же данных, будет выглядеть гораздо точнее, чем есть на самом деле. Это называется утечкой данных или переобучением: модель запомнила обучающие примеры вместо того, чтобы научиться обобщаемой закономерности.
Представьте студента, который изучает 100 практических вопросов, а затем сдаёт тест по тем же 100 вопросам. Он может набрать 100% — но этот результат ничего не скажет о том, понимает ли он предмет.
Разделение на обучающую и тестовую выборки предотвращает это:
- Обучение модели на одной части данных, чтобы она могла выявлять закономерности.
- Тестирование модели на отдельной, ранее не виденной части для измерения реальной производительности.
Типичное соотношение — 80% для обучения / 20% для тестирования, хотя правильное соотношение зависит от объёма имеющихся данных.
Функция train_test_split
scikit-learn предоставляет train_test_split в модуле model_selection. Она случайным образом перемешивает датасет и делит его на две части:
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)Четыре возвращаемых значения всегда идут в следующем порядке: обучающие признаки, тестовые признаки, обучающие метки, тестовые метки.
Ключевые параметры
| Параметр | Тип | Описание |
|---|---|---|
test_size | float или int | Доля (0–1) или абсолютное количество тестовых образцов. По умолчанию 0.25. |
train_size | float или int | Дополнение к test_size. Обычно задаётся один из двух параметров, но не оба. |
random_state | int | Зерно для генератора случайных чисел. Укажите любое целое число, чтобы сделать разделение воспроизводимым. |
stratify | array-like | Передайте y, чтобы сохранить пропорции классов в обеих выборках. Необходимо для несбалансированных датасетов. |
shuffle | bool | Нужно ли перемешивать данные перед разделением. По умолчанию True. Установите False для временных рядов. |
Влияние параметра test_size
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.datasets import load_iris
X, y = load_iris(return_X_y=True) # 150 samples
for ts in [0.1, 0.2, 0.3]:
X_tr, X_te, _, _ = train_test_split(X, y, test_size=ts, random_state=42)
print(f"test_size={ts}: train={len(X_tr)}, test={len(X_te)}")Вывод:
test_size=0.1: train=135, test=15
test_size=0.2: train=120, test=30
test_size=0.3: train=105, test=45random_state и воспроизводимость
Без random_state разделение каждый раз будет разным. Задайте любое целое число, чтобы получать воспроизводимые результаты:
import numpy as np
from sklearn.model_selection import train_test_split
X = np.arange(10).reshape(-1, 1)
y = np.arange(10)
_, X_te1, _, _ = train_test_split(X, y, test_size=0.3, random_state=42)
_, X_te2, _, _ = train_test_split(X, y, test_size=0.3, random_state=42)
print("Same random_state → same split:", list(X_te1.ravel()) == list(X_te2.ravel()))
# TrueКонкретное целое число (42, 0, 1 и т. д.) не важно — главное использовать одно и то же значение последовательно.
Пример регрессии: прогнозирование цен на недвижимость
Следующий пример генерирует синтетический датасет с площадями домов и ценами, обучает модель линейной регрессии и оценивает её на отложенной тестовой выборке.
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error, r2_score
# Generate synthetic data: house size (sq ft) → price
np.random.seed(42)
n = 200
sqft = np.random.randint(500, 3500, n).astype(float)
price = 150 * sqft + np.random.randn(n) * 20000
X = sqft.reshape(-1, 1)
y = price
# Split
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
print("Training samples:", len(X_train)) # 160
print("Testing samples:", len(X_test)) # 40
# Train
model = LinearRegression()
model.fit(X_train, y_train)
# Evaluate on the test set
y_pred = model.predict(X_test)
mse = mean_squared_error(y_test, y_pred)
r2 = r2_score(y_test, y_pred)
print(f"Mean Squared Error: {mse:,.0f}")
print(f"R² Score: {r2:.4f}")
print(f"Coefficient: {model.coef_[0]:.2f}")
print(f"Intercept: {model.intercept_:.2f}")Вывод:
Training samples: 160
Testing samples: 40
Mean Squared Error: 489,271,263
R² Score: 0.9651
Coefficient: 147.55
Intercept: 5237.02Интерпретация метрик:
- MSE (среднеквадратичная ошибка) — это среднее значение квадратов разностей между прогнозами и фактическими значениями. Чем меньше — тем лучше, но масштаб зависит от целевой переменной (здесь — доллары).
- R² принимает значения от 0 до 1. Значение 0.965 означает, что модель объясняет около 96,5% дисперсии цен на недвижимость — хорошее соответствие для этого простого датасета.
Подробнее о линейной регрессии см. в разделе Линейная регрессия в Python.
Пример классификации: виды ирисов
Для задачи классификации точность и отчёт о классификации более информативны, чем MSE.
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.datasets import load_iris
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, classification_report
X, y = load_iris(return_X_y=True)
# stratify=y ensures each class is proportionally represented in both splits
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
print("Train class distribution:", np.bincount(y_train)) # [40 40 40]
print("Test class distribution: ", np.bincount(y_test)) # [10 10 10]
model = LogisticRegression(max_iter=200)
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
print("\nAccuracy:", round(accuracy_score(y_test, y_pred), 4))
print()
print(classification_report(y_test, y_pred,
target_names=["setosa", "versicolor", "virginica"]))Вывод:
Train class distribution: [40 40 40]
Test class distribution: [10 10 10]
Accuracy: 0.9667
precision recall f1-score support
setosa 1.00 1.00 1.00 10
versicolor 1.00 0.90 0.95 10
virginica 0.91 1.00 0.95 10
accuracy 0.97 30
macro avg 0.97 0.97 0.97 30
weighted avg 0.97 0.97 0.97 30Зачем нужен stratify=y: без него случайное разделение несбалансированного датасета может поместить большинство образцов редкого класса в обучающую выборку, не оставив ни одного в тестовой. stratify=y гарантирует, что каждый класс представлен в обеих выборках в одинаковых пропорциях.
Подробнее о классификации см. в разделах Логистическая регрессия в Python и Матрица ошибок в Python.
Распространённые ошибки
Предобработка до или после разделения?
Всегда выполняйте разделение перед подгонкой любых шагов предобработки (масштабирование, кодирование, вменение). Если сначала масштабировать все данные, а затем делить, тестовая выборка повлияет на параметры масштабировщика — это форма утечки данных.
Правильный порядок:
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train) # fit ONLY on training data
X_test_scaled = scaler.transform(X_test) # transform test with same parametersПодробное руководство см. в разделе Масштабирование признаков в Python.
Перемешивание и данные временных рядов
train_test_split перемешивает данные по умолчанию. Для данных временных рядов это неверно — вы будете обучаться на будущих данных для предсказания прошлого. Установите shuffle=False и убедитесь, что данные отсортированы в хронологическом порядке перед разделением:
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, shuffle=False
)Когда одного разделения недостаточно
Единственное разделение 80/20 даёт одну оценку производительности модели, которая зависит от того, какие образцы попали в тестовую выборку. Кросс-валидация повторяет разделение несколько раз и усредняет оценки, давая гораздо более стабильный результат — особенно на небольших датасетах.
Выбор метрики оценки
Правильная метрика зависит от типа задачи:
| Задача | Распространённые метрики |
|---|---|
| Регрессия | MSE, RMSE, MAE, R² |
| Бинарная классификация | Accuracy, Precision, Recall, F1, AUC-ROC |
| Многоклассовая классификация | Accuracy, macro/weighted F1 |
Для бинарной классификации см. Кривая AUC-ROC в Python и Матрица ошибок в Python. Для настройки гиперпараметров после настройки разделения см. Grid Search в Python.
Резюме
- Разделяйте данные до любой предобработки, чтобы избежать утечки данных.
- Используйте
test_size=0.2как разумное значение по умолчанию; корректируйте в зависимости от размера датасета. - Устанавливайте
random_stateв любое целое число для получения воспроизводимых разделений. - Используйте
stratify=yдля задач классификации, особенно на несбалансированных данных. - Устанавливайте
shuffle=Falseдля данных временных рядов. - Единственное разделение train/test — это быстрый базовый вариант; используйте кросс-валидацию для более надёжных оценок производительности.