Ir al contenido
wannacode

Fuga de datos

En resumen: Hay fuga de datos cuando un modelo ve durante el entrenamiento información que no tendrá en la predicción real, así que la puntuación de la prueba parece mejor de lo que será el modelo en el uso real.

Problema

99,7 % de exactitud

Un colega ha construido el modelo de la lección sobre clasificación: qué clientes no pedirán después del 1 de julio. Solo que toma la tabla de clientes directamente del sistema, tal como está hoy, 1 de noviembre: número de pedidos, días desde el último pedido y pedido medio. Y te enseña:

Pythondatos: las tablas de la tienda

Hay 1279 clientes. La regla «todos vuelven» acierta con el 72,5 % de los clientes de prueba. El modelo acierta con el 99,7 %: de 320 clientes en los datos de prueba falla uno. En la lección sobre clasificación, con las mismas tres características, el modelo tenía un 72,2 %, por debajo de la regla. ¿Qué cambió? La tabla.

La idea

La respuesta está en las características

Mira cuántos días desde el último pedido tienen los clientes que vuelven (lost es 0) y los perdidos (lost es 1):

Pythondatos: las tablas de la tienda

Todo cliente que vuelve tiene de 0 a 122 días, y todo perdido de 123 a 363. Un solo número los separa sin un error. ¿Por qué? days_since se calcula hasta hoy. Quien pidió después del 1 de julio tiene un último pedido posterior al 1 de julio, así que está a menos de 123 días del 1 de noviembre. La característica contiene la etiqueta. Lo mismo vale para orders: cuenta también los pedidos posteriores al 1 de julio.

Esto se llama fuga de datos (data leakage): información que el modelo no tendrá cuando prediga se cuela en el entrenamiento. El 1 de julio no sabes quién pedirá en agosto. La regla es una: cada característica se calcula solo con lo que se conoce en el momento de la predicción.

Más precisamente, para curiosos

La fuga de datos se esconde en columnas que el sistema actualiza después del hecho: «pedidos totales», «última compra», «estado», «artículo devuelto». De cada columna pregunta cuándo se escribe su valor. Si es después del día de la predicción, la columna no sirve como característica, por útil que parezca. Las características más fuertes de un modelo son también las primeras que compruebas.

La idea

El número honesto

Las mismas características dos veces. Una con todos los pedidos hasta hoy, otra solo con los pedidos anteriores al 1 de julio y hasta el 1 de julio:

Pythondatos: las tablas de la tienda

Con el futuro en las características la exactitud es del 99,7 %. Con honestidad es del 72,2 %, y la línea base es 72,5 %. Con estas tres características el modelo no gana nada en exactitud, igual que en la lección sobre clasificación. Desagradable, pero cierto. Mejor saberlo ahora que después de repartir los cupones.

features(rows, now) es una función para las dos tablas: la única diferencia es qué pedidos le das y hasta qué día cuenta. .loc[honest.index] toma los mismos 1279 clientes en el mismo orden, así que las dos divisiones son idénticas: 959 clientes para el entrenamiento y 320 para la prueba.

Míralo

Cómo se cuela el futuro en la característica

El mismo código, despacio. Los puntos son los pedidos de cuatro clientes por mes, y el muro es el 1 de julio. La etiqueta sale de los puntos a su derecha. La característica del colega los cuenta también.

