Tenemos los tres ingredientes versionados —código, entorno y datos—, pero la receta todavía se cocina a mano: alguien tiene que acordarse de ejecutar la limpieza, luego las features, luego el entrenamiento, luego la evaluación, en ese orden, y de repetir los pasos correctos cuando algo cambia. En esta lección convertimos ese ritual en un pipeline declarado: un fichero dvc.yaml donde cada etapa dice qué comando ejecuta, de qué depende y qué produce, y un solo comando —dvc repro— que lo reconstruye todo re-ejecutando únicamente lo que cambió. Es la pieza que cierra el módulo y la promesa que llevamos persiguiendo desde el diagnóstico: que cualquier persona del equipo pueda clonar el repositorio y obtener el mismo modelo que Laura, sin Laura.

Contenido

  1. Qué es un pipeline de entrenamiento y por qué encadenar a mano es frágil
  2. El pipeline de CineClick en dvc.yaml
  3. params.yaml: los hiperparámetros como dependencia
  4. dvc repro: el grafo de dependencias en acción
  5. dvc dag y dvc metrics diff: ver el pipeline y comparar ejecuciones
  6. Cierre del módulo: qué hemos resuelto y qué sigue abierto

Qué es un pipeline de entrenamiento y por qué encadenar a mano es frágil

Un pipeline de entrenamiento es la secuencia de pasos que transforma datos crudos en un modelo evaluado: en nuestro caso, limpiar → construir features → entrenar → evaluar. La secuencia ya existe implícitamente en nuestro código desde 02-01; el problema es cómo se ejecuta. Hoy, el procedimiento es mental: Laura (o tú) lanza los pasos a mano. Y el encadenado manual falla de formas muy concretas:

  • Orden y omisiones: nada impide entrenar con unas features viejas porque se te olvidó re-ejecutar el paso anterior tras cambiar la limpieza. El modelo sale, no da error, y es sutilmente incorrecto — la peor clase de bug.
  • Re-ejecuciones innecesarias: la alternativa defensiva ("por si acaso, lo relanzo todo desde cero") desperdicia tiempo, y cuando la preparación de datos tarde horas, ese "por si acaso" será insostenible.
  • Conocimiento no transferible: el orden correcto, los comandos exactos y sus condiciones viven en la cabeza de alguien o en un README que envejece. Es el problema nº 10 del diagnóstico con corbata nueva.
  • Sin registro de qué produjo qué: aunque los datos estén versionados, nada ata esta versión del modelo a aquella ejecución con aquellos parámetros.

La solución es la misma que aplican make o los sistemas de build desde hace décadas: declarar los pasos con sus dependencias y salidas, y dejar que la herramienta calcule qué hay que ejecutar. DVC trae esto de serie, integrado con el versionado de datos de la lección anterior: las salidas de cada etapa quedan automáticamente bajo control de DVC.

El pipeline de CineClick en dvc.yaml

Primero, una pequeña adaptación del código: cada módulo del paquete gana una función main() ejecutable (python -m cineclick_churn.data, etc.) que lee su configuración y escribe sus salidas a disco — el patrón que dejamos preparado en 02-01. Los pasos se comunican por ficheros, no por variables en memoria: data.py escribe el CSV limpio, features.py lo lee y escribe la matriz de features, train.py la lee y escribe el modelo más el conjunto de test reservado, y evaluate.py lee ambos y escribe las métricas. Esa comunicación por ficheros es justo lo que permite a DVC saber quién depende de quién.

Con eso, el pipeline completo se declara en dvc.yaml, en la raíz del repo:

# dvc.yaml — el pipeline de entrenamiento de CineClick
stages:
  preparar_datos:
    cmd: python -m cineclick_churn.data
    deps:
      - data/raw/clientes_churn.csv
      - src/cineclick_churn/data.py
    outs:
      - data/processed/clientes_limpio.csv

  construir_features:
    cmd: python -m cineclick_churn.features
    deps:
      - data/processed/clientes_limpio.csv
      - src/cineclick_churn/features.py
    outs:
      - data/processed/features.csv

  entrenar:
    cmd: python -m cineclick_churn.train
    deps:
      - data/processed/features.csv
      - src/cineclick_churn/train.py
    params:
      - entrenar.test_size
      - entrenar.random_state
      - entrenar.n_estimators
    outs:
      - models/modelo_churn.pkl
      - data/processed/test.csv

  evaluar:
    cmd: python -m cineclick_churn.evaluate
    deps:
      - models/modelo_churn.pkl
      - data/processed/test.csv
      - src/cineclick_churn/evaluate.py
    metrics:
      - metrics.json:
          cache: false

