Módulo 2 · Datos

Lección 4 de 6

Particiones y fuga de datos

En esta páginaEl problema

El problema

Una planta guarda una lectura diaria de sensores de 60 máquinas durante ocho días. Mantenimiento etiquetó cada máquina con falla = 1 si falló en los 30 días siguientes. Así, las ocho lecturas de una máquina comparten la misma etiqueta. La columna horas_reparacion registra cuánto duró la reparación posterior a una falla. Estas seis filas vienen de dos máquinas:

maquina_idtemperatura_cvibracion_mm_spresion_kpahoras_reparacionfalla
M-0172.21.722770.00
M-0171.71.512700.00
M-0174.01.892790.00
M-0859.63.8930411.81
M-0858.73.6730411.81
M-0856.43.9431011.81

El próximo mes llegarán máquinas que no están en esta tabla. Necesitas estimar la exactitud de un modelo simple con las lecturas de esas máquinas nuevas: la fracción de lecturas que clasifica correctamente.

El registro es sintético y se genera con semilla. Cada máquina tiene su propio nivel base de temperatura, vibración y presión, y la etiqueta depende sobre todo de ese nivel base. La llamada a pd.concat simula un error del sistema de registro: 40 lecturas se guardaron dos veces.

Mostrar códigoOcultar códigoPython
Python 3 · ejemplo reproducible
import numpy as np
import pandas as pd

rng = np.random.default_rng(3)
n_maquinas, n_lecturas = 60, 8
maquina = np.repeat(np.arange(n_maquinas), n_lecturas)  # máquina de cada lectura

temperatura_base = rng.normal(60, 6, n_maquinas)
vibracion_base = rng.normal(3, 0.8, n_maquinas)
presion_base = rng.normal(300, 30, n_maquinas)
riesgo = (
    (vibracion_base - 3) / 0.8
    + 0.6 * (temperatura_base - 60) / 6
    + rng.normal(0, 0.4, n_maquinas)
)
falla = (riesgo[maquina] > 0.8).astype(int)  # cada lectura hereda la etiqueta de su máquina

temperatura = temperatura_base[maquina] + rng.normal(0, 1.0, maquina.size)
vibracion = vibracion_base[maquina] + rng.normal(0, 0.15, maquina.size)
presion = presion_base[maquina] + rng.normal(0, 5, maquina.size)
horas_base = rng.uniform(2, 12, n_maquinas)
horas = np.where(falla == 1, horas_base[maquina], 0.0)

registros = pd.DataFrame(
    {
        "maquina_id": [f"M-{i + 1:02d}" for i in maquina],
        "temperatura_c": temperatura.round(1),
        "vibracion_mm_s": vibracion.round(2),
        "presion_kpa": presion.round(0),
        "horas_reparacion": horas.round(1),
        "falla": falla,
    }
)
registros = pd.concat(
    [registros, registros.sample(40, random_state=0)], ignore_index=True
)

La tabla tiene 520 filas y 6 columnas: 480 lecturas de 60 máquinas y 40 copias. La proporción de filas con falla es 0.233.

Particiones estratificadas

Una partición separa las filas en conjuntos con funciones distintas. Entrenamiento ajusta el modelo y cualquier paso de preprocesamiento. Validación compara decisiones: columnas, modelo o parámetros. Prueba da la estimación final y se usa una sola vez, al terminar.

train_test_split separa una tabla en dos partes. Para obtener tres, se llama dos veces: la primera llamada aparta 20 % para prueba y la segunda aparta 25 % del resto para validación. El argumento stratify recibe la columna objetivo y reparte cada clase en la misma proporción entre las partes. random_state fija el sorteo para que la partición se repita igual.

Pregunta. Con 520 filas, escribe cuántas filas tendrá cada partición y qué proporción de fallas esperas en cada una.

Python 3 · ejemplo reproducible
from sklearn.model_selection import train_test_split

temporal, prueba = train_test_split(
    registros, test_size=0.2, stratify=registros["falla"], random_state=0
)
entrenamiento, validacion = train_test_split(
    temporal, test_size=0.25, stratify=temporal["falla"], random_state=0
)  # 25 % del 80 % restante = 20 % del total

