Кафедра ШІзики

Класифікація 101 - Дерева рішень

Кафедра ШІзики

Автор

30 хвилин

Час читання

28.10.2025

Дата публікації

Рівень:
Початківець
Теги: #дерева-рішень #класифікація #навчання-з-учителем #sklearn #ентропія

📌 Новачок у Python? Ця стаття використовує приклади коду на Python. Якщо ви раніше не працювали з Python, почніть з нашого гайду Налаштування Python для ML!

Класифікація 101 - Дерева рішень 🌲

Вступ: Від регресії до класифікації

У Статті 5 ми побудували нашу першу модель машинного навчання - лінійну регресію. Ми прогнозували скільки піц буде продано на основі температури. Це називається регресією - передбачення числа.

У Статті 6 ми навчилися оцінювати моделі за допомогою метрик, таких як Accuracy, Precision, Recall, та F1-score.

Тепер настав час зробити наступний крок: класифікація - передбачення категорії.

Приклади класифікаційних задач

У реальному житті:

  • 📧 Email: Спам чи не спам?
  • 🏥 Медицина: Хворий чи здоровий?
  • 💳 Банки: Схвалити кредит чи відхилити?
  • 🎬 Netflix: Сподобається фільм чи ні?

У нашій піцерії:

  • 🍕 Розмір замовлення: Велике (30+ піц) чи мале (< 30 піц)?
  • 👥 Клієнт: VIP чи звичайний?
  • Доставка: Пізно чи вчасно?
  • 🔄 Повторне замовлення: Замовить знову чи ні?

Сьогодні ми зосередимося на першій задачі: прогнозування розміру замовлення.

Чому дерева рішень?

Дерева рішень - це найбільш інтуїтивний алгоритм класифікації, тому що:

  1. Візуальні - виглядають як блок-схеми, які всі розуміють
  2. Інтерпретовані - можна пояснити кожне рішення
  3. Потужні - обробляють нелінійні патерни
  4. Основа - фундамент для 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?

Питання / Question
Результат / Result
Так / Yes
Ні / No

💡 Порада / Tip: Наведіть на вузли, щоб побачити деталі / Hover over nodes to see details

Дерево рішень задає послідовність питань і приймає рішення на основі відповідей. Це інтуїтивно і зрозуміло для людей!

Ще один приклад:

Чим зайнятись на вихідних? / Weekend activity?

Питання / Question
Результат / Result
Так / Yes
Ні / No

💡 Порада / Tip: Наведіть на вузли, щоб побачити деталі / Hover over nodes to see details

Побудова дерева для піці (крок за кроком)

Тепер застосуймо це до нашої задачі з прогнозування великих замовлень піци. Подивимося, яке дерево може побудувати алгоритм:

Прогноз замовлення піци / Pizza Order Prediction

Питання / Question
Результат / Result
Так / Yes
Ні / No

💡 Порада / Tip: Наведіть на вузли, щоб побачити деталі / Hover over nodes to see details

Компоненти дерева

Тепер, коли ви бачили інтерактивні дерева вище, давайте розберемо їх анатомію:

Термінологія:

  • 🔵 Root Node (Кореневий вузол): Перше питання, з якого починаємо (білий вузол з синьою рамкою)
  • 🔵 Internal Node (Внутрішній вузол): Проміжне питання (також білий з синьою рамкою)
  • 🟢🔴 Leaf Node (Листок): Остаточне рішення з кольоровим тлом (фіолетовий = ВЕЛИКЕ, рожевий = МАЛЕ)
  • ➡️ Branch (Гілка): З’єднання між вузлами (плавна крива лінія)
  • 📏 Depth (Глибина): Кількість рівнів у дереві (від кореня до найглибшого листка)

💡 Порада: Наведіть курсор на вузли в інтерактивних деревах вище, щоб побачити ефекти!

Як алгоритм вирішує, що запитати?

Це ключове питання! Алгоритм:

  1. Спробує всі можливі питання:

    • “temperature < 10?” vs “temperature < 15?” vs “temperature < 20?”
    • “is_friday_evening = 1?” vs “is_peak_hour = 1?”
  2. Обчислить “корисність” кожного питання:

    • Яке питання найкраще розділяє замовлення на Великі та Малі?
    • Використовує метрику під назвою Information Gain (Приріст інформації)
  3. Вибере найкраще питання:

    • Питання з найвищим приростом інформації
  4. Повторить процес для кожної гілки, доки не досягне критерію зупинки

Математика за магією - Спрощено

Приріст інформації (Information Gain)

Інтуїція: “Яке питання найбільше зменшує нашу невизначеність?”

До розділення:

  • Маємо мішаний мішок замовлень: [Велике, Мале, Велике, Мале, Велике]
  • Невизначеність висока (не знаємо, що витягнемо)

Після розділення:

  • Ліва гілка: [Велике, Велике, Велике] ← Невизначеність низька
  • Права гілка: [Мале, Мале] ← Невизначеність низька

Приріст інформації = Невизначеність до розділення - Невизначеність після розділення

Ентропія (Entropy) - Вимірювання невизначеності

Аналогія з мішком піци:

Уявіть, що у вас є мішок з квитанціями замовлень. Кожна квитанція позначена “Велике” або “Мале”. Ви засовуєте руку із зав’язаними очима і витягуєте одну. Наскільки ви здивовані тим, що отримали?

Сценарій 1: Чистий мішок (Entropy = 0)

