Train, Validate, Test - Правильне розділення даних
Кафедра ШІзики
Автор
20 хвилин
Час читання
28.10.2025
Дата публікації
📌 Новачок у Python? Ця стаття використовує приклади коду Python. Якщо ви раніше не працювали з Python, почніть з нашого посібника Налаштування Python для ML!
Train, Validate, Test - Правильне розділення даних 🎯
Уявіть, що ви готуєтесь до важливого іспиту. Ви старанно вчитесь, розв’язуєте задачі і відчуваєте впевненість. У день іспиту ви сідаєте і… зачекайте, це ті САМІ запитання, які ви практикували! Ви отримуєте 100%. Чудово, правда?
Неправильно. Ви насправді не навчились - ви просто запам’ятали відповіді.
Саме це відбувається, коли ML моделі “вчаться” і “тестуються” на одних і тих самих даних.
Ось шокуюча істина: Більшість новачків роблять свої моделі чудовими, випадково шахраюючи на тесті. Вони тренують на повному датасеті, оцінюють на тому ж датасеті і отримують неймовірні результати… які повністю розвалюються в реальному світі.
У цій статті ви навчитеся:
- Чому ми повинні розділяти дані перед тренуванням
- Як робити прості train/test розділення (80/20, 70/30)
- Коли використовувати train/validation/test (3-стороннє розділення)
- Що таке крос-валідація і коли її використовувати
- Особливе розділення для незбалансованих даних і часових рядів
- Поширені помилки, що викликають витік даних
Давайте переконаємось, що ваші моделі справді працюють у продакшені! 🚀
Проблема перенавчання - Чому ми розділяємо дані 🤔
Пастка запам’ятовування
Давайте використаємо наш приклад піцерії. У вас є дані з січня з цими фічами:
- Температура, день тижня, година, тощо
- Ціль: Кількість проданих піц
Сценарій 1: Тренування та тестування на тих самих даних ❌
# Тренуємо модель на січневих даних
model.fit(january_data)
# Тестуємо модель на... січневих даних знову!
score = model.score(january_data)
# Score: 99% - Чудово! 🎉
# Розгортаємо у продакшен...
# Лютий настає...
# Продуктивність моделі: 45% - Катастрофа! 😱Що сталося? Модель запам’ятала специфічні патерни січня замість того, щоб навчитись загальним правилам:
- “15 січня о 19:00 продати 23 піци” (запам’ятала)
- проти
- “П’ятничні вечори зазвичай продають 20-25 піц” (навчилась)
Перше марне для лютого. Друге - цінне знання.
Реальний тест
Сценарій 2: Тренування та тестування на окремих даних ✅
# Тренуємо на січневих даних
model.fit(january_data)
# Тестуємо на лютневих даних (модель НІКОЛИ це не бачила!)
score = model.score(february_data)
# Score: 78% - Більш реалістично!
# Розгортаємо у продакшен...
# Березень настає...
# Продуктивність моделі: 76% - Послідовно! ✅Тепер модель навчилась справжнім патернам, які узагальнюються на нові дані.
Типи узагальнення
НЕДОНАВЧАННЯ ГАРНА МОДЕЛЬ ПЕРЕНАВЧАННЯ
(Занадто проста) (Саме те) (Занадто складна)
Тренування: 60% Тренування: 85% Тренування: 99%
Тест: 58% Тест: 82% Тест: 45%
Модель занадто проста Модель навчилась Модель запам'ятала
Не може захопити узагальнюваним конкретні приклади
патерни правиламНаша мета: Знайти золоту середину, де модель працює добре і на тренувальних, і на тестових даних.
Просте розділення Train/Test - Основа 📊
Найбазовіший підхід: розділити дані на дві частини.
Правило 80/20
from sklearn.model_selection import train_test_split
import pandas as pd
# Наші дані піцерії
data = pd.read_csv('pizza_orders_clean.csv')
# Фічі (X) та ціль (y)
X = data.drop('num_pizzas', axis=1)
y = data['num_pizzas']
# Розділяємо: 80% train, 20% test
X_train, X_test, y_train, y_test = train_test_split(
X, y,
test_size=0.2, # 20% для тестування
random_state=42 # Відтворюваність
)
print(f"Всього зразків: {len(X)}")
print(f"Тренувальних зразків: {len(X_train)} ({len(X_train)/len(X)*100:.1f}%)")
print(f"Тестових зразків: {len(X_test)} ({len(X_test)/len(X)*100:.1f}%)")Вихід:
Всього зразків: 10000
Тренувальних зразків: 8000 (80.0%)
Тестових зразків: 2000 (20.0%)Чому це працює
| Набір | Призначення | Що модель знає |
|---|---|---|
| Тренування (80%) | Вивчити патерни | Бачить всі фічі та мітки |
| Тест (20%) | Оцінити продуктивність | НІКОЛИ не бачила під час тренування |
Тестовий набір імітує нові, невидимі дані з продакшену.
Вибір співвідношення розділення
| Розділення | Коли використовувати | Переваги | Недоліки |
|---|---|---|---|
| 80/20 | Стандартний вибір | Хороший баланс | За замовчуванням для більшості випадків |
| 70/30 | Менші датасети (<1000 зразків) | Більше тестових даних для надійної оцінки | Менше тренувальних даних |
| 90/10 | Великі датасети (>100,000 зразків) | Максимізація тренувальних даних | Малий тестовий набір може бути менш надійним |
| 60/20/20 | Коли потрібен валідаційний набір | Найкраща практика для налаштування гіперпараметрів | Більш складно (див. наступний розділ) |
Правило великого пальця: Починайте з 80/20, якщо немає конкретної причини змінити.
Параметр random_state
# Без random_state - різні результати кожного разу!
split1 = train_test_split(X, y, test_size=0.2)
split2 = train_test_split(X, y, test_size=0.2)
# Різні розділення!
# З random_state - відтворювані результати
split1 = train_test_split(X, y, test_size=0.2, random_state=42)
split2 = train_test_split(X, y, test_size=0.2, random_state=42)
# Точно ті самі розділення!Завжди встановлюйте random_state під час експериментів, щоб можна було справедливо порівнювати результати!
Train/Validation/Test - 3-стороннє розділення 🎯
Проблема налаштування гіперпараметрів
Проблема: Ви тренуєте модель, налаштовуєте гіперпараметри тестуючи різні значення, і обираєте найкращий на основі тестової продуктивності.
Зачекайте… ви щойно перенавчилися на тестовому наборі?
Так! Неодноразово тестуючи на тестовому наборі для вибору гіперпараметрів, ви “пролили” інформацію з тестового набору в процес вибору моделі.
Рішення: Використовуйте три набори замість двох!
Три набори пояснені
from sklearn.model_selection import train_test_split
# Перше розділення: відокремлюємо тестовий набір
X_temp, X_test, y_temp, y_test = train_test_split(
X, y,
test_size=0.2, # 20% для фінального тестування
random_state=42
)
# Друге розділення: розділяємо залишок на train та validation
X_train, X_val, y_train, y_val = train_test_split(
X_temp, y_temp,
test_size=0.25, # 0.25 з 80% = 20% оригіналу
random_state=42
)
print(f"Тренувальний набір: {len(X_train)} зразків ({len(X_train)/len(X)*100:.0f}%)")
print(f"Валідаційний набір: {len(X_val)} зразків ({len(X_val)/len(X)*100:.0f}%)")
print(f"Тестовий набір: {len(X_test)} зразків ({len(X_test)/len(X)*100:.0f}%)")Вихід:
Тренувальний набір: 6000 зразків (60%)
Валідаційний набір: 2000 зразків (20%)
Тестовий набір: 2000 зразків (20%)Призначення кожного набору
| Набір | Призначення | Використовується для | Модель бачить? |
|---|---|---|---|
| Тренування | Вивчити патерни | Фітування параметрів моделі (ваги, коефіцієнти) | ✅ Так - повністю видимі |
| Валідація | Налаштувати гіперпараметри | Вибір найкращих гіперпараметрів, раннє зупинення | ❌ Ні - але впливає на рішення |
| Тест | Фінальна оцінка | Одноразова перевірка продуктивності перед розгортанням | ❌ Ні - повністю недоторканий |
Приклад повного робочого процесу
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_absolute_error
# Спробуйте різні гіперпараметри на валідаційному наборі
best_mae = float('inf')
best_params = None
for n_trees in [50, 100, 200]:
for max_depth in [10, 20, None]:
# Тренуємо на тренувальному наборі
model = RandomForestRegressor(
n_estimators=n_trees,
max_depth=max_depth,
random_state=42
)
model.fit(X_train, y_train)
# Оцінюємо на валідаційному наборі
val_pred = model.predict(X_val)
mae = mean_absolute_error(y_val, val_pred)
# Відстежуємо найкращі гіперпараметри
if mae < best_mae:
best_mae = mae
best_params = {'n_estimators': n_trees, 'max_depth': max_depth}
print(f"Найкращі гіперпараметри: {best_params}")
print(f"Валідаційний MAE: {best_mae:.2f}")
# Тренуємо фінальну модель з найкращими гіперпараметрами на тренувальних даних
final_model = RandomForestRegressor(**best_params, random_state=42)
final_model.fit(X_train, y_train)
# ОДНОРАЗОВА оцінка на тестовому наборі
test_pred = final_model.predict(X_test)
test_mae = mean_absolute_error(y_test, test_pred)
print(f"\n🎯 Фінальний тестовий MAE: {test_mae:.2f} піц")
print("Це продуктивність, яку ми очікуємо в продакшені!")Ключове правило: Торкайтесь тестового набору ЛИШЕ ОДИН РАЗ, в самому кінці!
K-Fold крос-валідація - Отримання більш надійних оцінок 🔄
Проблема одиночних розділень
З одиночним train/test розділенням, ваша оцінка продуктивності залежить від того, які зразки потрапили в тестовий набір. Ви можете мати удачу (або невдачу)!
# Різні випадкові розділення дають різні показники!
for i in range(5):
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=i
)
model = RandomForestRegressor(random_state=42)
model.fit(X_train, y_train)
score = model.score(X_test, y_test)
print(f"Розділення {i+1}: R² = {score:.3f}")Вихід:
Розділення 1: R² = 0.843
Розділення 2: R² = 0.871
Розділення 3: R² = 0.826
Розділення 4: R² = 0.889
Розділення 5: R² = 0.854Яка “справжня” продуктивність? 🤔
Як працює K-Fold крос-валідація
Ідея: Розділити дані на K частин (фолдів), тренувати K разів, кожного разу використовуючи різний фолд як тестовий набір.
Приклад: 5-Fold крос-валідація
Fold 1: [TEST] [train] [train] [train] [train]
Fold 2: [train] [TEST] [train] [train] [train]
Fold 3: [train] [train] [TEST] [train] [train]
Fold 4: [train] [train] [train] [TEST] [train]
Fold 5: [train] [train] [train] [train] [TEST]
Фінальний показник = середнє всіх 5 тестових показниківКожен зразок потрапляє в тестовий набір рівно один раз!
Реалізація
from sklearn.model_selection import cross_val_score
from sklearn.ensemble import RandomForestRegressor
import numpy as np
# Створюємо модель
model = RandomForestRegressor(n_estimators=100, random_state=42)
# Виконуємо 5-fold крос-валідацію
scores = cross_val_score(
model, X, y,
cv=5, # 5 фолдів
scoring='r2' # Метрика R²
)
print("Показники для кожного фолду:", scores)
print(f"\nСереднє R²: {scores.mean():.3f}")
print(f"Стандартне відхилення: {scores.std():.3f}")
print(f"95% довірчий інтервал: {scores.mean():.3f} ± {1.96*scores.std():.3f}")Вихід:
Показники для кожного фолду: [0.843 0.871 0.826 0.889 0.854]
Середнє R²: 0.857
Стандартне відхилення: 0.023
95% довірчий інтервал: 0.857 ± 0.045Інтерпретація: R² моделі приблизно 0.857, з продуктивністю в діапазоні від ~0.81 до 0.90.
Коли використовувати K-Fold крос-валідацію
✅ Використовуйте K-Fold коли:
- Малі до середніх датасети (<10,000 зразків)
- Вам потрібні надійні оцінки продуктивності
- Порівняння кількох моделей або гіперпараметрів
- Вам потрібні довірчі інтервали
❌ Не використовуйте K-Fold коли:
- Дуже великі датасети (занадто дорого обчислювально)
- Дані часових рядів (використовуйте розділення на основі часу)
- Сильна незбалансованість класів (використовуйте stratified K-fold)
Вибір K
| Значення K | Переваги | Недоліки | Коли використовувати |
|---|---|---|---|
| K=5 | Швидко, хороший баланс | Менш точно | За замовчуванням, великі датасети |
| K=10 | Більш надійна оцінка | Повільніше | Стандартний вибір, середні датасети |
| K=n (LOO) | Максимум даних для тренування | Дуже повільно, висока варіація | Крихітні датасети (<100 зразків) |
Правило великого пальця: Використовуйте K=5 для швидких експериментів, K=10 для фінальної оцінки.
Стратифіковане розділення - Обробка незбалансованих даних ⚖️
Проблема незбалансованості
Уявіть, що ваша піцерія хоче прогнозувати, чи буде замовлення “великим” (5+ піц) чи “звичайним” (1-4 піци).
# Перевіряємо розподіл класів
print(data['is_large_order'].value_counts())Вихід:
False 9500 (95%)
True 500 (5%)Лише 5% замовлень великі!
Проблема з випадковим розділенням:
# Випадкове розділення
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
print("Розподіл тренувального набору:")
print(y_train.value_counts(normalize=True))
print("\nРозподіл тестового набору:")
print(y_test.value_counts(normalize=True))Вихід:
Розподіл тренувального набору:
False 0.951 (95.1%)
True 0.049 (4.9%)
Розподіл тестового набору:
False 0.940 (94.0%)
True 0.060 (6.0%)Тестовий набір має 6% великих замовлень замість 5% - не репрезентативно!
Стратифіковане розділення на допомогу
# Стратифіковане розділення - зберігає розподіл класів
X_train, X_test, y_train, y_test = train_test_split(
X, y,
test_size=0.2,
stratify=y, # Зберігає розподіл y
random_state=42
)
print("Розподіл тренувального набору:")
print(y_train.value_counts(normalize=True))
print("\nРозподіл тестового набору:")
print(y_test.value_counts(normalize=True))Вихід:
Розподіл тренувального набору:
False 0.950 (95.0%)
True 0.050 (5.0%)
Розподіл тестового набору:
False 0.950 (95.0%)
True 0.050 (5.0%)Ідеально! Обидва набори мають рівно 5% великих замовлень. ✅
Стратифікований K-Fold
from sklearn.model_selection import StratifiedKFold, cross_val_score
# Стратифікований K-Fold забезпечує однаковий розподіл класів у кожному фолді
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
scores = cross_val_score(
model, X, y,
cv=skf,
scoring='f1'
)
print(f"Показники стратифікованого K-Fold: {scores}")
print(f"Середнє F1: {scores.mean():.3f} ± {scores.std():.3f}")Коли використовувати стратифікацію:
- Бінарна класифікація з незбалансованістю
- Мультикласова класифікація з нерівними класами
- Регресія зі скошеним розподілом цілі
Розділення часових рядів - Ніякого підглядання в майбутнє! 📅
Проблема подорожі в часі
НЕ РОБІТЬ ТАК з даними часових рядів: ❌
# НЕПРАВИЛЬНО: Випадкове розділення для часових рядів
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
# Проблема: Тренувальний набір містить МАЙБУТНІ дані!
# Тренування: [5 січ, 20 лют, 15 бер, ...]
# Тест: [3 січ, 8 лют, 1 бер, ...]
# Ви прогнозуєте 3 січня використовуючи дані з лютого та березня!
# Це подорож у часі - шахрайство!У реальному продакшені: Ви можете використовувати лише минулі дані для прогнозування майбутнього.
Послідовне розділення
# Спочатку сортуємо дані по даті!
data = data.sort_values('date')
# Розділяємо послідовно - тренуємо на минулому, тестуємо на майбутньому
split_point = int(len(data) * 0.8)
train_data = data[:split_point]
test_data = data[split_point:]
print(f"Період тренування: {train_data['date'].min()} до {train_data['date'].max()}")
print(f"Період тестування: {test_data['date'].min()} до {test_data['date'].max()}")Вихід:
Період тренування: 2024-01-01 до 2024-10-12
Період тестування: 2024-10-13 до 2024-12-31Ключовий принцип: Тренуємо на минулому, тестуємо на майбутньому. Без перекриття!
Крос-валідація часових рядів
from sklearn.model_selection import TimeSeriesSplit
import matplotlib.pyplot as plt
# Створюємо розділення часових рядів
tscv = TimeSeriesSplit(n_splits=5)
# Візуалізуємо розділення
fig, ax = plt.subplots(figsize=(12, 6))
for i, (train_idx, test_idx) in enumerate(tscv.split(X)):
# Малюємо train та test для кожного фолду
ax.scatter(train_idx, [i]*len(train_idx), c='blue', marker='s', s=10, label='Train' if i==0 else '')
ax.scatter(test_idx, [i]*len(test_idx), c='red', marker='s', s=10, label='Test' if i==0 else '')
ax.set_xlabel('Індекс зразку (Час →)')
ax.set_ylabel('Fold')
ax.set_title('Крос-валідація часових рядів')
ax.legend()
plt.tight_layout()
plt.show()Як це працює:
Fold 1: [train ] [test]
Fold 2: [train ] [test]
Fold 3: [train ] [test]
Fold 4: [train ] [test]
Fold 5: [train ] [test]Тренувальний набір зростає по мірі просування вперед у часі - імітує реальний сценарій!
Реалізація
from sklearn.model_selection import TimeSeriesSplit, cross_val_score
tscv = TimeSeriesSplit(n_splits=5)
scores = cross_val_score(
model, X, y,
cv=tscv,
scoring='neg_mean_absolute_error'
)
# Примітка: негативне, бо sklearn хоче максимізувати показники
mae_scores = -scores
print(f"MAE для кожного фолду: {mae_scores}")
print(f"Середній MAE: {mae_scores.mean():.2f} піц")Поширені помилки та як їх уникнути 🚨
Помилка 1: Витік даних - Фітування скейлера на повному датасеті
❌ НЕПРАВИЛЬНО:
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)
# Проблема: Модель бачила статистику тестових даних (середнє, std)✅ ПРАВИЛЬНО:
# Спочатку розділяємо
X_train, X_test, y_train, y_test = train_test_split(X, y)
# Фітуємо скейлер ЛИШЕ на тренувальних даних
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train) # Вчимося з train
X_test_scaled = scaler.transform(X_test) # Застосовуємо до test
# Тестові дані справді невидимі!Ключовий принцип: Завжди розділяйте перед будь-якою трансформацією даних!
Помилка 2: Використання тестового набору кілька разів
❌ НЕПРАВИЛЬНО:
# Спробуйте модель 1
model1 = RandomForest()
model1.fit(X_train, y_train)
score1 = model1.score(X_test, y_test) # 0.85
# Недостатньо добре, спробуйте модель 2
model2 = XGBoost()
model2.fit(X_train, y_train)
score2 = model2.score(X_test, y_test) # 0.87
# Все ще пробуємо... модель 3
model3 = NeuralNet()
model3.fit(X_train, y_train)
score3 = model3.score(X_test, y_test) # 0.89
# Обираємо модель 3, бо вона показала найкраще на тестовому наборі
# Проблема: Ви перенавчилися на тестовому наборі!✅ ПРАВИЛЬНО:
# Використовуємо валідаційний набір або крос-валідацію для порівняння моделей
for model in [RandomForest(), XGBoost(), NeuralNet()]:
scores = cross_val_score(model, X_train, y_train, cv=5)
print(f"{model.__class__.__name__}: {scores.mean():.3f}")
# Обираємо найкращу модель на основі крос-валідації
# Тестуємо ОДИН РАЗ на тестовому наборі в самому кінці
final_model.fit(X_train, y_train)
final_score = final_model.score(X_test, y_test)
print(f"Фінальний тестовий показник: {final_score:.3f}")Помилка 3: Не перемішування даних
❌ НЕПРАВИЛЬНО:
# Дані відсортовані по даті або категорії
# Без перемішування, перші 80% можуть бути лише січневими даними
X_train, X_test = X[:8000], X[8000:]✅ ПРАВИЛЬНО:
# Перемішуємо для випадкового розділення (окрім часових рядів!)
X_train, X_test, y_train, y_test = train_test_split(
X, y,
test_size=0.2,
shuffle=True, # За замовчуванням True
random_state=42
)Помилка 4: Забування встановити random_state
❌ НЕПРАВИЛЬНО:
# Різні результати кожного запуску!
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
# Неможливо відтворити результати або порівняти справедливо✅ ПРАВИЛЬНО:
# Відтворювані результати
X_train, X_test, y_train, y_test = train_test_split(
X, y,
test_size=0.2,
random_state=42 # Будь-яке число, але тримайте його послідовним
)Помилка 5: Тестування на тренувальних даних
❌ НЕПРАВИЛЬНО:
model.fit(X_train, y_train)
score = model.score(X_train, y_train) # Тестування на тренувальних даних!
print(f"Точність моделі: {score:.2f}") # Завжди виглядає чудово!✅ ПРАВИЛЬНО:
model.fit(X_train, y_train)
score = model.score(X_test, y_test) # Тестування на невидимих даних
print(f"Точність моделі: {score:.2f}") # Реалістична оцінкаЗібрати все разом - Повний робочий процес 🏗️
Давайте створимо повний, готовий до продакшену робочий процес, використовуючи все, що ми навчились!
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split, cross_val_score, StratifiedKFold
from sklearn.preprocessing import StandardScaler
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_absolute_error, r2_score
# Крок 1: Завантаження та підготовка даних
print("🍕 Піцерія Мама ML - Повний робочий процес ML\n")
data = pd.read_csv('pizza_orders_clean.csv')
# Фічі та ціль
X = data.drop(['num_pizzas', 'date', 'order_id'], axis=1)
y = data['num_pizzas']
print(f"Всього зразків: {len(X)}\n")
# Крок 2: Початкове train/test розділення (80/20)
# КРИТИЧНО: Розділяємо ПЕРЕД будь-якою попередньою обробкою!
X_train_full, X_test, y_train_full, y_test = train_test_split(
X, y,
test_size=0.2,
random_state=42,
shuffle=True
)
print(f"Тренувальний набір: {len(X_train_full)} зразків")
print(f"Тестовий набір: {len(X_test)} зразків (ЗАМКНУТИЙ 🔒)\n")
# Крок 3: Створюємо валідаційний набір з тренувальних даних
X_train, X_val, y_train, y_val = train_test_split(
X_train_full, y_train_full,
test_size=0.25, # 25% з 80% = 20% загалом
random_state=42
)
print(f"Фінальний тренувальний набір: {len(X_train)} зразків (60%)")
print(f"Валідаційний набір: {len(X_val)} зразків (20%)\n")
# Крок 4: Попередня обробка - фітуємо лише на тренувальних даних!
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_val_scaled = scaler.transform(X_val)
X_test_scaled = scaler.transform(X_test)
print("✅ Дані масштабовано (без витоку!)\n")
# Крок 5: Налаштування гіперпараметрів використовуючи валідаційний набір
print("🔧 Налаштування гіперпараметрів на валідаційному наборі...\n")
best_score = float('inf')
best_params = None
for n_estimators in [50, 100, 150]:
for max_depth in [10, 20, 30]:
model = RandomForestRegressor(
n_estimators=n_estimators,
max_depth=max_depth,
random_state=42,
n_jobs=-1
)
model.fit(X_train_scaled, y_train)
val_pred = model.predict(X_val_scaled)
mae = mean_absolute_error(y_val, val_pred)
if mae < best_score:
best_score = mae
best_params = {
'n_estimators': n_estimators,
'max_depth': max_depth
}
print(f"Найкращі гіперпараметри: {best_params}")
print(f"Валідаційний MAE: {best_score:.2f} піц\n")
# Крок 6: Крос-валідація для більш надійної оцінки
print("📊 Крос-валідація на повному тренувальному наборі...\n")
final_model = RandomForestRegressor(**best_params, random_state=42, n_jobs=-1)
cv_scores = cross_val_score(
final_model,
scaler.fit_transform(X_train_full),
y_train_full,
cv=5,
scoring='neg_mean_absolute_error'
)
cv_mae = -cv_scores.mean()
cv_std = cv_scores.std()
print(f"Крос-валідаційний MAE: {cv_mae:.2f} ± {cv_std:.2f} піц\n")
# Крок 7: Тренуємо фінальну модель на всіх тренувальних даних
print("🎓 Тренування фінальної моделі на повному тренувальному наборі...\n")
final_model.fit(
scaler.fit_transform(X_train_full),
y_train_full
)
# Крок 8: ОДНОРАЗОВА оцінка на тестовому наборі
print("🎯 ФІНАЛЬНА ТЕСТОВА ОЦІНКА (лише один раз!):\n")
print("="*50)
test_pred = final_model.predict(X_test_scaled)
test_mae = mean_absolute_error(y_test, test_pred)
test_r2 = r2_score(y_test, test_pred)
print(f"Тестовий MAE: {test_mae:.2f} піц")
print(f"Тестовий R²: {test_r2:.3f}")
print("="*50)
print("\n💡 Це продуктивність, яку ми очікуємо в продакшені!")
# Крок 9: Аналіз важливості фіч
feature_importance = pd.DataFrame({
'feature': X.columns,
'importance': final_model.feature_importances_
}).sort_values('importance', ascending=False)
print("\n📊 Топ 5 найважливіших фіч:")
print(feature_importance.head().to_string(index=False))
# Крок 10: Зберігаємо модель та скейлер для продакшену
import joblib
joblib.dump(final_model, 'pizza_model.pkl')
joblib.dump(scaler, 'pizza_scaler.pkl')
print("\n✅ Модель та скейлер збережено для продакшену!")
print("\nРобочий процес завершено! 🎉")Очікуваний вихід:
🍕 Піцерія Мама ML - Повний робочий процес ML
Всього зразків: 10000
Тренувальний набір: 8000 зразків
Тестовий набір: 2000 зразків (ЗАМКНУТИЙ 🔒)
Фінальний тренувальний набір: 6000 зразків (60%)
Валідаційний набір: 2000 зразків (20%)
✅ Дані масштабовано (без витоку!)
🔧 Налаштування гіперпараметрів на валідаційному наборі...
Найкращі гіперпараметри: {'n_estimators': 100, 'max_depth': 20}
Валідаційний MAE: 0.42 піц
📊 Крос-валідація на повному тренувальному наборі...
Крос-валідаційний MAE: 0.45 ± 0.03 піц
🎓 Тренування фінальної моделі на повному тренувальному наборі...
🎯 ФІНАЛЬНА ТЕСТОВА ОЦІНКА (лише один раз!):
==================================================
Тестовий MAE: 0.47 піц
Тестовий R²: 0.876
==================================================
💡 Це продуктивність, яку ми очікуємо в продакшені!
📊 Топ 5 найважливіших фіч:
feature importance
is_friday_evening 0.245
hour_sin 0.187
temperature 0.156
is_peak_hour 0.128
rolling_avg_3h 0.092
✅ Модель та скейлер збережено для продакшену!
Робочий процес завершено! 🎉Швидкий довідник 📋
Шпаргалка з розділення даних
# 1. Просте розділення 80/20
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
# 2. Трьохстороннє розділення (60/20/20)
X_temp, X_test, y_temp, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
X_train, X_val, y_train, y_val = train_test_split(X_temp, y_temp, test_size=0.25, random_state=42)
# 3. Стратифіковане розділення (для незбалансованих даних)
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, stratify=y, random_state=42
)
# 4. K-Fold крос-валідація
from sklearn.model_selection import cross_val_score
scores = cross_val_score(model, X, y, cv=5)
# 5. Стратифікований K-Fold
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)
# 6. Розділення часових рядів
from sklearn.model_selection import TimeSeriesSplit
tscv = TimeSeriesSplit(n_splits=5)
scores = cross_val_score(model, X, y, cv=tscv)Дерево рішень: Яке розділення мені використовувати?
Це дані часових рядів?
├─ ТАК → Використайте TimeSeriesSplit або послідовне розділення
└─ НІ
└─ Датасет незбалансований?
├─ ТАК → Використайте стратифіковане розділення / StratifiedKFold
└─ НІ
└─ Потрібне налаштування гіперпараметрів?
├─ ТАК → Використайте train/val/test (60/20/20)
└─ НІ
└─ Малий датасет (<1000 зразків)?
├─ ТАК → Використайте K-Fold крос-валідацію (K=5 або 10)
└─ НІ → Використайте просте train/test розділення (80/20)Золоті правила
- Завжди розділяйте ПЕРЕД попередньою обробкою (масштабування, кодування, тощо)
- Встановлюйте random_state для відтворюваності
- Використовуйте стратифікацію для незбалансованих даних
- Ніколи не використовуйте тестовий набір для вибору моделі
- Торкайтесь тестового набору ЛИШЕ ОДИН РАЗ, в самому кінці
- Для часових рядів: тренуйте на минулому, тестуйте на майбутньому
- Використовуйте крос-валідацію для надійних оцінок
Підсумок та наступні кроки 🎯
Що ви навчились
1. Чому розділення даних критично важливе
- Запобігти перенавчанню тестуючи на невидимих даних
- Імітувати умови реального розгортання
- Отримати чесні оцінки продуктивності
2. Просте розділення Train/Test
- Правило 80/20 за замовчуванням
- Завжди встановлюйте random_state
- Тестовий набір імітує продакшн дані
3. Розділення Train/Validation/Test
- Використовуйте валідаційний набір для налаштування гіперпараметрів
- Тримайте тестовий набір повністю недоторканим
- Торкайтесь тестового набору лише один раз
4. K-Fold крос-валідація
- Більш надійні оцінки продуктивності
- Кожен зразок використовується для тренування та тестування
- Стандартний вибір: K=5 або K=10
5. Спеціальні випадки
- Стратифіковане розділення для незбалансованих даних
- Розділення часових рядів (ніякого підглядання в майбутнє!)
- Послідовна валідація для темпоральних даних
6. Поширені пастки
- Витік даних від попередньої обробки перед розділенням
- Використання тестового набору кілька разів
- Випадкове розділення для часових рядів
Подорож даними досі
Сирі дані
↓
[Стаття 1: Якість даних] ✅ Очистити дані
↓
[Стаття 2: EDA] ✅ Зрозуміти патерни
↓
[Стаття 3: Фіча-інженірінг] ✅ Створити ML-готові фічі
↓
[Стаття 4: Train/Test розділення] ✅ Правильно розділити дані (Ви тут!)
↓
Готові тренувати моделі!
↓
[Стаття 5: Лінійна регресія] → Далі!Що далі?
У Статті 5: Ваша перша ML модель - Лінійна регресія, ми нарешті натренуємо модель:
- Розуміння лінійних зв’язків
- Тренування вашого першого алгоритму
- Інтерпретація параметрів моделі
- Прогнозування на нових даних
- Оцінка продуктивності моделі
Тепер ви знаєте, як правильно розділяти дані. Далі ми використаємо ці знання для тренування та оцінки вашої першої моделі машинного навчання!
Практичні вправи
Вправа 1: Базове розділення
# Маючи датасет з 1000 зразками, створіть:
# 1. Розділення 80/20 train/test з random_state=42
# 2. Перевірте, що розміри правильні
# 3. Перевірте, чи random_state дає відтворювані результатиВправа 2: Трьохстороннє розділення
# Створіть розділення 60/20/20 train/validation/test
# Виведіть розмір кожного набору
# Перевірте, що вони складаються до 100% оригінальних данихВправа 3: Знайдіть помилку
# Що не так з цим кодом?
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
X_train, X_test = train_test_split(X_scaled, test_size=0.2)Вправа 4: Часові ряди
# У вас є замовлення піци з січня-грудня 2024
# Створіть правильне розділення часових рядів:
# - Train: Січень-Вересень
# - Test: Жовтень-Грудень
# Переконайтесь, що немає витоку даних!Готові тренувати вашу першу модель? З правильно розділеними даними, ваші моделі справді працюватимуть в реальному світі! 🚀
Пам’ятайте: Спочатку розділяємо, потім тренуємо, тестуємо останнім! 🎯
Щасливого розділення! 💪✨