Módulo 9 · Problemas integrados

Lección 1 de 4

Descomposición y baseline

En esta páginaEl problema

El problema

Un concurso publica este enunciado:

Tiempo de entrega. Una cadena de comida registró 1600 pedidos a domicilio durante un mes. Para cada pedido de prueba.csv, predice cuántos minutos pasan desde que el pedido sale de la cocina hasta que llega al cliente.

Cada pedido tiene id, distancia_km, hora (hora de salida, de 12 a 22), lluvia (1 si llovía), productos (número de productos) y zona (centro, norte, sur u oriente).

archivocontenido
entrenamiento.csv1200 pedidos con sus datos y la columna minutos_entrega
prueba.csv400 pedidos con los mismos datos, sin minutos_entrega
muestra_envio.csvejemplo de envío válido: cada pedido de prueba.csv con el valor 30 minutos
reglavalor
métricaMAE: promedio del error absoluto en minutos; menor es mejor
archivoCSV con las columnas id,minutos, igual que muestra_envio.csv
filascada id de prueba.csv exactamente una vez, en cualquier orden
valoresnúmero mayor o igual que 0, sin celdas vacías
cómputoel cuaderno se ejecuta en CPU, sin internet, en menos de 20 minutos
envíosdos por día; el puntaje se calcula con los 400 pedidos de prueba.csv

Descomposición del enunciado

Un enunciado de competencia fija cinco componentes: la entrada que recibe el modelo, la salida que produce, la métrica que la califica, el formato del archivo y las restricciones de tiempo y cómputo. Cada componente limita una decisión posterior.

La celda plegada simula a los organizadores. Genera 1600 pedidos y guarda el texto de los tres archivos en entrenamiento_csv, prueba_csv y muestra_csv. respuestas_organizador contiene los minutos reales de prueba; sirve para calcular el MAE de prueba, como hace el organizador.

Mostrar códigoOcultar códigoPython
Python 3 · ejemplo reproducible
import io

import numpy as np
import pandas as pd

rng = np.random.default_rng(9)
n = 1600
pedidos = pd.DataFrame(
    {
        "id": [f"{i:05d}" for i in rng.choice(np.arange(1, 20000), size=n, replace=False)],
        "distancia_km": np.round(rng.gamma(2.0, 1.2, n) + 0.3, 1),
        "hora": rng.integers(12, 23, n),
        "lluvia": (rng.random(n) < 0.2).astype(int),
        "productos": rng.integers(1, 7, n),
        "zona": rng.choice(["centro", "norte", "sur", "oriente"], n),
    }
)
retraso = rng.exponential(25, n) * (rng.random(n) < 0.12)
minutos = np.round(
    8
    + 4 * pedidos["distancia_km"]
    + 7 * pedidos["lluvia"]
    + 1.5 * pedidos["productos"]
    + rng.gamma(2.0, 3.0, n)
    + retraso
).astype(int)

entrenamiento_csv = pedidos.iloc[:1200].assign(minutos_entrega=minutos[:1200]).to_csv(index=False)
prueba_csv = pedidos.iloc[1200:].to_csv(index=False)
muestra_csv = pd.DataFrame({"id": pedidos["id"].iloc[1200:], "minutos": 30}).to_csv(index=False)
respuestas_organizador = pd.Series(minutos.iloc[1200:].to_numpy(), index=pedidos["id"].iloc[1200:])

Los tres archivos existen solo como texto. pd.read_csv(io.StringIO(texto)) lee ese texto igual que leería el archivo en disco. shape da el número de filas y de columnas, y splitlines()[:3] toma las tres primeras líneas del texto de la muestra.

Pregunta. Con las tablas del enunciado, escribe la forma de entrenamiento y la de prueba. Decide qué columnas pueden ser entrada del modelo.

Python 3 · ejemplo reproducible
entrenamiento = pd.read_csv(io.StringIO(entrenamiento_csv))
prueba = pd.read_csv(io.StringIO(prueba_csv))

print(entrenamiento.shape, prueba.shape)
print(entrenamiento.columns.tolist())
print(prueba.columns.tolist())
print("\n".join(muestra_csv.splitlines()[:3]))
Salida esperada

La salida esperada está oculta.

