Класифікація 101 - Дерева рішень
Кафедра ШІзики
Автор
30 хвилин
Час читання
28.10.2025
Дата публікації
📌 Новачок у Python? Ця стаття використовує приклади коду на Python. Якщо ви раніше не працювали з Python, почніть з нашого гайду Налаштування Python для ML!
Класифікація 101 - Дерева рішень 🌲
Вступ: Від регресії до класифікації
У Статті 5 ми побудували нашу першу модель машинного навчання - лінійну регресію. Ми прогнозували скільки піц буде продано на основі температури. Це називається регресією - передбачення числа.
У Статті 6 ми навчилися оцінювати моделі за допомогою метрик, таких як Accuracy, Precision, Recall, та F1-score.
Тепер настав час зробити наступний крок: класифікація - передбачення категорії.
Приклади класифікаційних задач
У реальному житті:
- 📧 Email: Спам чи не спам?
- 🏥 Медицина: Хворий чи здоровий?
- 💳 Банки: Схвалити кредит чи відхилити?
- 🎬 Netflix: Сподобається фільм чи ні?
У нашій піцерії:
- 🍕 Розмір замовлення: Велике (30+ піц) чи мале (< 30 піц)?
- 👥 Клієнт: VIP чи звичайний?
- ⏰ Доставка: Пізно чи вчасно?
- 🔄 Повторне замовлення: Замовить знову чи ні?
Сьогодні ми зосередимося на першій задачі: прогнозування розміру замовлення.
Чому дерева рішень?
Дерева рішень - це найбільш інтуїтивний алгоритм класифікації, тому що:
- ✅ Візуальні - виглядають як блок-схеми, які всі розуміють
- ✅ Інтерпретовані - можна пояснити кожне рішення
- ✅ Потужні - обробляють нелінійні патерни
- ✅ Основа - фундамент для Random Forests (Стаття 9)
Бізнес-задача: Прогнозування великих замовлень
Сценарій
Ресторан “Мама ML’s Pizza” хоче оптимізувати роботу для великих замовлень на кейтеринг.
Задача класифікації: Передбачити, чи замовлення буде Велике (30+ піц, потрібна спеціальна підготовка) чи Мале (< 30 піц, стандартна підготовка).
Чому це важливо?
Великі замовлення потребують:
- 👥 Додатковий персонал (планування за 2 години)
- 🔥 Спеціальна стратегія роботи печей
- 📦 Масова підготовка інгредієнтів
- 🚚 Резервування фургона для доставки
Вартість помилок:
-
False Negative (передбачили Мале, насправді Велике):
- Хаос на кухні, затримки, незадоволені клієнти
- Втрата: ~500 грн на замовлення
-
False Positive (передбачили Велике, насправді Мале):
- Марне витрачені години персоналу
- Втрата: ~200 грн на замовлення
Висновок: Нам потрібна модель з високим Recall (знайти всі великі замовлення), навіть якщо Precision буде трохи нижчою.
Наші дані
Ознаки (Features):
Features:
- temperature: Температура (°C)
- hour: Година доби (0-23)
- is_friday_evening: П'ятниця ввечері (0/1)
- is_peak_hour: Пікова година (0/1)
- is_weekend: Вихідний день (0/1)
- rolling_avg_3h: Середній продаж за останні 3 години
- price_per_pizza: Ціна за піцу ($)
Target (що прогнозуємо):
- order_size: "Large" (1) або "Small" (0)Реальні патерни, які ми очікуємо виявити:
- Холодна п’ятниця ввечері → 85% великих замовлень
- Вихідні + пікова година → 70% великих замовлень
- Вівторок вдень + спека → 90% малих замовлень
Як працюють дерева рішень
Аналогія з блок-схемою
Дерева рішень - це як блок-схеми, з якими всі знайомі. Почнемо з простого прикладу:
Чи брати парасольку? / Should I take an umbrella?
💡 Порада / Tip: Наведіть на вузли, щоб побачити деталі / Hover over nodes to see details
Дерево рішень задає послідовність питань і приймає рішення на основі відповідей. Це інтуїтивно і зрозуміло для людей!
Ще один приклад:
Чим зайнятись на вихідних? / Weekend activity?
💡 Порада / Tip: Наведіть на вузли, щоб побачити деталі / Hover over nodes to see details
Побудова дерева для піці (крок за кроком)
Тепер застосуймо це до нашої задачі з прогнозування великих замовлень піци. Подивимося, яке дерево може побудувати алгоритм:
Прогноз замовлення піци / Pizza Order Prediction
💡 Порада / Tip: Наведіть на вузли, щоб побачити деталі / Hover over nodes to see details
Компоненти дерева
Тепер, коли ви бачили інтерактивні дерева вище, давайте розберемо їх анатомію:
Термінологія:
- 🔵 Root Node (Кореневий вузол): Перше питання, з якого починаємо (білий вузол з синьою рамкою)
- 🔵 Internal Node (Внутрішній вузол): Проміжне питання (також білий з синьою рамкою)
- 🟢🔴 Leaf Node (Листок): Остаточне рішення з кольоровим тлом (фіолетовий = ВЕЛИКЕ, рожевий = МАЛЕ)
- ➡️ Branch (Гілка): З’єднання між вузлами (плавна крива лінія)
- 📏 Depth (Глибина): Кількість рівнів у дереві (від кореня до найглибшого листка)
💡 Порада: Наведіть курсор на вузли в інтерактивних деревах вище, щоб побачити ефекти!
Як алгоритм вирішує, що запитати?
Це ключове питання! Алгоритм:
-
Спробує всі можливі питання:
- “temperature < 10?” vs “temperature < 15?” vs “temperature < 20?”
- “is_friday_evening = 1?” vs “is_peak_hour = 1?”
-
Обчислить “корисність” кожного питання:
- Яке питання найкраще розділяє замовлення на Великі та Малі?
- Використовує метрику під назвою Information Gain (Приріст інформації)
-
Вибере найкраще питання:
- Питання з найвищим приростом інформації
-
Повторить процес для кожної гілки, доки не досягне критерію зупинки
Математика за магією - Спрощено
Приріст інформації (Information Gain)
Інтуїція: “Яке питання найбільше зменшує нашу невизначеність?”
До розділення:
- Маємо мішаний мішок замовлень: [Велике, Мале, Велике, Мале, Велике]
- Невизначеність висока (не знаємо, що витягнемо)
Після розділення:
- Ліва гілка: [Велике, Велике, Велике] ← Невизначеність низька
- Права гілка: [Мале, Мале] ← Невизначеність низька
Приріст інформації = Невизначеність до розділення - Невизначеність після розділення
Ентропія (Entropy) - Вимірювання невизначеності
Аналогія з мішком піци:
Уявіть, що у вас є мішок з квитанціями замовлень. Кожна квитанція позначена “Велике” або “Мале”. Ви засовуєте руку із зав’язаними очима і витягуєте одну. Наскільки ви здивовані тим, що отримали?
Сценарій 1: Чистий мішок (Entropy = 0)
Мішок: [Велике, Велике, Велике, Велике, Велике]
Ви витягуєте: Велике
Рівень здивування: 0 (ви знали, що це буде Велике!)
Ентропія: 0Сценарій 2: Максимальний хаос (Entropy = 1)
Мішок: [Велике, Мале, Велике, Мале, Велике, Мале]
Ви витягуєте: ???
Рівень здивування: Максимальний (не маєте уявлення!)
Ентропія: 1Сценарій 3: Переважно великі (Entropy ≈ 0.72)
Мішок: [Велике, Велике, Велике, Велике, Мале]
Ви витягуєте: Ймовірно Велике... але можливо Мале?
Рівень здивування: Середній
Ентропія: ≈ 0.72Математична формула (демістифікована):
Переклад:
- = ймовірність кожного класу
- запитує: “Скільки запитань так/ні потрібно для ідентифікації?”
- Знак мінус робить значення позитивним
Візуальне представлення:
Шкала ентропії:
0.0 ════════════════════════ 1.0
Чисто Змішано 50-50
[BBBB] [BBBM] [BBMM] [BMBM]де B = Велике, M = Мале
Коефіцієнт Джині (Gini Impurity)
Аналогія з кухнею піцерії:
“Уявіть, що ви випадково вибираєте два замовлення зі стосу. Яка ймовірність, що вони різних розмірів?”
Сценарій 1: Всі великі (Gini = 0)
Вибираєте два замовлення: Обидва великі
Ймовірність відмінності: 0%
Gini: 0Сценарій 2: 50-50 мікс (Gini = 0.5)
Вибираєте два замовлення: 50% шанс [Велике, Мале] або [Мале, Велике]
Ймовірність відмінності: 50%
Gini: 0.5Сценарій 3: 80% Великі, 20% Малі (Gini = 0.32)
Вибираєте два замовлення:
- 64% шанс обидва Великі [0.8 × 0.8]
- 4% шанс обидва Малі [0.2 × 0.2]
- 32% шанс різні [2 × 0.8 × 0.2]
Ймовірність відмінності: 32%
Gini: 0.32Математична формула (спрощена):
Для двох класів (Велике та Мале):
Приклад: 80% Великі, 20% Малі
Порівняння Entropy та Gini
| Замовлення | % Великі | % Малі | Entropy | Gini | Інтерпретація |
|---|---|---|---|---|---|
| [BBBBB] | 100% | 0% | 0.00 | 0.00 | Ідеально! Всі великі |
| [BBBB M] | 80% | 20% | 0.72 | 0.32 | Переважно великі |
| [BBB MM] | 60% | 40% | 0.97 | 0.48 | Трохи більше великих |
| [BB MMM] | 40% | 60% | 0.97 | 0.48 | Трохи більше малих |
| [BM BM BM] | 50% | 50% | 1.00 | 0.50 | Максимальний хаос! |
Висновок: “Вам не потрібно рахувати це вручну! Sklearn робить це автоматично. Просто зрозумійте: нижча нечистота = краще розділення.”
Інтерактивний калькулятор критеріїв розділення
Порівняння Gini vs Entropy
Інтерпретація / Interpretation
Ключові інсайти / Key Insights:
- ✅ Обидві метрики вимірюють "безлад" або "нечистоту"
- ✅ Both metrics measure "disorder" or "impurity"
- 📉 Нижче значення = краще (більш організовано)
- 📉 Lower value = better (more organized)
- 🎯 Gini швидше обчислюється (sklearn default)
- 🎯 Gini is faster to compute (sklearn default)
- 🔬 Entropy трохи точніша теоретично
- 🔬 Entropy is slightly more accurate theoretically
- 💡 На практиці різниця мінімальна (корел. ≈ 0.98)
- 💡 In practice, difference is minimal (corr. ≈ 0.98)
Побудова вашого першого дерева рішень
Тепер перейдемо до практики! Давайте побудуємо справжнє дерево рішень на наших даних про піцу.
Крок 1: Підготовка даних
import pandas as pd
import numpy as np
from sklearn.tree import DecisionTreeClassifier, plot_tree
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report, confusion_matrix, accuracy_score
import matplotlib.pyplot as plt
# Встановимо seed для відтворюваності
np.random.seed(42)
n_samples = 500
# Створюємо синтетичні дані про піцу
data = pd.DataFrame({
'temperature': np.random.uniform(0, 35, n_samples),
'is_friday_evening': np.random.binomial(1, 0.15, n_samples),
'is_peak_hour': np.random.binomial(1, 0.3, n_samples),
'is_weekend': np.random.binomial(1, 0.28, n_samples),
'rolling_avg_3h': np.random.uniform(15, 35, n_samples),
'price_per_pizza': np.random.uniform(10, 15, n_samples)
})
# Генеруємо реалістичний цільовий показник
# Великі замовлення (1) залежать від кількох факторів
data['order_size_score'] = (
(data['is_friday_evening'] * 15) + # П'ятниця сильно впливає
(data['is_peak_hour'] * 8) + # Пікова година
(-0.3 * data['temperature']) + # Холодна погода = більше піци
(0.5 * data['rolling_avg_3h']) + # Momentum
(data['is_weekend'] * 5) + # Вихідні
np.random.normal(0, 5, n_samples) # Випадковість
)
# Порогове значення: >= 30 = Велике, < 30 = Мале
data['order_size'] = (data['order_size_score'] >= 30).astype(int)
# Видаляємо допоміжну колонку
data = data.drop('order_size_score', axis=1)
print(f"📊 Набір даних створено: {len(data)} замовлень")
print(f"🍕 Великих замовлень: {data['order_size'].sum()} ({data['order_size'].mean()*100:.1f}%)")
print(f"🍕 Малих замовлень: {len(data) - data['order_size'].sum()} ({(1-data['order_size'].mean())*100:.1f}%)")Крок 2: Розділення на навчальний та тестовий набори
# Визначаємо ознаки та цільову змінну
features = ['temperature', 'is_friday_evening', 'is_peak_hour',
'is_weekend', 'rolling_avg_3h', 'price_per_pizza']
X = data[features]
y = data['order_size']
# Розділяємо дані (80% навчальні, 20% тестові)
X_train, X_test, y_train, y_test = train_test_split(
X, y,
test_size=0.2,
stratify=y, # Збалансоване розділення класів
random_state=42
)
print(f"✅ Навчальний набір: {len(X_train)} замовлень")
print(f"✅ Тестовий набір: {len(X_test)} замовлень")Крок 3: Навчання дерева рішень
# Створюємо і навчаємо дерево рішень
tree = DecisionTreeClassifier(
max_depth=4, # Обмежуємо глибину
min_samples_split=30, # Мінімум 30 зразків для розділення
min_samples_leaf=15, # Мінімум 15 зразків у листку
criterion='gini', # Використовуємо Gini (можна 'entropy')
random_state=42
)
# Навчаємо модель
tree.fit(X_train, y_train)
print("✅ Дерево рішень навчено!")
print(f"📏 Глибина дерева: {tree.get_depth()}")
print(f"🍃 Кількість листків: {tree.get_n_leaves()}")Крок 4: Візуалізація дерева
# Створюємо красиву візуалізацію дерева
plt.figure(figsize=(20, 10))
plot_tree(
tree,
feature_names=features,
class_names=['Мале', 'Велике'],
filled=True, # Кольорові вузли
rounded=True, # Заокруглені кути
fontsize=10,
proportion=True # Показати пропорції
)
plt.title("Дерево рішень для прогнозування розміру замовлення піци", fontsize=16, pad=20)
plt.tight_layout()
plt.savefig('decision_tree_pizza.png', dpi=300, bbox_inches='tight')
plt.show()
print("💾 Візуалізація збережена як 'decision_tree_pizza.png'")Крок 5: Текстове представлення дерева
from sklearn.tree import export_text
# Експортуємо правила дерева у текстовому вигляді
tree_rules = export_text(tree, feature_names=features)
print("\n📜 Правила дерева:")
print(tree_rules)Приклад виводу:
|--- is_friday_evening <= 0.50
| |--- temperature <= 15.20
| | |--- is_peak_hour <= 0.50
| | | |--- class: 0 (Мале)
| | |--- is_peak_hour > 0.50
| | | |--- class: 1 (Велике)
| |--- temperature > 15.20
| | |--- class: 0 (Мале)
|--- is_friday_evening > 0.50
| |--- rolling_avg_3h <= 25.00
| | |--- class: 0 (Мале)
| |--- rolling_avg_3h > 25.00
| | |--- class: 1 (Велике)Крок 6: Оцінка моделі (використовуємо метрики зі Статті 6!)
# Робимо прогнози на тестовому наборі
y_pred = tree.predict(X_test)
# Обчислюємо всі метрики з Article 6
print("📊 Продуктивність моделі:")
print(f"Accuracy: {accuracy_score(y_test, y_pred):.3f}")
# Детальний звіт
print("\n📋 Детальний звіт класифікації:")
print(classification_report(y_test, y_pred, target_names=['Мале', 'Велике']))
# Матриця плутанини
cm = confusion_matrix(y_test, y_pred)
print("\n🔢 Матриця плутанини:")
print(" Прогноз")
print(" Мале Велике")
print(f"Факт Мале {cm[0,0]:3d} {cm[0,1]:3d}")
print(f" Велике {cm[1,0]:3d} {cm[1,1]:3d}")
# Аналіз помилок з бізнес-перспективи
print("\n💼 Бізнес-вплив:")
print(f"False Positives (прогноз Велике, насправді Мале): {cm[0,1]}")
print(f" → Вартість: ~{cm[0,1] * 200} грн (марний персонал)")
print(f"False Negatives (прогноз Мале, насправді Велике): {cm[1,0]}")
print(f" → Вартість: ~{cm[1,0] * 500} грн (хаос на кухні)")
print(f"\n💰 Загальні втрати: ~{cm[0,1] * 200 + cm[1,0] * 500} грн")Крок 7: Робимо реальні прогнози
# Сценарій 1: Холодна п'ятниця ввечері
scenario_1 = pd.DataFrame({
'temperature': [8],
'is_friday_evening': [1],
'is_peak_hour': [1],
'is_weekend': [0],
'rolling_avg_3h': [28],
'price_per_pizza': [12.99]
})
prediction_1 = tree.predict(scenario_1)[0]
probability_1 = tree.predict_proba(scenario_1)[0]
print("🍕 Сценарій 1: Холодна п'ятниця ввечері")
print(f" Прогноз: {'ВЕЛИКЕ ЗАМОВЛЕННЯ' if prediction_1 == 1 else 'Мале замовлення'}")
print(f" Ймовірності: Мале {probability_1[0]:.1%}, Велике {probability_1[1]:.1%}")
print(f" 💡 Дія: {'Викликати додатковий персонал!' if prediction_1 == 1 else 'Стандартна підготовка'}")
# Сценарій 2: Спекотний вівторок вдень
scenario_2 = pd.DataFrame({
'temperature': [32],
'is_friday_evening': [0],
'is_peak_hour': [0],
'is_weekend': [0],
'rolling_avg_3h': [18],
'price_per_pizza': [11.50]
})
prediction_2 = tree.predict(scenario_2)[0]
probability_2 = tree.predict_proba(scenario_2)[0]
print("\n🍕 Сценарій 2: Спекотний вівторок вдень")
print(f" Прогноз: {'ВЕЛИКЕ ЗАМОВЛЕННЯ' if prediction_2 == 1 else 'Мале замовлення'}")
print(f" Ймовірності: Мале {probability_2[0]:.1%}, Велике {probability_2[1]:.1%}")
print(f" 💡 Дія: {'Викликати додатковий персонал!' if prediction_2 == 1 else 'Стандартна підготовка'}")Крок 8: Важливість ознак
# Дізнаємося, які ознаки найважливіші
importances = tree.feature_importances_
feature_importance_df = pd.DataFrame({
'feature': features,
'importance': importances
}).sort_values('importance', ascending=False)
print("\n📊 Важливість ознак:")
print(feature_importance_df)
# Візуалізація
plt.figure(figsize=(10, 6))
plt.barh(feature_importance_df['feature'], feature_importance_df['importance'], color='#667eea')
plt.xlabel('Важливість', fontsize=12)
plt.title('Які ознаки найбільше впливають на розмір замовлення?', fontsize=14, pad=15)
plt.grid(axis='x', alpha=0.3)
plt.tight_layout()
plt.show()Перенавчання в деревах рішень
Проблема запам’ятовування
Дерева рішень можуть рости нескінченно глибокими. Це призводить до перенавчання (overfitting):
- ✅ Ідеальна точність на навчальних даних
- ❌ Жахлива точність на нових даних
Аналогія: Студент, який запам’ятовує питання з екзамену замість розуміння концепцій.
Приклад:
Дерево з 20 рівнями:
"Якщо temperature = 18.3°C AND is_friday = 1 AND hour = 19
AND rolling_avg = 24.7 AND price = 12.45 → ВЕЛИКЕ"
Це працює для одного конкретного замовлення в минулому,
але марно для нових замовлень!Порівняння: Перенавчена vs Збалансована модель
# 1. Перенавчене дерево (занадто складне)
tree_overfit = DecisionTreeClassifier(
max_depth=None, # Без обмежень!
min_samples_split=2, # Розділяти навіть з 2 зразками
min_samples_leaf=1 # Листок може містити 1 зразок
)
tree_overfit.fit(X_train, y_train)
# 2. Збалансоване дерево (оптимальне)
tree_balanced = DecisionTreeClassifier(
max_depth=5,
min_samples_split=50,
min_samples_leaf=20
)
tree_balanced.fit(X_train, y_train)
# Порівняємо результати
print("🔴 Перенавчене дерево:")
print(f" Точність на навчальних даних: {tree_overfit.score(X_train, y_train):.3f}")
print(f" Точність на тестових даних: {tree_overfit.score(X_test, y_test):.3f}")
print(f" Глибина: {tree_overfit.get_depth()}")
print(f" Кількість листків: {tree_overfit.get_n_leaves()}")
print("\n✅ Збалансоване дерево:")
print(f" Точність на навчальних даних: {tree_balanced.score(X_train, y_train):.3f}")
print(f" Точність на тестових даних: {tree_balanced.score(X_test, y_test):.3f}")
print(f" Глибина: {tree_balanced.get_depth()}")
print(f" Кількість листків: {tree_balanced.get_n_leaves()}")
print("\n💡 Різниця (Overfitting Gap):")
overfit_gap_1 = tree_overfit.score(X_train, y_train) - tree_overfit.score(X_test, y_test)
overfit_gap_2 = tree_balanced.score(X_train, y_train) - tree_balanced.score(X_test, y_test)
print(f" Перенавчене: {overfit_gap_1:.3f} (ПОГАНО!)")
print(f" Збалансоване: {overfit_gap_2:.3f} (добре)")Типовий вивід:
🔴 Перенавчене дерево:
Точність на навчальних даних: 0.998 ← ЗАНАДТО ДОБРЕ!
Точність на тестових даних: 0.742
Глибина: 18
Кількість листків: 147
✅ Збалансоване дерево:
Точність на навчальних даних: 0.856
Точність на тестових даних: 0.834 ← Краща генералізація!
Глибина: 5
Кількість листків: 23
💡 Різниця (Overfitting Gap):
Перенавчене: 0.256 (ПОГАНО!)
Збалансоване: 0.022 (добре)Інтерактивне порівняння глибини
Вплив глибини дерева на перенавчання
Типові випадки / Common Cases:
Ключові гіперпараметри для контролю складності
| Параметр | Призначення | Типові значення |
|---|---|---|
max_depth | Максимальна глибина дерева | 3-10 для простих задач |
min_samples_split | Мінімум зразків для розділення вузла | 20-100 |
min_samples_leaf | Мінімум зразків у листковому вузлі | 10-50 |
max_leaf_nodes | Максимальна кількість листків | 10-50 |
criterion | Міра розділення | ’gini’ або ‘entropy’ |
min_impurity_decrease | Мінімальне зниження нечистоти для розділення | 0.0-0.01 |
Пошук оптимальної глибини
# Спробуємо різні глибини
depths = [1, 2, 3, 4, 5, 6, 8, 10, 15, 20]
train_scores = []
test_scores = []
for depth in depths:
tree = DecisionTreeClassifier(max_depth=depth, random_state=42)
tree.fit(X_train, y_train)
train_scores.append(tree.score(X_train, y_train))
test_scores.append(tree.score(X_test, y_test))
# Візуалізація кривої навчання
plt.figure(figsize=(10, 6))
plt.plot(depths, train_scores, 'o-', label='Навчальна точність', linewidth=2, markersize=8)
plt.plot(depths, test_scores, 's-', label='Тестова точність', linewidth=2, markersize=8)
plt.xlabel('Максимальна глибина', fontsize=12)
plt.ylabel('Точність', fontsize=12)
plt.title('Перенавчання в деревах рішень', fontsize=14, pad=15)
plt.legend(fontsize=11)
plt.grid(True, alpha=0.3)
# Відмічаємо оптимальну глибину
optimal_depth = depths[test_scores.index(max(test_scores))]
plt.axvline(optimal_depth, color='red', linestyle='--', linewidth=2,
label=f'Оптимальна глибина: {optimal_depth}')
plt.legend(fontsize=11)
plt.tight_layout()
plt.show()
print(f"🎯 Оптимальна глибина: {optimal_depth}")
print(f" Тестова точність: {max(test_scores):.3f}")Обрізання дерева (Pruning)
Два підходи:
-
Pre-pruning (Попереднє обрізання): Зупиняємо ріст рано
- Використовуємо
max_depth,min_samples_splitтощо - Це те, що ми робили вище
- ✅ Підтримується в sklearn
- Використовуємо
-
Post-pruning (Післяобрізання): Вирощуємо повне дерево, потім відрізаємо гілки
- Вирощуємо дерево до кінця
- Видаляємо гілки, які не поліпшують валідаційну точність
- ❌ Не підтримується нативно в sklearn
- 💡 Використовуйте
ccp_alpha(cost complexity pruning) як альтернативу
Приклад cost complexity pruning:
# Знаходимо оптимальний alpha
path = tree.cost_complexity_pruning_path(X_train, y_train)
ccp_alphas = path.ccp_alphas
# Тренуємо дерева з різними alpha
trees = []
for ccp_alpha in ccp_alphas:
tree = DecisionTreeClassifier(random_state=42, ccp_alpha=ccp_alpha)
tree.fit(X_train, y_train)
trees.append(tree)
# Знаходимо найкраще дерево
test_scores = [tree.score(X_test, y_test) for tree in trees]
optimal_tree = trees[test_scores.index(max(test_scores))]
print(f"✅ Оптимальне обрізане дерево:")
print(f" Alpha: {ccp_alphas[test_scores.index(max(test_scores))]:.5f}")
print(f" Тестова точність: {max(test_scores):.3f}")
print(f" Глибина: {optimal_tree.get_depth()}")Коли дерева відмінні (і коли ні)
Ідеальні випадки використання ✅
1. Нелінійні залежності
# Температура має складний вплив? Дерева впораються!
# Не потрібно створювати polynomial features вручнуПриклад:
- Холодна погода (< 10°C) → багато піци (комфортна їжа)
- Приємна погода (10-25°C) → середньо
- Спека (> 25°C) → мало піци (легкі салати)
Лінійна регресія потребує ручного створення цих зон. Дерева виявляють їх автоматично!
2. Взаємодія ознак
Автоматичне виявлення:
"Вплив п'ятниці залежить від температури"
→ Дерево автоматично створить:
IF friday AND cold → Large
IF friday AND hot → Small3. Змішані типи даних
- ✅ Числові ознаки: temperature, rolling_avg
- ✅ Категоріальні: is_friday, is_weekend
- ✅ Жодного попереднього кодування!
4. Інтерпретованість
Покажіть дерево вашому менеджеру - миттєве розуміння:
"Якщо це п'ятниця вечором І середній продаж > 25,
тоді ми отримаємо велике замовлення з 85% ймовірністю"5. Важливість ознак
feature_importances_ показує:
is_friday_evening: 0.42 ← Найважливіше!
rolling_avg_3h: 0.28
temperature: 0.15
is_peak_hour: 0.09
is_weekend: 0.04
price_per_pizza: 0.02 ← Майже не впливає6. Стійкість до викидів
Одне екстремальне замовлення (200 піц на весілля) не зруйнує дерево, як це могло б статися з лінійною регресією.
Обмеження ❌
1. Перенавчання
Найбільша проблема дерев!
- Дерева люблять запам’ятовувати
- Потребують ретельного налаштування гіперпараметрів
- Без контролю: глибина 20+, точність на тренуванні 99.9%, на тесті 60%
2. Нестабільність
Невелика зміна в даних → зовсім інше дерево!
# Додайте 10 нових замовлень → дерево може повністю змінитися
# Проблема для системи в productionРішення: Random Forests (Стаття 9) - комбінуємо багато дерев!
3. Лінійні залежності
Якщо залежність справді лінійна:
Sales = 2.5 × Temperature + 10Лінійна регресія знайде це за 1 секунду. Дерево рішень створить 50+ розділень, щоб наблизити пряму лінію!
4. Екстраполяція
Дерева не можуть прогнозувати поза діапазоном навчання:
# Навчальні дані: temperature від 0°C до 35°C
# Прогноз для 40°C: Дерево використає найближчий листок (35°C)
# Лінійна регресія: Екстраполює тренд5. Незбалансовані дані
Якщо 95% замовлень малі, 5% великі:
- Дерево схиляється до класу більшості
- Потребує class_weight=‘balanced’ або SMOTE
Порівняння: Лінійна регресія vs Дерева рішень
| Аспект | Лінійна регресія | Дерева рішень |
|---|---|---|
| Найкраще для | Лінійні залежності | Нелінійні патерни |
| Інтерпретованість | Коефіцієнти | Візуальна блок-схема |
| Ризик перенавчання | Низький | Високий |
| Взаємодія ознак | Ручна | Автоматична |
| Швидкість | Дуже швидка | Швидка |
| Екстраполяція | Так | Ні |
| Обробка викидів | Чутлива | Стійка |
| Змішані типи даних | Потребує кодування | Працює напряму |
| Регуляризація | L1, L2 | Обмеження глибини |
Інтерактивна побудова дерева
Спробуйте побудувати своє дерево рішень інтерактивно:
Побудуйте дерево рішень
Додавайте розділення і дивіться, як дерево вчиться класифікувати замовлення піци / Add splits and watch the tree learn to classify pizza orders
Додати розділення / Add Split:
Структура дерева / Tree Structure:
Легенда / Legend:
Практичний приклад: Повна оцінка
Давайте об’єднаємо все разом - побудуємо, оцінимо та проаналізуємо дерево:
import pandas as pd
import numpy as np
from sklearn.tree import DecisionTreeClassifier, plot_tree, export_text
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.metrics import (
accuracy_score, precision_score, recall_score, f1_score,
confusion_matrix, classification_report, roc_auc_score, roc_curve
)
import matplotlib.pyplot as plt
import seaborn as sns
# Встановлюємо стиль для графіків
sns.set_style("whitegrid")
np.random.seed(42)
# === 1. ПІДГОТОВКА ДАНИХ ===
print("=" * 50)
print("📊 КРОК 1: ПІДГОТОВКА ДАНИХ")
print("=" * 50)
# Створюємо реалістичні дані (можна завантажити ваші реальні дані)
n_samples = 500
data = pd.DataFrame({
'temperature': np.random.uniform(0, 35, n_samples),
'is_friday_evening': np.random.binomial(1, 0.15, n_samples),
'is_peak_hour': np.random.binomial(1, 0.3, n_samples),
'is_weekend': np.random.binomial(1, 0.28, n_samples),
'rolling_avg_3h': np.random.uniform(15, 35, n_samples),
'price_per_pizza': np.random.uniform(10, 15, n_samples)
})
# Генеруємо realistic target
data['order_size'] = (
(data['is_friday_evening'] * 15) +
(data['is_peak_hour'] * 8) +
(-0.3 * data['temperature']) +
(0.5 * data['rolling_avg_3h']) +
(data['is_weekend'] * 5) +
np.random.normal(0, 5, n_samples)
) >= 30
data['order_size'] = data['order_size'].astype(int)
print(f"✅ Набір даних: {len(data)} замовлень")
print(f" Великі: {data['order_size'].sum()} ({data['order_size'].mean()*100:.1f}%)")
print(f" Малі: {len(data) - data['order_size'].sum()}")
# === 2. НАВЧАННЯ МОДЕЛІ ===
print("\n" + "=" * 50)
print("🌲 КРОК 2: НАВЧАННЯ ДЕРЕВА РІШЕНЬ")
print("=" * 50)
features = ['temperature', 'is_friday_evening', 'is_peak_hour',
'is_weekend', 'rolling_avg_3h', 'price_per_pizza']
X = data[features]
y = data['order_size']
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, stratify=y, random_state=42
)
tree = DecisionTreeClassifier(
max_depth=4,
min_samples_split=30,
min_samples_leaf=15,
random_state=42
)
tree.fit(X_train, y_train)
print(f"✅ Модель навчена!")
print(f" Глибина: {tree.get_depth()}")
print(f" Листки: {tree.get_n_leaves()}")
# === 3. ОЦІНКА МОДЕЛІ (ВИКОРИСТОВУЄМО ARTICLE 6!) ===
print("\n" + "=" * 50)
print("📊 КРОК 3: ОЦІНКА МОДЕЛІ")
print("=" * 50)
y_pred = tree.predict(X_test)
y_prob = tree.predict_proba(X_test)[:, 1]
print("Базові метрики:")
print(f" Accuracy: {accuracy_score(y_test, y_pred):.3f}")
print(f" Precision: {precision_score(y_test, y_pred):.3f}")
print(f" Recall: {recall_score(y_test, y_pred):.3f}")
print(f" F1-Score: {f1_score(y_test, y_pred):.3f}")
print(f" ROC AUC: {roc_auc_score(y_test, y_prob):.3f}")
# Детальний звіт
print("\n📋 Детальний звіт:")
print(classification_report(y_test, y_pred, target_names=['Мале', 'Велике']))
# Матриця плутанини
cm = confusion_matrix(y_test, y_pred)
print("🔢 Матриця плутанини:")
print(f" Прогноз")
print(f" Мале Велике")
print(f"Факт Мале {cm[0,0]:3d} {cm[0,1]:3d}")
print(f" Велике {cm[1,0]:3d} {cm[1,1]:3d}")
# Бізнес-вплив
print("\n💼 Бізнес-вплив:")
print(f"False Positives: {cm[0,1]} → Втрата ~{cm[0,1] * 200} грн")
print(f"False Negatives: {cm[1,0]} → Втрата ~{cm[1,0] * 500} грн")
print(f"Загальні втрати: ~{cm[0,1] * 200 + cm[1,0] * 500} грн")
# === 4. КРОС-ВАЛІДАЦІЯ ===
print("\n" + "=" * 50)
print("🔄 КРОК 4: КРОС-ВАЛІДАЦІЯ")
print("=" * 50)
cv_scores = cross_val_score(tree, X, y, cv=5, scoring='f1')
print(f"F1-Score (5-fold CV): {cv_scores.mean():.3f} (+/- {cv_scores.std() * 2:.3f})")
# === 5. ВАЖЛИВІСТЬ ОЗНАК ===
print("\n" + "=" * 50)
print("📊 КРОК 5: ВАЖЛИВІСТЬ ОЗНАК")
print("=" * 50)
importances = pd.DataFrame({
'feature': features,
'importance': tree.feature_importances_
}).sort_values('importance', ascending=False)
print(importances.to_string(index=False))
# === 6. ПОЯСНЕННЯ ПРОГНОЗУ ===
print("\n" + "=" * 50)
print("💡 КРОК 6: ПОЯСНЕННЯ КОНКРЕТНОГО ПРОГНОЗУ")
print("=" * 50)
sample = X_test.iloc[[0]]
prediction = tree.predict(sample)[0]
probability = tree.predict_proba(sample)[0]
print("Вхідні дані:")
print(sample.to_string())
print(f"\nПрогноз: {'ВЕЛИКЕ' if prediction == 1 else 'МАЛЕ'}")
print(f"Ймовірності: Мале {probability[0]:.1%}, Велике {probability[1]:.1%}")
# Показуємо шлях через дерево
print("\nШлях через дерево:")
decision_path = tree.decision_path(sample).toarray()[0]
feature = tree.tree_.feature
threshold = tree.tree_.threshold
for node_id in decision_path.nonzero()[0]:
if tree.tree_.feature[node_id] != -2: # Not a leaf
feat_idx = feature[node_id]
feat_name = features[feat_idx]
feat_value = sample.iloc[0, feat_idx]
thresh = threshold[node_id]
if feat_value <= thresh:
print(f" ✓ {feat_name} = {feat_value:.2f} <= {thresh:.2f}")
else:
print(f" ✗ {feat_name} = {feat_value:.2f} > {thresh:.2f}")
print("\n✅ АНАЛІЗ ЗАВЕРШЕНО!")Зв’язок з іншими статтями
Зв’язок зі Статтею 5 (Лінійна регресія)
Той самий датасет, два підходи:
from sklearn.linear_model import LinearRegression
# Регресія (Стаття 5): прогнозуємо точну кількість
lr = LinearRegression()
lr.fit(X_train, data.loc[X_train.index, 'actual_pizzas_sold'])
print(f"Прогноз піц: {lr.predict(sample)[0]:.1f}")
# Класифікація (Стаття 7): прогнозуємо категорію
tree = DecisionTreeClassifier()
tree.fit(X_train, y_train)
print(f"Прогноз розміру: {'Велике' if tree.predict(sample)[0] == 1 else 'Мале'}")Коли використовувати що:
- Потрібне точне число (планування інвентарю) → Лінійна регресія
- Потрібне рішення так/ні (залучати персонал?) → Дерево рішень
Зв’язок зі Статтею 6 (Метрики оцінки)
Ми використали всі метрики зі Статті 6:
- ✅ Confusion Matrix
- ✅ Precision, Recall, F1
- ✅ ROC AUC
- ✅ Бізнес-інтерпретація помилок
Вибір правильної метрики:
- FN (пропустити велике замовлення) гірше за FP (хибна тривога)
- Фокус на Recall (знайти всі великі замовлення)
- Готові до трохи нижчого Precision
Анонс Статті 9 (Random Forests)
Дерева рішень потужні… але нестабільні!
Проблема: Змініть одну точку даних → зовсім інше дерево!
Рішення: Що якщо натренувати 100 різних дерев і проголосувати?
Це саме те, що роблять Random Forests!
У Статті 9 ви дізнаєтесь:
- Bagging: Навчання кількох дерев на випадкових підмножинах
- Feature randomness: Кожне дерево бачить різні ознаки
- Ensemble voting: Мудрість натовпу
- Чому Random Forests в топ-3 найпопулярніших алгоритмів
Прев’ю:
дерево_1: "Велике замовлення!" (впевненість: 65%)
дерево_2: "Мале замовлення!" (впевненість: 55%)
дерево_3: "Велике замовлення!" (впевненість: 70%)
...
дерево_100: "Велике замовлення!" (впевненість: 60%)
Фінальний прогноз: Велике замовлення!
(60 дерев проголосували за Велике, 40 за Мале)
→ Набагато надійніше, ніж будь-яке окреме дерево!Висновки та наступні кроки
Ключові висновки
-
Дерева рішень = Інтуїтивно:
- Візуальні блок-схеми
- Легко пояснити менеджменту
- Автоматичне виявлення патернів
-
Потужні, але потребують обережності:
- Обробляють нелінійні залежності
- Автоматична взаємодія ознак
- Але легко перенавчаються!
-
Контроль складності:
max_depth: 3-10 для більшості задачmin_samples_split: 20-100min_samples_leaf: 10-50- Завжди валідуйте на окремому тестовому наборі
-
Використовуйте з Article 6:
- Accuracy, Precision, Recall, F1
- Confusion Matrix для бізнес-інсайтів
- ROC AUC для незбалансованих даних
-
Обмеження:
- Перенавчання (потребує налаштування)
- Нестабільність (вирішується в Random Forests)
- Не екстраполюють
Що далі?
Стаття 8: Overfitting vs Underfitting - Finding the Sweet Spot
- Глибоке занурення в bias-variance tradeoff
- Крив і навчання
- Regularization для дерев
- Cross-validation strategies
Стаття 9: Ensemble Methods - Random Forests
- Комбінування кількох дерев
- Bagging та feature randomness
- Gradient Boosting (XGBoost, LightGBM)
- Чому ансамблі перемагають
Додаткові ресурси
-
Sklearn Decision Trees: https://scikit-learn.org/stable/modules/tree.html
-
Візуалізація дерев: http://www.r2d3.us/visual-intro-to-machine-learning-part-1/
-
Математика за деревами: “Introduction to Statistical Learning” - Chapter 8
-
Advanced Pruning: Breiman, Friedman, Olshen, Stone (1984) - “Classification and Regression Trees”