UAX
← Calibración y Métricas
2

UD2: Estabilidad y Sesgos

Calibración, métricas y explicabilidad de los modelos

Temas: Sesgo vs Varianza · Validación cruzada (K-fold, LOO, LPO, Estratificada, Agrupada, Series temporales) · SMOTE · Caso práctico: Detección de fraude

Introducción

La estabilidad y los sesgos son conceptos fundamentales para evaluar la calidad y confiabilidad de los modelos de Machine Learning. Un modelo que generaliza correctamente debe equilibrar el sesgo (bias) y la varianza, dos fuentes de error que determinan su comportamiento tanto en los datos de entrenamiento como en datos nuevos.

La validación cruzada es la técnica principal para estimar de forma robusta el rendimiento de un modelo, reduciendo la dependencia de una división concreta de los datos.

Tema 1: Sesgo y Varianza (Bias-Variance Tradeoff)

Sesgo (Bias)

El sesgo representa los errores sistemáticos que comete un modelo debido a suposiciones simplistas sobre los datos. Un modelo con alto sesgo:

  • Es poco flexible (modelos lineales para datos no lineales)
  • Produce subajuste (underfitting): el modelo no captura los patrones del dataset
  • Tiene mal rendimiento tanto en entrenamiento como en test

Varianza

La varianza mide la sensibilidad del modelo a las fluctuaciones en los datos de entrenamiento. Un modelo con alta varianza:

  • Es demasiado complejo (memoriza los datos de entrenamiento)
  • Produce sobreajuste (overfitting): funciona muy bien en entrenamiento pero mal en test
  • Es inestable: pequeños cambios en los datos de entrenamiento cambian mucho las predicciones

El equilibrio sesgo-varianza

El tradeoff sesgo-varianza implica que reducir uno tiende a aumentar el otro. El objetivo es encontrar el punto de equilibrio donde el error total (sesgo² + varianza + ruido irreducible) sea mínimo. Las técnicas de regularización, la validación cruzada y la selección adecuada de hiperparámetros ayudan a encontrar ese punto óptimo.

Error Total = Sesgo² + Varianza + Ruido irreducible

Tema 2: Validación Cruzada

La validación cruzada es un método de evaluación que divide el conjunto de datos en múltiples subconjuntos para estimar el rendimiento del modelo de forma más fiable que una única división train/test. Scikit-learn proporciona implementaciones para todos los iteradores descritos a continuación.

2.1. Iteradores para datos i.i.d.

K-fold

El método más común. Divide las muestras en k pliegues (folds) iguales. En cada iteración, un pliegue se usa como test y los k-1 restantes como entrenamiento. Se repite k veces y se promedia el rendimiento.

from sklearn.model_selection import KFold
kf = KFold(n_splits=5, shuffle=True, random_state=42)

K-fold repetido (Repeated K-Fold)

Repite el K-Fold n veces con diferentes divisiones aleatorias. Proporciona estimaciones más estables del rendimiento a costa de mayor tiempo de cómputo.

from sklearn.model_selection import RepeatedKFold
rkf = RepeatedKFold(n_splits=5, n_repeats=10, random_state=42)

LOO (Leave One Out)

Caso extremo de K-fold donde k = n (número de muestras). Cada muestra es el conjunto de test una sola vez. Produce estimaciones muy poco sesgadas pero con alta varianza y coste computacional elevado.

from sklearn.model_selection import LeaveOneOut
loo = LeaveOneOut()

LPO (Leave P Out)

Similar a LOO pero el conjunto de test contiene p muestras en lugar de 1. El número de combinaciones posibles crece exponencialmente con p, por lo que se usa solo para valores pequeños de p.

from sklearn.model_selection import LeavePOut
lpo = LeavePOut(p=2)

2.2. Iteradores con estratificación

KFold Estratificado (StratifiedKFold)

Preserva el porcentaje de muestras de cada clase en cada pliegue. Esencial cuando las clases están desbalanceadas para garantizar que todos los pliegues son representativos.

from sklearn.model_selection import StratifiedKFold
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)

División aleatoria estratificada (StratifiedShuffleSplit)

Variación de ShuffleSplit que mantiene la proporción de clases. Útil cuando se necesitan múltiples evaluaciones rápidas preservando el balance de clases.

from sklearn.model_selection import StratifiedShuffleSplit
sss = StratifiedShuffleSplit(n_splits=5, test_size=0.2, random_state=42)

2.3. Iteradores para datos agrupados

KFold agrupado (GroupKFold)

Garantiza que las muestras de un mismo grupo no aparezcan en entrenamiento y test simultáneamente. Fundamental cuando hay múltiples registros del mismo sujeto o entidad.

