Logistic Regression Classification

Метрики якості класифікації

Confusion Matrix, Accuracy, Precision, Recall, F1-Score, ROC-AUC — як правильно оцінювати моделі класифікації та уникати типових помилок

Метрики якості класифікації

🎯 Мета підрозділу

  • Зрозуміти обмеження метрики Accuracy
  • Опанувати Confusion Matrix та її інтерпретацію
  • Вивчити метрики Precision, Recall, F1-Score
  • Познайомитися з ROC-кривою та AUC
  • Навчитися обирати правильну метрику для задачі
  • Зрозуміти проблему незбалансованих класів

🔑 Ключові терміни

  • Confusion Matrix — матриця помилок класифікації
  • Accuracy — загальна точність моделі
  • Precision — точність позитивних прогнозів
  • Recall (Sensitivity) — повнота виявлення позитивного класу
  • F1-Score — гармонійне середнє Precision та Recall
  • ROC-AUC — площа під ROC-кривою

Історія про систему виявлення раку

Лікарня розробила AI-систему для автоматичного аналізу рентгенівських знімків грудної клітки з метою раннього виявлення раку легень. Після навчання модель показала 95% точності на тестовій вибірці.

Керівництво лікарні в захваті: "95% — це чудовий результат! Впроваджуємо систему в роботу!"

Але головний онколог доктор Коваль насторожився та попросив детальніший звіт. Виявилося наступне:

📊 Статистика датасету

  • Здорові пацієнти: 950 знімків (95%)
  • Хворі на рак: 50 знімків (5%)
  • Загалом: 1000 знімків

🤖 Поведінка моделі

Модель завжди прогнозує "здоровий" для будь-якого знімка.

Результат: 950 правильних прогнозів з 1000 → 95% accuracy!

⚠️ Критична проблема

Модель не виявила жодного хворого пацієнта!

Усі 50 випадків раку пропущено. Система марна для медичної діагностики.

Урок: метрика Accuracy сама по собі може бути оманливою, особливо для незбалансованих датасетів. Потрібні додаткові метрики, щоб оцінити реальну якість класифікації.

Що не так з Accuracy?

Accuracy (точність) — це найпростіша та найінтуїтивніша метрика класифікації:

Завантаження...

Де:

  • TP (True Positive) — правильно передбачені позитивні випадки
  • TN (True Negative) — правильно передбачені негативні випадки
  • FP (False Positive) — хибнопозитивні (помилково визнані позитивними)
  • FN (False Negative) — хибнонегативні (пропущені позитивні випадки)

Проблема 1: Незбалансовані класи

Коли один клас домінує над іншим (наприклад, 95% на 5%), модель може досягти високої accuracy, просто завжди передбачаючи більшинський клас.

accuracy_paradox.ipynb
Python 3.11 (ipykernel)
Code
In [1]:
0.05s
import numpy as np
from sklearn.metrics import accuracy_score

# Дані: 950 здорових + 50 хворих
y_true = np.array([0] * 950 + [1] * 50)

# "Розумна" модель: завжди прогнозує клас 0
y_pred_naive = np.zeros(1000)

# "Ідеальна" модель: правильно класифікує всіх
y_pred_perfect = y_true.copy()

# Реальна модель: 90% правильно для кожного класу
y_pred_real = y_true.copy()
# Помиляється у 10% випадків для кожного класу
errors_class_0 = np.random.choice(950, size=95, replace=False)
errors_class_1 = np.random.choice(np.arange(950, 1000), size=5, replace=False)
y_pred_real[errors_class_0] = 1
y_pred_real[errors_class_1] = 0

print("Точність різних моделей:")
print(f"Наївна модель (завжди '0'): {accuracy_score(y_true, y_pred_naive):.1%}")
print(f"Реальна модель (90% для кожного класу): {accuracy_score(y_true, y_pred_real):.1%}")
print(f"Ідеальна модель: {accuracy_score(y_true, y_pred_perfect):.1%}")

import matplotlib.pyplot as plt
from sklearn.metrics import recall_score

names = ['Наївна', 'Реальна', 'Ідеальна']
preds = [y_pred_naive, y_pred_real, y_pred_perfect]
acc = [accuracy_score(y_true, p) for p in preds]
rec = [recall_score(y_true, p) for p in preds]

x = np.arange(len(names))
plt.figure(figsize=(9, 5))
plt.bar(x - 0.2, acc, 0.4, label='Accuracy', color='#3b82f6')
plt.bar(x + 0.2, rec, 0.4, label='Recall (клас "хворий")', color='#ef4444')
for i in range(len(names)):
    plt.text(x[i] - 0.2, acc[i] + 0.02, f'{acc[i]:.0%}', ha='center')
    plt.text(x[i] + 0.2, rec[i] + 0.02, f'{rec[i]:.0%}', ha='center')
plt.xticks(x, names)
plt.ylim(0, 1.15)
plt.title('Accuracy обманює на незбалансованих даних', fontsize=13)
plt.legend()
plt.show()
Out[1]:

Точність різних моделей:
Наївна модель (завжди '0'): 95.0%
Реальна модель (90% для кожного класу): 90.0%
Ідеальна модель: 100.0%
Парадокс: наївна модель, яка нічого не вміє, має вищу accuracy (95%), ніж розумна модель (90%), яка правильно класифікує 90% випадків у кожному класі!

Проблема 2: Не враховує ціну помилок

У різних задачах помилки мають різну вагу:

ЗадачаFalse Negative (пропустили)False Positive (хибна тривога)
Діагностика раку💀 Критично (пацієнт не отримає лікування)⚠️ Неприємно (зайві обстеження)
Детекція спаму😐 Трохи дратує (спам у поштовій скриньці)😡 Дуже погано (важливий лист втрачено)
Виявлення шахрайства💸 Втрата грошей😕 Незручність для клієнта
Прогноз відтоку клієнтів📉 Втрата клієнта💰 Вартість утримуючих акцій

