Какие методы решения проблемы несбалансированных выборок знаешь?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Методы решения проблемы несбалансированных выборок
Несбалансированные выборки (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:
- Для каждого примера редкого класса найти k-ближайших соседей (обычно k=5)
- Выбрать случайного соседа
- Создать синтетический пример на линии между ними
# Визуализация
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:1 | SMOTE + Взвешивание | Генерируем примеры + даём больший вес |
| 1000:1 | SMOTE + Undersampling | Генерируем + удаляем избыток большинства |
| Очень дисбалансирован | Anomaly Detection | Третируем как выбросы |
Ключевые выводы
✅ Никогда не используйте Accuracy на дисбалансированных данных
✅ SMOTE часто лучше, чем простой oversampling
✅ Комбинируйте методы (SMOTE + взвешивание)
✅ Всегда применяйте балансировку только на обучающей выборке
✅ Используйте ROC-AUC и F1 для оценки качества