componenteen este concurso
entradadistancia_km, hora, lluvia, productos y zona de cada pedido
salidaun número de minutos por pedido de prueba: una regresión
métricaMAE en minutos, calculado con los 400 pedidos
formatoid,minutos, 400 filas, cada id una vez, valores ≥ 0 sin celdas vacías
restriccionesCPU, sin internet, menos de 20 minutos, dos envíos por día

Sin internet, el cuaderno no puede descargar modelos preentrenados ni datos externos. Con dos envíos por día, las decisiones se toman con una validación local, no con el puntaje del organizador.

Baseline constante

El baseline más simple ignora la entrada y predice el mismo número para todos los pedidos. La lección compara tres estrategias de DummyRegressor: la media de entrenamiento, la mediana de entrenamiento y un valor fijo. El valor fijo 30 reproduce muestra_envio.csv. Para compararlas, 300 pedidos de entrenamiento quedan como validación.

train_test_split separa esos 300 pedidos: test_size=300 fija cuántos van a validación y random_state=0 hace que la partición sea siempre la misma. Los otros 900 quedan en X_dev y y_dev para ajustar.

Python 3 · ejemplo reproducible
from sklearn.model_selection import train_test_split

entradas = ["distancia_km", "hora", "lluvia", "productos", "zona"]
X_dev, X_val, y_dev, y_val = train_test_split(
    entrenamiento[entradas],
    entrenamiento["minutos_entrega"],
    test_size=300,
    random_state=0,
)

Cada estrategia se ajusta con fit sobre X_dev y y_dev, y predict devuelve la constante para cada pedido de validación. mean_absolute_error promedia la diferencia absoluta entre los minutos reales y los predichos.

Pregunta. Decide cuál es mayor en estos datos, la media o la mediana de minutos_entrega. Ordena las tres constantes por MAE de validación, de menor a mayor.

Python 3 · ejemplo reproducible
from sklearn.dummy import DummyRegressor
from sklearn.metrics import mean_absolute_error

mae_validacion = {}
for estrategia in ["constant", "mean", "median"]:
    # constant=30 solo se usa con strategy="constant"
    constante = DummyRegressor(strategy=estrategia, constant=30).fit(X_dev, y_dev)
    pred_val = constante.predict(X_val)
    mae_validacion[estrategia] = mean_absolute_error(y_val, pred_val)
    # todas las predicciones son iguales: pred_val[0] es la constante
    print(f"{estrategia:8}  constante={pred_val[0]:5.1f}  MAE={mae_validacion[estrategia]:.2f}")
Salida esperada

La salida esperada está oculta.

Verificación del formato

El baseline elegido se reentrena con los 1200 pedidos y predice prueba. Este primer envío toma los id de prueba tal como los leyó pandas y nombra la columna de salida como en entrenamiento. to_csv(index=False) convierte la tabla en el texto del archivo, sin la columna del índice.

Pregunta. Compara línea por línea las tres primeras líneas de cada archivo. Escribe cuántas diferencias esperas y en qué líneas.

Python 3 · ejemplo reproducible
baseline = DummyRegressor(strategy="median").fit(
    entrenamiento[entradas], entrenamiento["minutos_entrega"]
)
envio_1 = pd.DataFrame(
    {"id": prueba["id"], "minutos_entrega": baseline.predict(prueba[entradas])}
)
texto_1 = envio_1.to_csv(index=False)

print("\n".join(texto_1.splitlines()[:3]))
print("---")
print("\n".join(muestra_csv.splitlines()[:3]))
Salida esperada

La salida esperada está oculta.

Las reglas se agrupan en tres funciones pequeñas que reciben las tablas ya leídas y devuelven una lista de problemas. La primera compara el encabezado y el número de filas con los de la muestra.

Python 3 · ejemplo reproducible
def revisar_forma(envio, muestra):
    problemas = []
    if envio.columns.tolist() != muestra.columns.tolist():
        problemas.append(f"columnas {envio.columns.tolist()}; se esperan {muestra.columns.tolist()}")
    if len(envio) != len(muestra):
        problemas.append(f"{len(envio)} filas; se esperan {len(muestra)}")
    return problemas

La segunda revisa los id. duplicated() marca cada id que ya apareció antes, y la resta de conjuntos encuentra los id de un archivo que no están en el otro.

