← Назад к вопросам

Как работает ROC-кривая?

1.3 Junior🔥 201 комментариев
#Машинное обучение#Метрики и оценка моделей

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI30 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

ROC-кривая: как она работает и что она показывает

ROC (Receiver Operating Characteristic) — одна из самых важных метрик в классификации. Расскажу как она устроена, почему она так полезна, и как её интерпретировать.

Основная идея

РОК-кривая показывает trade-off между ловлей положительных примеров и случайными срабатываниями при разных порогах решения.

        TPR (True Positive Rate)
        ↑ 
      1 |     /
        |    /
      0.8  / ← ROC-кривая
        | /
      0.5|/________
        |/
      0 |________→ FPR (False Positive Rate)
        0       0.5       1

Ключевые компоненты

TPR (True Positive Rate) = Recall

Из всех положительных примеров, сколько мы правильно предсказали:

TPR = TP / (TP + FN)

# Пример:
# 100 больных людей
# Модель правильно определила 80 → TPR = 80/100 = 0.80

FPR (False Positive Rate) = 1 - Specificity

Из всех отрицательных примеров, сколько мы неправильно предсказали как положительные:

FPR = FP / (FP + TN)

# Пример:
# 100 здоровых людей
# Модель неправильно определила 10 как больных → FPR = 10/100 = 0.10

Как строится ROC-кривая

Модель выдаёт вероятности, а не жёсткие классы. Изменяя порог решения (threshold), мы получаем разные TPR и FPR.

from sklearn.metrics import roc_curve, roc_auc_score
import numpy as np
import matplotlib.pyplot as plt

# Истинные метки
y_true = np.array([1, 1, 1, 1, 0, 0, 0, 0])

# Вероятности модели
y_proba = np.array([0.9, 0.8, 0.7, 0.3, 0.6, 0.5, 0.2, 0.1])

print('Истинные метки (1=болезнь, 0=здоров):')
print(y_true)
print('\nВероятности модели (вероятность болезни):')
print(y_proba)

print('\n' + '='*70)
print('ВЫЧИСЛЯЕМ TPR И FPR ДЛЯ РАЗНЫХ ПОРОГОВ')
print('='*70)

thresholds = [0.9, 0.8, 0.7, 0.6, 0.5, 0.3, 0.2, 0.0]

for threshold in thresholds:
    # Предсказываем: 1 если вероятность >= threshold
    y_pred = (y_proba >= threshold).astype(int)
    
    # Вычисляем TP, FP, TN, FN
    TP = ((y_true == 1) & (y_pred == 1)).sum()
    FP = ((y_true == 0) & (y_pred == 1)).sum()
    TN = ((y_true == 0) & (y_pred == 0)).sum()
    FN = ((y_true == 1) & (y_pred == 0)).sum()
    
    TPR = TP / (TP + FN) if (TP + FN) > 0 else 0
    FPR = FP / (FP + TN) if (FP + TN) > 0 else 0
    
    print(f'\nПороговое значение: {threshold}')
    print(f'  Предсказания: {y_pred} → TP={TP}, FP={FP}, TN={TN}, FN={FN}')
    print(f'  TPR (чувствительность): {TPR:.2f}')
    print(f'  FPR (ложные срабатывания): {FPR:.2f}')

Вывод:

  • При низком пороге (0.1): много положительных предсказаний → TPR↑, FPR↑
  • При высоком пороге (0.9): мало положительных предсказаний → TPR↓, FPR↓

Визуализация ROC-кривой

from sklearn.metrics import roc_curve, auc
import matplotlib.pyplot as plt

# Используем встроенную функцию
fpr, tpr, thresholds = roc_curve(y_true, y_proba)
auc_score = auc(fpr, tpr)

print('FPR значения:', fpr)
print('TPR значения:', tpr)
print('\nПороги для каждой точки:')
for f, t, th in zip(fpr, tpr, thresholds):
    print(f'  FPR={f:.2f}, TPR={t:.2f}, Threshold={th:.2f}')

# Рисуем
plt.figure(figsize=(10, 8))
plt.plot(fpr, tpr, marker='o', linewidth=2, markersize=8, label=f'ROC (AUC = {auc_score:.3f})')
plt.plot([0, 1], [0, 1], 'k--', label='Random Classifier (AUC = 0.5)')

plt.xlabel('False Positive Rate (чем меньше - лучше)', fontsize=12)
plt.ylabel('True Positive Rate (чем больше - лучше)', fontsize=12)
plt.title('ROC-кривая', fontsize=14)
plt.legend(fontsize=11)
plt.grid(alpha=0.3)
plt.xlim([-0.05, 1.05])
plt.ylim([-0.05, 1.05])
plt.show()

