Python & Data Science

¿Qué es la validación cruzada y cómo evitar hacerla mal?

1. La trampa del ‘examen de práctica’

Imagina que estás estudiando para un examen de historia. Tu profesor te entrega una prueba de práctica con 50 preguntas. Pasas la noche memorizando las respuestas exactas. Al día siguiente, el examen real tiene las mismas 50 preguntas. Lo apruebas con facilidad.

Pero, ¿realmente aprendiste historia? Probablemente no. Cambia una fecha o un nombre y habrías reprobado.

En el aprendizaje automático, a esto lo llamamos sobreajuste. Tu modelo es ese estudiante: memoriza el ruido y las peculiaridades específicas de tus datos de entrenamiento en lugar de aprender los patrones subyacentes. Pruébelo con los mismos datos con los que lo entrenaste y le estarás permitiendo hacer trampa.

Esto es lo que sucede cuando un modelo “memoriza” un conjunto de datos simple. Creamos una relación curva pero le decimos al modelo que la siga tan de cerca que pierde la visión general.

import numpy as np
import matplotlib.pyplot as plt
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error

# 1. Create fake data: a simple curve with some noise
np.random.seed(42)
x = np.linspace(0, 1, 20)
y = np.cos(1.5 * np.pi * x) + np.random.normal(0, 0.1, 20)

# 2. Fit a model that is way too complex (Degree 15 for 20 points)
model = make_pipeline(PolynomialFeatures(15), LinearRegression())
model.fit(x.reshape(-1, 1), y)

# 3. Check performance on the training data
train_preds = model.predict(x.reshape(-1, 1))
train_error = mean_squared_error(y, train_preds)
print(f"Training Error: {train_error:.4f}")

# 4. Check performance on new 'unseen' data
x_test = np.linspace(0, 1, 20)
y_test = np.cos(1.5 * np.pi * x_test) + np.random.normal(0, 0.1, 20)
test_preds = model.predict(x_test.reshape(-1, 1))
test_error = mean_squared_error(y_test, test_preds)
print(f"Test Error: {test_error:.4f}")

Lo que esto realmente significa: El error de entrenamiento es casi cero (0.0001), lo cual parece excelente. Pero el error de prueba es masivo — a menudo 10x o 100x mayor. El modelo memorizó los puntos de entrenamiento tan perfectamente que pasó por alto la curva real. Esa es la “Trampa de la Prueba de Práctica.”

2. ¿Qué está pasando realmente?

Solucionamos esto con validación cruzada (CV). Imagina tus datos como un pastel: en lugar de cortar una sola rebanada para probar, rotamos el pastel para que cada pieza tenga su turno como conjunto de prueba. La versión más común es la K-Fold Cross-Validation.

Dividimos los datos en KK partes iguales (generalmente 5 o 10), entrenamos con K1K-1 partes y probamos con la restante. Repetimos hasta que cada parte haya servido como conjunto de prueba exactamente una vez.

La parte complicada: aquí no estamos construyendo un único modelo final. Estamos construyendo una serie de modelos temporales para poner a prueba nuestra estrategia. Si esa estrategia se sostiene sin importar qué rebanada apartemos, podemos estar seguros de que también se sostendrá en el mundo real.

Así es como se barajan los datos en cada paso:

from sklearn.model_selection import KFold

data = np.array(["A", "B", "C", "D", "E"])
kf = KFold(n_splits=5)

for i, (train_index, test_index) in enumerate(kf.split(data)):
    print(f"Fold {i+1}:")
    print(f"  Train: {data[train_index]}")
    print(f"  Test:  {data[test_index]}")

En cada paso, el modelo ve un subconjunto diferente. Si solo está memorizando, fallará en la letra “Test” que todavía no ha visto.

3. El mayor error: preprocesar antes de dividir los datos

Este es el error más común entre los “profesionales”. Supongamos que tienes valores faltantes en tus datos y los rellenas con el promedio de la columna. Si calculas ese promedio usando el conjunto de datos completo antes de dividirlo en conjuntos de entrenamiento y prueba, has filtrado información del futuro.

El “promedio” que usaste para rellenar las filas de entrenamiento ahora contiene información sobre las filas de prueba. Tu modelo ha echado un vistazo a las respuestas.

Lo que esto significa en la práctica: tus métricas de validación cruzada se verán genial, pero el modelo falla en producción — ya no hay un promedio del “futuro” que lo ayude.

La solución: usa un Pipeline. Garantiza que el escalado o la imputación ocurran solo con los datos de entrenamiento dentro de cada fold.

