W3docs

Validación cruzada en Python: K-Fold, Estratificada, LOOCV

Aprende validación cruzada en Python: K-Fold, K-Fold estratificada, Leave-One-Out, CV anidada y pipelines con scikit-learn — con ejemplos ejecutables.

La validación cruzada es el método estándar para estimar qué tan bien funcionará un modelo de machine learning con datos no vistos. En lugar de depender de una única división entrenamiento/prueba — que puede producir una puntuación demasiado optimista o pesimista dependiendo de qué muestras terminan en cada parte — la validación cruzada entrena y evalúa el modelo múltiples veces en diferentes particiones de los datos y promedia los resultados.

Esta página cubre:

  • Por qué una única división entrenamiento/prueba no es suficiente
  • Validación cruzada K-Fold y cómo elegir k
  • K-Fold estratificada para distribuciones de clases desequilibradas
  • Validación cruzada Leave-One-Out (LOO) para conjuntos de datos pequeños
  • Evaluación de múltiples métricas con cross_validate
  • Uso de un Pipeline dentro de la validación cruzada para prevenir la fuga de datos
  • Validación cruzada anidada para el ajuste imparcial de hiperparámetros

Todos los ejemplos usan scikit-learn y el conjunto de datos Iris integrado, por lo que puedes ejecutarlos de inmediato sin descargar nada.

Por Qué Importa la Validación Cruzada

Un flujo de trabajo de evaluación ingenuo divide los datos una vez, entrena en una parte y prueba en la otra. La puntuación que obtienes depende en gran medida de qué muestras llegaron a cada parte — una división afortunada puede hacer que un modelo débil parezca bueno; una desafortunada puede hacer que un modelo fuerte parezca malo.

La validación cruzada resuelve esto repitiendo el proceso de entrenamiento/prueba k veces, cada vez usando una porción diferente de los datos como conjunto de prueba. La puntuación final es el promedio de todos los pliegues, que es mucho más estable que una sola medición.

La validación cruzada también aprovecha al máximo los datos limitados: cada muestra se usa tanto para entrenamiento como para evaluación a lo largo de todo el experimento.

Consulta la página de división entrenamiento/prueba para la técnica de referencia más simple que la validación cruzada mejora.

Validación Cruzada K-Fold

K-Fold es la estrategia de validación cruzada más ampliamente utilizada. Los datos se dividen en k pliegues de igual tamaño. En cada una de las k iteraciones:

  1. Un pliegue se reserva como el conjunto de prueba.
  2. Los k - 1 pliegues restantes forman el conjunto de entrenamiento.
  3. El modelo se entrena desde cero y se puntúa en el pliegue de prueba.

Después de k iteraciones tienes k puntuaciones. Su media es la estimación del rendimiento validado de forma cruzada; su desviación estándar te dice qué tan consistente es ese rendimiento en diferentes segmentos de datos.

Para k = 5 y un conjunto de datos de 150 muestras, cada pliegue contiene 30 muestras (20 %) para prueba y 120 muestras (80 %) para entrenamiento.

Ejemplo Básico de K-Fold

from sklearn.model_selection import KFold, cross_val_score
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import load_iris

# Load the built-in Iris dataset (150 samples, 4 features, 3 classes)
iris = load_iris()
X, y = iris.data, iris.target

model = LogisticRegression(max_iter=200)
kfold = KFold(n_splits=5, shuffle=True, random_state=42)

scores = cross_val_score(model, X, y, cv=kfold, scoring='accuracy')

print("Fold scores:", scores.round(4))
# Fold scores: [1.     1.     0.9333 0.9667 0.9667]

print("Mean accuracy: %.4f  Std: %.4f" % (scores.mean(), scores.std()))
# Mean accuracy: 0.9733  Std: 0.0249

La función cross_val_score maneja toda la iteración internamente. Parámetros clave:

ParámetroPropósito
estimatorCualquier modelo de scikit-learn (o Pipeline)
X, yMatriz de características y vector objetivo
cvObjeto validador cruzado o entero (p. ej. cv=5)
scoringCadena de métrica — 'accuracy', 'f1_macro', 'roc_auc', etc.

Elegir k

  • k = 5 o k = 10 es recomendado para la mayoría de los conjuntos de datos. Estos valores ofrecen un buen equilibrio sesgo-varianza en la estimación.
  • Mayor k (p. ej. 10) produce menor sesgo pero mayor varianza en la estimación, y es más costoso computacionalmente.
  • Menor k (p. ej. 3) es más rápido pero la estimación es más sensible a cómo se dividieron los datos.
  • Para conjuntos de datos muy pequeños (menos de ~100 muestras) considera Leave-One-Out en su lugar.

Inspección Manual de los Pliegues

Puedes iterar sobre los pliegues tú mismo cuando necesitas inspeccionar qué entra en cada división o cuando quieres ejecutar lógica personalizada por pliegue:

from sklearn.model_selection import KFold
from sklearn.datasets import load_iris
import numpy as np

iris = load_iris()
X, y = iris.data, iris.target

