División train/test con rsample

r
machine-learning
tidymodels
Por qué dividir los datos es la pieza más importante de cualquier ML honesto. initial_split() con estratificación, validation set, cross-validation con vfold_cv() y el caso especial de series temporales con rolling origin.

Por qué dividir los datos

La regla número uno del machine learning honesto: el modelo nunca evalúa sobre datos que ha visto.

Si entrenas un modelo y luego mides su precisión sobre los mismos datos, lo que mides es memorización, no generalización. Un random forest con 5000 árboles puede ajustar perfectamente los datos de entrenamiento, y predecir mal completamente con datos nuevos. Ese fenómeno se llama sobreajuste y es la causa de la mitad de los modelos de ML que fallan en producción.

La solución es separar datos en train y test desde el principio. El test set se reserva, el modelo no lo ve hasta el último paso, cuando ya está completamente entrenado.

rsample es el paquete de tidymodels que gestiona esta división.

initial_split(): train + test

library(tidymodels)

# Datos de ejemplo
data(diamonds, package = "ggplot2")

set.seed(123)
split <- initial_split(diamonds, prop = 0.8)
split
#> <Training/Testing/Total>
#> <43153/10787/53940>

prop = 0.8 reserva el 80 % para train y 20 % para test. Las proporciones típicas:

  • 80/20 o 75/25, datasets pequeños o medianos.
  • 70/30: cuando quieres más margen de evaluación.
  • 90/10: datasets muy grandes donde 10 % ya es muchos miles de filas.

Para extraer cada parte:

train <- training(split)
test  <- testing(split)

Estratificación: por qué importa

Imagina un problema de clasificación con clases desbalanceadas (1 % de positivos, 99 % de negativos, fraude, enfermedad rara, etc.). Una división aleatoria simple puede dejar casi sin positivos en test, haciendo la evaluación poco fiable.

La estratificación divide manteniendo la proporción de la variable objetivo en cada grupo:

set.seed(123)
split <- initial_split(diamonds, prop = 0.8, strata = cut)

Con strata = cut, train y test tienen aproximadamente la misma distribución de la variable cut. Estratifica siempre que tu outcome sea categórico o numérico con sesgo notable.

Para numéricas, rsample automáticamente bineariza la variable y estratifica por bin. Sin trabajo extra.

Validation set: la tercera división

Cuando vas a tunear hiperparámetros, no debes mirar el test set durante el tuning, eso lo “contamina” y deja de ser una evaluación honesta.

Dos opciones:

Opción A: validation set explícito (cuando hay datos abundantes)

set.seed(123)
splits <- initial_validation_split(diamonds, prop = c(0.6, 0.2))
train <- training(splits)
val   <- validation(splits)
test  <- testing(splits)

60 % train, 20 % validation (para tunear), 20 % test (para evaluación final). Si tienes ≥ 10 000 observaciones, esta opción es la más limpia.

Opción B: cross-validation sobre el train set (cuando hay datos limitados)

set.seed(123)
split <- initial_split(diamonds, prop = 0.8, strata = cut)
train <- training(split)

folds <- vfold_cv(train, v = 5, strata = cut)

Aquí no reservamos validation explícito. En su lugar, hacemos k-fold CV sobre el train set para tunear. Es lo más común en datasets de tamaño moderado.

vfold_cv: cross-validation estándar

set.seed(123)
folds <- vfold_cv(train, v = 5, strata = cut)
folds
#> #  5-fold cross-validation using stratification
#> # A tibble: 5 × 2
#>   splits                 id
#>   <list>                 <chr>
#> 1 <split [34522/8631]>   Fold1
#> 2 <split [34522/8631]>   Fold2
#> 3 <split [34522/8631]>   Fold3
#> 4 <split [34523/8630]>   Fold4
#> 5 <split [34523/8630]>   Fold5

5-fold significa que el train se parte en 5 grupos. El modelo se entrena 5 veces, cada vez con 4 grupos como entrenamiento y 1 como evaluación. La métrica final es el promedio.

Heurísticas para v:

  • v = 5: el default sensato.
  • v = 10: si tienes capacidad de cómputo y datos limitados.
  • v = 3: modelos muy lentos y datos abundantes.

Más allá de v = 10 rara vez vale la pena. Para LOOCV (v = n), hay funciones específicas pero raramente es necesario.

group_vfold_cv: cuando hay clusters en los datos

Si tus observaciones están agrupadas (varios pacientes por hospital, varios productos por categoría), una división estándar puede meter observaciones del mismo grupo en train y test, y el modelo “memoriza” patrones del grupo en lugar de generalizar.

group_vfold_cv(train, group = hospital_id, v = 5)

Asegura que todas las observaciones de un mismo grupo estén en el mismo fold. Es la división correcta cuando hay clusters jerárquicos.

Bootstrap

rsample también soporta bootstrap, muestreo con reemplazo:

bootstraps(train, times = 1000)

Genera 1000 muestras del mismo tamaño que train, cada una con reemplazo. Útil para:

  • Estimar incertidumbre en métricas (IC bootstrap del AUC, por ejemplo).
  • Random forests internamente lo usan, pero la CV externa para tuning sigue siendo k-fold.

Series temporales: el caso especial

Las divisiones aleatorias no funcionan con series temporales. Predecir el pasado a partir del futuro es trampa, en producción nunca vas a tener esa información.

La división correcta para series temporales es rolling origin: el train son los datos anteriores a un punto, el test los posteriores, y se desliza el corte:

sliding_period(
  datos_ts,
  index = fecha,
  period = "month",
  lookback = 12,    # 12 meses de train
  assess_stop = 3   # 3 meses de evaluación
)

Si tu problema involucra cualquier variable temporal y vas a predecir el futuro, usa rolling origin o variantes (sliding_window, sliding_index).

Trampas habituales

  • set.seed() ausente. Sin semilla, cada ejecución da una división distinta, los resultados no son reproducibles. Pon set.seed() antes de cualquier función de rsample.
  • Tocar el test set durante el desarrollo. Cada vez que evalúas en test set y vuelves a tunear, el test contamina el modelo. Test es para la evaluación final, una sola vez.
  • No estratificar con clases desbalanceadas. Sin estratificación, una división aleatoria con 1 % de positivos puede dejar cero positivos en test. Estratifica siempre que el outcome sea categórico.
  • Cross-validation con datos agrupados. Si tus observaciones tienen estructura (varios sujetos por institución, varias mediciones por paciente), vfold_cv estándar mete observaciones correlacionadas en train y test, la métrica resultante es optimista. Usa group_vfold_cv.
  • División aleatoria con series temporales. Casi siempre invalida la evaluación. Si la variable de tiempo importa, usa sliding_period.

En la siguiente entrega

Has dividido los datos correctamente. La siguiente pieza es el preprocesado, transformar las features antes de entrenar. Hay una regla crítica: fit en train solamente, aplica en test. Si la violas, los datos se filtran del test al train y tu evaluación deja de ser honesta. recipes gestiona esto correctamente. Lo siguiente.