Accuracy однаково штрафує за FN та FP, що не відповідає реальності.

Аналогія (Пожежна сигналізація):
  • False Negative: пожежа не виявлена, будинок згорів 🔥 → катастрофа!
  • False Positive: сигналізація спрацювала без пожежі 🚨 → незручність, але не критично.
Ці помилки не рівнозначні, але Accuracy їх рівномірно враховує.

Confusion Matrix: матриця помилок

Confusion Matrix (матриця плутанини) — це таблиця, яка детально показує, як саме модель помилялася, розбиваючи всі прогнози на чотири категорії.

Структура матриці для бінарної класифікації

Завантаження...

Чотири квадранти матриці

✅ True Positive (TP)

Правильно позитивні:

  • Факт: клас 1 (хворий)
  • Прогноз: клас 1 (хворий)
  • Результат: Правильно! Модель виявила хворобу.

✅ True Negative (TN)

Правильно негативні:

  • Факт: клас 0 (здоровий)
  • Прогноз: клас 0 (здоровий)
  • Результат: Правильно! Модель не помилилася.

❌ False Positive (FP)

Помилка I роду (хибна тривога):

  • Факт: клас 0 (здоровий)
  • Прогноз: клас 1 (хворий)
  • Результат: Помилка! Здорового визнали хворим.

❌ False Negative (FN)

Помилка II роду (пропуск):

  • Факт: клас 1 (хворий)
  • Прогноз: клас 0 (здоровий)
  • Результат: Помилка! Хворого не виявили.
Мнемонічне правило:
  • True/False — це про правильність прогнозу
  • Positive/Negative — це про сам прогноз (що модель сказала)
Наприклад, False Positive = модель сказала "Positive" (1), але це False (неправда).

Приклад побудови Confusion Matrix

confusion_matrix_example.ipynb
Python 3.11 (ipykernel)
Code
In [1]:
0.12s
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay

# Реальні діагнози (ground truth)
y_true = np.array([0, 0, 1, 1, 0, 1, 0, 1, 0, 0,
                   0, 1, 0, 0, 1, 1, 0, 1, 0, 0])

# Прогнози моделі
y_pred = np.array([0, 0, 1, 0, 0, 1, 1, 1, 0, 0,
                   0, 1, 0, 0, 0, 1, 0, 1, 0, 0])

# Побудова Confusion Matrix
cm = confusion_matrix(y_true, y_pred)

print("Confusion Matrix:")
print(cm)
print(f"\nTrue Negatives (TN): {cm[0, 0]}")
print(f"False Positives (FP): {cm[0, 1]}")
print(f"False Negatives (FN): {cm[1, 0]}")
print(f"True Positives (TP): {cm[1, 1]}")
Out[1]:
Confusion Matrix:
[[11  1]
 [ 2  6]]

True Negatives (TN): 11
False Positives (FP): 1
False Negatives (FN): 2
True Positives (TP): 6
In [2]:
0.18s
# Візуалізація Confusion Matrix
fig, ax = plt.subplots(figsize=(8, 6))
disp = ConfusionMatrixDisplay(confusion_matrix=cm,
                               display_labels=['Здоровий (0)', 'Хворий (1)'])
disp.plot(cmap='Blues', ax=ax, colorbar=False)
plt.title('Confusion Matrix: Діагностика хвороби', fontsize=14, pad=20)
plt.xlabel('Прогноз моделі', fontsize=12)
plt.ylabel('Справжній діагноз', fontsize=12)
plt.tight_layout()
plt.show()

# Інтерпретація
total = cm.sum()
correct = cm[0, 0] + cm[1, 1]
accuracy = correct / total

print(f"\nЗагальна кількість пацієнтів: {total}")
print(f"Правильних діагнозів: {correct} ({accuracy:.1%})")
print(f"Помилкових діагнозів: {total - correct}")
print(f"\nЗ них:")
print(f"  - Хибна тривога (FP): {cm[0, 1]} (здорового визнали хворим)")
print(f"  - Пропуск хвороби (FN): {cm[1, 0]} (хворого не виявили)")
Out[2]:

Загальна кількість пацієнтів: 20
Правильних діагнозів: 17 (85.0%)
Помилкових діагнозів: 3

З них:
  - Хибна тривога (FP): 1 (здорового визнали хворим)
  - Пропуск хвороби (FN): 2 (хворого не виявили)

Precision: точність позитивних прогнозів

Precision (точність) відповідає на питання: "Серед усіх випадків, які модель позначила як позитивні, скільки насправді були позитивними?"

Завантаження...

Інтуїтивне розуміння

Аналогія (Пошукова система):Ви шукаєте статті про "машинне навчання". Пошуковик видав 100 результатів. З них лише 70 насправді про ML, а 30 — про інші теми (помилкові спрацювання).Precision = 70 / 100 = 0.7 (70%)Це означає, що 70% результатів релевантні. Чим вищий Precision, тим менше "сміття" у видачі.

Коли важливий високий Precision?

ЗадачаЧому важливий Precision?
Детекція спамуЯкщо важливий лист потрапить у спам (FP), користувач пропустить важливу інформацію 😡
Рекомендаційна системаЯкщо рекомендувати погані товари (FP), користувач втратить довіру до системи 👎
Фінансові транзакціїБлокування легальної транзакції (FP) дратує клієнта та призводить до скарг 💳
Юридичні документиХибне позначення документа як "конфіденційний" (FP) блокує доступ для тих, кому він потрібен 📄
Загальне правило: Precision важливий, коли ціна хибної тривоги (False Positive) висока. Краще бути обережним та прогнозувати "позитивний" тільки тоді, коли справді впевнений.

Приклад обчислення Precision

precision_example.ipynb
Python 3 (ipykernel)
Code
In [1]:
from sklearn.metrics import precision_score

# З попереднього прикладу
y_true = np.array([0, 0, 1, 1, 0, 1, 0, 1, 0, 0,
                   0, 1, 0, 0, 1, 1, 0, 1, 0, 0])