import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
orders = pd.read_csv('year_orders.csv', parse_dates=['ordered_at'])
items = pd.read_csv('year_items.csv')
items['value'] = items['quantity'] * items['unit_price']
orders['basket'] = orders['id'].map(items.groupby('order_id')['value'].sum())
cutoff = pd.Timestamp('2026-07-01')
before = orders[orders['ordered_at'] < cutoff]
after = orders[orders['ordered_at'] >= cutoff]
def features(rows, now):
people = rows.groupby('customer_id').agg(
orders=('id', 'size'), last=('ordered_at', 'max'), basket=('basket', 'mean'))
people['days_since'] = (now - people['last']).dt.days
return people[['orders', 'days_since', 'basket']]
honest = features(before, cutoff)
leaky = features(orders, pd.Timestamp('2026-11-01')).loc[honest.index]
y = (~honest.index.isin(after['customer_id'])).astype(int)
for name, X in [('con el futuro', leaky), ('honesto', honest)]:
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.25, random_state=42, stratify=y)
model = LogisticRegression(max_iter=1000).fit(X_train, y_train)
print(name, round(model.score(X_test, y_test), 3))
print('clientes:', len(honest), 'entrenamiento:', len(X_train), 'prueba:', len(X_test))
print('línea base:', round(1 - y_test.mean(), 3))
Paso a paso, en palabras
  1. orders son todos los pedidos del año, hasta el 31 de octubre de 2026. Los puntos son los pedidos de cuatro clientes por mes.
  2. cutoff es el 1 de julio. A la izquierda del muro está before: lo que sabes. A la derecha está after: lo que predices.
  3. lost es 1 si el cliente no tiene ningún pedido a la derecha. La etiqueta se hace con los puntos posteriores al 1 de julio.
  4. La tabla del colega cuenta hasta hoy. orders cuenta también los puntos de la derecha, y days_since se mide desde hoy: los mismos puntos con los que se hace la etiqueta.
  5. La división: 959 clientes para entrenar y 320 para la prueba, de 1279.
  6. Con el futuro en las características, la exactitud es 99,7%. La línea base «todos vuelven» es 72,5%.
  7. De forma honesta: las características terminan el 1 de julio. Los puntos de la derecha no se cuentan, y days_since se mide desde el muro.
  8. La exactitud honesta es 72,2%, por debajo de la línea base. 99,7% fue una fuga de datos, no un modelo.

Los clientes de la escena son un boceto: los números de los pies de imagen son reales, desde el número de clientes hasta las dos exactitudes.

Pruébalo

Una característica más

1. El 1 de julio decides quién recibe un cupón. ¿Qué característica es honesta para el modelo?

El colega quiere una característica más: recent, el número de pedidos del cliente en los últimos 30 días. Calcúlala con honestidad, a fecha del 1 de julio: guarda en month los pedidos de los 30 días anteriores a cutoff, en people['recent'] su número para cada cliente (0 para los que no tienen pedido) y en score la exactitud del modelo con las cuatro características.

Pythondatos: las tablas de la tienda

La idea

Preparación antes de la división

La regresión logística funciona mejor cuando las características están en una escala parecida: days_since llega a 240, y orders rara vez pasa de 10. El StandardScaler resta a cada característica su media y divide entre la desviación estándar. Así cada característica queda centrada en 0, con una dispersión de 1. El escalador aprende las medias con fit, de los datos que le das. El colega le da todo:

Pythondatos: las tablas de la tienda

El escalador aprendió de los 1279 clientes: las medias son 5,88 pedidos, 67,42 días y 10,69 €. Los datos de entrenamiento solos tienen 5,96, 67,84 y 10,68. La diferencia viene de los 320 clientes de prueba: influyeron en la preparación antes de que el modelo los viera. Los datos de prueba ya no son del todo desconocidos.

Aquí la exactitud no cambia: es otra vez 72,2 %, porque las medias están cerca. No siempre es así. La fuga crece con pocos datos y con una preparación que mira la etiqueta, por ejemplo cuando eliges las características según cómo se relacionan con lost en todas las filas. Rellenar valores que faltan con la media (fillna) es el mismo caso: la media se calcula solo con los datos de entrenamiento.

La idea

Pipeline: la preparación aprende solo de los datos de entrenamiento

Un Pipeline une la preparación y el modelo en un objeto. make_pipeline(StandardScaler(), LogisticRegression()) hace una cadena de dos pasos:

  • pipe.fit(X_train, y_train) entrena el escalador y luego el modelo, solo con los datos de entrenamiento;
  • pipe.score(X_test, y_test) y pipe.predict escalan los datos de prueba con las medias aprendidas y le preguntan al modelo.
Pythondatos: las tablas de la tienda

Las medias del escalador en pipe son 5,96, 67,84 y 10,68: exactamente las de los datos de entrenamiento. La exactitud es del 72,2 %. Ahora el orden no puede fallar: la preparación es parte del modelo. Un Pipeline ayuda también en la próxima lección: en la validación cruzada cada división aprende el escalador de nuevo.

1. ¿Qué hace pipe.score(X_test, y_test) cuando pipe = make_pipeline(StandardScaler(), LogisticRegression()) se entrenó con los datos de entrenamiento?
Más precisamente, para curiosos

pipe[0] es el primer paso, y pipe[-1] es el modelo. Una cadena puede tener más pasos: SimpleImputer rellena los valores que faltan, OneHotEncoder hace columnas 0/1 a partir de categorías, y ColumnTransformer aplica preparaciones distintas a columnas distintas. Todos aprenden con un solo fit, solo con los datos de entrenamiento.