Desmenucemos la anatomía de una etapa (stage):

  • cmd: el comando que la ejecuta. Cualquier comando de shell vale; usamos los módulos del paquete, que es nuestro código testeado.
  • deps: sus dependencias — ficheros de datos y también el código fuente del paso. Esto es clave: si alguien modifica data.py, DVC sabe que la limpieza (y todo lo que cuelga de ella) debe repetirse. El código es una dependencia más.
  • params: dependencias de grano fino sobre valores del params.yaml (siguiente apartado). Solo los valores listados afectan a la etapa.
  • outs: lo que produce. DVC toma el control de estas salidas: las cachea por hash (como hacía dvc add, pero ahora automáticamente) y las añade a .gitignore. El modelo modelo_churn.pkl pasa a estar versionado por DVC sin que hagamos nada más — su resguardo vive ahora en dvc.lock, el fichero que DVC genera con los hashes exactos de cada ejecución y que sí se commitea.
  • metrics: una salida especial — un pequeño JSON de métricas que sí queremos legible y comparable (por eso cache: false: es diminuto y nos interesa verlo en Git, con sus difs).

La etapa evaluar escribe el metrics.json con las métricas honestas de 02-01:

{"accuracy": 0.86, "precision": 0.61, "recall": 0.43, "f1": 0.5}

Un detalle de diseño que merece pausa: entrenar produce dos salidas, el modelo y test.csv (la partición de test). ¿Por qué? Porque evaluar debe medir sobre exactamente las filas que el entrenamiento no vio; si cada etapa repitiera el split por su cuenta, un despiste en los parámetros podría evaluar sobre datos de entrenamiento. Materializar el test como fichero hace la frontera explícita y auditable.

params.yaml: los hiperparámetros como dependencia

En 02-01 pusimos los parámetros en configs/config.yaml. DVC introduce una distinción útil que adoptamos ahora: las rutas (dónde está cada cosa) ya están declaradas en el propio dvc.yaml vía deps/outs, mientras que los hiperparámetros (los valores que cambias cuando experimentas) se mudan a params.yaml, el fichero que DVC lee por convención:

# params.yaml — hiperparámetros del pipeline
entrenar:
  test_size: 0.2
  random_state: 42
  n_estimators: 100

El código de train.py lo carga igual que cargaba el config (yaml.safe_load), así que el cambio es menor. La ganancia es conceptual y práctica a la vez: al declarar params: [entrenar.n_estimators, ...] en la etapa, cada hiperparámetro se convierte en una dependencia rastreada individualmente. DVC no vigila "el fichero params.yaml cambió" sino "el valor entrenar.n_estimators cambió" — y solo invalida las etapas que dependen de ese valor concreto. Editar un comentario del YAML no re-ejecuta nada.

dvc repro: el grafo de dependencias en acción

Todo declarado. El comando estrella:

dvc repro

En la primera ejecución, DVC recorre el grafo en orden y ejecuta las cuatro etapas, cacheando cada salida y anotando en dvc.lock los hashes de todas las dependencias y salidas de cada etapa. Ese dvc.lock se commitea junto a dvc.yaml y params.yaml: es la constancia exacta de "con estas entradas se produjeron estas salidas".

La magia aparece en la segunda ejecución. Supón que quieres probar un bosque más grande — editas params.yaml y cambias n_estimators: 100 por n_estimators: 300:

dvc repro
Stage 'preparar_datos' didn't change, skipping
Stage 'construir_features' didn't change, skipping
Running stage 'entrenar':
> python -m cineclick_churn.train
Running stage 'evaluar':
> python -m cineclick_churn.evaluate
Updating lock file 'dvc.lock'