y_pred = np.array([0, 0, 1, 0, 0, 1, 1, 1, 0, 0,
                   0, 1, 0, 0, 0, 1, 0, 1, 0, 0])

# Обчислення Precision
precision = precision_score(y_true, y_pred)

# Ручний підрахунок для перевірки
TP = ((y_true == 1) & (y_pred == 1)).sum()
FP = ((y_true == 0) & (y_pred == 1)).sum()
precision_manual = TP / (TP + FP)

print(f"Precision (sklearn): {precision:.3f}")
print(f"Precision (ручний): {precision_manual:.3f}")
print(f"\nІнтерпретація:")
print(f"Серед {TP + FP} прогнозів 'хворий', насправді хворих — {TP}")
print(f"Точність позитивних прогнозів: {precision:.1%}")

import matplotlib.pyplot as plt

plt.figure(figsize=(8, 3))
plt.barh(['Прогнози\n"хворий"'], [TP], color='#10b981', label=f'TP = {TP}')
plt.barh(['Прогнози\n"хворий"'], [FP], left=[TP], color='#ef4444', label=f'FP = {FP}')
plt.title(f'Precision = {TP}/{TP + FP} = {precision:.1%}', fontsize=13)
plt.xlabel('Кількість прогнозів')
plt.legend(loc='lower right')
plt.show()
Out[1]:

Precision (sklearn): 0.857
Precision (ручний): 0.857

Інтерпретація:
Серед 7 прогнозів 'хворий', насправді хворих — 6
Точність позитивних прогнозів: 85.7%

Recall: повнота виявлення позитивного класу

Recall (повнота, чутливість, sensitivity) відповідає на питання: "Серед усіх справжніх позитивних випадків, скільки модель змогла виявити?"

Завантаження...

Інтуїтивне розуміння

Аналогія (Пошукова система, продовження):У світі існує 200 статей про машинне навчання. Пошуковик видав 100 результатів, з яких 70 — правильні (TP).Це означає, що пошуковик знайшов 70 зі 200 існуючих статей.Recall = 70 / 200 = 0.35 (35%)Пошуковик виявив лише 35% релевантних документів. Решта 130 статей (65%) — пропущено (False Negatives).

Коли важливий високий Recall?

ЗадачаЧому важливий Recall?
Діагностика ракуПропустити хвороби (FN) — смертельно небезпечно! Треба виявити всіх хворих 🏥
Виявлення шахрайстваПропустити шахрайську транзакцію (FN) = втрата грошей 💸
Пошук зниклих людейНе знайти людину (FN) може коштувати життя 🚨
Виявлення вразливостей у кодіПропустити баг безпеки (FN) = потенційний злом системи 🔓
Загальне правило: Recall важливий, коли ціна пропуску (False Negative) висока. Краще перестрахуватися та перевірити більше випадків, навіть якщо частина з них виявиться хибною тривогою.

Приклад обчислення Recall

recall_example.ipynb
Python 3 (ipykernel)
Code
In [1]:
from sklearn.metrics import recall_score

# Обчислення Recall
recall = recall_score(y_true, y_pred)

# Ручний підрахунок
TP = ((y_true == 1) & (y_pred == 1)).sum()
FN = ((y_true == 1) & (y_pred == 0)).sum()
recall_manual = TP / (TP + FN)

print(f"Recall (sklearn): {recall:.3f}")
print(f"Recall (ручний): {recall_manual:.3f}")
print(f"\nІнтерпретація:")
print(f"Усього справжніх хворих: {TP + FN}")
print(f"Виявлено моделлю: {TP}")
print(f"Пропущено (FN): {FN}")
print(f"Повнота виявлення: {recall:.1%}")

import matplotlib.pyplot as plt

plt.figure(figsize=(8, 3))
plt.barh(['Справжні\nхворі'], [TP], color='#10b981', label=f'Виявлено TP = {TP}')
plt.barh(['Справжні\nхворі'], [FN], left=[TP], color='#f59e0b', label=f'Пропущено FN = {FN}')
plt.title(f'Recall = {TP}/{TP + FN} = {recall:.1%}', fontsize=13)
plt.xlabel('Кількість пацієнтів')
plt.legend(loc='lower right')
plt.show()
Out[1]:

Recall (sklearn): 0.750
Recall (ручний): 0.750

Інтерпретація:
Усього справжніх хворих: 8
Виявлено моделлю: 6
Пропущено (FN): 2
Повнота виявлення: 75.0%

Precision vs Recall: компроміс

Precision та Recall часто конфліктують між собою: покращення однієї метрики може погіршити іншу.

Чому виникає компроміс?

Recall можна штучно підвищити, знизивши поріг класифікації — модель почне частіше прогнозувати "позитивний" клас. Але це призведе до збільшення False Positives, що знизить Precision.

precision_recall_tradeoff.ipynb
Python 3.11 (ipykernel)
Code
In [1]:
0.14s
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import precision_score, recall_score
from sklearn.datasets import make_classification

# Генерація синтетичних даних
X, y = make_classification(n_samples=200, n_features=10, n_informative=8,
                           n_redundant=2, n_classes=2, weights=[0.7, 0.3],
                           random_state=42)

# Навчання моделі
model = LogisticRegression(random_state=42)
model.fit(X, y)

# Отримання ймовірностей
y_probs = model.predict_proba(X)[:, 1]

# Тестування різних порогів
thresholds = [0.3, 0.5, 0.7, 0.9]
results = []

for threshold in thresholds:
    y_pred = (y_probs >= threshold).astype(int)
    precision = precision_score(y, y_pred)
    recall = recall_score(y, y_pred)
    
    results.append({
        'Поріг': threshold,
        'Precision': f'{precision:.3f}',
        'Recall': f'{recall:.3f}',
        'Прогнозів "1"': y_pred.sum()
    })

import pandas as pd
results_df = pd.DataFrame(results)
print(results_df.to_string(index=False))

