Дисбаланс классов: веса, порог и правильные метрики

При дисбалансе один класс встречается намного реже другого: дефекты, мошеннические операции или аварии могут составлять доли процента. Модель с высокой accuracy способна полностью игнорировать редкое событие, поэтому проверку начинают с матрицы ошибок и цены решений.

Сначала определите положительный класс

Редкий класс не обязательно положительный в коде. Проверьте значения y, порядок classes_ и смысл каждого исхода. Ошибка в соответствии меток меняет интерпретацию precision, recall и столбца predict_proba.

Затем посчитайте baseline: что произойдёт, если всегда прогнозировать большинство? Его accuracy показывает, насколько мало сообщает общая доля правильных ответов.

Метрики для редкого события

Recall измеряет долю найденных положительных объектов, precision — долю настоящих среди срабатываний. PR-кривая показывает их компромисс при разных порогах. F1 объединяет две величины, но предполагает одинаковую значимость и скрывает конкретное число ошибок.

ROC-AUC полезна для ранжирования, однако большое число true negatives может сделать картину менее наглядной. Показывайте также average precision или PR-AUC, матрицу ошибок и абсолютные количества на рабочем пороге.

Веса классов

Многие алгоритмы принимают class_weight='balanced' и усиливают вклад редкого класса в функцию потерь. Это меняет обучение, но не гарантирует нужного рабочего компромисса.

from sklearn.datasets import make_classification
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import precision_recall_curve
from sklearn.model_selection import train_test_split
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler

X, y = make_classification(
    n_samples=3_000,
    n_features=12,
    n_informative=6,
    weights=[0.95, 0.05],
    flip_y=0.01,
    random_state=42,
)
X_train, X_valid, y_train, y_valid = train_test_split(
    X, y, test_size=0.25, random_state=42, stratify=y
)

model = make_pipeline(
    StandardScaler(),
    LogisticRegression(class_weight='balanced', max_iter=2_000),
)
model.fit(X_train, y_train)

positive_label = 1
positive_index = list(model.classes_).index(positive_label)
scores = model.predict_proba(X_valid)[:, positive_index]
precision, recall, thresholds = precision_recall_curve(
    y_valid, scores, pos_label=positive_label
)

# Пример требования: найти вариант с recall не ниже 0.90.
candidates = [(p, r, t) for p, r, t in zip(precision, recall, thresholds) if r >= 0.90]
best_precision, best_recall, threshold = max(candidates, key=lambda item: item[0])
print(best_precision, best_recall, threshold)

Порог выбирается на validation. После выбора правило фиксируют и один раз проверяют на test вместе с абсолютным числом FP и FN.

Повторная выборка

Oversampling увеличивает представительство редкого класса, undersampling уменьшает большинство. Эти операции выполняются только внутри train-фолда. Если сначала создать копии редких объектов, а затем разделить таблицу, один исходный пример попадёт в обучение и проверку.

Синтетические методы не создают новую реальность: они интерполируют доступные наблюдения и могут усиливать шум. Сравните их с весами, изменением порога и сбором настоящих примеров.

Дисбаланс может меняться после запуска

Доля событий влияет на precision. Модель с тем же recall и false positive rate будет давать другую чистоту срабатываний, если редкий класс стал встречаться реже. Поэтому отслеживайте base rate, объём ручной проверки и качество на размеченных данных во времени.

Для очень редких событий случайный test может содержать слишком мало положительных случаев. Нужны достаточно длинное окно, интервальная оценка и предметно важные стресс-сценарии.

Практика: переведите метрики в очередь

Пусть сервис обрабатывает 100 000 заявок в день, положительный класс составляет 0,5%, recall равен 0,8, а precision — 0,2. Посчитайте найденные события, пропуски и примерный размер очереди проверок. Решите, выдерживает ли её команда, и сформулируйте ограничение для выбора порога.

Что важно запомнить

Источники