Python 3 · ejemplo reproducible
def revisar_ids(envio, muestra):
    problemas = []
    repetidos = envio["id"].duplicated().sum()
    if repetidos:
        problemas.append(f"{repetidos} id repetidos")
    faltan = set(muestra["id"]) - set(envio["id"])
    sobran = set(envio["id"]) - set(muestra["id"])
    if faltan or sobran:
        problemas.append(f"faltan {len(faltan)} id de la muestra; sobran {len(sobran)} id desconocidos")
    return problemas

La tercera revisa los valores. pd.to_numeric(..., errors="coerce") convierte la columna en números y deja NaN en las celdas vacías o con texto.

Python 3 · ejemplo reproducible
def revisar_valores(envio):
    problemas = []
    # iloc[:, -1]: la última columna, se llame como se llame
    valores = pd.to_numeric(envio.iloc[:, -1], errors="coerce")
    if valores.isna().any():
        problemas.append(f"{valores.isna().sum()} valores vacíos o no numéricos")
    if (valores < 0).any():
        problemas.append(f"{(valores < 0).sum()} valores negativos")
    return problemas

revisar_envio lee el texto del envío y el de la muestra con dtype={"id": str}, como los compara el organizador. Devuelve una lista con todos los problemas, en lugar de detenerse en el primero.

Python 3 · ejemplo reproducible
def revisar_envio(texto, texto_muestra):
    envio = pd.read_csv(io.StringIO(texto), dtype={"id": str})
    muestra = pd.read_csv(io.StringIO(texto_muestra), dtype={"id": str})
    return revisar_forma(envio, muestra) + revisar_ids(envio, muestra) + revisar_valores(envio)

Pregunta. Escribe qué reglas del enunciado incumple texto_1. Para la regla de id, estima si faltan pocos o muchos.

Python 3 · ejemplo reproducible
problemas_1 = revisar_envio(texto_1, muestra_csv)
print(len(problemas_1), "problemas")
for problema in problemas_1:
    print("-", problema)
Salida esperada

La salida esperada está oculta.

Envío válido

La corrección lee prueba con dtype={"id": str} y usa los nombres de columna de la muestra. La celda mide el tiempo desde la lectura hasta el texto del envío para comprobar la restricción de cómputo: time.perf_counter() devuelve un reloj en segundos, y la resta de dos lecturas da el tiempo transcurrido.

Pregunta. Escribe las tres primeras líneas del nuevo envío. Decide qué devolverá revisar_envio.

Python 3 · ejemplo reproducible
import time

inicio = time.perf_counter()
entrenamiento = pd.read_csv(io.StringIO(entrenamiento_csv), dtype={"id": str})
prueba = pd.read_csv(io.StringIO(prueba_csv), dtype={"id": str})
baseline = DummyRegressor(strategy="median").fit(
    entrenamiento[entradas], entrenamiento["minutos_entrega"]
)
envio = pd.DataFrame({"id": prueba["id"], "minutos": baseline.predict(prueba[entradas])})
texto = envio.to_csv(index=False)
segundos = time.perf_counter() - inicio

print("\n".join(texto.splitlines()[:3]))
print("problemas:", revisar_envio(texto, muestra_csv))
print("menos de 20 minutos:", segundos < 20 * 60)
Salida esperada

La salida esperada está oculta.

El organizador lee el envío con los id como texto, empareja cada id con su respuesta real y calcula el MAE con los 400 pedidos de prueba. respuestas_organizador.loc[leido["id"]] toma las respuestas en el orden de los id del envío.

Python 3 · ejemplo reproducible
leido = pd.read_csv(io.StringIO(texto), dtype={"id": str})
reales = respuestas_organizador.loc[leido["id"]]
mae_prueba = mean_absolute_error(reales, leido["minutos"])

Pregunta. Con el MAE de validación de la mediana, escribe un rango para el MAE de prueba.

Ver resultado

La tabla da el MAE del baseline de la mediana en dos conjuntos. La columna «conjunto» indica dónde se mide: validación (300 pedidos apartados de entrenamiento) o prueba (los 400 pedidos que califica el organizador). La columna «pedidos» cuenta los pedidos de cada conjunto y «MAE de la mediana» es el error absoluto promedio en minutos. Por ejemplo, la fila de prueba dice que, en los 400 pedidos de prueba.csv, la predicción constante de 32.0 minutos se equivoca en promedio 9.79 minutos.