kfold = KFold(n_splits=5, shuffle=True, random_state=42)

for fold, (train_idx, test_idx) in enumerate(kfold.split(X), start=1):
    X_train, X_test = X[train_idx], X[test_idx]
    y_train, y_test = y[train_idx], y[test_idx]
    print(f"Fold {fold}: train={len(train_idx)} samples, test={len(test_idx)} samples")

Salida:

Fold 1: train=120 samples, test=30 samples
Fold 2: train=120 samples, test=30 samples
Fold 3: train=120 samples, test=30 samples
Fold 4: train=120 samples, test=30 samples
Fold 5: train=120 samples, test=30 samples

Validación Cruzada K-Fold Estratificada

La K-Fold simple divide los datos por orden de índice. Con distribuciones de clases desequilibradas esto puede hacer que algunos pliegues contengan muy pocos ejemplos de una clase minoritaria, haciendo que la puntuación sea poco confiable.

K-Fold estratificada garantiza que cada pliegue contenga aproximadamente la misma proporción de cada clase que el conjunto de datos completo. Usa StratifiedKFold siempre que tu objetivo sea categórico:

from sklearn.model_selection import StratifiedKFold, cross_val_score
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import load_iris

iris = load_iris()
X, y = iris.data, iris.target

model = LogisticRegression(max_iter=200)
skfold = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)

scores = cross_val_score(model, X, y, cv=skfold, scoring='accuracy')

print("Fold scores:", scores.round(4))
# Fold scores: [1.     0.9667 0.9333 1.     0.9333]

print("Mean accuracy: %.4f  Std: %.4f" % (scores.mean(), scores.std()))
# Mean accuracy: 0.9667  Std: 0.0298

StratifiedKFold es el validador cruzado predeterminado que se usa dentro de GridSearchCV y RandomizedSearchCV para problemas de clasificación — obtienes estratificación automáticamente en esos contextos.

Validación Cruzada Leave-One-Out

La validación cruzada Leave-One-Out (LOO) es el caso extremo: k es igual al número de muestras. En cada iteración, una muestra es el conjunto de prueba y todas las muestras restantes forman el conjunto de entrenamiento. Para un conjunto de datos de 150 muestras eso significa 150 ciclos de entrenamiento/evaluación.

from sklearn.model_selection import LeaveOneOut, cross_val_score
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import load_iris

iris = load_iris()
X, y = iris.data, iris.target

model = LogisticRegression(max_iter=200)
loocv = LeaveOneOut()

scores = cross_val_score(model, X, y, cv=loocv, scoring='accuracy')

print(f"Number of folds: {len(scores)}")        # 150
print(f"Mean accuracy: {scores.mean():.4f}")     # 0.9667
print(f"Std deviation: {scores.std():.4f}")      # 0.1795

Cuándo usar LOO:

  • Tu conjunto de datos tiene menos de ~100 muestras y no puedes permitirte reservar datos para prueba.
  • Quieres la estimación de menor sesgo del rendimiento del modelo.

Desventajas de LOO:

  • Costo computacional muy alto — el modelo se reentrena n veces.
  • Alta varianza en la estimación: la puntuación de prueba de cada pliegue es 0 o 1 (clasificación binaria) o un único punto, por lo que la desviación estándar no es significativa para pliegues individuales.

Para la mayoría de los conjuntos de datos, K-Fold con k=5 o k=10 es un mejor equilibrio.

Evaluación de Múltiples Métricas a la Vez

cross_val_score solo puede calcular una métrica por llamada. Usa cross_validate para calcular varias métricas simultáneamente y también recuperar puntuaciones de entrenamiento para detectar sobreajuste:

from sklearn.model_selection import KFold, cross_validate
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import load_iris
import numpy as np

iris = load_iris()
X, y = iris.data, iris.target

model = LogisticRegression(max_iter=200)
kfold = KFold(n_splits=5, shuffle=True, random_state=42)

cv_results = cross_validate(
    model, X, y,
    cv=kfold,
    scoring=['accuracy', 'f1_macro'],
    return_train_score=True,
)

print("Test accuracy: ", cv_results['test_accuracy'].round(4))
# Test accuracy:  [1.     1.     0.9333 0.9667 0.9667]

print("Train accuracy:", cv_results['train_accuracy'].round(4))
# Train accuracy: [0.975  0.9583 0.9833 0.975  0.9833]

print("Test F1-macro: ", cv_results['test_f1_macro'].round(4))
# Test F1-macro:  [1.     1.     0.9259 0.9691 0.971 ]

Comparar las puntuaciones de entrenamiento y prueba entre pliegues es una forma rápida de detectar sobreajuste: si la precisión de entrenamiento es consistentemente mucho mayor que la precisión de prueba, el modelo está memorizando los datos de entrenamiento. Consulta la discusión sobre sesgo y varianza para más contexto.

Uso de un Pipeline Dentro de la Validación Cruzada