for nombre, parte in [
    ("entrenamiento", entrenamiento),
    ("validación", validacion),
    ("prueba", prueba),
]:
    print(f"{nombre:<13} {len(parte):>3} filas  fallas {parte['falla'].mean():.3f}")
Salida esperada

La salida esperada está oculta.

Pregunta. Sin stratify, la proporción de fallas en prueba depende de qué filas caigan ahí. Estima entre qué valores variará con cinco semillas distintas.

Ver resultado

La tabla repite la primera división, 20 % de registros para prueba, sin stratify y con random_state de 0 a 4. Cada fila da la semilla, la proporción de fallas en prueba y cuántas filas de prueba son fallas. Por ejemplo, la fila con 1, 0.192 y 20 de 104 dice que con random_state=1 la prueba recibe solo 20 fallas.

semillaproporción de fallas en pruebafallas en prueba
00.23124 de 104
10.19220 de 104
20.26928 de 104
30.24025 de 104
40.23124 de 104
Lectura. Sin estratificar, la proporción de fallas en prueba va de 0.192 a 0.269. En 104 filas, eso significa entre 20 y 28 fallas. Una exactitud medida con 20 fallas y otra medida con 28 no comparan lo mismo. stratify elimina esa variación, que no depende del modelo.

Preprocesamiento antes de dividir

StandardScaler resta a cada columna su media y divide entre su desviación estándar. fit calcula esas dos cifras con unas filas; transform las aplica a cualquier tabla. El modelo de esta lección mide distancias entre filas, así que las columnas necesitan escalas comparables: sin escalar, las diferencias en presion_kpa son de decenas de kPa y las de vibracion_mm_s rondan 1 mm/s, así que la presión dominaría la distancia. Tras fit, el atributo mean_ guarda las medias calculadas.

Pregunta. Un escalador se ajusta con las 520 filas y otro solo con entrenamiento. ¿Sus medias serán idénticas?

Python 3 · ejemplo reproducible
from sklearn.preprocessing import StandardScaler

columnas = ["temperatura_c", "vibracion_mm_s", "presion_kpa"]
escalador_todo = StandardScaler().fit(registros[columnas])
escalador_ent = StandardScaler().fit(entrenamiento[columnas])

print(escalador_todo.mean_.round(2))
print(escalador_ent.mean_.round(2))
Salida esperada

La salida esperada está oculta.

Un paso que aprende más de los datos amplifica el efecto. El caso siguiente es extremo: 100 filas, 1000 columnas de ruido y un objetivo asignado al azar. Ninguna columna contiene información real sobre el objetivo.

Mostrar códigoOcultar códigoPython
Python 3 · ejemplo reproducible
rng_ruido = np.random.default_rng(0)
X_ruido = rng_ruido.normal(size=(100, 1000))
y_ruido = rng_ruido.integers(0, 2, size=100)

Con matrices, train_test_split recibe X y y por separado y devuelve cuatro partes: las dos mitades de X y después las dos de y.

Python 3 · ejemplo reproducible
X_a, X_b, y_a, y_b = train_test_split(
    X_ruido, y_ruido, test_size=0.5, stratify=y_ruido, random_state=0
)

print(X_a.shape, X_b.shape)
print(y_a.mean(), y_b.mean())
Salida esperada

La salida esperada está oculta.

Cada mitad tiene 50 filas y 1000 columnas de ruido normal. En las dos, 52 % de las etiquetas son 1.

SelectKBest conserva las k columnas más asociadas con el objetivo. KNeighborsClassifier(n_neighbors=1) es el vecino más cercano: busca la fila de entrenamiento más parecida y copia su etiqueta. score predice las filas que recibe y devuelve la exactitud.

Pregunta. La mitad de las filas entrena y la otra mitad se reserva. ¿Qué exactitud esperas en la mitad reservada si el selector se ajusta con las 100 filas? ¿Y si se ajusta solo con la mitad de entrenamiento?

Python 3 · ejemplo reproducible
from sklearn.feature_selection import SelectKBest
from sklearn.neighbors import KNeighborsClassifier