from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import cross_val_score
from sklearn.datasets import make_classification
from sklearn.svm import SVC

# Create a fake dataset
X, y = make_classification(n_samples=100, random_state=42)

# WRONG WAY: Scaling the whole dataset first
# scaler = StandardScaler()
# X_scaled = scaler.fit_transform(X) # <--- LEAKAGE HAPPENS HERE

# RIGHT WAY: Put the scaler in a Pipeline
clf_pipeline = Pipeline([
    ('scaler', StandardScaler()),
    ('svc', SVC())
])

# cross_val_score handles the splitting correctly
scores = cross_val_score(clf_pipeline, X, y, cv=5)
print(f"Correct CV Scores: {scores}")

Con el Pipeline en su lugar, StandardScaler aprende el “promedio” solo de los folds de entrenamiento, nunca del fold de prueba.

4. Cuando la validación cruzada te miente

La validación cruzada no es magia. Asume que tus datos son “Independientes e Idénticamente Distribuidos” (IID) y que el orden de tus filas no importa.

¿Qué sucede si estás prediciendo precios de acciones? El K-Fold estándar podría entrenar con los datos del miércoles para predecir el lunes. Eso no funciona en la práctica.

Para datos de series de tiempo, necesitas una “ventana deslizante” o una “ventana expansiva” donde el conjunto de prueba siempre esté cronológicamente después del conjunto de entrenamiento.

from sklearn.model_selection import TimeSeriesSplit

X_time = np.array([1, 2, 3, 4, 5, 6])
tscv = TimeSeriesSplit(n_splits=3)

for train_index, test_index in tscv.split(X_time):
    print(f"Train: {train_index} | Test: {test_index}")

¿Ves la diferencia? El índice de prueba siempre es mayor —posterior en el tiempo— que el índice de entrenamiento. Usa el K-Fold normal aquí y tu exactitud sería una mentira.

5. Interpretando los números

Cuando ejecutas cross_val_score, obtienes un arreglo de números. La mayoría de las personas toman el promedio y siguen adelante. No lo hagas.

Observa la Desviación Estándar.

  • Escenario A: Puntuación media 90%, Desviación Estándar 1%. Este modelo es una estrella de rock. Es estable y consistente sin importar los datos que vea.
  • Escenario B: Puntuación media 90%, Desviación Estándar 15%. Este modelo es una apuesta. Algunos folds alcanzan 99%, otros obtienen 60%. Es altamente sensible a los datos específicos con los que fue entrenado.
# Let's check the stability of our previous model
print(f"Mean Accuracy: {scores.mean():.2f}")
print(f"Standard Deviation: {scores.std():.2f}")

if scores.std() > 0.05:
    print("Warning: The model is unstable! Results vary too much between folds.")
else:
    print("The model is stable across different data slices.")

¿Qué significa esto para nosotros? Una desviación estándar alta es una señal de alerta. Generalmente significa que necesitas más datos, o que tu modelo es demasiado complejo para los datos que tienes.

Resumen

Entonces, algunas cosas para tener en cuenta:

  1. El sobreajuste es memorizar el examen de práctica. No significa que realmente hayas aprendido el material.
  2. El K-Fold CV pone cada parte de los datos en el conjunto de prueba en algún momento.
  3. La fuga de datos ocurre cuando escalas o procesas tus datos antes de dividirlos. Usa Pipelines para mantenerte a salvo.
  4. Los datos de series de tiempo necesitan un manejo especial para que no predigas el pasado usando el futuro.
  5. La desviación estándar te indica si tu modelo es confiable o si simplemente tuvo suerte.

La próxima vez que construyas un modelo, no confíes en una sola puntuación de precisión. Revisa la varianza. Asegúrate de que tu modelo no esté espiando las respuestas.

Comprueba tu comprensión

Las preguntas a continuación avanzan desde el simple recuerdo hasta el diseño abierto, siguiendo aproximadamente la taxonomía de Bloom.

Recordar ¿Qué hace la validación cruzada K-Fold de manera diferente a una simple división de entrenamiento/prueba?

Comprender Con tus propias palabras, explica por qué ajustar un StandardScaler en todo el conjunto de datos antes de dividirlo causa fuga de datos, utilizando el encuadre del artículo de “echar un vistazo a las respuestas”.

Aplicar Usando la regla de estabilidad del artículo (std > 0.05 es una señal de alerta), ¿se marcaría un modelo con puntajes de validación cruzada de [0.91, 0.89, 0.92, 0.60, 0.90] como inestable? Calcula aproximadamente si su desviación estándar excedería ese umbral.