DVC comparó hashes: los datos crudos no cambiaron, data.py y features.py tampoco, así que la limpieza y las features se saltan — sus salidas cacheadas siguen siendo válidas. Solo entrenar (cuyo parámetro cambió) y evaluar (que depende del modelo nuevo) se re-ejecutan. Con datasets grandes, esto es la diferencia entre iterar en minutos o en horas. Y funciona en todas las direcciones: si en vez del parámetro cambia data/raw/clientes_churn.csv (extracto nuevo vía dvc add... o directamente como dependencia), se re-ejecuta todo, porque todo cuelga de los datos; si solo cambia evaluate.py, se re-ejecuta únicamente la evaluación. Nunca de más, nunca de menos: el "¿qué tengo que relanzar?" ha dejado de ser una decisión humana.

dvc dag y dvc metrics diff: ver el pipeline y comparar ejecuciones

Para ver el grafo que DVC ha construido a partir de las dependencias declaradas:

dvc dag
+----------------+
| preparar_datos |
+----------------+
        *
+--------------------+
| construir_features |
+--------------------+
        *
   +----------+
   | entrenar |
   +----------+
        *
   +---------+
   | evaluar |
   +---------+

Que en versión bonita es este DAG (grafo dirigido acíclico — dirigido porque los datos fluyen en un sentido, acíclico porque ningún paso puede depender de sí mismo):

flowchart TD
    RAW[("data/raw/<br/>clientes_churn.csv<br/>(DVC)")] --> P
    P["preparar_datos<br/><small>data.py</small>"] --> |clientes_limpio.csv| F
    F["construir_features<br/><small>features.py</small>"] --> |features.csv| T
    PARAMS[["params.yaml<br/>n_estimators, test_size,<br/>random_state"]] --> T
    T["entrenar<br/><small>train.py</small>"] --> |modelo_churn.pkl| E
    T --> |test.csv| E
    E["evaluar<br/><small>evaluate.py</small>"] --> M[["metrics.json"]]

Y para el experimento de los 300 árboles, la pregunta de negocio es "¿mejoró?". Como metrics.json está declarado como métrica, DVC sabe compararlo entre el estado actual y el último commit:

dvc metrics diff
Path          Metric     HEAD    workspace    Change
metrics.json  accuracy   0.86    0.8613       0.0013
metrics.json  f1         0.5     0.512        0.012
metrics.json  precision  0.61    0.618        0.008
metrics.json  recall     0.43    0.44         0.01

Veredicto: triplicar los árboles rasca una mejora marginal en todas las métricas — probablemente no compensa el triple de tiempo de entrenamiento e inferencia, así que revertimos el parámetro (o commiteamos el cambio si decidimos que sí). Lo importante es el flujo: cambiar un valor, dvc repro, dvc metrics diff, decidir con datos. Ahora bien, seamos honestos sobre los límites: esto compara el workspace contra un commit, de uno en uno. Cuando quieras explorar veinte combinaciones de hiperparámetros, verlas en una tabla, ordenarlas por recall y recuperar cualquiera de ellas, este mecanismo se queda corto — esa comodidad es exactamente lo que aporta MLflow, y es la primera parada del módulo 3.

El cierre del círculo es el flujo completo de un compañero nuevo. Marc, en una máquina limpia:

git clone <repo> && cd cineclick-churn
python -m venv .venv && source .venv/bin/activate
pip install pip-tools && pip-sync requirements.lock && pip install -e . --no-deps
dvc pull        # trae los datos (y salidas cacheadas) del remote
dvc repro       # "Data and pipelines are up to date."

Ese mensaje final — todo al día, nada que re-ejecutar — es la reproducibilidad hecha frase: los hashes de datos, código y parámetros de la máquina de Marc coinciden exactamente con los del dvc.lock commiteado, luego su pipeline produciría el mismo modelo que el de Laura. Y si borra models/ y relanza dvc repro, lo comprueba empíricamente: mismas métricas, decimal a decimal. CineClick nunca había estado aquí.

Cierre del módulo: qué hemos resuelto y qué sigue abierto