for nombre, X_ajuste, y_ajuste in [
    ("selector con todas las filas", X_ruido, y_ruido),
    ("selector solo con entrenamiento", X_a, y_a),
]:
    selector = SelectKBest(k=10).fit(X_ajuste, y_ajuste)  # elige columnas con estas filas
    vecino = KNeighborsClassifier(n_neighbors=1)
    vecino.fit(selector.transform(X_a), y_a)
    print(f"{nombre}: {vecino.score(selector.transform(X_b), y_b):.2f}")
Salida esperada

La salida esperada está oculta.

Duplicados y grupos

De vuelta a las máquinas. La función evaluar aplica la regla anterior: ajusta el escalador y el vecino más cercano solo con entrenamiento, y mide la exactitud en validación.

Pregunta. Con la partición por filas de la primera sección, ¿qué exactitud en validación esperas?

Python 3 · ejemplo reproducible
def evaluar(entrenamiento, validacion, columnas):
    escalador = StandardScaler().fit(entrenamiento[columnas])
    vecino = KNeighborsClassifier(n_neighbors=1)
    vecino.fit(escalador.transform(entrenamiento[columnas]), entrenamiento["falla"])
    return vecino.score(
        escalador.transform(validacion[columnas]), validacion["falla"]
    )


print(f"{evaluar(entrenamiento, validacion, columnas):.3f}")
Salida esperada

La salida esperada está oculta.

merge sin el argumento on une por todas las columnas comunes, así que solo empareja filas idénticas. drop_duplicates quita las filas repetidas para no contar dos veces la misma copia. isin marca si cada valor aparece en otra columna.

Pregunta. ¿Cuántas filas de validación tienen una copia exacta en entrenamiento? ¿Qué fracción de las filas de validación pertenece a máquinas que también aparecen en entrenamiento?

Python 3 · ejemplo reproducible
copias = validacion.merge(entrenamiento.drop_duplicates())
maquina_vista = validacion["maquina_id"].isin(entrenamiento["maquina_id"])

print(f"Filas con copia exacta en entrenamiento: {len(copias)}")
print(f"Fracción de filas de máquinas vistas: {maquina_vista.mean():.3f}")
Salida esperada

La salida esperada está oculta.

GroupShuffleSplit reparte grupos completos. El argumento groups indica a qué máquina pertenece cada fila, y todas las filas de una máquina quedan en la misma partición. Aquí test_size cuenta máquinas, no filas. split devuelve las posiciones de las filas de cada parte y next toma la única partición generada.

Python 3 · ejemplo reproducible
from sklearn.model_selection import GroupShuffleSplit


def dividir_por_maquina(tabla, fraccion):
    divisor = GroupShuffleSplit(n_splits=1, test_size=fraccion, random_state=0)
    posiciones_a, posiciones_b = next(divisor.split(tabla, groups=tabla["maquina_id"]))
    return tabla.iloc[posiciones_a], tabla.iloc[posiciones_b]  # posiciones, no etiquetas

Antes de dividir, drop_duplicates elimina las copias exactas de registros. Las filas de la nueva prueba ya estuvieron en la partición por filas, pero el cambio a grupos se decide por la estructura de los registros —varias lecturas por máquina y copias exactas—, no por el puntaje en esas máquinas. Como en la primera sección, dos llamadas producen las tres particiones.

Pregunta. Con 60 máquinas, escribe cuántas máquinas tendrá cada partición.

Python 3 · ejemplo reproducible
unicos = registros.drop_duplicates()
temporal_g, prueba_g = dividir_por_maquina(unicos, 0.2)
entrenamiento_g, validacion_g = dividir_por_maquina(temporal_g, 0.25)

for nombre, parte in [
    ("entrenamiento", entrenamiento_g),
    ("validación", validacion_g),
    ("prueba", prueba_g),
]:
    print(f"{nombre:<13} {parte['maquina_id'].nunique():>2} máquinas {len(parte):>3} filas")

