Splits Aleatorios vs Validación Temporal¶
sklearn.model_selection.train_test_split es útil cuando las observaciones
pueden tratarse como aproximadamente independientes e idénticamente distribuidas.
Esa no es la pregunta que Jano busca responder.
Cuando los datos están correlacionados en el tiempo, la pregunta suele ser más operativa:
¿Cómo se habría comportado el modelo si solo hubiese visto el pasado y luego tuviera que predecir el futuro?
Un split aleatorio puede ocultar esa pregunta porque mezcla fechas entre train y test.
El primer snippet asume que scikit-learn está instalado solo para ilustrar el baseline común. Jano no requiere scikit-learn.
El Problema¶
Imaginá un dataset diario donde la distribución del target cambia cerca del final del período:
import pandas as pd
from sklearn.model_selection import train_test_split
frame = pd.DataFrame(
{
"timestamp": pd.date_range("2025-01-01", periods=120, freq="D"),
"feature": range(120),
"target": [0] * 80 + [1] * 40,
}
)
train_random, test_random = train_test_split(
frame,
test_size=0.2,
shuffle=True,
random_state=7,
)
temporal_leakage = (
train_random["timestamp"].max() > test_random["timestamp"].min()
)
print(temporal_leakage)
# True
El problema no es que scikit-learn esté mal. train_test_split hace lo que
debe hacer: muestreo aleatorio. El problema es que el muestreo aleatorio es la
abstracción equivocada para una validación temporal parecida a producción.
En este setup, train puede contener observaciones de fechas posteriores a algunas observaciones de test. Si el target cambia en el tiempo, la evaluación puede volverse demasiado optimista porque el modelo ya vio parte del régimen futuro.
La Versión Con Jano¶
Con Jano, el split no se define como un porcentaje aleatorio de filas. Se define como una política temporal:
import pandas as pd
from jano import TemporalPartitionSpec, WalkForwardPolicy
frame = pd.DataFrame(
{
"timestamp": pd.date_range("2025-01-01", periods=120, freq="D"),
"feature": range(120),
"target": [0] * 80 + [1] * 40,
}
)
policy = WalkForwardPolicy(
time_col="timestamp",
partition=TemporalPartitionSpec(
layout="train_test",
train_size="60D",
test_size="14D",
gap_before_test="1D",
),
step="14D",
strategy="rolling",
)
plan = policy.plan(frame, title="Validación temporal productiva")
print(
plan.to_frame()[
[
"iteration",
"train_start",
"train_end",
"train_rows",
"test_start",
"test_end",
"test_rows",
]
].head()
)
El plan vuelve explícito el contrato temporal antes de entrenar cualquier modelo:
iteration train_start train_end train_rows test_start test_end test_rows
0 2025-01-01 2025-03-02 60 2025-03-03 2025-03-17 14
1 2025-01-15 2025-03-16 60 2025-03-17 2025-03-31 14
2 2025-01-29 2025-03-30 60 2025-03-31 2025-04-14 14
3 2025-02-12 2025-04-13 60 2025-04-14 2025-04-28 14
Qué Cambia¶
La diferencia está en el contrato de evaluación:
train_test_splitresponde: ¿este modelo generaliza a una muestra aleatoria del mismo período mezclado?Jano responde: ¿cómo se comportaría este modelo a medida que avanza el tiempo bajo una política concreta de entrenamiento y evaluación?
Eso te da:
ventanas ordenadas de train y test
duración explícita de train/test
gaps explícitos para modelar latencia de datos o labels
folds repetidos en lugar de una única estimación estática
un objeto
plan()que puede inspeccionarse, filtrarse y auditarse antes de hacer slicing del dataset
Ahí es donde entra Jano: no como reemplazo de scikit-learn, sino como la capa de validación temporal previa al entrenamiento del modelo.