Pasemos revista al diagnóstico de la lección 01-04, que era nuestra lista de tareas:

# Problema Estado tras el módulo 2
1 Rutas absolutas y personales ✅ Resuelto (02-01): rutas relativas en configuración y dvc.yaml
2 Sin semilla aleatoria ✅ Resuelto (02-01): random_state=42 en split y modelo, split estratificado
3 Datos sin versionar ✅ Resuelto (02-03): DVC + remote; viaje en el tiempo con git checkout + dvc checkout
8 Dependencias implícitas ✅ Resuelto (02-02): pyproject.toml + requirements.lock + .python-version
9 Preprocesado no reutilizable ✅ Encarrilado (02-01): features.py importable; el servicio del módulo 4 lo consumirá
10 Proceso 100% manual 🟡 Parcial (02-04): repetible con dvc repro, pero sigue lanzándose a mano y en local — programarlo y orquestarlo llega en 05-03
5 Métrica única y engañosa 🟡 Parcial (02-01): precision/recall/F1 en metrics.json; comparar muchos experimentos con comodidad → módulo 3
4 Modelo sin versionar ni metadatos 🟡 Parcial: el .pkl está en la caché de DVC atado a su commit, pero sin registro formal de versiones/estados → model registry, módulo 3
7 Sin registro de experimentos ❌ Abierto: dvc metrics diff compara de dos en dos; el historial cómodo y consultable → MLflow, módulo 3
6 Sin tests ❌ Abierto (salvo la semilla plantada en 02-01): tests de datos y modelo en CI → módulo 5

Cuatro problemas cerrados, tres bien encarrilados, y los tres restantes con dueño y fecha. Nada mal para un módulo.

Errores Comunes y Consejos

  • Error: olvidar el código en las deps. Si entrenar solo declara features.csv como dependencia, un cambio en train.py no re-ejecutará nada y seguirás sirviendo el modelo del código viejo. Regla: cada etapa depende de sus datos de entrada y de su código.
  • Error: pasos que se comunican por variables en memoria. Si un solo script hace features+entrenamiento "porque es más cómodo", DVC no puede saltarse la parte cara cuando solo cambia la barata. La granularidad del pipeline la marcan los ficheros intermedios: si quieres poder saltarte un paso, materializa su salida.
  • Error: editar a mano una salida (outs). Retocar data/processed/features.csv con Excel dura hasta el siguiente dvc repro, que lo regenerará desde sus dependencias. Todo cambio debe entrar por el principio del grafo: datos crudos, código o parámetros.
  • Error: no commitear dvc.lock. Sin él, el resto del equipo no sabe qué hashes corresponden a la última ejecución buena, y dvc repro de Marc no puede verificar que está al día. dvc.yaml, dvc.lock y params.yaml viajan juntos en el commit.
  • Consejo: mantén dvc repro barato. La disciplina de "¿mejoró?" con dvc metrics diff solo se practica si iterar cuesta minutos. Si un paso se vuelve lento, trocéalo (la parte cara y estable separada de la barata y cambiante) para que la caché trabaje a tu favor.

Ejercicios

Ejercicio 1

Para cada uno de estos cambios, indica qué etapas re-ejecutará el siguiente dvc repro (preparar_datos / construir_features / entrenar / evaluar) y por qué: (a) cambias entrenar.test_size de 0.2 a 0.25 en params.yaml; (b) el data engineer entrega el extracto de agosto y sustituyes data/raw/clientes_churn.csv; (c) corriges un docstring en evaluate.py; (d) añades una regla nueva de limpieza en data.py; (e) reordenas los comentarios de params.yaml sin tocar ningún valor.

Ejercicio 2

El equipo quiere probar limitar la profundidad de los árboles para acelerar la inferencia. Describe los cambios exactos necesarios para incorporar el hiperparámetro max_depth al pipeline (qué ficheros tocas y qué añades en cada uno), y el flujo completo para evaluar si max_depth: 10 merece la pena frente a la línea base.

Ejercicio 3

