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

Что показывает метрика R^2?

2.0 Middle🔥 242 комментариев
#Метрики и оценка моделей#Статистика и A/B тестирование

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

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

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

Метрика R² (коэффициент детерминации): полное объяснение

R² (R-squared, коэффициент детерминации) — это одна из самых важных метрик в регрессии. Её часто неправильно понимают, но она критична для оценки качества моделей.

Определение

R² показывает какую долю вариативности целевой переменной объясняет модель.

R² = 1 - (SS_res / SS_tot)

Где:

  • SS_res (Sum of Squares Residual) = Σ(y_true - y_pred)² — необъяснённая вариативность
  • SS_tot (Sum of Squares Total) = Σ(y_true - y_mean)² — полная вариативность

Интуитивное объяснение

Исходная задача: Как хорошо предсказать y?

Простейший способ: Всегда предсказываем среднее значение y
Ошибка = SS_tot (полная вариативность)

Модель: Использует признаки X для предсказания
Ошибка = SS_res (остаток после модели)

R² = 1 - (SS_res / SS_tot)
    = (SS_tot - SS_res) / SS_tot
    = объяснённая вариативность / полная вариативность

Практический пример

import numpy as np
from sklearn.metrics import r2_score, mean_squared_error
from sklearn.linear_model import LinearRegression
from sklearn.datasets import make_regression

# Создаём данные
X, y = make_regression(n_samples=100, n_features=5, noise=20, random_state=42)

# Обучаем модель
model = LinearRegression()
model.fit(X, y)
y_pred = model.predict(X)

# Вычисляем R²
r2 = r2_score(y, y_pred)
print(f'R² = {r2:.4f}')

# Вручную
mean_y = y.mean()
ss_tot = np.sum((y - mean_y) ** 2)
ss_res = np.sum((y - y_pred) ** 2)
r2_manual = 1 - (ss_res / ss_tot)
print(f'R² (ручной расчёт) = {r2_manual:.4f}')

# Вывод: R² ≈ 0.9876
# Модель объясняет 98.76% вариативности! Отлично!

Интерпретация значений R²

R² = 1.0  →  Идеальная модель (предсказывает точно)
R² = 0.9  →  Отличная модель (90% объяснено)
R² = 0.7  →  Хорошая модель (70% объяснено)
R² = 0.5  →  Приемлемая модель (50% объяснено)
R² = 0.3  →  Плохая модель (30% объяснено)
R² = 0.0  →  Модель не лучше чем просто "берём среднее"
R² < 0.0  →  Модель ХУЖЕчем просто берём среднее (очень плохо!)

Визуализация

import matplotlib.pyplot as plt

fig, axes = plt.subplots(2, 2, figsize=(12, 10))

# Случай 1: R² = 0.95 (отличная модель)
ax = axes[0, 0]
np.random.seed(42)
X = np.linspace(0, 10, 100)
y = 2*X + 1 + np.random.normal(0, 2, 100)
y_pred = 2*X + 1
r2_1 = r2_score(y, y_pred)
ax.scatter(y, y_pred, alpha=0.6)
ax.plot([y.min(), y.max()], [y.min(), y.max()], 'r--')
ax.set_title(f'Отличная модель: R² = {r2_1:.4f}')
ax.set_xlabel('Истинные значения')
ax.set_ylabel('Предсказанные значения')

# Случай 2: R² = 0.5 (приемлемая модель)
ax = axes[0, 1]
y = 2*X + 1 + np.random.normal(0, 8, 100)
y_pred = 2*X + 1 + np.random.normal(0, 5, 100)
r2_2 = r2_score(y, y_pred)
ax.scatter(y, y_pred, alpha=0.6)
ax.plot([y.min(), y.max()], [y.min(), y.max()], 'r--')
ax.set_title(f'Приемлемая модель: R² = {r2_2:.4f}')
ax.set_xlabel('Истинные значения')
ax.set_ylabel('Предсказанные значения')

# Случай 3: R² ≈ 0 (плохая модель)
ax = axes[1, 0]
y = np.random.normal(5, 2, 100)
y_pred = np.full(100, y.mean())  # Просто предсказываем среднее
r2_3 = r2_score(y, y_pred)
ax.scatter(y, y_pred, alpha=0.6)
ax.set_title(f'Случайные предсказания: R² = {r2_3:.4f}')
ax.set_xlabel('Истинные значения')
ax.set_ylabel('Предсказанные значения')