import matplotlib.pyplot as plt

ts = np.linspace(0.05, 0.95, 91)
prec = [precision_score(y, (y_probs >= t).astype(int), zero_division=0) for t in ts]
rec = [recall_score(y, (y_probs >= t).astype(int)) for t in ts]

plt.figure(figsize=(10, 6))
plt.plot(ts, prec, color='#3b82f6', linewidth=2.5, label='Precision')
plt.plot(ts, rec, color='#ef4444', linewidth=2.5, label='Recall')
for t in thresholds:
    plt.axvline(t, color='gray', linestyle=':', alpha=0.6)
plt.xlabel('Поріг класифікації', fontsize=12)
plt.ylabel('Значення метрики', fontsize=12)
plt.title('Компроміс Precision / Recall', fontsize=14)
plt.legend()
plt.grid(alpha=0.3)
plt.show()
Out[1]:

 Поріг Precision  Recall  Прогнозів "1"
    0.3     0.623   0.787             77
    0.5     0.773   0.557             44
    0.7     0.885   0.377             26
    0.9     1.000   0.148              9
Спостереження:
  • Низький поріг (0.3): висока повнота (79% хворих виявлено), але невисока точність (лише 62% прогнозів "хворий" правильні)
  • Високий поріг (0.9): висока точність (100% прогнозів вірні), але низька повнота (лише 15% хворих виявлено)

Як обрати баланс?

⚖️ Медицина (рак)

Пріоритет: Recall

Пропустити хворобу (FN) — смертельно. Хибна тривога (FP) — зайві обстеження, але не критично.

Стратегія: знизити поріг до 0.3–0.4, щоб виявити якомога більше хворих.

📧 Фільтр спаму

Пріоритет: Precision

Важливий лист у спам (FP) — дуже погано. Спам у вхідних (FN) — трохи дратує.

Стратегія: підвищити поріг до 0.7–0.8, щоб бути впевненим перед блокуванням.

🎯 Рекомендації товарів

Баланс

Погана рекомендація (FP) псує UX. Пропуск хорошого товару (FN) — упущена можливість.

Стратегія: використати F1-Score для балансу.


F1-Score: гармонійне середнє

F1-Score — це єдина метрика, яка поєднує Precision та Recall у одне число через гармонійне середнє.

Завантаження...

Чому гармонійне середнє, а не арифметичне?

Гармонійне середнє жорстко штрафує за низьке значення однієї з метрик:

f1_vs_arithmetic.ipynb
Python 3 (ipykernel)
Code
In [1]:
# Порівняння середніх
precision_cases = [
    (0.9, 0.9),  # Обидві метрики високі
    (0.9, 0.1),  # Precision високий, Recall низький
    (0.5, 0.5),  # Обидві середні
]

for p, r in precision_cases:
    arithmetic = (p + r) / 2
    harmonic = 2 * (p * r) / (p + r) if (p + r) > 0 else 0
    
    print(f"Precision={p:.1f}, Recall={r:.1f}")
    print(f"  Арифметичне середнє: {arithmetic:.3f}")
    print(f"  F1-Score (гармонійне): {harmonic:.3f}")
    print()

import matplotlib.pyplot as plt

labels = [f'P={p}, R={r}' for p, r in precision_cases]
arith = [(p + r) / 2 for p, r in precision_cases]
harm = [2 * p * r / (p + r) for p, r in precision_cases]

x = np.arange(len(labels))
plt.figure(figsize=(9, 5))
plt.bar(x - 0.2, arith, 0.4, label='Арифметичне середнє', color='#94a3b8')
plt.bar(x + 0.2, harm, 0.4, label='F1 (гармонійне)', color='#3b82f6')
for i in range(len(labels)):
    plt.text(x[i] - 0.2, arith[i] + 0.02, f'{arith[i]:.2f}', ha='center')
    plt.text(x[i] + 0.2, harm[i] + 0.02, f'{harm[i]:.2f}', ha='center')
plt.xticks(x, labels)
plt.ylim(0, 1.1)
plt.title('F1 жорстко штрафує дисбаланс метрик', fontsize=13)
plt.legend()
plt.show()
Out[1]:

Precision=0.9, Recall=0.9
  Арифметичне середнє: 0.900
  F1-Score (гармонійне): 0.900

Precision=0.9, Recall=0.1
  Арифметичне середнє: 0.500
  F1-Score (гармонійне): 0.180

Precision=0.5, Recall=0.5
  Арифметичне середнє: 0.500
  F1-Score (гармонійне): 0.500
Висновок: F1-Score не дозволяє "обдурити" систему, досягнувши високого значення однієї метрики за рахунок другої. Щоб отримати високий F1, потрібно збалансувати Precision та Recall.

Приклад обчислення F1-Score

f1_calculation.ipynb
Python 3 (ipykernel)
Code
In [1]:
from sklearn.metrics import f1_score, precision_score, recall_score

# З попередніх прикладів
precision = precision_score(y_true, y_pred)
recall = recall_score(y_true, y_pred)
f1 = f1_score(y_true, y_pred)

# Ручний підрахунок
f1_manual = 2 * (precision * recall) / (precision + recall)

print(f"Precision: {precision:.3f}")
print(f"Recall: {recall:.3f}")
print(f"F1-Score (sklearn): {f1:.3f}")
print(f"F1-Score (ручний): {f1_manual:.3f}")
print(f"\nІнтерпретація:")
print(f"Модель досягла балансу між точністю ({precision:.1%})")
print(f"та повнотою ({recall:.1%}) з F1 = {f1:.3f}")

import matplotlib.pyplot as plt

names = ['Precision', 'Recall', 'F1-Score']
vals = [precision, recall, f1]

plt.figure(figsize=(8, 5))
bars = plt.bar(names, vals, color=['#3b82f6', '#ef4444', '#8b5cf6'])
for bar, v in zip(bars, vals):
    plt.text(bar.get_x() + bar.get_width() / 2, v + 0.02, f'{v:.3f}', ha='center', fontsize=12)
