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

Какие методы решения проблемы несбалансированных выборок знаешь?

2.0 Middle🔥 231 комментариев
#Machine Learning

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

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

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

Методы решения проблемы несбалансированных выборок

Несбалансированные выборки (imbalanced datasets) — это классическая проблема в машинном обучении и анализе данных, когда одна класс существенно преобладает над другой. Например, фрод встречается в 0.1% транзакций, а обычные операции в 99.9%. Это приводит к смещённым моделям. Разберу методы борьбы с этой проблемой.

1. Переsampling методы

Oversampling (Передискретизация меньшинства)

Идея: Увеличить количество примеров редкого класса путём дублирования или синтеза новых примеров.

from sklearn.utils import resample
import pandas as pd
import numpy as np

# Исходные данные
df = pd.DataFrame({
    'feature': np.random.randn(1000),
    'label': [0]*990 + [1]*10  # Несбалансированные
})

print(f"Исходное распределение: {df['label'].value_counts().to_dict()}")
# {0: 990, 1: 10}

# Простой oversampling (дублирование)
minority_class = df[df['label'] == 1]
minority_upsampled = resample(minority_class, 
                               n_samples=len(df[df['label']==0]),
                               replace=True)

df_balanced = pd.concat([df[df['label']==0], minority_upsampled])
print(f"После oversampling: {df_balanced['label'].value_counts().to_dict()}")
# {0: 990, 1: 990}

Преимущества:

  • ✅ Не теряем информацию
  • ✅ Увеличивает размер выборки

Недостатки:

  • ❌ Может привести к переобучению (дублируем те же примеры)
  • ❌ Увеличивает память и время обучения

SMOTE (Synthetic Minority Over-sampling Technique)

Идея: Генерировать синтетические примеры редкого класса путём интерполяции между соседними примерами.

from imblearn.over_sampling import SMOTE
from sklearn.model_selection import train_test_split

X = df.drop('label', axis=1)
y = df['label']

# Применяем SMOTE только на обучающем наборе!
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.3, random_state=42
)

smote = SMOTE(random_state=42)
X_train_balanced, y_train_balanced = smote.fit_resample(X_train, y_train)

print(f"После SMOTE: {pd.Series(y_train_balanced).value_counts().to_dict()}")

Как работает SMOTE:

  1. Для каждого примера редкого класса найти k-ближайших соседей (обычно k=5)
  2. Выбрать случайного соседа
  3. Создать синтетический пример на линии между ними
# Визуализация
from sklearn.datasets import make_classification
import matplotlib.pyplot as plt

X, y = make_classification(n_samples=1000, n_features=2, 
                           weights=[0.99, 0.01], random_state=42)

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

# До SMOTE
axes[0].scatter(X[y==0, 0], X[y==0, 1], label='Класс 0')
axes[0].scatter(X[y==1, 0], X[y==1, 1], label='Класс 1')
axes[0].set_title('До SMOTE')
axes[0].legend()

# После SMOTE
smote = SMOTE()
X_balanced, y_balanced = smote.fit_resample(X, y)
axes[1].scatter(X_balanced[y_balanced==0, 0], X_balanced[y_balanced==0, 1], 
                alpha=0.5, label='Класс 0')
axes[1].scatter(X_balanced[y_balanced==1, 0], X_balanced[y_balanced==1, 1], 
                label='Класс 1')
axes[1].set_title('После SMOTE')
axes[1].legend()
plt.tight_layout()
plt.show()

Преимущества:

  • ✅ Генерирует новые, реалистичные примеры
  • ✅ Не дублирует точные копии
  • ✅ Работает лучше, чем простой oversampling

2. Undersampling (Недодискретизация большинства)

Идея: Уменьшить количество примеров большинства.

from imblearn.under_sampling import RandomUnderSampler

rus = RandomUnderSampler(random_state=42)
X_balanced, y_balanced = rus.fit_resample(X_train, y_train)

print(f"После undersampling: {pd.Series(y_balanced).value_counts()}")

Преимущества:

  • ✅ Уменьшает размер выборки (быстрее обучение)
  • ✅ Простой метод

Недостатки:

  • ❌ Теряем информацию из большинства класса
  • ❌ Может привести к недообучению

Когда использовать: Когда большого класса очень много (>100k примеров).

3. Комбинированные методы

SMOTE + Tomek Links

Удаляем примеры большинства, которые находятся рядом с меньшинством (шумные примеры).

from imblearn.pipeline import Pipeline
from imblearn.over_sampling import SMOTE
from imblearn.under_sampling import TomekLinks

pipeline = Pipeline([
    ('smote', SMOTE(random_state=42)),
    ('tomek', TomekLinks())
])

X_balanced, y_balanced = pipeline.fit_resample(X_train, y_train)

SMOTE + ENN (Edited Nearest Neighbors)

from imblearn.over_sampling import SMOTE
from imblearn.under_sampling import EditedNearestNeighbours
from imblearn.pipeline import Pipeline

pipeline = Pipeline([
    ('smote', SMOTE()),
    ('enn', EditedNearestNeighbours())
])

X_balanced, y_balanced = pipeline.fit_resample(X_train, y_train)

4. Взвешивание классов