# Случай 4: R² < 0 (ХУЖЕчем среднее)
ax = axes[1, 1]
y = 2*X + 1 + np.random.normal(0, 3, 100)
y_pred = -X + 10 + np.random.normal(0, 3, 100)  # Совершенно неправильная модель
r2_4 = r2_score(y, y_pred)
ax.scatter(y, y_pred, alpha=0.6)
ax.set_title(f'Ужасная модель: R² = {r2_4:.4f}')
ax.set_xlabel('Истинные значения')
ax.set_ylabel('Предсказанные значения')

plt.tight_layout()
plt.show()

Математическая декомпозиция

Вариативность разбивается на две части:

SS_tot = SS_exp + SS_res

Где:
SS_tot = Σ(y_i - ȳ)²         Полная вариативность
SS_exp = Σ(ŷ_i - ȳ)²         Объяснённая вариативность (моделью)
SS_res = Σ(y_i - ŷ_i)²       Остаточная вариативность (ошибка)

Поэтому:
R² = SS_exp / SS_tot = (SS_tot - SS_res) / SS_tot

Пример вычисления R² вручную

# Истинные и предсказанные значения
y_true = np.array([3.0, -0.5, 2.0, 7.0])
y_pred = np.array([2.5, 0.0, 2.0, 8.0])

# Среднее истинного значения
y_mean = y_true.mean()  # 2.875
print(f'Среднее y: {y_mean}')

# SS_tot: полная вариативность
ss_tot = np.sum((y_true - y_mean) ** 2)
print(f'SS_tot = (3-2.875)² + (-0.5-2.875)² + (2-2.875)² + (7-2.875)²')
print(f'       = 0.0156 + 11.391 + 0.766 + 16.891')
print(f'       = {ss_tot:.4f}')

# SS_res: остаточная вариативность
ss_res = np.sum((y_true - y_pred) ** 2)
print(f'\nSS_res = (3-2.5)² + (-0.5-0)² + (2-2)² + (7-8)²')
print(f'       = 0.25 + 0.25 + 0 + 1')
print(f'       = {ss_res:.4f}')

# R²
r2 = 1 - (ss_res / ss_tot)
print(f'\nR² = 1 - ({ss_res:.4f} / {ss_tot:.4f})')
print(f'   = 1 - {ss_res/ss_tot:.4f}')
print(f'   = {r2:.4f}')

# Результат:
# SS_tot = 29.0625
# SS_res = 1.5
# R² = 0.9485 (отличная модель!)

R² vs MSE

from sklearn.metrics import mean_squared_error, mean_absolute_error

y_true = np.array([3, -0.5, 2, 7, 4.5])
y_pred = np.array([2.5, 0.0, 2, 8, 4])

# R²
r2 = r2_score(y_true, y_pred)
print(f'R² = {r2:.4f}')  # 0.9486

# MSE
mse = mean_squared_error(y_true, y_pred)
print(f'MSE = {mse:.4f}')  # 0.3

# RMSE
rmse = np.sqrt(mse)
print(f'RMSE = {rmse:.4f}')  # 0.5477

# MAE
mae = mean_absolute_error(y_true, y_pred)
print(f'MAE = {mae:.4f}')  # 0.4

# Разница:
# R² = НОРМАЛИЗОВАННАЯ метрика (0 до 1)
# MSE = АБСОЛЮТНАЯ ошибка (зависит от единиц y)
# RMSE = В единицах y, легче интерпретировать
# MAE = Средняя ошибка в единицах y

Важные свойства R²

1. Может быть отрицательным!

# Если модель хуже чем просто "берём среднее"
y_true = np.array([1, 2, 3, 4, 5])
y_pred = np.array([5, 4, 3, 2, 1])  # Совершенно неправильно

r2 = r2_score(y_true, y_pred)
print(f'R² = {r2:.4f}')  # -2.5 (ОТРИЦАТЕЛЬНОЕ!)

# Это означает: ошибки модели в 3.5 раза больше чем у "среднего"

2. Зависит от масштаба данных

# Если умножить y на 10, R² не изменится
y = np.array([1, 2, 3, 4, 5])
y_scaled = y * 10

# Модель предсказывает с тем же относительным качеством
r2_original = r2_score(y, y_pred_original)
r2_scaled = r2_score(y_scaled, y_pred_scaled * 10)

# r2_original == r2_scaled (R² инвариантен к масштабу)

3. Увеличивается при добавлении признаков (даже бесполезных!)

from sklearn.linear_model import LinearRegression

# Данные
X = np.random.randn(100, 5)
y = X[:, 0] + np.random.normal(0, 0.1, 100)

# Модель с 5 признаками
model_5 = LinearRegression()
model_5.fit(X, y)
r2_5 = model_5.score(X, y)
print(f'R² с 5 признаками: {r2_5:.4f}')  # 0.9234

