Más allá del 99% de Accuracy: La guía técnica para domar el desbalanceo de clases

Imagina que acabas de entrenar un modelo para detectar transacciones fraudulentas. Ejecutas tu script de validación y ves un número brillante en la pantalla: Accuracy: 99.1%. Te reclinas en la silla, satisfecho. El modelo está listo para producción, ¿verdad?

Probablemente no. De hecho, es muy probable que tu modelo sea completamente inútil.

Si tu dataset contiene solo un 0.9% de casos de fraude (la clase minoritaria) y un 99.1% de transacciones legítimas (la clase mayoritaria), un modelo que prediga siempre «Transacción Legítima» tendrá una exactitud del 99.1%. Pero su capacidad para detectar fraude es cero.

Este es el problema del Desbalanceo de Clases (Class Imbalance). En este artículo, dejaremos de lado los consejos básicos y profundizaremos en cómo abordar este problema desde una perspectiva técnica, manipulando desde la función de pérdida hasta la generación sintética de datos.


1. El abandono de la «Accuracy»

En entornos desbalanceados, la Accuracy es una métrica envenenada. Debemos desacoplar el rendimiento del modelo en métricas que penalicen los Falsos Negativos (dejar pasar un fraude) o los Falsos Positivos (bloquear una tarjeta válida).

La trampa de la Curva ROC

Es común usar el AUC-ROC (Area Under the Receiver Operating Characteristic Curve). Sin embargo, la curva ROC utiliza la Tasa de Falsos Positivos (FPR) en su eje X:

FPR = FP / (FP + TN)

Dado que en datasets desbalanceados el número de True Negatives (TN) es masivo, el denominador crece, haciendo que el FPR sea artificialmente pequeño. Esto suaviza la curva y puede dar una falsa sensación de alto rendimiento.

La alternativa robusta: Precision-Recall AUC (PR-AUC)

Para problemas altamente desbalanceados, la métrica reina es el Área bajo la Curva de Precisión-Recall. Esta métrica ignora los TN y se enfoca puramente en cómo el modelo maneja la clase minoritaria (Positiva).

Precision = TP / (TP + FP)

Recall = TP / (TP + FN)

Un modelo robusto debe mantener una precisión alta sin sacrificar el recall (encontrar todas las agujas en el pajar sin traer demasiada paja).


2. Estrategias a Nivel de Algoritmo (Cost-Sensitive Learning)

Antes de tocar tus datos, deberías intentar ajustar cómo tu algoritmo aprende de ellos. La mayoría de los clasificadores modernos (SVM, Random Forest, XGBoost, Redes Neuronales) asumen que todas las clases tienen la misma importancia. Podemos romper esa asunción.

Class Weights (Pesos de Clase)

La idea es penalizar logarítmicamente la función de pérdida (Loss Function) más severamente cuando el modelo se equivoca en la clase minoritaria.

Si tenemos una clase mayoritaria y una minoritaria, el peso w para una clase j suele calcularse inversamente a su frecuencia:

w_j = N_total / (n_classes * N_j)

En scikit-learn, esto es tan simple como un parámetro, pero lo que ocurre tras bambalinas es que el algoritmo modifica su criterio de optimización (ej. Gini Impurity en árboles o Cross-Entropy en redes neuronales) multiplicando el error por el peso.

from sklearn.ensemble import RandomForestClassifier

# El modo "balanced" calcula los pesos automáticamente
rf_model = RandomForestClassifier(n_estimators=100, 
                           class_weight='balanced', 
                           random_state=42)

En XGBoost o LightGBM, el parámetro equivalente es scale_pos_weight, que idealmente debería ser la suma de negativos dividida entre la suma de positivos.


3. Estrategias a Nivel de Datos (Resampling)

Si el ajuste de pesos no es suficiente, debemos modificar la distribución del espacio de características.

Undersampling vs. Oversampling

  • Random Undersampling: Eliminar aleatoriamente ejemplos de la clase mayoritaria.
    • Riesgo: Pérdida de información valiosa. Solo recomendado si tienes «Big Data» real (millones de filas).
  • Random Oversampling: Duplicar ejemplos de la clase minoritaria.
    • Riesgo: Overfitting severo. El modelo memoriza los ejemplos duplicados en lugar de generalizar las características de la clase.

SMOTE (Synthetic Minority Over-sampling Technique)

Aquí es donde la técnica se pone interesante. En lugar de duplicar, SMOTE crea nuevos datos sintéticos «interpolando» entre muestras existentes.

Cómo funciona matemáticamente:

  1. Selecciona una muestra de la clase minoritaria A.
  2. Encuentra sus k vecinos más cercanos (k-NN) de la misma clase.
  3. Selecciona uno de esos vecinos, B.
  4. Genera un nuevo punto P en algún lugar aleatorio de la línea que une A y B en el espacio vectorial:

P = A + λ * (B – A)

Donde λ (lambda) es un número aleatorio entre 0 y 1.

Implementación técnica con imbalanced-learn:

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

# Dividir ANTES de aplicar SMOTE para evitar data leakage
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)

# Aplicar SMOTE solo al set de entrenamiento
smote = SMOTE(k_neighbors=5, random_state=42)
X_train_resampled, y_train_resampled = smote.fit_resample(X_train, y_train)

print(f"Antes: {sum(y_train==1)} | Después: {sum(y_train_resampled==1)}")

Nota Técnica: Nunca apliques SMOTE (ni ningún resampling) antes de separar tu set de validación/test. Si lo haces, filtrarás información sintética al set de prueba y tus métricas serán falsamente altas (Data Leakage).


4. Métodos Avanzados: Hybrid Sampling & Ensemble

A veces, añadir ruido sintético (SMOTE) en zonas donde las clases se solapan mucho crea más confusión. Aquí entran las técnicas híbridas como SMOTEenn o SMOTETomek.

Estas técnicas aplican un pipeline de dos pasos:

  1. Oversampling (SMOTE): Generan sintéticos para equilibrar.
  2. Cleaning (Undersampling): Usan Tomek Links o Edited Nearest Neighbors (ENN) para eliminar puntos (sintéticos o reales) que están demasiado cerca de la frontera de decisión de la clase opuesta, «limpiando» el margen de separación.

BalancedBaggingClassifier

En lugar de re-muestrear todo el dataset una vez, podemos usar ensambles. El BalancedRandomForestClassifier (de la librería imblearn) hace un downsampling de la clase mayoritaria diferente para cada árbol del bosque. Esto permite que el modelo vea toda la data mayoritaria a través del colectivo de árboles, pero cada árbol individual entrena con datos balanceados.


Conclusión

El desbalanceo de clases no se soluciona simplemente «consiguiendo más datos». Requiere un enfoque quirúrgico:

  1. Cambia tu métrica: Olvida el Accuracy; usa PR-AUC o F1-Score.
  2. Empieza simple: Usa class_weight='balanced' antes de generar datos falsos.
  3. Sintetiza con cuidado: Usa SMOTE si es necesario, pero ten cuidado con el ruido en las fronteras (considera SMOTETomek).
  4. Valida correctamente: El resampling solo ocurre dentro del training loop.

La próxima vez que veas un 99% de accuracy, sé escéptico. El verdadero valor de un científico de datos está en encontrar el 1% que nadie más puede ver.