Un error común es ajustar los pasos de preprocesamiento (como el escalado de características o la imputación) en todo el conjunto de datos antes de la validación cruzada. Esto filtra información del pliegue de prueba al proceso de entrenamiento, dando como resultado una puntuación excesivamente optimista.

El patrón correcto es envolver el preprocesamiento y el modelo juntos en un Pipeline y pasar el pipeline a cross_val_score. scikit-learn reajusta todo el pipeline — incluyendo el escalador — de forma independiente dentro de cada pliegue:

from sklearn.model_selection import StratifiedKFold, cross_val_score
from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.datasets import load_iris

iris = load_iris()
X, y = iris.data, iris.target

# Correct: preprocessing is fitted only on training folds
pipe = Pipeline([
    ('scaler', StandardScaler()),
    ('clf', LogisticRegression(max_iter=200)),
])

skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
scores = cross_val_score(pipe, X, y, cv=skf, scoring='accuracy')

print("Fold scores:", scores.round(4))
# Fold scores: [1.     0.9667 0.9    1.     0.9   ]

print("Mean accuracy: %.4f  Std: %.4f" % (scores.mean(), scores.std()))
# Mean accuracy: 0.9533  Std: 0.0452

Usa siempre un Pipeline cuando tu flujo de trabajo incluya cualquier paso que aprenda de los datos (escalado, PCA, codificación, imputación).

Validación Cruzada Anidada

Cuando usas la validación cruzada tanto para ajustar hiperparámetros como para evaluar el rendimiento del modelo en los mismos datos, corres el riesgo de sobreajustar a los pliegues de validación — los hiperparámetros elegidos son los que obtuvieron la mejor puntuación en esas particiones particulares, por lo que la puntuación reportada es optimista.

La validación cruzada anidada separa las dos preocupaciones:

  • Bucle interno: seleccionar hiperparámetros mediante búsqueda en cuadrícula en los pliegues de entrenamiento.
  • Bucle externo: evaluar el mejor modelo encontrado por el bucle interno en un pliegue de prueba reservado.
from sklearn.model_selection import GridSearchCV, StratifiedKFold, cross_val_score
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import load_iris

iris = load_iris()
X, y = iris.data, iris.target

# Inner CV: hyperparameter selection
inner_cv = StratifiedKFold(n_splits=3, shuffle=True, random_state=2)
param_grid = {'C': [0.01, 0.1, 1, 10]}
gs = GridSearchCV(
    LogisticRegression(max_iter=300),
    param_grid,
    cv=inner_cv,
    scoring='accuracy',
)

# Outer CV: unbiased performance estimation
outer_cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=1)
nested_scores = cross_val_score(gs, X, y, cv=outer_cv, scoring='accuracy')

print("Nested CV fold scores:", nested_scores.round(4))
# Nested CV fold scores: [0.9667 1.     0.9333 1.     0.9   ]

print("Mean accuracy: %.4f" % nested_scores.mean())
# Mean accuracy: 0.9600

El enfoque anidado proporciona una estimación imparcial del rendimiento del modelo final desplegado. Úsalo siempre que estés reportando resultados en un contexto de investigación o comparando algoritmos. Para un flujo de trabajo de despliegue simple donde de todos modos reentrenas con todos los datos disponibles, un único bucle externo con GridSearchCV suele ser suficiente. Consulta la página de búsqueda en cuadrícula para un recorrido detallado del ajuste de hiperparámetros.

Errores Comunes

Preprocesamiento fuera del pliegue

Ajustar un escalador en el conjunto de datos completo antes de llamar a cross_val_score — en lugar de dentro de un Pipeline — filtra las estadísticas del pliegue de prueba al entrenamiento. La solución es usar siempre un Pipeline.

Uso incorrecto de random_state

Si estableces shuffle=True sin un random_state, cada ejecución produce una división diferente y tus resultados no son reproducibles. Siempre establece random_state a un entero fijo cuando reportes números.

Interpretación de la desviación estándar

Una alta desviación estándar entre pliegues no siempre es mala — puede reflejar variabilidad genuina en el conjunto de datos (p. ej. algunos pliegues son más fáciles que otros). Examina las puntuaciones individuales de cada pliegue antes de sacar conclusiones.

Validación cruzada en datos de series temporales

K-Fold mezcla los datos aleatoriamente, lo que introduciría información futura en las ventanas de entrenamiento pasadas para problemas de series temporales. Usa TimeSeriesSplit de scikit-learn en su lugar, que respeta el orden temporal.

Referencia Rápida

TécnicaCuándo usarClase de scikit-learn
K-FoldOpción predeterminada para la mayoría de tareas de regresión/clasificaciónKFold
K-Fold estratificadaClasificación con clases desequilibradasStratifiedKFold
Leave-One-OutConjuntos de datos muy pequeños (< ~100 muestras)LeaveOneOut
CV anidadaReportar puntuaciones imparciales con ajuste de hiperparámetrosGridSearchCV dentro de cross_val_score
CV de series temporalesDatos con orden temporalTimeSeriesSplit

Temas Relacionados

Was this page helpful?