compartidas = set(validacion_g["maquina_id"]) & set(entrenamiento_g["maquina_id"])  # intersección
print(f"Máquinas compartidas: {len(compartidas)}")
Salida esperada

La salida esperada está oculta.

Pregunta. Con la partición por máquina, ¿qué exactitud esperas en validación?

Python 3 · ejemplo reproducible
print(f"Exactitud en validación: {evaluar(entrenamiento_g, validacion_g, columnas):.3f}")
print(f"Filas sin falla en validación: {1 - validacion_g['falla'].mean():.3f}")
Salida esperada

La salida esperada está oculta.

Columnas con fuga

Una columna puede contener el objetivo de forma indirecta. Pasa cuando su valor se registra después del evento que se quiere predecir. groupby("falla") separa las filas por clase y agg(["min", "max"]) resume cada grupo con esas dos funciones.

Pregunta. En entrenamiento, ¿qué valores mínimo y máximo tendrá horas_reparacion en las máquinas sin falla? ¿Y en las máquinas con falla?

Python 3 · ejemplo reproducible
print(entrenamiento_g.groupby("falla")["horas_reparacion"].agg(["min", "max"]))
Salida esperada

La salida esperada está oculta.

Pregunta. Con la partición por máquina, ¿qué exactitud en validación esperas si agregas horas_reparacion a las tres columnas de sensores?

Python 3 · ejemplo reproducible
con_horas = columnas + ["horas_reparacion"]

print(f"Sensores: {evaluar(entrenamiento_g, validacion_g, columnas):.3f}")
print(f"Sensores y horas: {evaluar(entrenamiento_g, validacion_g, con_horas):.3f}")
print(f"Solo horas: {evaluar(entrenamiento_g, validacion_g, ['horas_reparacion']):.3f}")
Salida esperada

La salida esperada está oculta.

Prueba y tabla pública

En muchas competencias, las predicciones sobre el conjunto de prueba se califican en dos partes. La tabla pública muestra durante la competencia el puntaje sobre una parte. La tabla privada usa la otra parte y define el resultado final.

La simulación siguiente tiene 200 filas ocultas: 100 públicas y 100 privadas. Hay 300 entregas y cada una predice al azar.

Cada entrega recibe dos exactitudes: la pública, sobre las primeras 100 filas, y la privada, sobre las otras 100. Se conserva la entrega con mejor exactitud pública, la que encabezaría la tabla pública.

Pregunta. ¿La exactitud privada de la entrega elegida será parecida a su exactitud pública?

Ver resultado

La tabla da la exactitud pública y privada de la entrega elegida y, como referencia, la exactitud privada promedio de las 300 entregas. La columna medida dice qué se midió y exactitud, la fracción de aciertos. Por ejemplo, la fila «pública de la elegida» con 0.62 dice que la entrega elegida acierta 62 de las 100 filas públicas.

medidaexactitud
pública de la elegida0.62
privada de la elegida0.46
privada promedio de las 300 entregas0.502
Lectura. La entrega elegida marca 0.62 en la tabla pública y 0.46 en la privada. Ninguna entrega sabe nada: en promedio, las 300 marcan 0.502 en la privada. Elegir por la tabla pública seleccionó la entrega con más suerte en esas 100 filas. Cuando un puntaje decide qué entrega se conserva, sus filas funcionan como validación y el puntaje elegido queda sesgado hacia arriba. Lo mismo ocurre con una prueba local: las decisiones se toman con validación y la prueba se consulta una vez.

Las decisiones de esta lección ya están tomadas: partición por máquina, sin duplicados, escalador ajustado en entrenamiento y solo las tres columnas de sensores. El modelo final se entrena con entrenamiento y validación juntos (temporal_g) y se mide una sola vez en prueba.

Pregunta. ¿La exactitud en prueba estará más cerca del valor por máquina de validación o del valor por filas?

Python 3 · ejemplo reproducible
print(f"Máquinas en prueba: {prueba_g['maquina_id'].nunique()}")
print(f"Exactitud en prueba: {evaluar(temporal_g, prueba_g, columnas):.3f}")
print(f"Filas sin falla en prueba: {1 - prueba_g['falla'].mean():.3f}")
Salida esperada

