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.
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
- Carga del dataset con pandas
- División con StratifiedKFold (5 pliegues) para preservar la proporción de fraudes
- Aplicación de SMOTE dentro de cada pliegue (solo sobre entrenamiento) — resultado: 6.964 muestras de cada clase
- Entrenamiento de un clasificador
- 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}")