plt.ylim(0, 1.1)
plt.title('F1 лежить між Precision та Recall', fontsize=13)
plt.show()
Out[1]:

Precision: 0.857
Recall: 0.750
F1-Score (sklearn): 0.800
F1-Score (ручний): 0.800

Інтерпретація:
Модель досягла балансу між точністю (85.7%)
та повнотою (75.0%) з F1 = 0.800

Узагальнена F-міра: Fβ-Score

Іноді потрібно більше уваги приділити Recall або Precision. Для цього існує узагальнена Fβ-Score:

Завантаження...

Де Завантаження... — вага важливості Recall відносно Precision:

  • β = 1: F1-Score (рівна вага)
  • β = 2: F2-Score (Recall в 2 рази важливіший за Precision)
  • β = 0.5: F0.5-Score (Precision в 2 рази важливіший за Recall)
fbeta_score.ipynb
Python 3 (ipykernel)
Code
In [1]:
from sklearn.metrics import fbeta_score

# Обчислення різних F-мір
f05 = fbeta_score(y_true, y_pred, beta=0.5)  # Пріоритет Precision
f1 = fbeta_score(y_true, y_pred, beta=1.0)   # Баланс
f2 = fbeta_score(y_true, y_pred, beta=2.0)   # Пріоритет Recall

print(f"F0.5 (пріоритет Precision): {f05:.3f}")
print(f"F1   (баланс):              {f1:.3f}")
print(f"F2   (пріоритет Recall):    {f2:.3f}")
print(f"\nPrecision: {precision:.3f}")
print(f"Recall:    {recall:.3f}")

import matplotlib.pyplot as plt

names = ['Precision', 'F0.5', 'F1', 'F2', 'Recall']
vals = [precision, f05, f1, f2, recall]
colors = ['#3b82f6', '#60a5fa', '#8b5cf6', '#f87171', '#ef4444']

plt.figure(figsize=(9, 5))
bars = plt.bar(names, vals, color=colors)
for bar, v in zip(bars, vals):
    plt.text(bar.get_x() + bar.get_width() / 2, v + 0.01, f'{v:.3f}', ha='center', fontsize=12)
plt.ylim(0.6, 0.92)
plt.title('Fβ зміщується до Precision (β<1) або Recall (β>1)', fontsize=13)
plt.show()
Out[1]:

F0.5 (пріоритет Precision): 0.833
F1   (баланс):              0.800
F2   (пріоритет Recall):    0.769

Precision: 0.857
Recall:    0.750
Коли використовувати:
  • F0.5 — для задач, де FP дорожче за FN (спам-фільтр, юридичні документи)
  • F1 — загальний випадок, коли обидві помилки рівнозначні
  • F2 — для задач, де FN дорожче за FP (медицина, безпека)

ROC-крива та AUC: оцінка якості моделі

ROC (Receiver Operating Characteristic) крива — це графік, який показує залежність між True Positive Rate (TPR) та False Positive Rate (FPR) при зміні порогу класифікації.

AUC (Area Under the Curve) — площа під ROC-кривою, яка узагальнює якість моделі одним числом від 0 до 1.

Визначення TPR та FPR

Завантаження...
Завантаження...
Інтуїтивне розуміння:
  • TPR (Recall): яка частка справжніх позитивних випадків правильно виявлена
  • FPR: яка частка справжніх негативних випадків помилково класифікована як позитивні
Ідеальна модель: TPR = 1.0 (виявляє всіх хворих), FPR = 0.0 (не помиляється зі здоровими).

Побудова ROC-кривої

roc_curve_visualization.ipynb
Python 3.11 (ipykernel)
Code
In [1]:
0.15s
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_curve, roc_auc_score

# Генерація даних
X, y = make_classification(n_samples=500, n_features=10, n_informative=8,
                           n_classes=2, weights=[0.6, 0.4], random_state=42)

# Розділення на навчальну та тестову вибірки
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.3, random_state=42, stratify=y
)

# Навчання моделі
model = LogisticRegression(random_state=42)
model.fit(X_train, y_train)

# Отримання ймовірностей для побудови ROC
y_probs = model.predict_proba(X_test)[:, 1]

print(f"Навчальна вибірка: {X_train.shape}")
print(f"Тестова вибірка: {X_test.shape}")
print(f"Розподіл класів у тесті: {np.bincount(y_test)}")
Out[1]:
Навчальна вибірка: (350, 10)
Тестова вибірка: (150, 10)
Розподіл класів у тесті: [90 60]
In [2]:
0.22s
# Обчислення TPR та FPR для різних порогів
fpr, tpr, thresholds = roc_curve(y_test, y_probs)
auc_score = roc_auc_score(y_test, y_probs)

# Побудова графіка
plt.figure(figsize=(10, 7))

# ROC-крива моделі
plt.plot(fpr, tpr, linewidth=2.5, color='#3b82f6', 
         label=f'Logistic Regression (AUC = {auc_score:.3f})')

# Діагональ (випадкова модель)
plt.plot([0, 1], [0, 1], 'k--', linewidth=2, alpha=0.7,
         label='Випадкова модель (AUC = 0.5)')

# Ідеальна модель
plt.plot([0, 0, 1], [0, 1, 1], 'g:', linewidth=2, alpha=0.7,
         label='Ідеальна модель (AUC = 1.0)')

plt.xlabel('False Positive Rate (FPR)', fontsize=12)
plt.ylabel('True Positive Rate (TPR / Recall)', fontsize=12)
plt.title('ROC-крива', fontsize=14, pad=15)
plt.legend(loc='lower right', fontsize=11)
plt.grid(alpha=0.3)
plt.xlim([-0.02, 1.02])
plt.ylim([-0.02, 1.02])
plt.tight_layout()
plt.show()

print(f"\nAUC (Area Under Curve): {auc_score:.3f}")
Out[2]:

AUC (Area Under Curve): 0.946

Інтерпретація AUC

AUC = 1.0
Ідеальна модель. Існує поріг, при якому модель ідеально розділяє класи (TPR=1, FPR=0).
AUC > 0.9
Відмінна модель. Дуже висока якість класифікації. Підходить для production.
0.8 < AUC ≤ 0.9
Хороша модель. Прийнятна якість для більшості задач. Можна покращити налаштуванням гіперпараметрів.
0.7 < AUC ≤ 0.8
Задовільна модель. Працює краще за випадкове вгадування, але є простір для покращення.
0.5 < AUC ≤ 0.7
Слабка модель. Лише трохи краща за випадкову. Потрібні додаткові ознаки або інший алгоритм.
AUC = 0.5
Випадкова модель. Еквівалент кидка монети. Модель не навчилася нічого корисного.
AUC < 0.5
Анти-модель. Гірше за випадкове вгадування. Можливо, мітки класів перемішані.
Переваги AUC:
  • Незалежна від порогу: не потрібно обирати конкретний поріг
  • Стійка до незбалансованих класів: краще за Accuracy для imbalanced datasets
  • Одне число: зручно порівнювати моделі між собою
Недоліки AUC:
  • Не показує, як модель працює при конкретному порозі
  • Може бути оманливою, якщо вартість FP та FN сильно відрізняється

Precision-Recall крива

Для сильно незбалансованих датасетів (наприклад, 99% класу 0, 1% класу 1) ROC-крива може бути занадто оптимістичною. У таких випадках краще використовувати Precision-Recall (PR) криву.

precision_recall_curve.ipynb
Python 3.11 (ipykernel)
Code
In [1]:
0.18s
from sklearn.metrics import precision_recall_curve, average_precision_score

# Створення сильно незбалансованого датасету (95% : 5%)
X_imb, y_imb = make_classification(n_samples=1000, n_features=10, n_informative=8,
                                    n_classes=2, weights=[0.95, 0.05], random_state=42)

X_train_imb, X_test_imb, y_train_imb, y_test_imb = train_test_split(
    X_imb, y_imb, test_size=0.3, random_state=42, stratify=y_imb
)

# Навчання моделі
model_imb = LogisticRegression(random_state=42, max_iter=1000)
model_imb.fit(X_train_imb, y_train_imb)
y_probs_imb = model_imb.predict_proba(X_test_imb)[:, 1]

# Обчислення метрик
precision, recall, pr_thresholds = precision_recall_curve(y_test_imb, y_probs_imb)
avg_precision = average_precision_score(y_test_imb, y_probs_imb)

fpr_imb, tpr_imb, roc_thresholds = roc_curve(y_test_imb, y_probs_imb)
auc_imb = roc_auc_score(y_test_imb, y_probs_imb)

print(f"Розподіл класів у тестовій вибірці:")
print(f"  Клас 0: {(y_test_imb == 0).sum()} ({(y_test_imb == 0).mean():.1%})")
print(f"  Клас 1: {(y_test_imb == 1).sum()} ({(y_test_imb == 1).mean():.1%})")
print(f"\nROC-AUC: {auc_imb:.3f}")
print(f"Average Precision: {avg_precision:.3f}")
Out[1]:
Розподіл класів у тестовій вибірці:
  Клас 0: 285 (95.0%)
  Клас 1: 15 (5.0%)

ROC-AUC: 0.976
Average Precision: 0.743
In [2]:
0.25s
# Візуалізація обох кривих
fig, axes = plt.subplots(1, 2, figsize=(14, 6))

# ROC-крива
axes[0].plot(fpr_imb, tpr_imb, linewidth=2.5, color='#3b82f6',
             label=f'AUC = {auc_imb:.3f}')
axes[0].plot([0, 1], [0, 1], 'k--', linewidth=2, alpha=0.7)
axes[0].set_xlabel('False Positive Rate', fontsize=12)
axes[0].set_ylabel('True Positive Rate', fontsize=12)
axes[0].set_title('ROC-крива (здається відмінно!)', fontsize=13)
axes[0].legend(loc='lower right')
axes[0].grid(alpha=0.3)

# Precision-Recall крива
axes[1].plot(recall, precision, linewidth=2.5, color='#f59e0b',
             label=f'AP = {avg_precision:.3f}')
axes[1].axhline(y=(y_test_imb == 1).mean(), color='k', linestyle='--',
                linewidth=2, alpha=0.7, label='Baseline (випадкова)')
axes[1].set_xlabel('Recall', fontsize=12)
axes[1].set_ylabel('Precision', fontsize=12)
axes[1].set_title('PR-крива (реалістичніша оцінка)', fontsize=13)
axes[1].legend(loc='lower left')
axes[1].grid(alpha=0.3)

plt.tight_layout()
plt.show()
Out[2]:
Спостереження: ROC-AUC = 0.976 виглядає чудово, але Average Precision = 0.743 показує реальну картину. Для незбалансованих датасетів PR-крива точніше відображає якість моделі на меншинському класі.

Specificity та інші метрики

Крім основних метрик, існують додаткові показники, корисні в певних контекстах.

Specificity (True Negative Rate)

Specificity — це аналог Recall для негативного класу:

Завантаження...

Відповідає на питання: "Яка частка справжніх негативних випадків правильно визначена?"

specificity_example.ipynb
Python 3 (ipykernel)
Code
In [1]:
from sklearn.metrics import confusion_matrix

# Обчислення Specificity вручну
cm = confusion_matrix(y_test, model.predict(X_test))
TN, FP, FN, TP = cm.ravel()

specificity = TN / (TN + FP)
sensitivity = TP / (TP + FN)  # Це той самий Recall

print(f"Confusion Matrix:")
print(cm)
print(f"\nSensitivity (Recall для класу 1): {sensitivity:.3f}")
print(f"Specificity (Recall для класу 0): {specificity:.3f}")
print(f"\nІнтерпретація:")
print(f"  Серед {TP + FN} хворих виявлено {TP} ({sensitivity:.1%})")
print(f"  Серед {TN + FP} здорових правильно визначено {TN} ({specificity:.1%})")