Идея: Назначить больший вес редкому классу во время обучения модели.

from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier

# Автоматический расчёт весов
class_weights = 'balanced'  # или dict с явными весами

# Логистическая регрессия
model = LogisticRegression(class_weight=class_weights, max_iter=1000)
model.fit(X_train, y_train)

# Random Forest
rf_model = RandomForestClassifier(class_weight=class_weights, random_state=42)
rf_model.fit(X_train, y_train)

# Явные веса
from sklearn.utils.class_weight import compute_class_weight

weights = compute_class_weight('balanced', classes=np.unique(y_train), y=y_train)
class_weight_dict = dict(zip(np.unique(y_train), weights))
print(f"Веса классов: {class_weight_dict}")
# {0: 0.5, 1: 49.5} — редкий класс получает вес в 99 раз больше

Преимущества:

  • ✅ Простой метод
  • ✅ Работает с большинством алгоритмов
  • ✅ Не требует дополнительной памяти

5. Threshold Moving

Идея: Изменить порог классификации вместо того, чтобы менять данные.

По умолчанию порог = 0.5, но для несбалансированных данных можно сдвинуть.

from sklearn.linear_model import LogisticRegression
from sklearn.metrics import precision_recall_curve
import numpy as np

# Обучаем на исходных данных
model = LogisticRegression()
model.fit(X_train, y_train)

# Получаем вероятности
y_proba = model.predict_proba(X_test)[:, 1]

# Находим оптимальный порог
precision, recall, thresholds = precision_recall_curve(y_test, y_proba)

# F1-score для каждого порога
f1_scores = 2 * (precision * recall) / (precision + recall + 1e-10)
opt_idx = np.argmax(f1_scores)
opt_threshold = thresholds[opt_idx]

print(f"Оптимальный порог: {opt_threshold:.4f}")

# Применяем новый порог
y_pred = (y_proba >= opt_threshold).astype(int)

Преимущества:

  • ✅ Не требует переборки данных
  • ✅ Быстрый метод

6. Anomaly Detection подход

Третируем редкий класс как аномалию.

from sklearn.ensemble import IsolationForest

# Обучаем на большинстве класса
X_majority = X[y == 0]

if_model = IsolationForest(contamination=0.01, random_state=42)
y_pred = if_model.fit_predict(X)

# -1 = аномалия (редкий класс), 1 = норма (большинство)

7. Правильная оценка качества модели

Важно: На несбалансированных данных Accuracy неинформативен.

from sklearn.metrics import (
    accuracy_score, 
    precision_score, 
    recall_score, 
    f1_score,
    roc_auc_score,
    confusion_matrix,
    classification_report
)

y_pred = model.predict(X_test)
y_proba = model.predict_proba(X_test)[:, 1]

print(f"Accuracy: {accuracy_score(y_test, y_pred):.4f}")  # Может быть 99% даже при плохой модели
print(f"Precision: {precision_score(y_test, y_pred):.4f}")  # Из предсказанных 1, сколько правильно?
print(f"Recall: {recall_score(y_test, y_pred):.4f}")  # Из всех 1, сколько нашли?
print(f"F1: {f1_score(y_test, y_pred):.4f}")  # Гармоническое среднее precision и recall
print(f"ROC-AUC: {roc_auc_score(y_test, y_proba):.4f}")  # Устойчива к дисбалансу

print("\nMatrица ошибок:")
print(confusion_matrix(y_test, y_pred))

print("\nПодробный отчёт:")
print(classification_report(y_test, y_pred))

Рекомендуемые метрики:

  • ROC-AUC (устойчива к дисбалансу)
  • Precision-Recall curve
  • F1-score
  • Confusion matrix

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

from imblearn.pipeline import Pipeline as ImbPipeline
from sklearn.preprocessing import StandardScaler
from sklearn.ensemble import RandomForestClassifier
from imblearn.over_sampling import SMOTE

# Полный pipeline
pipeline = ImbPipeline([
    ('scaler', StandardScaler()),
    ('smote', SMOTE(random_state=42)),
    ('model', RandomForestClassifier(
        class_weight='balanced',
        n_estimators=100,
        random_state=42
    ))
])

# Обучаем
pipeline.fit(X_train, y_train)

# Оцениваем
from sklearn.metrics import roc_auc_score
y_proba = pipeline.predict_proba(X_test)[:, 1]
auc = roc_auc_score(y_test, y_proba)
print(f"ROC-AUC: {auc:.4f}")

Выбор метода: матрица решений

СоотношениеРекомендуемый методПричина
10:1Взвешивание классовПростой метод, хорошо работает
100:1SMOTE + ВзвешиваниеГенерируем примеры + даём больший вес
1000:1SMOTE + UndersamplingГенерируем + удаляем избыток большинства
Очень дисбалансированAnomaly DetectionТретируем как выбросы

Ключевые выводы

✅ Никогда не используйте Accuracy на дисбалансированных данных

✅ SMOTE часто лучше, чем простой oversampling

✅ Комбинируйте методы (SMOTE + взвешивание)

✅ Всегда применяйте балансировку только на обучающей выборке

✅ Используйте ROC-AUC и F1 для оценки качества