Validación cruzada y separación de datos
Separar datos parece trivial y es donde se filtran los errores más caros: información del futuro, filas repetidas entre particiones y decisiones tomadas mirando el conjunto de prueba.
Para este tema conviene tener claro:Qué significa que un modelo aprenda
Un modelo con 99% de exactitud en validación y 60% en producción no suele ser un problema de modelado: es un problema de cómo se separaron los datos. La validación mide lo que se le permite medir, y si algo se filtró, mide una fantasía.
Entrenamiento, validación y prueba
Los datos se parten en tres. Entrenamiento, con el que el modelo ajusta sus parámetros. Validación, con el que se eligen los hiperparámetros y se compara entre modelos. Prueba, que se usa una sola vez, al final, para estimar el desempeño real.
Los tres son necesarios justamente porque elegir también es aprender. Si se usan los mismos datos para elegir el modelo y para estimar su error, esa estimación queda optimista: el proceso se ajustó a esos datos aunque cada modelo individual no lo haya hecho.
Antes de seguir, predecí
Rotar la partición k veces
Con pocos datos, apartar un tercio duele y además la estimación depende mucho de qué filas cayeron en cada lado. La validación cruzada en partes resuelve las dos cosas: se divide en bloques, se entrena veces dejando uno afuera cada vez, y se promedian los resultados.
Todos los datos sirven para entrenar y para validar, en rondas distintas. El costo es entrenar veces, y valores de 5 o 10 son lo habitual. La variabilidad entre rondas también informa: si los resultados difieren mucho, la estimación es frágil.
Los datos se parten en cinco bloques del mismo tamaño.
La fuga de datos y su forma clásica
El error grave se llama fuga de datos: información del conjunto de validación que se coló en el entrenamiento. La forma clásica es normalizar o imputar usando estadísticas calculadas sobre todo el conjunto antes de partirlo.
Parece inofensivo y no lo es: el promedio usado para escalar ya contiene información de los datos de validación. La regla es que toda transformación que aprenda algo de los datos debe ajustarse sólo con el entrenamiento y aplicarse después al resto. Por eso los pipelines existen: encapsulan esa disciplina para que no dependa de la memoria de quien programa.
Con series temporales, partir al azar está mal
Con series temporales, la partición al azar está mal por definición: entrenar con datos de marzo para predecir febrero es usar el futuro para predecir el pasado, y eso no va a existir en producción.
La partición tiene que ser temporal: entrenar con lo viejo, validar con lo nuevo, y repetir moviendo la ventana hacia adelante. Conviene además dejar un hueco entre entrenamiento y validación si hay efectos que se arrastran, para que no se filtren por continuidad.
Cuando las filas no son independientes
El otro caso frecuente es cuando las filas no son independientes. Varias consultas del mismo paciente, varias fotos del mismo producto, varias transacciones del mismo cliente.
Si el mismo grupo cae en entrenamiento y en validación, el modelo puede reconocer al grupo en vez de aprender el patrón, y la métrica sale inflada. La partición tiene que ser por grupo: todas las filas de un cliente van juntas a un solo lado.
Estratificar con clases desbalanceadas
Con clases desbalanceadas conviene estratificar: que cada partición conserve la proporción original de clases. Sin eso, con un 2% de positivos, una partición puede quedarse casi sin ninguno y la métrica se vuelve ruido.
Y una advertencia sobre el conjunto de prueba: cada vez que se lo mira para decidir algo, deja de ser una estimación limpia. Si hubo que usarlo varias veces, lo honesto es decirlo, o conseguir datos nuevos para la evaluación final.
La validación que no filtra información
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import cross_val_score, StratifiedKFold, TimeSeriesSplit
# Todo el preprocesamiento adentro del pipeline: se ajusta con cada partición de entrenamiento
modelo = Pipeline([
("escalar", StandardScaler()),
("clasificador", LogisticRegression()),
])
# Estratificado: cada ronda conserva la proporción de clases
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=0)
scores = cross_val_score(modelo, X, y, cv=cv, scoring="average_precision")
print(scores.mean(), scores.std()) # el desvío importa tanto como el promedio
# Con series de tiempo, las particiones no pueden ser al azar
cv_tiempo = TimeSeriesSplit(n_splits=5) # cada ronda entrena con el pasado y valida con el futuroPoner el escalador adentro del pipeline no es prolijidad: es lo que impide la fuga. Si se escala antes de la validación cruzada, cada ronda de entrenamiento ya vio las estadísticas de su propio conjunto de validación, y el resultado sale optimista.
El desvío estándar de los cinco puntajes se reporta junto con el promedio por un motivo concreto: si las rondas difieren mucho, la estimación es frágil y la diferencia entre dos modelos puede ser ruido.
Qué esquema corresponde
| Situación | Esquema | Por qué |
|---|---|---|
| Datos independientes y balanceados | k-fold, k = 5 o 10 | el default razonable |
| Clases desbalanceadas | estratificado | si no, alguna ronda puede no tener positivos |
| Filas agrupadas por entidad | agrupado por esa entidad | evita memorizar el grupo |
| Series de tiempo | particiones temporales | entrenar con el futuro es fuga |
| Muy pocos datos | dejar uno afuera | usa todo, y es caro |
| Muchos datos | una sola partición alcanza | la validación cruzada es k veces más cara |
Cierre
Entrenamiento para ajustar, validación para elegir, prueba una sola vez. La validación cruzada aprovecha datos escasos, y las trampas son siempre las mismas: transformaciones ajustadas antes de partir, particiones al azar en series temporales y filas del mismo grupo repartidas entre lados.
Autoevaluación
¿Lo entendiste?
Práctica