import matplotlib.pyplot as plt

fig, ax = plt.subplots(figsize=(6.5, 5.5))
ax.imshow(cm, cmap='Blues')
for i in range(2):
    for j in range(2):
        ax.text(j, i, cm[i, j], ha='center', va='center', fontsize=22,
                color='white' if cm[i, j] > 50 else '#1f2937')
ax.set_xticks([0, 1]); ax.set_xticklabels(['Здоровий', 'Хворий'])
ax.set_yticks([0, 1]); ax.set_yticklabels(['Здоровий', 'Хворий'])
ax.set_xlabel('Прогноз'); ax.set_ylabel('Факт')
ax.set_title(f'Specificity = {specificity:.1%}  |  Sensitivity = {sensitivity:.1%}', fontsize=12)
plt.show()
Out[1]:

Confusion Matrix:
[[85  5]
 [ 3 57]]

Sensitivity (Recall для класу 1): 0.950
Specificity (Recall для класу 0): 0.944

Інтерпретація:
  Серед 60 хворих виявлено 57 (95.0%)
  Серед 90 здорових правильно визначено 85 (94.4%)

Negative Predictive Value (NPV)

NPV — це аналог Precision для негативних прогнозів:

Завантаження...

Відповідає на питання: "Серед усіх прогнозів 'негативний', скільки справді були негативними?"

Зведена таблиця метрик

МетрикаФормулаПитанняКоли важлива
PrecisionЗавантаження...Скільки з прогнозів "+1" правильні?Висока ціна FP
Recall (Sensitivity)Завантаження...Скільки справжніх "+1" виявлено?Висока ціна FN
SpecificityЗавантаження...Скільки справжніх "0" правильно визначено?Баланс обох класів
NPVЗавантаження...Скільки з прогнозів "0" правильні?Важлива якість негативних прогнозів
F1-ScoreЗавантаження...Баланс Precision та RecallЗагальна оцінка
AccuracyЗавантаження...Скільки всього правильних прогнозів?Збалансовані класи

Classification Report: комплексний звіт

Sklearn надає зручний інструмент classification_report, який виводить усі основні метрики одним викликом.

classification_report_example.ipynb
Python 3.11 (ipykernel)
Code
In [1]:
0.08s
from sklearn.metrics import classification_report

# Прогнози моделі
y_pred = model.predict(X_test)

# Генерація звіту
report = classification_report(y_test, y_pred, 
                               target_names=['Здоровий', 'Хворий'])

print(report)

import matplotlib.pyplot as plt

rep = classification_report(y_test, y_pred, target_names=['Здоровий', 'Хворий'],
                            output_dict=True)
classes = ['Здоровий', 'Хворий']
metrics = ['precision', 'recall', 'f1-score']
colors = ['#3b82f6', '#ef4444', '#8b5cf6']

x = np.arange(len(classes))
plt.figure(figsize=(9, 5))
for k, (m, c) in enumerate(zip(metrics, colors)):
    vals = [rep[cl][m] for cl in classes]
    plt.bar(x + (k - 1) * 0.25, vals, 0.25, label=m, color=c)
    for xi, v in zip(x, vals):
        plt.text(xi + (k - 1) * 0.25, v + 0.005, f'{v:.2f}', ha='center', fontsize=10)
plt.xticks(x, classes)
plt.ylim(0.85, 1.0)
plt.title('Метрики по класах (classification_report)', fontsize=13)
plt.legend()
plt.show()
Out[1]:

              precision    recall  f1-score   support

    Здоровий       0.97      0.94      0.95        90
      Хворий       0.92      0.95      0.93        60

    accuracy                           0.95       150
   macro avg       0.94      0.95      0.94       150
weighted avg       0.95      0.95      0.95       150

Розшифровка рядків звіту

Рядки для кожного класу
Показують Precision, Recall та F1-Score окремо для класу 0 та класу 1. Support — кількість об'єктів цього класу в тестовій вибірці.
accuracy
Загальна точність моделі (правильні прогнози / всі прогнози).
macro avg
Незважене середнє метрик по класах. Кожен клас має однакову вагу, незалежно від кількості об'єктів.
weighted avg
Зважене середнє по support. Класи з більшою кількістю об'єктів мають більшу вагу.
Що використовувати:
  • macro avg — коли класи рівнозначні (навіть якщо їх кількість різна)
  • weighted avg — коли важливіший більший клас

Проблема незбалансованих класів

Незбалансовані класи (imbalanced classes) — це ситуація, коли один клас зустрічається значно частіше за інший (наприклад, 95% на 5%).

Чому це проблема?

  1. Модель схильна передбачати більшинський клас: вона отримує високу accuracy, просто завжди прогнозуючи клас 0
  2. Меншинський клас (часто найважливіший!) ігнорується: наприклад, в медицині хворі — це меншість, але їх виявлення критично важливе
  3. Стандартні метрики (Accuracy) оманливі

Стратегії боротьби з незбалансованістю

Рекомендації:
  1. Завжди починайте з аналізу розподілу класів
  2. Не покладайтеся лише на Accuracy — використовуйте Recall, Precision, F1, AUC
  3. Спробуйте class_weight='balanced' — це найпростіший та часто ефективний спосіб
  4. Для екстремальної незбалансованості (99:1) — SMOTE + ансамблі

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

Розглянемо повний пайплайн оцінювання кількох моделей на одному датасеті.

model_comparison.ipynb
Python 3.11 (ipykernel)
Code
In [1]:
0.28s
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, roc_auc_score

# Навчання трьох різних моделей
models = {
    'Logistic Regression': LogisticRegression(random_state=42, max_iter=1000),
    'Decision Tree': DecisionTreeClassifier(random_state=42, max_depth=5),
    'Random Forest': RandomForestClassifier(random_state=42, n_estimators=100)
}