Analizar El artículo dice que el K-Fold estándar asume que los datos son “Independientes e Idénticamente Distribuidos” (IID) y rompe este supuesto para los datos de series temporales. Explica paso a paso por qué usar TimeSeriesSplit no solo renombra los pliegues, sino que cambia fundamentalmente a qué pregunta responde la validación (probar en el futuro vs. probar en una porción aleatoria).

Evaluar La comparación del Escenario A vs. B del artículo (ambos con 90% de precisión media, pero 1% vs. 15% de desviación estándar) argumenta que el Escenario A es “una estrella de rock” y el B es “una apuesta”. Critica el hecho de confiar únicamente en la desviación estándar aquí: con solo 5 pliegues, ¿es una desviación estándar de 1% realmente una fuerte evidencia de estabilidad, o podría ser simplemente suerte por tener un pequeño número de pliegues — qué te gustaría ver antes de confiar plenamente en el Escenario A?

Crear Diseña una estrategia de validación para un nuevo escenario: un modelo que predice el riesgo de reingreso hospitalario, donde los pacientes de la misma sala del hospital comparten resultados correlacionados (un brote o un problema de personal en una sala afecta a todos allí). ¿Sería seguro el K-Fold estándar aquí, o estos datos violan el supuesto IID de manera similar a los datos de series temporales? Propón una estrategia de división alternativa y justifícala.


Aplica lo que aprendiste

Escenario: Envió el pipeline StandardScaler → SVC del artículo con CV de 5 pliegues sobre un dataset de 100 filas de make_classification(random_state=42). Las diapositivas de revisión de stakeholders muestran una precisión media ≈ 90%, pero el VP de Datos objeta: “¿Noventa por ciento en cien filas? ¿Cómo sé que este no es el caso clásico de sobreajuste polinómico — grado 15, veinte puntos, MSE de entrenamiento 0.0001 y luego un colapso de 10–100× en datos no vistos?”

Entregable (250–350 palabras): Un memo para ese VP defendiendo el 90%. Su memo debe:

  1. Nombrar dos modos de falla que habrían inflado este número si hubiera sido descuidado, y explicar en una frase cada uno cómo su configuración los evita:

    • la trampa del examen de práctica (el polinomio de grado 15 / 20 puntos: MSE de entrenamiento ≈ 0.0001 vs. MSE de prueba 10–100× mayor), y
    • fuga de datos por ajustar StandardScaler en el dataset completo antes de dividir (el error de “echar un vistazo a las respuestas” que el artículo señala como el error más común entre profesionales). Vincule ambos con por qué usó Pipeline([('scaler', …), ('svc', …)]) + cross_val_score(..., cv=5) — el escalador solo aprende las medias del fold de entrenamiento, nunca del fold reservado.
  2. Reportar la media y la desviación estándar a nivel de fold, aplicar la regla de bandera roja std > 0.05 del artículo, y contrastar explícitamente su situación con el Escenario B del artículo (media de 90%, std de 15% — “una apuesta”) en lugar de simplemente reafirmar el 90% del titular.

  3. Exponer al menos una salvedad honesta que un revisor cuidadoso plantearía: por ejemplo, 5 pliegues es una muestra pequeña para confiar incluso en una std baja; o, si las 100 filas estuvieran ordenadas temporalmente, un K-Fold simple permitiría que el miércoles prediga al lunes y necesitaría TimeSeriesSplit (índice de prueba siempre posterior al de entrenamiento).

Rúbrica (cada casilla debe poder marcarse):

  • 250–350 palabras, dirigido a un VP no técnico, con la jerga definida en su primer uso.
  • Nombra TANTO la trampa del examen de práctica como la fuga por preprocesamiento antes de la división, con el Pipeline como mecanismo de prevención para cada uno.
  • Reporta la media y la std, aplica la regla > 0.05, y contrasta con el Escenario B en lugar de reafirmar la media.
  • Expone ≥ 1 salvedad (suerte con pocos pliegues, supuesto IID / serie temporal, o similar) — sin exagerar.
  • Termina con una recomendación de avanzar/no avanzar anclada en la evidencia de CV (media + std + salvedad), no solo en la precisión del titular.

Esta traducción fue generada automáticamente y puede contener errores. Si el idioma inglés es tu preferencia, puedes leer el artículo original en inglés .

¿Buscas otra cosa?

Busca en todos los artículos por título, resumen o tema.