La idea

Un cliente en el entrenamiento y en la prueba

La tabla se pegó a partir de dos exportaciones, y cada cliente está dos veces. En el código lo hace pd.concat: pone las dos tablas una debajo de la otra. Parece inofensivo: los mismos datos, solo que más. Pruebas un árbol de decisión (DecisionTreeClassifier) que crece sin límite de profundidad:

Pythondatos: las tablas de la tienda

Hay 2558 filas, y 1279 de ellas son repeticiones. Con los duplicados el árbol acierta el 89,4 % de los datos de prueba, por encima de la línea base. Sin ellos, el 58,4 %. Con una división al azar la mayoría de las filas de prueba tienen un gemelo en el entrenamiento, y un árbol sin límite se memoriza cada fila. No predice, recuerda. Un cliente nuevo no tiene gemelo: el 58,4 % es el número real, por debajo de la línea base, porque el árbol está sobreajustado. De eso trata la próxima lección.

La regla: quitas los duplicados (drop_duplicates) antes de la división. Aquí reset_index() convierte customer_id en una columna, para que se quiten las repeticiones del mismo cliente y no dos clientes distintos con los mismos números.

Más precisamente, para curiosos

A veces un cliente tiene varias filas reales, por ejemplo una por pedido. No hay duplicados, pero si sus filas están tanto en el entrenamiento como en la prueba, el modelo vuelve a aprender al cliente, no la regla. Entonces divides por cliente: GroupShuffleSplit(n_splits=1, test_size=0.25, random_state=42).split(X, y, groups=rows['customer_id']) pone todas las filas de un cliente en el mismo lado.

La idea

Días al azar de una serie temporal

En la lección sobre regresión dividiste por tiempo. Aquí ves por qué una división al azar miente. La predicción es para los pedidos por día, a partir del número del día y el día de la semana. El modelo es un bosque aleatorio (RandomForestRegressor): muchos árboles, cada uno entrenado con filas algo distintas, y la predicción es su promedio.

Pythondatos: las tablas de la tienda

Con días al azar el bosque se equivoca en 4,88 pedidos al día de media, y la media (DummyRegressor) en 5,76: el bosque parece mejor. Entrenado hasta el 31 de julio y probado con agosto a octubre, el bosque se equivoca en 7,96, y la media en 5,44. El bosque es peor que la media.

Con una división al azar, para acertar el 15 de septiembre el bosque ha visto el 14 y el 16 de septiembre. En la vida real, el 31 de julio no tienes ni un solo día de septiembre. Además, los árboles no continúan una tendencia: un día posterior al último conocido es para ellos como los últimos días conocidos. Y de agosto a octubre hay también una sorpresa: septiembre es más flojo. Por qué, lo descubriremos en el proyecto. Un modelo entrenado hasta julio no puede saberlo, y tú tampoco el 31 de julio. Por eso 7,96 es el número honesto.

Ejercicio

Arregla el modelo del colega

Arregla el código del colega. Las características salen solo de los pedidos anteriores a cutoff, y days_since se cuenta hasta cutoff. El escalador y la regresión logística están en pipe, entrenado solo con los datos de entrenamiento. Guarda en score la exactitud de pipe en los datos de prueba.

Pythondatos: las tablas de la tienda

El colega también tiene un modelo para los ingresos por día, sin los pedidos cancelados. Pruébalo con honestidad. Entrena el bosque aleatorio (n_estimators=50, random_state=42) y un DummyRegressor con los días hasta el 31 de julio (late es la máscara de los días desde el 1 de agosto). Luego guarda en mae el error (MAE) del bosque en agosto a octubre, y en base_mae el error de la media.

Pythondatos: las tablas de la tienda

Test

Ponte a prueba

1. Un modelo para «¿volverá el cliente?» da un 99 % de exactitud, y la línea base es 72,5 %. ¿Qué haces primero?
2. Rellenas los valores que faltan de una columna con su media. ¿De qué filas calculas la media?
3. Tras drop_duplicates la exactitud del árbol baja del 89,4 % al 58,4 %. ¿Qué número es el correcto?
4. Predices los pedidos por día para los próximos tres meses. ¿Cómo divides los datos?
5. ¿Por qué pones el escalador y el modelo en un Pipeline, en lugar de escalar la tabla una vez al principio?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·