Módulo 9 · Problemas integrados
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(1si llovía),productos(número de productos) yzona(centro,norte,suruoriente).
| archivo | contenido |
|---|---|
entrenamiento.csv | 1200 pedidos con sus datos y la columna minutos_entrega |
prueba.csv | 400 pedidos con los mismos datos, sin minutos_entrega |
muestra_envio.csv | ejemplo de envío válido: cada pedido de prueba.csv con el valor 30 minutos |
| regla | valor |
|---|---|
| métrica | MAE: promedio del error absoluto en minutos; menor es mejor |
| archivo | CSV con las columnas id,minutos, igual que muestra_envio.csv |
| filas | cada id de prueba.csv exactamente una vez, en cualquier orden |
| valores | número mayor o igual que 0, sin celdas vacías |
| cómputo | el cuaderno se ejecuta en CPU, sin internet, en menos de 20 minutos |
| envíos | dos 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ódigo
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.
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]))
La salida esperada está oculta.
| componente | en este concurso |
|---|---|
| entrada | distancia_km, hora, lluvia, productos y zona de cada pedido |
| salida | un número de minutos por pedido de prueba: una regresión |
| métrica | MAE en minutos, calculado con los 400 pedidos |
| formato | id,minutos, 400 filas, cada id una vez, valores ≥ 0 sin celdas vacías |
| restricciones | CPU, 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.
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.
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}")
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.
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]))
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.
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.
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.
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.
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.
problemas_1 = revisar_envio(texto_1, muestra_csv)
print(len(problemas_1), "problemas")
for problema in problemas_1:
print("-", problema)
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.
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)
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.
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.
| conjunto | pedidos | MAE de la mediana |
|---|---|---|
| validación | 300 | 10.84 |
| prueba | 400 | 9.79 |
Práctica
Traza guiada
Antes de procesar, escribe la lista exacta que devolverá revisar_envio(texto_mal, muestra_corta).
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:
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:
- la tabla de cinco componentes del enunciado
- el texto de los tres archivos, con una muestra de envío que prediga
bajapara todos - el puntaje de validación de
DummyClassifier(strategy="most_frequent") - una versión de
revisar_envioque compruebe que cada valor pertenece a{"baja", "media", "alta"} - 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
iddesconocidos y tu archivo tiene el número correcto de filas, revisa siread_csvconvirtióiden número y borró ceros a la izquierda; lee la columna condtype={"id": str}. - Si revisas el
DataFrameen 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
entrenamientono existe enprueba, comominutos_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
entrenamientopueden ser entrada y por quéidyminutos_entregano 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_enviolee el texto condtype={"id": str}en lugar de revisar elDataFrameen memoria? - ¿Qué problema de
texto_1no 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
DummyRegressor: estrategiasmean,median,quantileyconstantpara baselines de regresión.- Métricas de regresión en scikit-learn: definición de MAE, error cuadrático medio y sus variantes.
pandas.read_csv: parámetrodtypepara fijar el tipo de columnas comoid.DataFrame.to_csv: escritura de CSV, encabezado eindex.