from sklearn.model_selection import GroupKFold
gkf = GroupKFold(n_splits=5)

KFold agrupado estratificado (StratifiedGroupKFold)

Combina las propiedades de StratifiedKFold y GroupKFold: preserva el porcentaje de clases y asegura que los grupos no se dividan entre train y test.

from sklearn.model_selection import StratifiedGroupKFold
sgkf = StratifiedGroupKFold(n_splits=5)

Dejar un grupo fuera (LeaveOneGroupOut)

En cada iteración, todas las muestras de un grupo completo forman el conjunto de test. Evalúa la capacidad de generalización a grupos no vistos.

from sklearn.model_selection import LeaveOneGroupOut
logo = LeaveOneGroupOut()

Dejar P grupos fuera (LeavePGroupsOut)

Extensión de LeaveOneGroupOut donde se dejan fuera p grupos en cada iteración. Útil para evaluar la generalización cuando se excluyen múltiples grupos simultáneamente.

from sklearn.model_selection import LeavePGroupsOut
lpgo = LeavePGroupsOut(n_groups=2)

2.4. Validación cruzada para series temporales

TimeSeriesSplit

Diseñado para datos temporales donde el orden importa. Los primeros pliegues se usan como entrenamiento y los siguientes como test, respetando la cronología. Nunca se usa información futura para predecir el pasado.

from sklearn.model_selection import TimeSeriesSplit
tscv = TimeSeriesSplit(n_splits=5)

for train_idx, test_idx in tscv.split(X):
    X_train, X_test = X[train_idx], X[test_idx]
    y_train, y_test = y[train_idx], y[test_idx]

Tema 3: Datos Desbalanceados y SMOTE

En muchos problemas reales (detección de fraude, diagnóstico médico, ciberseguridad), las clases están muy desbalanceadas. La clase minoritaria puede representar menos del 0,1% de los datos. En estos casos, un modelo que siempre predice la clase mayoritaria obtendrá alta accuracy pero será inútil en la práctica.

SMOTE (Synthetic Minority Over-sampling Technique)

SMOTE genera muestras sintéticas de la clase minoritaria interpolando entre instancias existentes de esa clase y sus vecinos más cercanos. A diferencia del sobremuestreo aleatorio (que simplemente duplica muestras), SMOTE crea ejemplos nuevos y variados.

from imblearn.over_sampling import SMOTE

smote = SMOTE(random_state=42)
X_resampled, y_resampled = smote.fit_resample(X_train, y_train)
# Ahora las clases están balanceadas

Caso Práctico: Detección de Fraude en Tarjetas de Crédito

Dataset: creditcard.csv — transacciones de tarjetas de crédito europeas de septiembre 2013. Dataset altamente desbalanceado: 284.315 transacciones legítimas vs 492 fraudes (0,172%).

Pipeline del caso práctico

  1. Carga del dataset con pandas
  2. División con StratifiedKFold (5 pliegues) para preservar la proporción de fraudes
  3. Aplicación de SMOTE dentro de cada pliegue (solo sobre entrenamiento) — resultado: 6.964 muestras de cada clase
  4. Entrenamiento de un clasificador
  5. Evaluación con F1-Score y Accuracy

Resultados obtenidos

  • Accuracy: 0.99 (alta pero engañosa por el desbalance)
  • F1-Score: 0.00 sin SMOTE (el modelo no detecta fraudes)
  • Tras SMOTE: mejora significativa en la detección de la clase minoritaria

Este resultado ilustra por qué en datasets desbalanceados la accuracy es una métrica inadecuada y es necesario usar F1-Score, Recall o AUC-ROC para evaluar correctamente el modelo.

import pandas as pd
from sklearn.model_selection import StratifiedKFold
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import f1_score, accuracy_score
from imblearn.over_sampling import SMOTE

data = pd.read_csv('creditcard.csv')
X = data.drop('Class', axis=1)
y = data['Class']

skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
smote = SMOTE(random_state=42)

for fold, (train_idx, test_idx) in enumerate(skf.split(X, y)):
    X_tr, X_te = X.iloc[train_idx], X.iloc[test_idx]
    y_tr, y_te = y.iloc[train_idx], y.iloc[test_idx]

    # Aplicar SMOTE solo sobre entrenamiento
    X_res, y_res = smote.fit_resample(X_tr, y_tr)

    clf = LogisticRegression(max_iter=1000)
    clf.fit(X_res, y_res)
    y_pred = clf.predict(X_te)

    print(f"Fold {fold+1}: Acc={accuracy_score(y_te, y_pred):.4f}, F1={f1_score(y_te, y_pred):.4f}")