Tenemos el lenguaje (07-01), las librerías base (07-02) y el mapa de herramientas (07-03). Falta lo que convierte todo eso en un taller en el que varias personas puedan trabajar hoy y dentro de un año: el entorno. En los módulos 3 a 6 hemos escrito scripts sueltos, con un fichero novamarket_ml.py del que importábamos funciones, sin decir nunca qué versión de Python o de scikit-learn hacía falta, dónde se guardaba cada cosa ni cómo comprobar que seguía funcionando. Esta lección ordena eso: entornos virtuales y gestión de dependencias (venv, pip, requirements.txt, conda, uv/poetry) y por qué fijar versiones es parte de la reproducibilidad, junto con las semillas; Jupyter con sus virtudes y sus trampas (el estado oculto); Colab y Kaggle como laboratorios con GPU gratuita; los IDE (VS Code, PyCharm); Git aplicado a proyectos de IA (qué se versiona y qué no); una estructura de proyecto recomendada que aplicaremos de verdad: refactorizaremos el código de NovaMarket en un paquete src/novamarket/ con datos.py, entrenar.py y tests con pytest, y lo ejecutaremos; y, para cerrar, cuándo hace falta GPU o nube y qué es Docker. Es importante porque el proyecto que Marta y Diego abordarán en el módulo 8 no cabe en un notebook: necesita esta estructura para sobrevivir al primer cambio de persona, de máquina o de versión.

Contenido

  1. El problema: "en mi máquina funciona"
  2. Entornos virtuales con venv y dependencias con pip
  3. requirements.txt, conda/mamba, uv y poetry
  4. Reproducibilidad: versiones + semillas
  5. Jupyter Notebook y JupyterLab: celdas, kernel y estado oculto
  6. Google Colab y Kaggle Notebooks
  7. IDE: VS Code y PyCharm
  8. Git para proyectos de IA: qué versionar y qué no
  9. Estructura de proyecto recomendada
  10. Código: refactorizar NovaMarket en src/novamarket/ con tests
  11. GPU, CPU y nube: cuándo hace falta
  12. Docker como entorno reproducible
  13. Errores Comunes y Consejos
  14. Ejercicios
  15. Conclusión

  1. El problema: "en mi máquina funciona"

Marta entrena el predictor de devoluciones en su portátil, obtiene AUC 0,844 y le pasa el script a un compañero. A él le falla al importar, o le da otro número, o el joblib no carga. Las causas son siempre las mismas: otra versión de Python o de una librería, otro orden de ejecución de las celdas del notebook, otra semilla o ninguna, datos distintos en un fichero con el mismo nombre. El entorno de desarrollo es el conjunto de decisiones que elimina esas causas una a una: aislar dependencias, fijar versiones, fijar semillas, ordenar el código, versionarlo, probarlo y, cuando hace falta, empaquetar la máquina entera.

  1. Entornos virtuales con venv y dependencias con pip

Un entorno virtual es una carpeta con su propio intérprete de Python (enlazado al del sistema) y su propia colección de paquetes, aislada de las demás. Cada proyecto tiene el suyo, y así el proyecto A puede usar una versión de pandas y el B otra sin pisarse. venv viene con Python:

# En la carpeta del proyecto (Linux/macOS; en Windows: .venv\Scripts\activate)
python3 -m venv .venv                 # crea la carpeta .venv con un Python limpio
source .venv/bin/activate             # activa: a partir de aquí, "python" y "pip" son los del entorno
python -c "import sys; print(sys.executable)"   # .../.venv/bin/python
pip install numpy pandas scikit-learn matplotlib torch jupyterlab pytest   # instala SOLO en el entorno
pip list                              # qué hay instalado y en qué versión
deactivate                            # vuelve al Python del sistema

pip instala paquetes desde PyPI (el índice público de Python) resolviendo dependencias: al pedir scikit-learn trae NumPy, SciPy, joblib y threadpoolctl. La regla de oro: nunca instales librerías en el Python del sistema; siempre en un entorno del proyecto. El entorno del curso es exactamente esto: un venv con NumPy, pandas, scikit-learn, Matplotlib, PyTorch (CPU) y, desde esta lección, pytest.

  1. requirements.txt, conda/mamba, uv y poetry

Para que otra persona (o tú dentro de un año) recree el entorno, se escribe la lista de paquetes con versiones en un fichero:

# requirements.txt del proyecto NovaMarket (las versiones son las del entorno del curso al escribir esto;
# en tu proyecto, las que uses tú)
numpy==2.5.2
pandas==3.0.5
scikit-learn==1.9.0
matplotlib==3.11.1
torch==2.13.0
joblib==1.5.3
jupyterlab
pytest
pip install -r requirements.txt       # instala exactamente eso
pip freeze > requirements-lock.txt    # vuelca TODO lo instalado con versión exacta (incluidas dependencias)

Convención habitual: un requirements.txt con lo que decides (paquetes directos, fijados con == los que importan) y un fichero de bloqueo generado con freeze para reproducir al bit. Alternativas que verás en equipos:

Herramienta Qué añade Fichero Cuándo
venv + pip Nada: lo estándar, sin instalar requirements.txt Por defecto; suficiente para casi todo
conda / mamba Gestiona también Python y librerías no Python (CUDA, compiladores, GDAL); entornos por nombre; mamba es conda rápido environment.yml Ciencia de datos con dependencias nativas, GPU, Windows
uv Instalador y gestor muy rápido (escrito en Rust), crea entornos, bloquea versiones, gestiona versiones de Python pyproject.toml + uv.lock Proyectos nuevos que quieren velocidad y bloqueo riguroso
poetry Gestión de dependencias y empaquetado con bloqueo pyproject.toml + poetry.lock Librerías y proyectos que se publican
pyenv Solo instala y cambia versiones de Python Cuando necesitas varias versiones de Python

Un environment.yml de conda equivalente, ilustrativo:

name: novamarket
channels: [conda-forge]
dependencies:
  - python=3.12
  - numpy
  - pandas
  - scikit-learn
  - matplotlib
  - pytorch-cpu
  - jupyterlab
  - pytest

conda env create -f environment.yml y conda activate novamarket. Elige una herramienta por proyecto y no las mezcles.

  1. Reproducibilidad: versiones + semillas

Un resultado es reproducible si otra persona obtiene el mismo número. Hacen falta dos cosas que hemos separado en el curso y que ahora juntamos:

  • Semillas: np.random.default_rng(42), random_state=42 en scikit-learn, torch.manual_seed(42), y shuffle con semilla en train_test_split y DataLoader. Sin ellas, los 3.000 pedidos, la partición y la inicialización de la red cambian en cada ejecución. (En GPU, algunas operaciones son no deterministas aunque fijes la semilla; PyTorch documenta cómo forzarlo a cambio de velocidad.)
  • Versiones: la misma semilla en otra versión de NumPy o de scikit-learn puede dar otra secuencia u otro resultado numérico, y un joblib guardado con una versión puede no cargar con la siguiente (07-01, 07-03). Por eso el requirements.txt con == y por eso, en la sección 10, el script de entrenamiento guardará junto al modelo un JSON con la versión de Python y de scikit-learn con las que se entrenó.

Reproducible = código versionado + datos identificados + versiones fijadas + semillas fijadas. Los cuatro; con tres, el número cambia.

  1. Jupyter Notebook y JupyterLab: celdas, kernel y estado oculto

Jupyter es el entorno interactivo por excelencia de la ciencia de datos: un documento (.ipynb) con celdas de código y de texto (Markdown), que se ejecutan una a una contra un kernel (un proceso Python vivo que guarda las variables). JupyterLab es la interfaz moderna con pestañas, explorador de ficheros y terminal. Se lanza con jupyter lab desde el entorno activado, y abre el navegador. Sus virtudes son evidentes: ves el head() y el gráfico junto al código, iteras rápido, documentas mientras exploras. Sus trampas vienen de lo mismo:

  • Estado oculto: las variables viven en el kernel, no en el documento. Si ejecutas la celda 5, luego la 2 y luego editas y ejecutas la 5 otra vez, el notebook que ves no corresponde al estado del kernel. El caso típico: borras la celda que definía pedidos y todo sigue funcionando... hasta que reinicias, y entonces NameError. Otro: cambias test_size en la celda 3 pero no reejecutas la 4, y el AUC que ves es el antiguo.
  • Orden de ejecución: los números entre corchetes [7] a la izquierda de cada celda dicen el orden real; si no van de arriba abajo, desconfía.
  • Buenas prácticas que evitan casi todos los disgustos: (1) antes de dar por bueno un resultado, "Restart Kernel and Run All" (reiniciar y ejecutar todo de arriba abajo; si falla, el notebook estaba mintiendo); (2) importaciones y parámetros en las primeras celdas; (3) no dejar celdas de prueba desordenadas: bórralas o muévelas a un apéndice; (4) funciones que se repiten, a un módulo .py del proyecto (sección 9) e importarlas, no copiarlas de notebook en notebook; (5) exportar a script cuando el código madura: jupyter nbconvert --to script exploracion.ipynb genera exploracion.py, o mejor, copiar a mano lo que vale al módulo; (6) limpiar salidas antes de subir a Git (los .ipynb con salidas guardan imágenes y tablas y hacen ilegibles las diferencias): jupyter nbconvert --clear-output --inplace, o herramientas como nbstripout.
  • Un notebook es para explorar y contar; el código que se ejecuta cada día (entrenar, servir) va en módulos y scripts. Esa frontera es la que trazaremos en la sección 10.

  1. Google Colab y Kaggle Notebooks

