Módulo 9 · Problemas integrados
Portafolio y simulacros
En esta páginaEl problema
El problema
Resolviste un problema de práctica: estimar la probabilidad de que una bomba de agua falle, a partir de sus horas de uso, su vibración, su presión y su temperatura. La tarea entrega dos archivos, datos/entrenamiento.csv y datos/prueba.csv. Tu carpeta contiene esto:
| archivo | contenido |
|---|---|
README.md | una línea: «Ejecuta el script.» |
requirements.txt | numpy, pandas>=2.0 y scikit-learn |
generar_envio.py | lee los datos, entrena un bosque aleatorio y escribe envio.csv con las columnas id,prob_falla |
datos/entrenamiento.csv | 900 bombas con la columna falla |
datos/prueba.csv | 300 bombas sin falla |
datos/mantenimiento.csv | días desde el último mantenimiento de cada bomba; el script lo descargó en una ejecución anterior |
En tu computadora, python generar_envio.py termina sin errores y escribe envio.csv. Ahora quieres ejecutar el mismo proyecto en las condiciones de una prueba individual de la IOAI 2026: tiempo limitado, sin red y con paquetes que no puedes instalar.
Condiciones publicadas
Las reglas de la IOAI 2026 (Contest Rules v4, mayo de 2026; Technical Appendix v4, junio de 2026) describen dos pruebas individuales. El documento permite actualizaciones antes del evento. Estas son las condiciones que afectan la ejecución de una solución:
| condición | publicado |
|---|---|
| formato | Individual 1 e Individual 2: tres tareas cada una, seis horas, trabajo individual |
| ejecución | máximo de 20 minutos por notebook o tarea, salvo que la consigna indique otro límite |
| entregas | máximo de 60 por tarea; las entregas simultáneas entran en cola |
| equipo | JupyterLab web con acceso a máquinas GPU; laptop Ubuntu sin GPU local; límite anunciado de 18 GB de VRAM; LLM integrado (Gemma 3, hasta 1,000 tokens de salida por consulta) |
| Python y paquetes | Python 3.13; versiones exactas publicadas antes del concurso; no se instalan paquetes; sin TensorFlow ni Keras |
| red y modelos | whitelist esperada de sitios de documentación; sin descargas externas de modelos ni API externas |
Tres de estas condiciones dependen del código y se simulan en una computadora propia: el límite de tiempo, la falta de red y un conjunto fijo de paquetes. La primera celda compara el entorno local con la tabla. importlib.util.find_spec devuelve None cuando un paquete no está instalado, sin importarlo, y torch.cuda.is_available indica si PyTorch encuentra una GPU.
Pregunta. Escribe qué imprimirá la celda en el entorno de esta guía para Python, TensorFlow, Keras y GPU. Marca qué líneas coinciden con la prueba.
import importlib.util
import sys
import torch
python_local = f"{sys.version_info.major}.{sys.version_info.minor}"
print(f"Python: local {python_local}; prueba 3.13")
for paquete in ["tensorflow", "keras"]:
instalado = importlib.util.find_spec(paquete) is not None
print(f"{paquete}: local {'instalado' if instalado else 'no instalado'}; prueba no disponible")
gpu_local = "sí" if torch.cuda.is_available() else "no"
print(f"GPU: local {gpu_local}; prueba sin GPU en la laptop, con GPU en JupyterLab")
La salida esperada está oculta.
Límite de tiempo
La celda plegada reconstruye la carpeta del problema en una carpeta temporal proyecto, con una semilla fija: los tres CSV de datos/, el README de una línea y el requirements.txt sin versiones. mantenimiento.csv ya existe, como en tu computadora.
Mostrar códigoOcultar código
import tempfile
from pathlib import Path
import numpy as np
import pandas as pd
rng = np.random.default_rng(4)
n = 1200
bombas = pd.DataFrame(
{
"id": [f"B-{i:04d}" for i in range(1, n + 1)],
"horas_uso": rng.integers(100, 9000, n),
"vibracion": np.round(rng.gamma(2.0, 1.5, n), 2),
"presion": np.round(rng.normal(4.0, 0.6, n), 2),
"temperatura": np.round(rng.normal(55, 8, n), 1),
}
)
dias = rng.integers(1, 400, n)
# La falla depende de las horas, la vibración y los días sin mantenimiento.
logit = -5 + 0.0003 * bombas["horas_uso"] + 0.6 * bombas["vibracion"] + 0.004 * dias
falla = (rng.random(n) < 1 / (1 + np.exp(-logit))).astype(int)
proyecto = Path(tempfile.mkdtemp(prefix="bombas_"))
(proyecto / "datos").mkdir()
bombas.iloc[:900].assign(falla=falla[:900]).to_csv(proyecto / "datos/entrenamiento.csv", index=False)
bombas.iloc[900:].to_csv(proyecto / "datos/prueba.csv", index=False)
pd.DataFrame({"id": bombas["id"], "dias_sin_mantenimiento": dias}).to_csv(
proyecto / "datos/mantenimiento.csv", index=False
)
(proyecto / "README.md").write_text("Ejecuta el script.\n")
(proyecto / "requirements.txt").write_text("numpy\npandas>=2.0\nscikit-learn\n")
for nombre in ["entrenamiento", "prueba", "mantenimiento"]:
print(nombre, pd.read_csv(proyecto / f"datos/{nombre}.csv").shape)
print(pd.read_csv(proyecto / "datos/entrenamiento.csv").head(3))
La salida esperada está oculta.
Cada bomba tiene un id, horas de uso, vibración, presión y temperatura. entrenamiento añade falla (0 o 1). mantenimiento tiene una fila por cada una de las 1200 bombas, con su id y dias_sin_mantenimiento.
SCRIPT_V1 es el contenido de generar_envio.py. Si falta datos/mantenimiento.csv, urlretrieve lo descarga de una URL. predict_proba(...)[:, 1] toma la probabilidad de la clase 1, es decir, de falla.
SCRIPT_V1 = """
from pathlib import Path
from urllib.request import urlretrieve
import pandas as pd
from sklearn.ensemble import RandomForestClassifier
if not Path("datos/mantenimiento.csv").exists():
urlretrieve("https://example.com/mantenimiento.csv", "datos/mantenimiento.csv")
mantenimiento = pd.read_csv("datos/mantenimiento.csv")
entrenamiento = pd.read_csv("datos/entrenamiento.csv").merge(mantenimiento, on="id")
prueba = pd.read_csv("datos/prueba.csv").merge(mantenimiento, on="id")
entradas = [columna for columna in prueba.columns if columna != "id"]
modelo = RandomForestClassifier(n_estimators=200)
modelo.fit(entrenamiento[entradas], entrenamiento["falla"])
prob_falla = modelo.predict_proba(prueba[entradas])[:, 1]
pd.DataFrame({"id": prueba["id"], "prob_falla": prob_falla.round(4)}).to_csv("envio.csv", index=False)
"""
(proyecto / "generar_envio.py").write_text(SCRIPT_V1)
El límite de la prueba se aplica a la ejecución completa. subprocess.run ejecuta el script en un proceso nuevo de Python, sin variables de una sesión anterior. Con timeout, detiene el proceso cuando supera el límite. time.perf_counter mide desde antes de lanzar el proceso hasta que termina, así que incluye el arranque de Python y las importaciones.
import subprocess
import time
LIMITE_S = 20 * 60
def ejecutar(carpeta, limite_s, argumentos=("generar_envio.py",)):
inicio = time.perf_counter()
try:
proceso = subprocess.run(
[sys.executable, *argumentos], # el mismo intérprete de esta sesión
cwd=carpeta, # las rutas relativas del script parten de la carpeta
capture_output=True, text=True,
timeout=limite_s,
)
except subprocess.TimeoutExpired:
return "tiempo agotado", time.perf_counter() - inicio
if proceso.returncode != 0:
return proceso.stderr.strip().splitlines()[-1], time.perf_counter() - inicio
return "ok", time.perf_counter() - inicio
ejecutar devuelve "ok", "tiempo agotado" o la última línea del error, junto con los segundos medidos. LIMITE_S guarda los 20 minutos de la prueba en segundos.
Pregunta. Se llama dos veces a ejecutar(proyecto, limite): con LIMITE_S (20 minutos) y con un límite de 0.1 s. Escribe el estado de cada ejecución y el número de líneas de envio.csv.
Ver resultado
Cada fila es una ejecución de generar_envio.py con ejecutar(proyecto, limite). «estado» es lo que devuelve ejecutar y «dentro del límite» dice si los segundos medidos quedaron por debajo del límite. Por ejemplo, la fila con 0.1 dice que el proceso se detuvo por tiempo y que los segundos medidos superaron el límite. La frase de abajo cuenta las líneas de envio.csv después de las dos ejecuciones.
| límite (s) | estado | dentro del límite |
|---|---|---|
| 1200 | ok | sí |
| 0.1 | tiempo agotado | no |
Al final, envio.csv tiene 301 líneas.
envio.csv: un encabezado y 300 filas. Con 0.1 s, subprocess.run detiene el proceso antes de que termine, y envio.csv conserva el contenido de la primera ejecución. El límite de 0.1 s solo sirve para ver el corte sin esperar 20 minutos.Ejecución sin red
La carpeta de trabajo acumula archivos de ejecuciones anteriores, como envio.csv y mantenimiento.csv. Una copia limpia contiene solo los archivos del proyecto: código, README, requirements y los datos que entrega la tarea. ARCHIVOS enumera esos archivos y copia_limpia los copia a una carpeta nueva. tempfile.mkdtemp crea una carpeta vacía con nombre único y shutil.copy copia un archivo.
import shutil
ARCHIVOS = [
"README.md",
"requirements.txt",
"generar_envio.py",
"datos/entrenamiento.csv",
"datos/prueba.csv",
]
def copia_limpia(carpeta, archivos=ARCHIVOS):
destino = Path(tempfile.mkdtemp(prefix="limpia_"))
for nombre in archivos:
(destino / nombre).parent.mkdir(parents=True, exist_ok=True) # crea datos/ si falta
shutil.copy(carpeta / nombre, destino / nombre)
return destino
La copia limpia contiene README.md, datos/entrenamiento.csv, datos/prueba.csv, generar_envio.py y requirements.txt. No están datos/mantenimiento.csv ni envio.csv, que quedaron de ejecuciones anteriores. Para simular la falta de red, GUARDIA reemplaza las funciones de socket que abren conexiones. Después ejecuta el script con runpy.run_path, en el mismo proceso. Cualquier conexión desde Python lanza OSError. SIN_RED pasa la guardia a python -c, que ejecuta el texto como programa.
GUARDIA = """
import runpy
import socket
def sin_red(*args, **kwargs):
raise OSError("red bloqueada en la simulación")
socket.getaddrinfo = sin_red
socket.create_connection = sin_red
socket.socket.connect = sin_red
runpy.run_path("generar_envio.py", run_name="__main__")
"""
SIN_RED = ("-c", GUARDIA)
Pregunta. En la copia limpia no existe datos/mantenimiento.csv. Escribe qué línea del script falla y qué error aparece.
limpia = copia_limpia(proyecto)
estado, segundos = ejecutar(limpia, LIMITE_S, SIN_RED)
print(estado)
La salida esperada está oculta.
La corrección usa solo los datos de la tarea. SCRIPT_V2 elimina la descarga y la unión con mantenimiento.
SCRIPT_V2 = """
import pandas as pd
from sklearn.ensemble import RandomForestClassifier
entrenamiento = pd.read_csv("datos/entrenamiento.csv")
prueba = pd.read_csv("datos/prueba.csv")
entradas = [columna for columna in prueba.columns if columna != "id"]
modelo = RandomForestClassifier(n_estimators=200)
modelo.fit(entrenamiento[entradas], entrenamiento["falla"])
prob_falla = modelo.predict_proba(prueba[entradas])[:, 1]
pd.DataFrame({"id": prueba["id"], "prob_falla": prob_falla.round(4)}).to_csv("envio.csv", index=False)
"""
(proyecto / "generar_envio.py").write_text(SCRIPT_V2)
Pregunta. En una copia limpia nueva, con la misma guardia sin red, escribe el estado de la nueva ejecución y qué columnas usará el modelo como entrada.
Ver resultado
La primera frase da el estado que devuelve ejecutar al correr SCRIPT_V2 en una copia limpia sin red. La segunda da la lista entradas del script: las columnas de prueba.csv distintas de id, que el modelo recibe en fit y predict_proba. Por ejemplo, un estado ok significa que el proceso terminó sin error antes de los 1200 s.
ejecutar devuelve el estado ok.
El modelo recibe las columnas ['horas_uso', 'vibracion', 'presion', 'temperatura'].
prueba distintas de id.Versiones fijadas
En la prueba no se instalan paquetes: el script corre con las versiones del entorno. requirements.txt registra con qué versiones se produjo un envío. Una línea nombre==versión identifica una sola versión. Una línea sin versión, o con >=, acepta varias.
importlib.metadata.version devuelve la versión instalada de una distribución. revisar_requirements compara cada línea con esa versión.
from importlib.metadata import version
def revisar_requirements(texto):
problemas = []
for linea in texto.split(): # una línea por paquete, sin espacios internos
if "==" not in linea:
problemas.append(f"{linea}: sin versión exacta")
continue
nombre, fijada = linea.split("==")
instalada = version(nombre)
if fijada != instalada:
problemas.append(f"{linea}: instalada {instalada}")
return problemas
Pregunta. Escribe qué devolverá revisar_requirements para las tres líneas actuales del proyecto.
Ver resultado
La frase da la lista que devuelve revisar_requirements con el requirements.txt actual: un texto por cada línea con problema, con la línea y el motivo separados por dos puntos. Por ejemplo, 'numpy: sin versión exacta' dice que la línea numpy no contiene ==.
revisar_requirements devuelve ['numpy: sin versión exacta', 'pandas>=2.0: sin versión exacta', 'scikit-learn: sin versión exacta'].
pandas>=2.0, tanto pandas 2.2 como 3.0 cumplen la línea, y pandas 3.0 cambió el tipo inferido de las columnas de texto a str. Un requirements sin versiones no dice con qué entorno se produjo envio.csv.La corrección escribe cada paquete del que depende el script con la versión instalada: pandas y scikit-learn, que el script importa (este último como sklearn), y numpy, del que dependen ambos.
Pregunta. Escribe las tres líneas nuevas de requirements.txt y el resultado de la revisión.
paquetes = ["numpy", "pandas", "scikit-learn"]
requisitos = "".join(f"{paquete}=={version(paquete)}\n" for paquete in paquetes)
(proyecto / "requirements.txt").write_text(requisitos)
print(requisitos, end="")
print(revisar_requirements(requisitos))
La salida esperada está oculta.
Envío reproducible
Una huella SHA-256 resume el contenido de un archivo en 64 caracteres hexadecimales. Dos archivos con el mismo contenido tienen la misma huella; un byte distinto la cambia. Si dos ejecuciones desde copias limpias producen la misma huella, el script regenera el mismo envío.
hashlib.sha256(...).hexdigest() calcula la huella de los bytes de envio.csv. ejecutar_dos_veces hace dos copias limpias, ejecuta el script sin red en cada una y devuelve los dos estados y las dos huellas. Si una ejecución falla, su huella es None.
import hashlib
def huella(carpeta):
return hashlib.sha256((carpeta / "envio.csv").read_bytes()).hexdigest()
def ejecutar_dos_veces(carpeta, archivos=ARCHIVOS, limite_s=LIMITE_S):
estados, huellas = [], []
for _ in range(2):
limpia = copia_limpia(carpeta, archivos)
estado, segundos = ejecutar(limpia, limite_s, SIN_RED)
estados.append(estado)
huellas.append(huella(limpia) if estado == "ok" else None)
return estados, huellas
Pregunta. SCRIPT_V2 pasa sin red y dentro del límite. Escribe si las huellas de dos ejecuciones coincidirán.
estados, huellas = ejecutar_dos_veces(proyecto)
print("envíos idénticos:", huellas[0] == huellas[1])
La salida esperada está oculta.
La versión final fija random_state=SEMILLA y escribe un README con la versión de Python, el comando y la salida. SCRIPT_V3 se obtiene de SCRIPT_V2 con str.replace, que cambia solo la línea del modelo.
SCRIPT_V3 = "SEMILLA = 0\n" + SCRIPT_V2.replace(
"RandomForestClassifier(n_estimators=200)",
"RandomForestClassifier(n_estimators=200, random_state=SEMILLA)",
)
(proyecto / "generar_envio.py").write_text(SCRIPT_V3)
(proyecto / "README.md").write_text(
f"""# Fallas de bombas
Python {python_local}. Versiones de paquetes en requirements.txt.
Comando: python generar_envio.py
Salida: envio.csv con las columnas id,prob_falla
"""
)
revisar_proyecto reúne todas las comprobaciones de la lección sobre la carpeta del proyecto. Si falta algún archivo de ARCHIVOS, devuelve solo esa comprobación.
def revisar_proyecto(carpeta, archivos=ARCHIVOS, limite_s=LIMITE_S):
faltan = [nombre for nombre in archivos if not (carpeta / nombre).exists()]
if faltan:
return {"archivos completos": False}
readme = (carpeta / "README.md").read_text()
estados, huellas = ejecutar_dos_veces(carpeta, archivos, limite_s)
return {
"archivos completos": True,
"README con comando": "python generar_envio.py" in readme,
"README con Python": f"Python {python_local}" in readme,
"versiones fijadas": revisar_requirements((carpeta / "requirements.txt").read_text()) == [],
"sin red y a tiempo": estados == ["ok", "ok"],
"envío idéntico": huellas[0] is not None and huellas[0] == huellas[1],
}
Pregunta. Escribe el resultado de cada una de las seis comprobaciones.
for comprobacion, cumple in revisar_proyecto(proyecto).items():
print(f"{comprobacion}: {'sí' if cumple else 'no'}")
La salida esperada está oculta.
Práctica
Traza guiada
Antes de procesar, escribe la lista exacta que devolverá revisar_requirements(requisitos_ajenos) en el entorno de esta guía.
requisitos_ajenos = "numpy==2.4.4\npandas>=3.0\nscikit-learn==1.7.2\ntorch\n"
Comprueba con código. Después, corrige una línea a la vez hasta obtener [].
Paquete no instalado
La lista publicada de bibliotecas de la prueba incluye lightgbm, que no está en el entorno de esta guía. Añade lightgbm==4.6.0 a requisitos_ajenos. Antes de procesar, predice qué hace revisar_requirements con esa línea y comprueba con código.
Después, modifica la función para que, cuando un paquete no esté instalado, devuelva "lightgbm==4.6.0: no instalado en local" en la lista de problemas.
Proyecto de vinos
Construye otro proyecto en una carpeta temporal con load_wine de scikit-learn:
- añade una columna
id, mezcla las filas conrandom_state=0y escribedatos/entrenamiento.csvcon 130 filas y la columnaclase, ydatos/prueba.csvcon las 48 restantes sinclase - escribe
generar_envio.py, que entreneRandomForestClassifiercon semilla y genereenvio.csvcon las columnasid,clase - escribe README y
requirements.txtcon la estructura deFallas de bombas - pasa
revisar_proyectohasta obtener seis «sí» - quita
random_statedel script y vuelve a pasarrevisar_proyecto
Antes del paso 5, predice si envío idéntico cambia a «no» cuando el envío contiene clases en lugar de probabilidades.
Reto opcional
Un portafolio es una carpeta con un subproyecto por problema, cada uno con la estructura de esta lección. Crea portafolio/ con el proyecto de bombas y el de vinos. Recorre sus subcarpetas, aplica revisar_proyecto a cada una y construye un DataFrame con una fila por proyecto, una columna por comprobación y una columna con los segundos de la ejecución más lenta.
Errores que revisar
- Si el script funciona en tu carpeta y falla en la copia limpia, depende de un archivo que no está en
ARCHIVOS: una descarga previa, unenvio.csvviejo o un modelo guardado. - Si la guardia no detecta una descarga, el código la hace sin pasar por el módulo
socketde Python: desde otro proceso, por ejemplosubprocessconcurl, o desde una extensión nativa escrita en C o Rust que abre sus propias conexiones. Desconectar la red de la computadora bloquea también esas conexiones. - Si mides el tiempo dentro del script, la medición omite el arranque de Python y las importaciones; mide el proceso completo desde fuera, como
ejecutar. - Si las huellas cambian con
random_stateen el modelo, busca otra fuente de azar:train_test_splitsinrandom_state,np.randomsin generador fijo otorchsinmanual_seed. - Si
version()lanzaPackageNotFoundErrorcon un paquete instalado, el nombre de la línea no es el de la distribución:scikit-learn, nosklearn.
Cierre
Elige las comprobaciones de revisar_proyecto que simulan una condición publicada de la prueba y, para cada una, escribe qué parte de esa condición queda sin simular en tu computadora.
Lista de comprobación
- ¿Por qué
copia_limpiacopia la listaARCHIVOSy no la carpeta completa? - ¿Qué incluye el tiempo que mide
ejecutary que un reloj dentro del script no incluye? - ¿Por qué
pandas>=2.0no identifica el entorno que produjo un envío? - ¿Qué demuestra que dos huellas SHA-256 coincidan en tu computadora y qué no demuestra sobre la máquina de la prueba?
Documentación oficial
- IOAI 2026 en GitHub: repositorio oficial de la edición 2026.
subprocess:subprocess.run,capture_outputytimeout.importlib.metadata:versionyPackageNotFoundErrorpara consultar distribuciones instaladas.hashlib: huellas SHA-256 de archivos.