Un directivo escéptico pregunta: "¿y cómo me demostráis que el modelo que tenéis es reproducible, y no otra vez el 0.87 de Laura?". Diseña la demostración: la secuencia de comandos que ejecutarías delante de él en una máquina limpia, qué esperarías ver en cada paso, y cuál es el mensaje/resultado concreto que constituye la prueba.

Soluciones

Solución 1: (a) entrenar y evaluar — el parámetro es dependencia declarada de entrenar, y evaluar depende de sus salidas (modelo y test); las dos primeras etapas no dependen de él y se saltan. (b) Las cuatro — todo el grafo cuelga de los datos crudos. (c) Solo evaluarevaluate.py es dependencia únicamente de esa etapa (sí, DVC re-ejecuta aunque el cambio sea un docstring: DVC compara hashes de fichero, no semántica; es el precio de la seguridad). (d) Las cuatro — data.py es dependencia de preparar_datos, su salida cambia (o puede cambiar), y la invalidación se propaga aguas abajo. (e) Ninguna — las etapas dependen de valores concretos del params.yaml (entrenar.n_estimators, etc.), no del fichero como texto, y ningún valor cambió.

Solución 2: cambios: (1) en params.yaml, añadir max_depth: 10 bajo entrenar:; (2) en src/cineclick_churn/train.py, leer el valor y pasarlo al RandomForestClassifier(..., max_depth=params["max_depth"]); (3) en dvc.yaml, añadir entrenar.max_depth a la lista params de la etapa entrenar. Flujo de evaluación: dvc repro (solo re-ejecuta entrenar+evaluar; nota: la primera vez re-entrena de todas formas porque train.py cambió), dvc metrics diff para comparar contra HEAD; si las métricas aguantan (recall similar) con árboles más pequeños, commit de params.yaml+train.py+dvc.yaml+dvc.lock y dvc push; si no, git checkout -- . y dvc checkout para descartar.

Solución 3: en una máquina limpia (o un contenedor): (1) git clone + creación del entorno con pip-sync requirements.lock + pip install -e . --no-deps — se espera instalación sin errores con versiones exactas; (2) dvc pull — descarga los datos exactos referenciados por el commit; (3) dvc repro — se espera "Data and pipelines are up to date", que ya es en sí la verificación de hashes; (4) para la prueba fuerte: borrar models/modelo_churn.pkl y metrics.json y relanzar dvc repro — el pipeline re-entrena desde cero; (5) mostrar metrics.json y compararlo con el commiteado (o dvc metrics diff, esperando cambios vacíos). La prueba concreta: las métricas regeneradas coinciden decimal a decimal con las commiteadas (accuracy 0.86, precision 0.61, recall 0.43, F1 0.50), en una máquina donde Laura no ha tocado nada. Eso es exactamente lo que el 0.87 original no podía ofrecer.

Conclusión

El módulo 2 termina donde prometía: el notebook churn_final_v3_DEFINITIVO.ipynb es ya un proyecto con paquete instalable y testeado, entorno fijado con lock, datos versionados con DVC y un pipeline declarado en dvc.yaml —preparar_datos → construir_features → entrenar → evaluar— que cualquiera reconstruye con git clone, dvc pull y dvc repro, obteniendo el mismo modelo y las mismas métricas honestas (precision 0.61, recall 0.43, F1 0.50) decimal a decimal. La reproducibilidad, el cimiento de todo lo demás, está puesta. Pero al repasar el diagnóstico hemos visto lo que falta, y el experimento de los 300 árboles lo ha hecho tangible: cambiar un parámetro y comparar una ejecución contra otra funciona; explorar decenas de combinaciones, verlas en una tabla ordenada por recall, saber qué se probó hace tres semanas y con qué resultado, y gestionar qué versión del modelo está "bendecida" para producción — eso pide herramientas dedicadas. En el módulo 3 se las damos a CineClick: MLflow para el tracking de experimentos, su model registry para versionar y promocionar modelos, y una mirada crítica a los feature stores. Laura va a poder responder por fin, con datos, a la pregunta que quedó flotando en el módulo 1: qué era exactamente "la v2", y si de verdad era peor.

© Copyright 2026. Todos los derechos reservados