Cuando no tienes GPU, o quieres compartir un notebook sin que nadie instale nada, hay dos laboratorios gratuitos en el navegador:

  • Google Colab: notebooks Jupyter alojados por Google, con Python y las librerías habituales preinstaladas, y acceso a GPU (y TPU) gratuita con límites de tiempo de sesión, de horas por día y de memoria que cambian y que en la versión gratuita no se garantizan; versiones de pago amplían el cupo. La sesión es efímera: al cerrarse se pierde lo que haya en disco, así que los datos se suben en cada sesión o se leen de Google Drive.
  • Kaggle Notebooks: parecido, integrado con los conjuntos de datos y competiciones de Kaggle, con un cupo semanal de horas de GPU y muchos notebooks públicos de los que aprender.
  • Cómo subir un CSV (descripción, no ejecutable aquí): en Colab, el panel lateral de archivos tiene un botón de subida, o desde código from google.colab import files; files.upload() abre un diálogo; para ficheros grandes, montar Drive con drive.mount('/content/drive') y leer con pd.read_csv('/content/drive/MyDrive/pedidos.csv'). En Kaggle, "Add Data" añade un dataset (propio o público) que aparece bajo /kaggle/input/.
  • Cautelas: no subas datos personales de clientes reales a un servicio externo sin la evaluación de 02-04; fija versiones porque el entorno preinstalado cambia; y guarda el notebook y los modelos fuera de la sesión (Drive, Git) porque desaparecen.

  1. IDE: VS Code y PyCharm

Un editor con soporte de Python multiplica la productividad frente al notebook para el código de módulos:

  • Visual Studio Code con las extensiones Python y Jupyter: autocompletado, navegación al código, formateo, ejecución de notebooks dentro del editor y "celdas" en ficheros .py con # %% (lo mejor de ambos mundos: script versionable que se ejecuta por trozos), depurador con puntos de ruptura (mucho mejor que sembrar print para ver por qué el ColumnTransformer da 22 columnas en vez de 21), terminal integrado, Git integrado, y selección del intérprete del entorno virtual (Python: Select Interpreter.venv).
  • PyCharm: el IDE de JetBrains, más "todo incluido" (la edición Community es gratuita; la Professional añade soporte científico y notebooks). Excelente refactorización y depuración.
  • Otros: Spyder (interfaz tipo MATLAB/RStudio, con conda), Positron, o cualquier editor con servidor de lenguaje. La elección es personal; lo que no es opcional es usar el intérprete del entorno del proyecto y tener a mano depurador y tests.

  1. Git para proyectos de IA: qué versionar y qué no

Git guarda la historia del código y permite trabajar a varios sin pisarse; GitHub/GitLab lo alojan. En un proyecto de IA hay una particularidad: los datos y los modelos son grandes, cambian por razones distintas al código y a veces son confidenciales. La regla:

Se versiona en Git No se versiona en Git (se ignora con .gitignore)
Código (src/, tests/, scripts) Datos crudos y procesados (data/)
Ficheros de dependencias (requirements.txt, pyproject.toml) Modelos entrenados grandes (models/*.joblib, *.pt)
Notebooks sin salidas o con salidas ligeras Entornos virtuales (.venv/), cachés (__pycache__/, .ipynb_checkpoints/)
Metadatos pequeños: métricas en JSON, configuración Secretos: claves de API, contraseñas (.env)
README.md, documentación Ficheros generados que se recrean con el código

Para datos y modelos se usan DVC (Data Version Control: guarda en Git un fichero pequeño con el hash del dato y el dato en un almacenamiento externo, S3, Drive, un disco de red) o Git LFS (Large File Storage). Solo los mencionamos: para NovaMarket empezaremos con .gitignore y una carpeta compartida, y DVC cuando haga falta.

Flujo mínimo:

git init                                  # una vez, en la raíz del proyecto
git add .                                 # prepara los ficheros (los ignorados no entran)
git commit -m "Estructura del proyecto y módulo de datos"
git status                                # qué ha cambiado
git log --oneline                         # historia
git checkout -b experimento-boosting      # una rama por experimento o funcionalidad

  1. Estructura de proyecto recomendada

De los scripts sueltos de los módulos 3-6 a un proyecto que otra persona entiende en un minuto. Estructura para NovaMarket:

flowchart TB
    R["novamarket_ia/"] --> RM["README.md<br/>qué es, cómo instalar, cómo ejecutar"]
    R --> RQ["requirements.txt / pyproject.toml<br/>dependencias con versión"]
    R --> GI[".gitignore"]
    R --> D["data/<br/>raw/ (tal cual llegan)<br/>processed/ (limpios) — NO en Git"]
    R --> NB["notebooks/<br/>01_exploracion_pedidos.ipynb<br/>02_modelo_devoluciones.ipynb"]
    R --> S["src/novamarket/<br/>__init__.py<br/>datos.py<br/>entrenar.py<br/>predecir.py"]
    R --> M["models/<br/>modelo_devoluciones.joblib (ignorado)<br/>modelo_devoluciones.json (métricas, versiones)"]
    R --> T["tests/<br/>test_datos.py<br/>test_entrenar.py"]

Principios: data/raw/ es intocable (lo que llega se guarda tal cual; todo lo demás se recalcula con código); los notebooks se numeran y cuentan una historia; el código reutilizable vive en el paquete src/novamarket/ (la carpeta src/ evita que Python importe por accidente una copia sin instalar); models/ guarda artefactos con sus métricas al lado; tests/ comprueba lo que no puede romperse; y README.md dice cómo arrancar. Existen plantillas (Cookiecutter Data Science es la más conocida) que generan esto con más carpetas (reports/, configs/, docs/); empieza pequeño y añade cuando lo necesites.

  1. Código: refactorizar NovaMarket en src/novamarket/ con tests

Vamos a montarlo de verdad. Creamos la estructura, movemos las funciones de novamarket_ml.py (módulos 4-5) a src/novamarket/datos.py, escribimos entrenar.py como script ejecutable, un test mínimo con pytest y lo ejecutamos todo.

1. Crear la estructura (en una terminal, con el entorno activado):

mkdir -p novamarket_ia/{data/raw,data/processed,notebooks,src/novamarket,models,tests}
cd novamarket_ia
touch data/raw/.gitkeep data/processed/.gitkeep models/.gitkeep   # para que Git conserve carpetas vacías

2. src/novamarket/__init__.py (convierte la carpeta en paquete):

"""Paquete novamarket: código reutilizable del proyecto de IA de NovaMarket."""
__version__ = "0.1.0"

3. src/novamarket/datos.py: las funciones que ya conoces, sin cambios, con una cabecera. Reproducimos la primera completa; ensuciar_pedidos, preparar_pedidos y crear_preparacion (con sus listas NUMERICAS, BINARIAS, NOMINALES, ORDINALES) se copian tal cual de 04-03:

"""Datos de NovaMarket: generación sintética (módulo 4), limpieza (04-03) y preparación (ColumnTransformer)."""
import numpy as np
import pandas as pd

def generar_pedidos_ml(n=3000, semilla=42):
    """Genera n pedidos ficticios de NovaMarket con la etiqueta 'devuelto' (1 = devuelto)."""
    rng = np.random.default_rng(semilla)
    importe = np.round(rng.gamma(shape=2.0, scale=60.0, size=n) + 5, 2)
    num_articulos = rng.integers(1, 6, size=n)
    dias_entrega = rng.integers(1, 8, size=n)
    cliente_nuevo = rng.random(n) < 0.30
    categoria = rng.choice(["electronica", "hogar", "informatica", "accesorios"],
                           size=n, p=[0.35, 0.30, 0.20, 0.15])
    zona = rng.choice(["A", "B", "C"], size=n, p=[0.40, 0.35, 0.25])
    z = (-3.4 + 0.010 * (importe - 100) + 1.6 * cliente_nuevo + 0.35 * (dias_entrega - 4)
         + 1.0 * (categoria == "electronica") + 0.5 * (categoria == "informatica")
         - 0.2 * (num_articulos - 2) + 1.2 * cliente_nuevo * (importe - 100) / 100)
    prob = 1 / (1 + np.exp(-z))
    devuelto = (rng.random(n) < prob).astype(int)
    return pd.DataFrame({"importe": importe, "num_articulos": num_articulos, "dias_entrega": dias_entrega,
                         "cliente_nuevo": cliente_nuevo.astype(int), "categoria": categoria,
                         "codigo_postal_zona": zona, "devuelto": devuelto})

def generar_demanda_semanal(semanas=104, semilla=42): ...        # idéntica a 04-02
def ensuciar_pedidos(pedidos, semilla=42): ...                    # idéntica a 04-03
def preparar_pedidos(sucio): ...                                  # idéntica a 04-03: devuelve X, y
def crear_preparacion(): ...                                      # idéntico a 04-03: ColumnTransformer -> 21 columnas

4. src/novamarket/entrenar.py: funciones importables y script ejecutable, separados por if __name__ == "__main__":. Junto al modelo guarda un JSON con métrica, parámetros y versiones (sección 4):

"""Entrena el predictor de devoluciones (pipeline de 04-03 + regresión logística) y lo guarda.

Uso:  python -m novamarket.entrenar --n 3000 --semilla 42 --salida models/modelo_devoluciones.joblib
"""
import argparse, json, platform
from pathlib import Path

import joblib, sklearn
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline

from novamarket.datos import (crear_preparacion, ensuciar_pedidos,
                              generar_pedidos_ml, preparar_pedidos)


def entrenar(n=3000, semilla=42):
    """Genera los datos, entrena y devuelve (pipeline entrenado, AUC de test)."""
    X, y = preparar_pedidos(ensuciar_pedidos(generar_pedidos_ml(n, semilla), semilla))
    Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.25, random_state=semilla, stratify=y)
    pipe = Pipeline([("prep", crear_preparacion()),
                     ("modelo", LogisticRegression(max_iter=1000))]).fit(Xtr, ytr)
    auc = roc_auc_score(yte, pipe.predict_proba(Xte)[:, 1])
    return pipe, auc


def guardar(pipe, auc, ruta, n, semilla):
    """Guarda el modelo y, al lado, un JSON con métrica, parámetros y versiones (reproducibilidad)."""
    ruta = Path(ruta)
    ruta.parent.mkdir(parents=True, exist_ok=True)
    joblib.dump(pipe, ruta)
    meta = {"auc_test": round(float(auc), 4), "n": n, "semilla": semilla,
            "python": platform.python_version(), "scikit_learn": sklearn.__version__}
    ruta.with_suffix(".json").write_text(json.dumps(meta, indent=2))
    return meta


if __name__ == "__main__":                       # solo se ejecuta al lanzar el fichero como script
    parser = argparse.ArgumentParser(description="Entrena el predictor de devoluciones de NovaMarket")
    parser.add_argument("--n", type=int, default=3000)
    parser.add_argument("--semilla", type=int, default=42)
    parser.add_argument("--salida", default="models/modelo_devoluciones.joblib")
    args = parser.parse_args()
    pipe, auc = entrenar(args.n, args.semilla)
    meta = guardar(pipe, auc, args.salida, args.n, args.semilla)
    print(f"AUC test: {auc:.3f}  ->  {args.salida}")
    print(meta)

if __name__ == "__main__": es la línea que hace posible las dos cosas: cuando ejecutas el fichero, __name__ vale "__main__" y corre el bloque; cuando otro módulo (o un test) hace from novamarket.entrenar import entrenar, __name__ vale "novamarket.entrenar" y el bloque no se ejecuta. argparse convierte --n 3000 en args.n, con valores por defecto y --help gratis.

5. pyproject.toml: el fichero estándar de metadatos del proyecto; aquí también le dice a pytest dónde está el código:

[project]
name = "novamarket"
version = "0.1.0"
description = "Proyecto de IA de NovaMarket (curso Fundamentos de IA)"
requires-python = ">=3.10"

[build-system]
requires = ["setuptools>=64"]
build-backend = "setuptools.build_meta"

[tool.setuptools.packages.find]
where = ["src"]

[tool.pytest.ini_options]
pythonpath = ["src"]
testpaths = ["tests"]

pip install -e . (instalación editable) registra el paquete en el entorno apuntando a src/, de modo que import novamarket funciona desde cualquier carpeta y los cambios en el código se ven sin reinstalar. Alternativa sin instalar: PYTHONPATH=src python -m novamarket.entrenar.

6. tests/test_datos.py y tests/test_entrenar.py: pytest descubre funciones test_* y comprueba asserts. Probamos lo que no puede romperse: reproducibilidad, forma, las 21 columnas, ausencia de NaN, un AUC mínimo y que el guardado escribe los dos ficheros:

# tests/test_datos.py
import numpy as np
from novamarket.datos import generar_pedidos_ml, ensuciar_pedidos, preparar_pedidos, crear_preparacion

def test_generacion_reproducible():
    a = generar_pedidos_ml(500, semilla=42)
    b = generar_pedidos_ml(500, semilla=42)
    assert a.equals(b)                                   # misma semilla -> mismos datos
    assert not a.equals(generar_pedidos_ml(500, semilla=7))

def test_forma_y_columnas():
    pedidos = generar_pedidos_ml(3000, semilla=42)
    assert pedidos.shape == (3000, 7)
    assert set(pedidos.columns) >= {"importe", "categoria", "devuelto"}
    assert set(pedidos["devuelto"].unique()) == {0, 1}
    assert 0.10 < pedidos["devuelto"].mean() < 0.25      # tasa de devolución plausible (~16 %)

def test_preparacion_produce_21_columnas():
    X, y = preparar_pedidos(ensuciar_pedidos(generar_pedidos_ml(1000, 42), 42))
    matriz = crear_preparacion().fit_transform(X)
    assert matriz.shape == (len(X), 21)                  # las 21 columnas de 04-03
    assert not np.isnan(matriz).any()                    # sin NaN tras imputar
    assert len(X) == len(y)
# tests/test_entrenar.py
from novamarket.entrenar import entrenar, guardar

def test_entrenar_auc_razonable(tmp_path):               # tmp_path: carpeta temporal que pytest crea y borra
    pipe, auc = entrenar(n=1500, semilla=42)
    assert auc > 0.75                                    # umbral de calidad mínimo
    meta = guardar(pipe, auc, tmp_path / "m.joblib", 1500, 42)
    assert (tmp_path / "m.joblib").exists() and (tmp_path / "m.json").exists()
    assert meta["auc_test"] == round(auc, 4)

7. requirements.txt, .gitignore y README.md como en las secciones 3, 8 y 9 (el .gitignore ignora .venv/, __pycache__/, .ipynb_checkpoints/, data/raw/* y data/processed/* salvo los .gitkeep, models/*.joblib, models/*.pt y .env).

8. Ejecutar. Tests, entrenamiento y comprobación de Git, con las salidas reales:

$ python -m pytest -v
tests/test_datos.py::test_generacion_reproducible PASSED                 [ 25%]
tests/test_datos.py::test_forma_y_columnas PASSED                        [ 50%]
tests/test_datos.py::test_preparacion_produce_21_columnas PASSED         [ 75%]
tests/test_entrenar.py::test_entrenar_auc_razonable PASSED               [100%]
============================== 4 passed in 0.92s ===============================

$ pip install -e .
Successfully installed novamarket-0.1.0

$ python -m novamarket.entrenar --n 3000 --semilla 42
AUC test: 0.844  ->  models/modelo_devoluciones.joblib
{'auc_test': 0.8444, 'n': 3000, 'semilla': 42, 'python': '3.13.5', 'scikit_learn': '1.9.0'}

$ git init && git add -A && git status --short
A  .gitignore
A  README.md
A  data/processed/.gitkeep
A  data/raw/.gitkeep
A  models/.gitkeep
A  models/modelo_devoluciones.json        <- el JSON pequeño SÍ; el .joblib NO aparece (ignorado)
A  pyproject.toml
A  requirements.txt
A  src/novamarket/__init__.py
A  src/novamarket/datos.py
A  src/novamarket/entrenar.py
A  tests/test_datos.py
A  tests/test_entrenar.py

Cuatro tests en menos de un segundo, el mismo AUC 0,844 de 04-04 obtenido con un comando desde la terminal, un JSON que dice con qué versiones se entrenó, y un repositorio en el que el modelo binario no entra pero sus métricas sí. Este es el punto de partida del módulo 8: cuando en 08-01 añadamos predecir.py (que carga models/modelo_devoluciones.joblib como en 07-03) o cambiemos el modelo, los tests dirán en un segundo si algo se ha roto.

  1. GPU, CPU y nube: cuándo hace falta

Todo el curso ha corrido en CPU, y esa es la respuesta para la mayor parte de lo que hace NovaMarket: tablas de miles o cientos de miles de filas con scikit-learn, el MLP de 497 parámetros, las reglas, la optimización. La GPU hace falta cuando el cálculo son grandes multiplicaciones de matrices repetidas: entrenar CNN sobre imágenes reales, afinar un transformer, ejecutar LLM locales; ahí acelera de 10 a 100 veces. Comprobar y usarla en PyTorch:

import torch
print(torch.cuda.is_available())                             # False en el entorno del curso (PyTorch CPU)
dispositivo = torch.device("cuda" if torch.cuda.is_available() else "cpu")
red = red.to(dispositivo); xb = xb.to(dispositivo)          # modelo y datos deben estar en el mismo sitio

(En Mac con Apple Silicon el equivalente es torch.backends.mps.is_available() y "mps".) Opciones, de menor a mayor coste: la CPU del portátil; Colab/Kaggle para probar con GPU gratis (sección 6); una máquina virtual con GPU en la nube (AWS, Azure, Google Cloud y proveedores especializados) que se paga por hora y se apaga al terminar; servicios gestionados de ML de esos proveedores; o una GPU propia si el uso es continuo. La pregunta de Diego ("¿hace falta?") se responde midiendo: si el entrenamiento en CPU tarda minutos, no; si tarda días, sí, y aun así conviene primero reducir datos o modelo para iterar rápido y usar la GPU solo para la ejecución final.

  1. Docker como entorno reproducible

El último escalón de la reproducibilidad es empaquetar la máquina entera: sistema operativo, Python, librerías con versión, código y comando de arranque, en una imagen que corre igual en el portátil de Marta, en el servidor de NovaMarket y en la nube. Eso es Docker (o Podman). Un Dockerfile ilustrativo para servir el predictor con la API de 07-03:

# Ilustrativo: imagen para servir el predictor de devoluciones de NovaMarket
FROM python:3.12-slim                        # base: Linux mínimo con Python
WORKDIR /app                                 # carpeta de trabajo dentro del contenedor
COPY requirements.txt .                      # primero dependencias (se cachean si no cambian)
RUN pip install --no-cache-dir -r requirements.txt fastapi uvicorn
COPY src/ src/                               # el paquete novamarket
COPY models/modelo_devoluciones.joblib models/
COPY servir.py .                             # la API FastAPI de 07-03
ENV PYTHONPATH=/app/src
EXPOSE 8000
CMD ["uvicorn", "servir:app", "--host", "0.0.0.0", "--port", "8000"]

docker build -t novamarket-devoluciones . construye la imagen y docker run -p 8000:8000 novamarket-devoluciones la arranca; la web de NovaMarket llama a http://servidor:8000/riesgo-devolucion. Docker no sustituye a venv en el día a día del desarrollo (es más pesado), pero es la forma estándar de entregar un servicio y de garantizar que "en mi máquina funciona" signifique "en todas". Kubernetes (07-01) orquesta muchos contenedores; fuera del alcance del curso.

Errores Comunes y Consejos

  • Instalar todo en el Python del sistema. Tarde o temprano dos proyectos piden versiones incompatibles y algo del sistema deja de funcionar. Un entorno por proyecto, siempre.
  • requirements.txt sin versiones, o sin requirements.txt. Seis meses después, pip install trae versiones nuevas y el joblib no carga o el número cambia. Fija con == lo que importa y guarda un freeze.
  • Notebooks que solo funcionan en el orden en que se ejecutaron. "Restart Kernel and Run All" antes de dar nada por bueno; funciones a módulos; salidas limpias antes de subir a Git.
  • Datos y modelos en Git. El repositorio engorda hasta ser inutilizable y puedes publicar datos personales. .gitignore desde el primer commit; DVC o almacenamiento externo para lo grande.
  • Semillas sin versiones o versiones sin semillas. Las dos, y anotadas junto al modelo (el JSON de la sección 10).
  • Tests que prueban lo trivial y no lo frágil. Prueba la forma de la matriz, la ausencia de NaN, la reproducibilidad, un umbral de métrica; son los fallos que de verdad ocurren al tocar preparar_pedidos.
  • Comprar GPU antes de medir. Cronometra en CPU, reduce el problema, prueba en Colab; después decide.
  • Rutas absolutas en el código (/home/marta/pedidos.csv). Rutas relativas a la raíz del proyecto o configurables por argumento; pathlib.Path en vez de concatenar cadenas.

Ejercicios

Ejercicio 1. Añade al proyecto un módulo src/novamarket/predecir.py con una función cargar(ruta="models/modelo_devoluciones.joblib") y otra predecir_riesgo(modelo, pedido: dict) -> float que construya un DataFrame de una fila y devuelva la probabilidad de devolución, más un bloque if __name__ == "__main__": que prediga para un pedido de ejemplo. Escribe un test que entrene con entrenar(n=1500), guarde en tmp_path, cargue con cargar y compruebe que predecir_riesgo devuelve un número entre 0 y 1 e igual a pipe.predict_proba sobre el mismo pedido.

Ejercicio 2. Simula el problema del estado oculto: en un notebook (o mentalmente, describiendo las celdas), define en la celda 1 umbral = 0.5, en la celda 2 una función que use umbral, ejecuta la celda 3 que la llama; después cambia la celda 1 a umbral = 0.3 sin reejecutarla y vuelve a ejecutar la 3. ¿Qué umbral se aplica? ¿Qué muestra el notebook? ¿Qué pasa al hacer "Restart and Run All"? Propón dos cambios en la organización del notebook que eviten el problema.

Ejercicio 3. Escribe el .gitignore, el requirements.txt (con las versiones que uses) y un Dockerfile ilustrativo para un proyecto que sirva la red bayesiana de diagnóstico de incidencias de 06-03 como API. Justifica qué entra en Git y qué no, y por qué en este caso el "modelo" (las tablas de probabilidad condicional) probablemente debería versionarse en Git, a diferencia de modelo_devoluciones.joblib.

Soluciones

Solución 1.

# src/novamarket/predecir.py
"""Carga el predictor de devoluciones y predice el riesgo de un pedido."""
import joblib, pandas as pd

def cargar(ruta="models/modelo_devoluciones.joblib"):
    return joblib.load(ruta)

def predecir_riesgo(modelo, pedido):
    """pedido: dict con las 15 columnas de preparar_pedidos. Devuelve la probabilidad de devolución."""
    X = pd.DataFrame([pedido])
    return float(modelo.predict_proba(X)[0, 1])

if __name__ == "__main__":
    modelo = cargar()
    ejemplo = {"importe": 72.99, "num_articulos": 4, "dias_entrega": 6.0, "cliente_nuevo": 0,
               "categoria": "informatica", "codigo_postal_zona": "A", "metodo_pago": "tarjeta",
               "tipo_envio": "estandar", "dia_semana": 3, "mes": 11, "fin_de_semana": 0,
               "dias_desde_inicio": 87, "importe_por_articulo": 18.25, "pedidos_previos": 3,
               "tasa_devolucion_previa": 0.0}
    print(f"riesgo de devolución: {predecir_riesgo(modelo, ejemplo):.3f}")
# tests/test_predecir.py
from novamarket.entrenar import entrenar, guardar
from novamarket.predecir import cargar, predecir_riesgo
from novamarket.datos import generar_pedidos_ml, ensuciar_pedidos, preparar_pedidos

def test_predecir_coincide_con_pipeline(tmp_path):
    pipe, auc = entrenar(n=1500, semilla=42)
    guardar(pipe, auc, tmp_path / "m.joblib", 1500, 42)
    modelo = cargar(tmp_path / "m.joblib")
    X, _ = preparar_pedidos(ensuciar_pedidos(generar_pedidos_ml(500, 3), 3))
    pedido = X.iloc[0].to_dict()
    p = predecir_riesgo(modelo, pedido)
    assert 0.0 <= p <= 1.0
    assert abs(p - pipe.predict_proba(X.iloc[[0]])[0, 1]) < 1e-9

Ejecutado en el entorno del curso: pytest pasa 5 tests, y python -m novamarket.predecir imprime riesgo de devolución: 0.048 (el mismo pedido de 07-03). Con esto el proyecto tiene las tres piezas del ciclo (datos, entrenar, predecir) probadas.

Solución 2. Se aplica 0,5: la celda 1 se editó pero no se ejecutó, así que la variable umbral del kernel sigue valiendo 0,5; el notebook muestra umbral = 0.3 en la celda 1 y un resultado calculado con 0,5, es decir, miente. Al hacer "Restart and Run All" el kernel se vacía, se ejecuta la celda 1 con 0,3 y el resultado cambia; si alguien hubiera copiado el resultado anterior a un informe, sería irreproducible. Dos cambios: (1) parámetros y funciones al principio, y la regla de reejecutar desde arriba tras cualquier cambio de parámetro (o directamente "Run All Above" antes de la celda de resultados); (2) sacar la función a src/novamarket/ con umbral como argumento explícito (decidir(prob, umbral=0.5)) y llamarla con el valor visible en la celda, de modo que no haya variable global de la que depender.

Solución 3. .gitignore: .venv/, __pycache__/, .ipynb_checkpoints/, .env, y data/raw/* (los históricos de incidencias reales, confidenciales); no se ignora src/novamarket/red_incidencias.py ni un configs/cpt_incidencias.json con las tablas de probabilidad. requirements.txt: numpy==..., pandas==..., fastapi, uvicorn, pytest (y pgmpy==... si se sustituye la enumeración de 06-03 por pgmpy, 07-03). Dockerfile: igual que el de la sección 12, copiando src/, configs/ y servir_incidencias.py, con CMD ["uvicorn", "servir_incidencias:app", ...]. Por qué el "modelo" sí va en Git: las tablas de probabilidad condicional de la red de 06-03 son pocas decenas de números escritos o revisados por personas (conocimiento explícito, como las reglas de 06-02), pequeñas, legibles como texto y con valor de auditoría (¿quién cambió la probabilidad de "defecto de fábrica" y cuándo?): exactamente lo que Git hace bien. modelo_devoluciones.joblib es un binario generado por código a partir de datos: se regenera con entrenar.py, no se lee ni se revisa línea a línea, y su lugar es un almacén de artefactos con el JSON de métricas y versiones en Git.

Conclusión

Con esta lección el taller de Marta queda montado. Hemos visto por qué "en mi máquina funciona" es el enemigo y cómo se combate: entornos virtuales (venv) con un intérprete y unos paquetes por proyecto; dependencias fijadas en requirements.txt (o environment.yml, uv, poetry) y la ecuación de la reproducibilidad, versiones + semillas + código + datos identificados; Jupyter para explorar y contar, con la disciplina de reiniciar y ejecutar todo y de mover el código maduro a módulos; Colab y Kaggle como laboratorios con GPU gratuita y sesiones efímeras; VS Code y PyCharm con depurador y el intérprete del entorno; Git con la frontera clara entre lo que se versiona (código, dependencias, métricas pequeñas) y lo que no (datos, modelos grandes, secretos), y DVC en el horizonte; una estructura de proyecto (data/, notebooks/, src/novamarket/, models/, tests/) que hemos construido de verdad, con datos.py, entrenar.py ejecutable desde la terminal (AUC 0,844 y un JSON con versiones), un pyproject.toml y cuatro tests de pytest que pasan en un segundo; y las decisiones de infraestructura: CPU salvo prueba de lo contrario, GPU y nube cuando el cálculo lo pida, Docker para entregar el servicio igual en todas partes.

Y con esto cerramos el módulo 7. Empezamos en 07-01 eligiendo Python y SQL entre los lenguajes de la IA; en 07-02 aprendimos a manejar de verdad NumPy, pandas y Matplotlib con los pedidos y la demanda de NovaMarket; en 07-03 situamos cada librería en su tarea y guardamos y recargamos el predictor de devoluciones y el MLP; y en 07-04 lo hemos ordenado todo en un proyecto reproducible con entorno, estructura, Git y tests. Marta tiene el lenguaje, las herramientas y el taller; Diego tiene un entrenar.py que cualquiera puede ejecutar y un repositorio que no engorda con datos. Lo que no tienen todavía es un método para llevar un caso de uso de la idea a producción: definir el problema de negocio, decidir la métrica, montar los datos, iterar sobre el modelo, validarlo, desplegarlo y vigilarlo, aprendiendo de los proyectos que han salido bien y de los que no. Es el módulo 8, Proyectos y Casos de Estudio, que empieza en 08-01, Desarrollo de un Proyecto de IA, con Marta y Diego abordando de principio a fin, dentro de la estructura novamarket_ia/ que acabamos de crear, el proyecto del predictor de devoluciones.

Fundamentos de Inteligencia Artificial (IA)

Módulo 1: Introducción a la Inteligencia Artificial

Módulo 2: Principios Básicos de la IA

Módulo 3: Algoritmos en IA

Módulo 4: Aprendizaje Automático (Machine Learning)

Módulo 5: Redes Neuronales y Deep Learning

Módulo 6: Lógica y Sistemas Expertos

Módulo 7: Herramientas y Lenguajes de Programación en IA

Módulo 8: Proyectos y Casos de Estudio

Módulo 9: Ejercicios y Prácticas

Módulo 10: Recursos Adicionales

© Copyright 2026. Todos los derechos reservados