conjuntopedidosMAE de la mediana
validación30010.84
prueba4009.79
Lectura. El MAE de prueba sale un minuto por debajo del de validación. Los dos se calculan con muestras distintas de unos cientos de pedidos, así que no coinciden exactamente. El baseline tiene ahora un archivo válido, un MAE de validación de referencia y una revisión de formato automática. Cualquier mejora posterior se compara contra ese 10.84, con la misma partición; el MAE de prueba queda como referencia del envío, no como criterio para decidir.

Práctica

Traza guiada

Antes de procesar, escribe la lista exacta que devolverá revisar_envio(texto_mal, muestra_corta).

Python 3 · ejemplo reproducible
muestra_corta = "id,minutos\n00417,30\n05120,30\n12003,30\n"
texto_mal = "id,minutos\n417,31\n05120,29\n05120,-4\n"

Comprueba con código. Corrige un problema a la vez en texto_mal y vuelve a revisar hasta obtener [].

Métrica RMSE

Supón que el enunciado cambia la métrica a RMSE, la raíz del error cuadrático medio. Antes de procesar, predice cuál de las tres constantes gana y si revisar_envio necesita algún cambio. Repite la comparación de «Baseline constante» con root_mean_squared_error de sklearn.metrics y la misma partición.

Enunciado de clasificación

Escribe tu propio enunciado a partir de estos datos:

Python 3 · ejemplo reproducible
from sklearn.datasets import make_classification

X_tickets, y_tickets = make_classification(
    n_samples=1000,
    n_features=6,
    n_informative=4,
    n_classes=3,
    weights=[0.6, 0.3, 0.1],
    random_state=5,
)

Las etiquetas 0, 1 y 2 corresponden a las prioridades baja, media y alta. Los id son T-0001 a T-1000. Las últimas 250 filas forman prueba; guarda sus etiquetas aparte. La métrica es la exactitud balanceada (balanced_accuracy_score) y el archivo tiene las columnas id,prioridad con el texto de la prioridad. Produce:

  1. la tabla de cinco componentes del enunciado
  2. el texto de los tres archivos, con una muestra de envío que prediga baja para todos
  3. el puntaje de validación de DummyClassifier(strategy="most_frequent")
  4. una versión de revisar_envio que compruebe que cada valor pertenece a {"baja", "media", "alta"}
  5. un envío con un error de formato que tu revisión detecte y el envío corregido que pase sin problemas

Reto opcional

Calcula los cuartiles q1, q2 y q3 de distancia_km en X_dev. Asigna cada pedido a un intervalo con pd.cut(..., bins=[-np.inf, q1, q2, q3, np.inf]) y predice con la mediana de minutos_entrega de su intervalo en y_dev. Compara su MAE de validación con el de la mediana global. Genera su envío y pásalo por revisar_envio antes de calcular el MAE de prueba.

Errores que revisar

  • Si el organizador reporta id desconocidos y tu archivo tiene el número correcto de filas, revisa si read_csv convirtió id en número y borró ceros a la izquierda; lee la columna con dtype={"id": str}.
  • Si revisas el DataFrame en memoria en lugar del texto escrito, la revisión no ve lo que recibe el organizador: ceros perdidos, encabezados o una columna de índice.
  • Si usas la media como constante con MAE, los pocos retrasos largos la desplazan hacia arriba; la constante que minimiza el error absoluto es la mediana.
  • Si una columna de entrenamiento no existe en prueba, como minutos_entrega, no puede ser entrada del modelo.
  • Si eliges entre constantes con los envíos del día, gastas un recurso limitado en una decisión que la validación local ya resuelve.

Cierre

Para cada uno de los cinco componentes del enunciado de entregas, escribe qué decisión del baseline o qué comprobación de revisar_envio depende de él.

Lista de comprobación

  • ¿Qué columnas de entrenamiento pueden ser entrada y por qué id y minutos_entrega no lo son?
  • ¿Por qué la mediana es la mejor constante con MAE y la media lo sería con error cuadrático?
  • ¿Por qué revisar_envio lee el texto con dtype={"id": str} en lugar de revisar el DataFrame en memoria?
  • ¿Qué problema de texto_1 no cambia el número de filas y sí invalida el envío?
  • ¿Qué restricción del enunciado impide usar un modelo preentrenado descargado durante la ejecución?

Documentación oficial

Cargando progreso…