Мішок: [Велике, Велике, Велике, Велике, Велике]
Ви витягуєте: Велике
Рівень здивування: 0 (ви знали, що це буде Велике!)
Ентропія: 0

Сценарій 2: Максимальний хаос (Entropy = 1)

Мішок: [Велике, Мале, Велике, Мале, Велике, Мале]
Ви витягуєте: ???
Рівень здивування: Максимальний (не маєте уявлення!)
Ентропія: 1

Сценарій 3: Переважно великі (Entropy ≈ 0.72)

Мішок: [Велике, Велике, Велике, Велике, Мале]
Ви витягуєте: Ймовірно Велике... але можливо Мале?
Рівень здивування: Середній
Ентропія: ≈ 0.72

Математична формула (демістифікована):

Entropy=iP(класi)×log2(P(класi))\text{Entropy} = - \sum_{i} P(\text{клас}_i) \times \log_2(P(\text{клас}_i))

Переклад:

  • P(клас)P(\text{клас}) = ймовірність кожного класу
  • log2\log_2 запитує: “Скільки запитань так/ні потрібно для ідентифікації?”
  • Знак мінус робить значення позитивним

Візуальне представлення:

Шкала ентропії:
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

Математична формула (спрощена):

Gini=1iP(класi)2\text{Gini} = 1 - \sum_{i} P(\text{клас}_i)^2

Для двох класів (Велике та Мале):

Gini=1(P(Велике)2+P(Мале)2)\text{Gini} = 1 - (P(\text{Велике})^2 + P(\text{Мале})^2)

Приклад: 80% Великі, 20% Малі

Gini=1(0.82+0.22)=1(0.64+0.04)=10.68=0.32\begin{align*} \text{Gini} &= 1 - (0.8^2 + 0.2^2) \\ &= 1 - (0.64 + 0.04) \\ &= 1 - 0.68 \\ &= 0.32 \end{align*}

Порівняння Entropy та Gini

Замовлення% Великі% МаліEntropyGiniІнтерпретація
[BBBBB]100%0%0.000.00Ідеально! Всі великі
[BBBB M]80%20%0.720.32Переважно великі
[BBB MM]60%40%0.970.48Трохи більше великих
[BB MMM]40%60%0.970.48Трохи більше малих
[BM BM BM]50%50%1.000.50Максимальний хаос!

Висновок: “Вам не потрібно рахувати це вручну! Sklearn робить це автоматично. Просто зрозумійте: нижча нечистота = краще розділення.”

Інтерактивний калькулятор критеріїв розділення

Порівняння Gini vs Entropy

Візуалізація мішка / Bag Visualization:
📊
Gini Impurity
0.500
1 - (P²_large + P²_small)
Діапазон / Range: 0.0 - 0.5
🔢
Entropy
1.000
-Σ P × log₂(P)
Діапазон / Range: 0.0 - 1.0

Інтерпретація / 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 (добре)

Інтерактивне порівняння глибини

Вплив глибини дерева на перенавчання

1 (Underfitted) 5 (Balanced) 20 (Overfitted)
📚
Training Accuracy
0.850
Test Accuracy
0.834
⚠️
Overfitting Gap
0.016
✅ Добре / Good
🌿
Tree Complexity
23
листків / leaves

Типові випадки / 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)

Два підходи:

  1. Pre-pruning (Попереднє обрізання): Зупиняємо ріст рано

    • Використовуємо max_depth, min_samples_split тощо
    • Це те, що ми робили вище
    • ✅ Підтримується в sklearn
  2. 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 → Small

3. Змішані типи даних

  • ✅ Числові ознаки: 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

Current Entropy: 1.000
Current Gini: 0.500
Accuracy: 50%
Splits: 0

Додати розділення / Add Split:

Структура дерева / Tree Structure:

Легенда / Legend:
Велике замовлення / Large Order
Мале замовлення / Small Order

Практичний приклад: Повна оцінка

Давайте об’єднаємо все разом - побудуємо, оцінимо та проаналізуємо дерево:

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 за Мале)
→ Набагато надійніше, ніж будь-яке окреме дерево!

Висновки та наступні кроки

Ключові висновки

  1. Дерева рішень = Інтуїтивно:

    • Візуальні блок-схеми
    • Легко пояснити менеджменту
    • Автоматичне виявлення патернів
  2. Потужні, але потребують обережності:

    • Обробляють нелінійні залежності
    • Автоматична взаємодія ознак
    • Але легко перенавчаються!
  3. Контроль складності:

    • max_depth: 3-10 для більшості задач
    • min_samples_split: 20-100
    • min_samples_leaf: 10-50
    • Завжди валідуйте на окремому тестовому наборі
  4. Використовуйте з Article 6:

    • Accuracy, Precision, Recall, F1
    • Confusion Matrix для бізнес-інсайтів
    • ROC AUC для незбалансованих даних
  5. Обмеження:

    • Перенавчання (потребує налаштування)
    • Нестабільність (вирішується в 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)
  • Чому ансамблі перемагають

Додаткові ресурси

  1. Sklearn Decision Trees: https://scikit-learn.org/stable/modules/tree.html

  2. Візуалізація дерев: http://www.r2d3.us/visual-intro-to-machine-learning-part-1/

  3. Математика за деревами: “Introduction to Statistical Learning” - Chapter 8

  4. Advanced Pruning: Breiman, Friedman, Olshen, Stone (1984) - “Classification and Regression Trees”