Интерпретация ROC-кривой

              TPR
            1 |     Идеальная модель (верхний левый угол)
              | /|
              |/ | ← Хорошая модель
            0.5|  |
              |   | ← Средняя модель  
              |   |
            0 |___|________
              0  0.5  1
                    ↑
              Случайная (диагональ)

Три примера:

# МОДЕЛЬ 1: Идеальная
y_true_1 = np.array([1, 1, 1, 1, 0, 0, 0, 0])
y_proba_1 = np.array([1.0, 0.9, 0.8, 0.7, 0.1, 0.0, 0.0, 0.0])  # Идеально разделены!

# МОДЕЛЬ 2: Хорошая
y_proba_2 = np.array([0.9, 0.8, 0.6, 0.5, 0.4, 0.3, 0.2, 0.1])  # Хороший порядок

# МОДЕЛЬ 3: Случайная
y_proba_3 = np.random.rand(8)  # Случайные значения

for name, proba in [('Идеальная', y_proba_1), ('Хорошая', y_proba_2), ('Случайная', y_proba_3)]:
    auc_score = roc_auc_score(y_true_1, proba)
    print(f'{name:12} → AUC = {auc_score:.3f}')

AUC (Area Under the Curve)

Это площадь под ROC-кривой. Она имеет красивую интерпретацию:

# AUC = вероятность того, что модель правильно рангирует
# случайно выбранный положительный пример выше, чем отрицательный

from sklearn.metrics import roc_auc_score

y_true = np.array([1, 1, 0, 0])
y_proba = np.array([0.8, 0.6, 0.4, 0.2])  # Модель хорошо разделяет

auc = roc_auc_score(y_true, y_proba)
print(f'AUC = {auc}')  # 1.0 (идеально)

# Визуально:
# Все положительные примеры (0.8, 0.6) выше, чем отрицательные (0.4, 0.2)
# Пары: (0.8 > 0.4), (0.8 > 0.2), (0.6 > 0.4), (0.6 > 0.2)
# Все 4 пары правильны → AUC = 4/4 = 1.0

Интерпретация AUC:

AUC = 1.0  → Идеальная модель (невозможно в реальности)
AUC = 0.9  → Отличная модель (используй её!)
AUC = 0.8  → Хорошая модель (приемлемо)
AUC = 0.7  → Приемлемая модель (на границе)
AUC = 0.5  → Случайный классификатор (не используй)
AUC < 0.5  → Хуже случайного (проверь код!)

Практический пример: медицинская диагностика

from sklearn.metrics import roc_curve, roc_auc_score, confusion_matrix
import matplotlib.pyplot as plt

# Тесты на заболевание
y_true = np.array([  # 0 = здоров, 1 = болен
    1, 1, 1, 1, 1,  # 5 больных
    0, 0, 0, 0, 0   # 5 здоровых
])

y_proba = np.array([
    0.95, 0.88, 0.75, 0.62, 0.51,  # Больные (большие вероятности)
    0.40, 0.35, 0.22, 0.15, 0.05   # Здоровые (малые вероятности)
])

# Вычислим для разных порогов
fpr, tpr, thresholds = roc_curve(y_true, y_proba)
auc_score = roc_auc_score(y_true, y_proba)

print('='*70)
print('АНАЛИЗ ПОРОГОВ ДЛЯ МЕДИЦИНСКОГО ТЕСТА')
print('='*70)

for threshold in [0.3, 0.5, 0.7]:
    y_pred = (y_proba >= threshold).astype(int)
    
    tn, fp, fn, tp = confusion_matrix(y_true, y_pred).ravel()
    
    sensitivity = tp / (tp + fn)  # TPR: «поймали ли больных?»
    specificity = tn / (tn + fp)  # 1-FPR: «не обидели ли здоровых?»
    ppv = tp / (tp + fp)          # Precision: «сколько предсказаний верны?»
    
    print(f'\nПороговое значение: {threshold}')
    print(f'  Чувствительность (TPR): {sensitivity:.1%} — ловим {int(tp)} из {tp+fn} больных')
    print(f'  Специфичность (1-FPR): {specificity:.1%} — не беспокоим {int(tn)} из {tn+fp} здоровых')
    print(f'  Положительное предсказание (PPV): {ppv:.1%} — верны {int(tp)} из {tp+fp} тестов')

print(f'\n\nAUC-ROC = {auc_score:.3f}\n')

# Визуализация
plt.figure(figsize=(12, 5))

