Módulo 9 · Problemas integrados

Lección 4 de 4

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:

archivocontenido
README.mduna línea: «Ejecuta el script.»
requirements.txtnumpy, pandas>=2.0 y scikit-learn
generar_envio.pylee los datos, entrena un bosque aleatorio y escribe envio.csv con las columnas id,prob_falla
datos/entrenamiento.csv900 bombas con la columna falla
datos/prueba.csv300 bombas sin falla
datos/mantenimiento.csvdí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ónpublicado
formatoIndividual 1 e Individual 2: tres tareas cada una, seis horas, trabajo individual
ejecuciónmáximo de 20 minutos por notebook o tarea, salvo que la consigna indique otro límite
entregasmáximo de 60 por tarea; las entregas simultáneas entran en cola
equipoJupyterLab 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 paquetesPython 3.13; versiones exactas publicadas antes del concurso; no se instalan paquetes; sin TensorFlow ni Keras
red y modeloswhitelist 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.

Python 3 · ejemplo reproducible
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")
Salida esperada

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ódigoPython
Python 3 · ejemplo reproducible
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")
Python 3 · ejemplo reproducible
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))
Salida esperada

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.

Python 3 · ejemplo reproducible
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.

Python 3 · ejemplo reproducible
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)estadodentro del límite
1200oksí
0.1tiempo agotadono

Al final, envio.csv tiene 301 líneas.

Lectura. Con 1200 s, el script termina y escribe 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.

Python 3 · ejemplo reproducible
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.

Python 3 · ejemplo reproducible
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.

Python 3 · ejemplo reproducible
limpia = copia_limpia(proyecto)
estado, segundos = ejecutar(limpia, LIMITE_S, SIN_RED)
print(estado)
Salida esperada

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.

Python 3 · ejemplo reproducible
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'].

Lectura. El script termina sin red y dentro del límite. El modelo usa las cuatro columnas de 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.

Python 3 · ejemplo reproducible
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'].

Lectura. Ninguna línea identifica una versión. Con 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.

Python 3 · ejemplo reproducible
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))
Salida esperada

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.

Python 3 · ejemplo reproducible
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.

Python 3 · ejemplo reproducible
estados, huellas = ejecutar_dos_veces(proyecto)
print("envíos idénticos:", huellas[0] == huellas[1])
Salida esperada

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.

Python 3 · ejemplo reproducible
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.

Python 3 · ejemplo reproducible
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.

Python 3 · ejemplo reproducible
for comprobacion, cumple in revisar_proyecto(proyecto).items():
    print(f"{comprobacion}: {'sí' if cumple else 'no'}")
Salida esperada

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.

Python 3 · ejemplo reproducible
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:

  1. añade una columna id, mezcla las filas con random_state=0 y escribe datos/entrenamiento.csv con 130 filas y la columna clase, y datos/prueba.csv con las 48 restantes sin clase
  2. escribe generar_envio.py, que entrene RandomForestClassifier con semilla y genere envio.csv con las columnas id,clase
  3. escribe README y requirements.txt con la estructura de Fallas de bombas
  4. pasa revisar_proyecto hasta obtener seis «sí»
  5. quita random_state del script y vuelve a pasar revisar_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, un envio.csv viejo o un modelo guardado.
  • Si la guardia no detecta una descarga, el código la hace sin pasar por el módulo socket de Python: desde otro proceso, por ejemplo subprocess con curl, 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_state en el modelo, busca otra fuente de azar: train_test_split sin random_state, np.random sin generador fijo o torch sin manual_seed.
  • Si version() lanza PackageNotFoundError con un paquete instalado, el nombre de la línea no es el de la distribución: scikit-learn, no sklearn.

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_limpia copia la lista ARCHIVOS y no la carpeta completa?
  • ¿Qué incluye el tiempo que mide ejecutar y que un reloj dentro del script no incluye?
  • ¿Por qué pandas>=2.0 no 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

Cargando progreso…