La salida esperada está oculta.

Práctica

Traza guiada

Esta tabla tiene cuatro máquinas con distinto número de lecturas:

Python 3 · ejemplo reproducible
mini = pd.DataFrame(
    {
        "maquina_id": ["M-A", "M-A", "M-A", "M-B", "M-B", "M-C", "M-D", "M-D"],
        "vibracion_mm_s": [2.1, 2.3, 2.2, 3.9, 4.1, 3.0, 2.8, 2.7],
        "falla": [0, 0, 0, 1, 1, 0, 1, 1],
    }
)

Antes de ejecutar dividir_por_maquina(mini, 0.25), predice cuántas máquinas quedarán en la segunda parte y qué números de filas son posibles. Decide también si las lecturas de M-A pueden quedar separadas. Comprueba con nunique() y len(). Resultado esperado: la segunda parte tiene una máquina, porque 0.25 de cuatro máquinas es una. Según la máquina elegida, tendría 1, 2 o 3 filas; con random_state=0 queda M-C, con una fila. Si tu predicción contaba filas, revisa qué mide test_size en GroupShuffleSplit.

Semillas de partición

Agrega un parámetro semilla a dividir_por_maquina y úsalo como random_state. Repite las dos divisiones por máquina con la misma semilla, de 0 a 4. Para cada semilla, imprime la exactitud en validación y la fracción de filas sin falla. Antes de ejecutar, predice si las cinco exactitudes quedarán a menos de 0.02 de 0.823. Resultado esperado: van de 0.823 a 1.000, y la fracción sin falla va de 0.667 a 0.917. Explica qué implica esa variación al comparar dos decisiones que difieren en 0.02 de exactitud.

Revisión de particiones

Escribe revisar_particion(entrenamiento, validacion). Debe devolver un diccionario con cuatro valores: filas de validación con copia exacta en entrenamiento, máquinas compartidas y proporción de fallas en cada parte. Aplícala a la partición por filas y a la partición por máquina. Resultado esperado: por filas, 8 copias y 50 máquinas compartidas; por máquina, 0 y 0. Agrega un assert que falle si la partición por máquina tiene copias o máquinas compartidas.

Reto opcional

Agrega a unicos dos columnas nuevas. costo_reparacion vale 450 por horas_reparacion más ruido con semilla. antiguedad_anios asigna a cada máquina un entero entre 1 y 15 sorteado con semilla. Para cada columna, decide primero si su valor se conoce el día de la lectura. Después vuelve a dividir unicos con dividir_por_maquina, porque las particiones anteriores no tienen las columnas nuevas, y compara evaluar con y sin cada columna. Explica por qué la comparación numérica sola no basta para decidir si una columna tiene fuga.

Errores que revisar

  • Si la exactitud en validación es casi perfecta con un modelo simple, busca copias exactas entre particiones, grupos compartidos y columnas registradas después del evento.
  • Si fit o fit_transform recibe la tabla completa, validación o prueba, mueve el ajuste a entrenamiento y aplica solo transform a las demás partes.
  • Si la proporción de clases cambia mucho entre las partes de train_test_split, revisa que stratify reciba la columna objetivo de la misma tabla que estás dividiendo.
  • Si una parte de GroupShuffleSplit tiene más o menos filas de las que esperabas, cuenta grupos: test_size reparte máquinas y cada máquina aporta su propio número de filas.
  • Si mediste en prueba, cambiaste algo y volviste a medir, la segunda cifra ya es de validación, no una estimación final.

Cierre

Explica qué midió cada una de las cifras 1.000, 0.823 y 0.875, y cuál estima la exactitud con máquinas nuevas.

Lista de comprobación

  • ¿Con qué filas se ajusta cada fit de tu código?
  • ¿Alguna máquina o fila copiada aparece en más de una partición?
  • ¿Cada columna existe en el momento de la predicción?
  • ¿Por qué la mejor entrega en la tabla pública suele bajar en la privada?
  • ¿Cuántas veces se evaluó el modelo final en prueba?

Documentación oficial

Cargando progreso…