plt.subplot(1, 2, 1)
plt.plot(fpr, tpr, 'b-', linewidth=2, label=f'Наш тест (AUC={auc_score:.3f})')
plt.plot([0, 1], [0, 1], 'r--', label='Случайный тест (AUC=0.5)')
plt.fill_between(fpr, tpr, alpha=0.2)
plt.xlabel('FPR (доля ложных срабатываний)')
plt.ylabel('TPR (доля правильных срабатываний)')
plt.title('ROC-кривая медицинского теста')
plt.legend()
plt.grid(alpha=0.3)

plt.subplot(1, 2, 2)
plt.plot(thresholds, tpr[:len(thresholds)], 'b-', marker='o', label='TPR (ловим больных)')
plt.plot(thresholds, 1-fpr[:len(thresholds)], 'g-', marker='s', label='Specificity (не беспокоим здоровых)')
plt.xlabel('Пороговое значение')
plt.ylabel('Процент')
plt.title('Sensitivity vs Specificity')
plt.legend()
plt.grid(alpha=0.3)

plt.tight_layout()
plt.show()

ROC vs PR-кривая

Когда что использовать?

# ROC-кривая: независима от дисбаланса
# Хороша когда классы примерно одинаковые (50-50)

# PR-кривая: более чувствительна к редкому классу
# Хороша когда большой дисбаланс (95%-5%)

from sklearn.metrics import precision_recall_curve, auc

precision, recall, _ = precision_recall_curve(y_true, y_proba)
pr_auc = auc(recall, precision)

print(f'ROC-AUC: {auc_score:.3f}')
print(f'PR-AUC: {pr_auc:.3f}')
print(f'\nEсли датасет дисбалансирован → используй PR-AUC')
print(f'Если данные относительно сбалансированы → используй ROC-AUC')

Выбор оптимального порога

РОК-кривая помогает выбрать лучший порог:

from sklearn.metrics import f1_score

# Для максимизации F1-score
f1_scores = []
for threshold in thresholds:
    y_pred = (y_proba >= threshold).astype(int)
    f1 = f1_score(y_true, y_pred)
    f1_scores.append(f1)

best_threshold = thresholds[np.argmax(f1_scores)]
print(f'Оптимальный порог для F1: {best_threshold:.2f}')

# Для максимизации Youden's Index (TPR - FPR)
youden = tpr - fpr
best_idx = np.argmax(youden)
best_threshold_youden = thresholds[best_idx]
print(f'Оптимальный порог (Youden): {best_threshold_youden:.2f}')

Реальный пример: fraud detection

# Базовая история
from sklearn.datasets import make_classification
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import roc_curve, auc, confusion_matrix

# Финансовые транзакции (95% обычные, 5% мошеннические)
X, y = make_classification(
    n_samples=10000,
    weights=[0.95, 0.05],
    n_features=20,
    n_informative=10,
    random_state=42
)

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3)

# Модель
model = LogisticRegression()
model.fit(X_train, y_train)
y_proba_test = model.predict_proba(X_test)[:, 1]

# ROC-кривая
fpr, tpr, thresholds = roc_curve(y_test, y_proba_test)
auc_score = auc(fpr, tpr)

print('FRAUD DETECTION СИСТЕМА')
print(f'AUC-ROC: {auc_score:.3f}')
print(f'\nДля разных стратегий:')

# Стратегия 1: Ловим много, но false positives
thresh_aggressive = 0.2
y_pred = (y_proba_test >= thresh_aggressive).astype(int)
tn, fp, fn, tp = confusion_matrix(y_test, y_pred).ravel()
print(f'\nАгрессивная (порог={thresh_aggressive}): Ловим {tp}/{tp+fn} мошеннических, ложно срабатываем на {fp} честных')

# Стратегия 2: Консервативная
thresh_conservative = 0.7
y_pred = (y_proba_test >= thresh_conservative).astype(int)
tn, fp, fn, tp = confusion_matrix(y_test, y_pred).ravel()
print(f'Консервативная (порог={thresh_conservative}): Ловим {tp}/{tp+fn} мошеннических, ложно срабатываем на {fp} честных')

Вывод

ROC-кривая показывает:

  • Как меняется качество при разных порогах решения
  • Trade-off между ловлей положительного класса и ложными срабатываниями
  • AUC как общий показатель качества модели

Используй ROC для:

  • Сравнения разных моделей
  • Выбора оптимального порога
  • Оценки качества на сбалансированных данных

Помни:

  • ROC-кривая всегда монотонна (растёт слева направо)
  • Идеальная кривая идёт через точку (0, 1)
  • Случайная модель — диагональная линия
  • AUC интерпретируется как вероятность правильного ранжирования