O que é desbalanceamento de classes e por que importa
Em muitos problemas reais de classificação, as classes não aparecem em proporções parecidas. Em detecção de fraude, inadimplência ou churn, a classe que você quer prever costuma ser a minoritária — às vezes menos de 5% dos registros.
Um modelo treinado nesse cenário sem cuidado aprende o atalho mais fácil: prever sempre a classe majoritária. A acurácia fica alta (95% ou mais), mas o modelo não detecta nenhum caso relevante. Em produção, isso significa fraude não flagada, clientes em risco ignorados ou crédito negado sem critério.
O desbalanceamento não é um detalhe técnico — é o que separa um modelo que "parece bom no notebook" de um que resolve o problema de negócio. Antes de escolher um algoritmo, você precisa medir o desbalanceamento e escolher uma estratégia coerente com o custo de cada tipo de erro.
Conceitos essenciais
Três ideias guiam a maioria das soluções:
- Distribuição das classes — a razão entre minoritária e majoritária define a severidade do problema
- Métricas adequadas — acurácia engana; precision, recall, F1 e AUC-PR refletem melhor o desempenho na classe rara
- Estratégia de reamostragem — alterar o dataset de treino (oversampling, undersampling ou combinação) ou ajustar pesos no modelo
Duas famílias de técnica aparecem com frequência:
- Oversampling — cria exemplos sintéticos da classe minoritária (SMOTE, ADASYN) ou duplica registros existentes
- Undersampling — remove exemplos da classe majoritária para equilibrar (Random UnderSampler, Tomek links)
- Class weights — mantém o dataset original, mas penaliza mais erros na classe minoritária durante o treino
Nenhuma é universal. Oversampling pode causar overfitting em datasets pequenos; undersampling descarta informação; class weights funcionam bem com modelos que suportam class_weight, mas não resolvem todos os casos.
Diagnóstico: medir antes de tratar
O primeiro passo é quantificar o problema. Com pandas e scikit-learn:
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report, average_precision_score
# Carregar dados — target binário: 1 = inadimplente, 0 = adimplente
df = pd.read_csv("credit_data.csv")
X = df.drop("default", axis=1)
y = df["default"]
# Ver proporção das classes
print(y.value_counts(normalize=True))
# default
# 0 0.94
# 1 0.06
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, stratify=y, random_state=42
)
stratify=y no split é obrigatório em dados desbalanceados — garante que treino e teste mantenham a mesma proporção de classes.
Treine um baseline sem tratamento e avalie com métricas que importam para a classe minoritária:
from sklearn.ensemble import RandomForestClassifier
baseline = RandomForestClassifier(random_state=42)
baseline.fit(X_train, y_train)
y_pred = baseline.predict(X_test)
print(classification_report(y_test, y_pred))
print("AUC-PR:", average_precision_score(y_test, baseline.predict_proba(X_test)[:, 1]))
Se o recall da classe 1 estiver próximo de zero, você confirmou o problema — hora de aplicar uma técnica de reamostragem.
SMOTE: oversampling sintético
SMOTE (Synthetic Minority Over-sampling Technique) cria exemplos sintéticos da classe minoritária interpolando entre vizinhos próximos no espaço de features. Em vez de duplicar registros idênticos, gera pontos novos que preservam a estrutura local dos dados.
from imblearn.over_sampling import SMOTE
from imblearn.pipeline import Pipeline as ImbPipeline
smote = SMOTE(random_state=42, k_neighbors=5)
X_train_smote, y_train_smote = smote.fit_resample(X_train, y_train)
print(y_train_smote.value_counts())
# default
# 0 15000
# 1 15000
O parâmetro k_neighbors define quantos vizinhos usar na interpolação. Em datasets muito pequenos na classe minoritária, reduza k_neighbors (mínimo 1) para evitar erros.
Integre SMOTE em um pipeline para evitar data leakage — o oversampling deve acontecer apenas no fold de treino, nunca no conjunto de teste:
pipeline = ImbPipeline([
("smote", SMOTE(random_state=42)),
("classifier", RandomForestClassifier(random_state=42)),
])
pipeline.fit(X_train, y_train)
y_pred = pipeline.predict(X_test)
ADASYN: oversampling adaptativo
ADASYN (Adaptive Synthetic Sampling) é uma evolução do SMOTE. Em vez de gerar a mesma quantidade de exemplos sintéticos para todos os pontos da minoritária, concentra a geração nas regiões onde a classe majoritária domina — ou seja, onde o classificador teria mais dificuldade.
Isso faz sentido quando a fronteira de decisão é irregular: fraudes que "parecem" transações normais precisam de mais exemplos sintéticos nessa região.
from imblearn.over_sampling import ADASYN
adasyn = ADASYN(random_state=42, n_neighbors=5)
X_train_ada, y_train_ada = adasyn.fit_resample(X_train, y_train)
Na prática, SMOTE e ADASYN costumam ter desempenho parecido. ADASYN tende a ajudar quando a minoritária tem subclusters com densidades diferentes; SMOTE é mais simples de explicar e debugar.
Undersampling e combinações
Quando o dataset é enorme e a classe majoritária tem milhões de registros, oversampling pode inflar o treino desnecessariamente. Nesse caso, undersampling da majoritária é uma alternativa:
from imblearn.under_sampling import RandomUnderSampler
rus = RandomUnderSampler(random_state=42)
X_train_under, y_train_under = rus.fit_resample(X_train, y_train)
O risco é perder informação valiosa. Uma abordagem intermediária é combinar as duas:
from imblearn.combine import SMOTETomek
smt = SMOTETomek(random_state=42)
X_train_combo, y_train_combo = smt.fit_resample(X_train, y_train)
SMOTETomek aplica SMOTE e depois remove pares Tomek link — exemplos da majoritária que estão na fronteira e confundem o modelo. É uma técnica robusta para começar em projetos de crédito e fraude.
Class weights: alternativa sem reamostragem
Modelos como Random Forest, Logistic Regression e XGBoost aceitam pesos por classe. Você mantém o dataset original e instrui o algoritmo a penalizar mais erros na minoritária:
from sklearn.utils.class_weight import compute_class_weight
import numpy as np
weights = compute_class_weight("balanced", classes=np.unique(y_train), y=y_train)
class_weight = dict(zip(np.unique(y_train), weights))
model = RandomForestClassifier(class_weight=class_weight, random_state=42)
model.fit(X_train, y_train)
class_weight="balanced" no scikit-learn faz o mesmo automaticamente. A vantagem é simplicidade e velocidade; a desvantagem é que nem sempre recupera o recall tão bem quanto SMOTE em datasets muito desbalanceados (razão acima de 1:50).
Além do básico: pontos de atenção
Algumas armadilhas aparecem com frequência em projetos reais:
- Aplicar SMOTE antes do split — vaza informação do teste para o treino; use pipeline ou aplique só em
X_train - Otimizar acurácia — em 94/6, prever sempre 0 dá 94% de acurácia; use F1, recall ou AUC-PR na classe 1
- Oversampling em features categóricas sem encoding — SMOTE opera em espaço numérico; encode antes (one-hot, target encoding)
- Ignorar o threshold de decisão — após o treino, ajustar o threshold (ex.: 0.3 em vez de 0.5) costuma melhorar recall sem reamostragem
- Comparar técnicas no mesmo split — fixe
random_statee use validação cruzada estratificada para comparar SMOTE, ADASYN e class weights de forma justa
Para produção, documente qual técnica foi usada e congele o pipeline (incluindo o sampler) com joblib ou MLflow — reamostragem faz parte do artefato de treino, não é um passo manual descartável.
Conclusão
Dados desbalanceados são a regra em classificação aplicada a crédito, fraude e churn — não a exceção. O caminho prático é: medir a distribuição, treinar um baseline, escolher métricas adequadas e testar SMOTE, ADASYN ou class weights dentro de um pipeline que evite leakage.
Comece com SMOTETomek + Random Forest em validação cruzada estratificada. Se o recall da minoritária ainda for insuficiente, experimente ADASYN ou ajuste o threshold de decisão. O próximo passo natural é colocar esse pipeline em produção — o tema do post sobre score de crédito com Streamlit.