results = []

for name, model in models.items():
    # Навчання
    model.fit(X_train, y_train)
    
    # Прогнози
    y_pred = model.predict(X_test)
    y_probs = model.predict_proba(X_test)[:, 1]
    
    # Метрики
    results.append({
        'Model': name,
        'Accuracy': accuracy_score(y_test, y_pred),
        'Precision': precision_score(y_test, y_pred),
        'Recall': recall_score(y_test, y_pred),
        'F1-Score': f1_score(y_test, y_pred),
        'ROC-AUC': roc_auc_score(y_test, y_probs)
    })

# Порівняльна таблиця
import pandas as pd
results_df = pd.DataFrame(results)
print(results_df.to_string(index=False))
Out[1]:
                Model  Accuracy  Precision  Recall  F1-Score  ROC-AUC
 Logistic Regression     0.947      0.919   0.950     0.934    0.976
        Decision Tree     0.913      0.871   0.917     0.893    0.911
        Random Forest     0.953      0.935   0.950     0.942    0.984
In [2]:
0.25s
# Побудова графіка порівняння
fig, ax = plt.subplots(figsize=(12, 6))

metrics = ['Accuracy', 'Precision', 'Recall', 'F1-Score', 'ROC-AUC']
x = np.arange(len(metrics))
width = 0.25

for i, (name, row) in enumerate(results_df.iterrows()):
    values = [row[m] for m in metrics]
    ax.bar(x + i * width, values, width, label=row['Model'], alpha=0.8)

ax.set_xlabel('Метрики', fontsize=12)
ax.set_ylabel('Значення', fontsize=12)
ax.set_title('Порівняння моделей за різними метриками', fontsize=14, pad=15)
ax.set_xticks(x + width)
ax.set_xticklabels(metrics)
ax.legend()
ax.set_ylim([0.85, 1.0])
ax.grid(axis='y', alpha=0.3)
plt.tight_layout()
plt.show()

# Висновок
best_model = results_df.loc[results_df['ROC-AUC'].idxmax(), 'Model']
print(f"\n🏆 Найкраща модель за ROC-AUC: {best_model}")
Out[2]:

🏆 Найкраща модель за ROC-AUC: Random Forest

Типові помилки при оцінюванні моделей


Резюме статті

✅ Accuracy

Найпростіша метрика, але оманлива для незбалансованих класів. Використовуйте обережно.

✅ Confusion Matrix

Фундамент аналізу: TP, TN, FP, FN. Показує, як саме модель помиляється.

✅ Precision

Точність позитивних прогнозів. Важлива, коли FP дорогі (спам, фінанси).

✅ Recall

Повнота виявлення. Критична, коли FN дорогі (медицина, безпека).

✅ F1-Score

Гармонійне середнє Precision та Recall. Баланс для більшості задач.

✅ ROC-AUC

Площа під ROC-кривою. Оцінка якості моделі незалежно від порогу.

✅ PR-крива

Краща за ROC для сильно незбалансованих датасетів.

✅ Незбалансованість

Використовуйте class_weight='balanced', SMOTE або налаштування порогу.

Наступна стаття: У наступному розділі ми детально вивчимо функцію втрат логістичної регресії (Log Loss / Cross-Entropy), розберемо градієнтний спуск для навчання моделі та дізнаємося про регуляризацію (L1, L2). Ви зрозумієте математику "під капотом" логістичної регресії.

Інтерактивні запитання для самоперевірки


Практичні завдання


Контрольний чеклист: як правильно оцінити модель класифікації

Крок 1: Перевірте розподіл класів

print(np.bincount(y_train))
print(f"Клас 0: {(y_train == 0).mean():.1%}")
print(f"Клас 1: {(y_train == 1).mean():.1%}")

Якщо класи незбалансовані (>70% на <30%), будьте обережні з Accuracy.

Крок 2: Побудуйте Confusion Matrix

from sklearn.metrics import ConfusionMatrixDisplay
ConfusionMatrixDisplay.from_estimator(model, X_test, y_test)
plt.show()

Візуально оцініть співвідношення TP, TN, FP, FN.

Крок 3: Обчисліть основні метрики

from sklearn.metrics import classification_report
print(classification_report(y_test, y_pred, 
                           target_names=['Клас 0', 'Клас 1']))

Дивіться на Precision, Recall, F1 для кожного класу окремо.

Крок 4: Оцініть ROC-AUC або PR-AUC

# Для збалансованих класів
roc_auc = roc_auc_score(y_test, y_probs)

# Для незбалансованих класів
from sklearn.metrics import average_precision_score
pr_auc = average_precision_score(y_test, y_probs)

Крок 5: Налаштуйте поріг (якщо потрібно)

# Перевірте різні пороги
for threshold in [0.3, 0.5, 0.7]:
    y_pred_custom = (y_probs >= threshold).astype(int)
    print(f"Поріг {threshold}: Recall = {recall_score(y_test, y_pred_custom):.2f}")

Крок 6: Порівняйте з baseline

Створіть просту baseline-модель (наприклад, завжди передбачає більшинський клас) і переконайтеся, що ваша модель значно краща.


Фінальне резюме

🎯 Головний урок

Accuracy — це не достатньо! Завжди аналізуйте Confusion Matrix та метрики для кожного класу окремо.

📐 Вибір метрики

  • Медицина, безпека: Recall (не пропустити FN)
  • Спам, фінанси: Precision (не допустити FP)
  • Загальний випадок: F1-Score (баланс)

⚖️ Незбалансованість

Використовуйте class_weight='balanced', SMOTE або налаштування порогу. Перевіряйте PR-криву замість ROC.

📊 Порівняння моделей

Порівнюйте не одну метрику, а набір: Precision, Recall, F1, AUC. Будуйте таблиці та графіки.

Тепер ви готові професійно оцінювати моделі класифікації! 🚀

Copyright © 2026