# Модель с 50 случайными признаками (бесполезные!)
X_random = np.random.randn(100, 50)
model_50 = LinearRegression()
model_50.fit(X_random, y)
r2_50 = model_50.score(X_random, y)
print(f'R² с 50 случайными признаками: {r2_50:.4f}')  # 0.6789

# R² всё равно растёт!
# Поэтому используют adjusted R²

Adjusted R² (исправленный R²)

def adjusted_r2(r2, n, p):
    """Adjusted R² с штрафом за количество признаков
    n = число примеров
    p = число признаков
    """
    return 1 - ((1 - r2) * (n - 1) / (n - p - 1))

n = 100  # число примеров
p_5 = 5  # 5 признаков
p_50 = 50  # 50 признаков

adjusted_r2_5 = adjusted_r2(0.9234, n, p_5)
adjusted_r2_50 = adjusted_r2(0.6789, n, p_50)

print(f'Adjusted R² (5 признаков): {adjusted_r2_5:.4f}')
print(f'Adjusted R² (50 признаков): {adjusted_r2_50:.4f}')
# Adjusted R² штрафует за лишние признаки!

Когда R² может быть обманчивой

1. С выбросами

# Выброс может сильно повлиять на R²
y_true = np.array([1, 2, 3, 4, 5])
y_pred = np.array([1.1, 2.1, 3.1, 4.1, 500])  # Огромная ошибка на последнем

r2 = r2_score(y_true, y_pred)  # Может быть отрицательным
print(f'R² = {r2:.4f}')  # Очень плохо

2. С нелинейными отношениями

# Линейная модель плохо на нелинейных данных
X = np.linspace(-3, 3, 100)
y = X**2 + np.random.normal(0, 0.5, 100)

# Линейная модель
from sklearn.linear_model import LinearRegression
model = LinearRegression()
model.fit(X.reshape(-1, 1), y)
r2_linear = model.score(X.reshape(-1, 1), y)
print(f'R² (линейная модель): {r2_linear:.4f}')  # Плохо (~0.3)

# Полиномиальная модель
from sklearn.preprocessing import PolynomialFeatures
poly = PolynomialFeatures(2)
X_poly = poly.fit_transform(X.reshape(-1, 1))
model_poly = LinearRegression()
model_poly.fit(X_poly, y)
r2_poly = model_poly.score(X_poly, y)
print(f'R² (полиномиальная модель): {r2_poly:.4f}')  # Отлично (~0.95)
# Поэтому всегда проверяй остатки!

Диагностика через остатки

# Остатки должны быть:
# 1. Случайными (нет паттерна)
# 2. Нормально распределены
# 3. С постоянной дисперсией (гомоскедастичность)

residuals = y_true - y_pred

fig, axes = plt.subplots(2, 2, figsize=(12, 10))

# График остатков vs предсказаний
axes[0, 0].scatter(y_pred, residuals)
axes[0, 0].axhline(y=0, color='r', linestyle='--')
axes[0, 0].set_xlabel('Предсказанные значения')
axes[0, 0].set_ylabel('Остатки')
axes[0, 0].set_title('Остатки vs Предсказания')

# Q-Q plot (проверка нормальности)
from scipy import stats
stats.probplot(residuals, dist="norm", plot=axes[0, 1])
axes[0, 1].set_title('Q-Q Plot')

# Гистограмма остатков
axes[1, 0].hist(residuals, bins=20, edgecolor='black')
axes[1, 0].set_xlabel('Остатки')
axes[1, 0].set_ylabel('Частота')
axes[1, 0].set_title('Распределение остатков')

# Scale-Location plot
standardized = residuals / residuals.std()
axes[1, 1].scatter(y_pred, np.sqrt(np.abs(standardized)))
axes[1, 1].set_xlabel('Предсказанные значения')
axes[1, 1].set_ylabel('√|Стандартизированные остатки|')
axes[1, 1].set_title('Scale-Location')

plt.tight_layout()
plt.show()

Практические рекомендации

✓ ИСПОЛЬЗУЙ R² когда:
  - Нужна интерпретируемая метрика
  - Сравниваешь модели на одних данных
  - Остатки примерно нормальны

✗ НЕ полагайся только на R²:
  - Проверяй остатки (диагностика)
  - Используй cross-validation
  - Смотри на другие метрики (RMSE, MAE)
  - Помни про adjusted R²
  - Визуализируй результаты

Заключение

R² показывает:

  • Какую долю вариативности y объясняет модель
  • Значения от -∞ до 1.0
  • Легко интерпретируется (0-100%)
  • Но может быть обманчивой если не проверять остатки

Используй R² как первую оценку, но всегда:

  1. Проверяй остатки
  2. Смотри на adjusted R²
  3. Используй cross-validation
  4. Сравниваешь с baseline моделями
  5. Визуализируешь результаты