Código reproducible y entorno reproducible: dos de los tres artefactos del ciclo de vida están bajo control. Falta el que cambia a su propio ritmo y sin avisar: los datos. El clientes_churn.csv de CineClick sigue siendo un fichero que el data engineer regenera periódicamente, pisando el anterior — el problema nº 3 del diagnóstico ("el dataset de la v3 ya no existe"). En esta lección incorporamos DVC (Data Version Control), la herramienta que extiende la disciplina de Git a los datos: veremos por qué Git solo no sirve, cuál es el modelo mental de DVC (pequeños metaficheros en Git, datos reales en una caché), y el flujo completo aplicado a nuestro dataset, incluido el momento estrella: viajar en el tiempo entre versiones de los datos con dos comandos.
Contenido
- Por qué Git no sirve para datos
- El modelo mental de DVC: metaficheros + caché + remote
- Manos a la obra:
dvc initydvc add - El remote: compartir datos con
dvc pushydvc pull - Viajar en el tiempo: una versión nueva del dataset
- Qué versiona cada uno: Git vs. DVC
Por qué Git no sirve para datos
La reacción instintiva sería git add data/raw/clientes_churn.csv y a otra cosa. En la lección anterior lo mandamos al .gitignore sin justificarlo del todo; hagámoslo ahora. Git es una herramienta magnífica para lo que fue diseñada —texto, pequeño, con difs significativos— y los datasets incumplen las tres condiciones:
- Tamaño: Git guarda la historia completa del repositorio, y cada clon se la lleva entera. Nuestro CSV de juguete pesa unos pocos MB, pero el extracto real de una plataforma de streaming se mide en GB, y con un snapshot mensual el repositorio engorda sin freno: clonar el proyecto acabaría descargando años de datasets muertos. Los servidores Git (GitHub incluido) además imponen límites duros de tamaño de fichero.
- Binarios y difs inútiles: Git es eficiente porque almacena diferencias entre versiones de texto. Con un CSV grande que se regenera entero (o un Parquet, o imágenes), cada versión es en la práctica un blob nuevo completo, y un
git diffde dos extractos de 2 millones de filas no le sirve a ningún humano para nada. - Ritmos distintos: como vimos en la lección 01-02, código y datos cambian a ritmos diferentes y por causas diferentes. Mezclar en el mismo historial "refactorizo features.py" con "llegó el extracto de julio" ensucia ambos historiales.
Lo que sí queremos conservar de Git es su semántica: poder decir "el modelo de la release 1.2 se entrenó con estos datos exactos", volver a cualquier versión y compartir con el equipo. La solución no es meter los datos en Git, sino meter en Git una referencia verificable a los datos.
El modelo mental de DVC: metaficheros + caché + remote
DVC (Data Version Control) es una herramienta de línea de comandos, de código abierto, que trabaja encima de Git — no lo sustituye. Su truco es una separación limpia:
- En Git viven unos metaficheros diminutos de texto (extensión
.dvc) que describen cada dato versionado: esencialmente su hash MD5 (una huella digital del contenido), su tamaño y su ruta. Ocupan bytes y se difean de maravilla. - Fuera de Git, los ficheros reales viven en una caché local (
.dvc/cache/, organizada por hash) y, para compartirlos, en un remote: cualquier almacenamiento — una carpeta de red, S3, Google Cloud Storage, Azure Blob... - En tu directorio de trabajo,
data/raw/clientes_churn.csves (según el sistema) un enlace o copia materializada desde la caché: tú lo usas con normalidad, pandas ni se entera.
flowchart LR
subgraph Git["Repositorio Git (texto, ligero)"]
M["clientes_churn.csv.dvc<br/>md5: 3f2a..., size: 2.1 MB"]
end
subgraph Local["Máquina local"]
W["data/raw/clientes_churn.csv<br/>(fichero de trabajo)"]
C[".dvc/cache/<br/>(contenidos por hash)"]
end
R["Remote<br/>(carpeta compartida / S3 / GCS)"]
M -- "dvc checkout" --> W
W -- "dvc add" --> C
C -- "dvc push" --> R
R -- "dvc pull" --> CLa consecuencia elegante: cada commit de Git fija, vía el hash del metafichero, una versión exacta de los datos. Código y datos quedan atados en el mismo historial sin que los datos pasen por Git. Cambiar de versión de datos será tan simple como cambiar de commit y pedirle a DVC que materialice lo que ese commit referencia.
Manos a la obra: dvc init y dvc add
DVC es un paquete Python más, así que entra al proyecto por la puerta que montamos en 02-02: se añade dvc>=3.50 al extra dev de pyproject.toml, se regenera el lock con pip-compile y se sincroniza. Con el entorno activado, inicializamos:
dvc init git status # nuevos: .dvc/config .dvc/.gitignore .dvcignore git commit -m "Inicializa DVC en el proyecto"
dvc init crea el directorio .dvc/ (configuración y, más adelante, la caché) y algunos ficheros auxiliares, todos pensados para ser commiteados. DVC deja siempre migas de pan en Git: esa es la gracia.
Ahora ponemos el dataset bajo control. Primero retiramos data/ del .gitignore general que escribimos en 02-01 (DVC gestionará ignorados más finos por sí mismo) y ejecutamos:
Esto hace tres cosas: calcula el hash del fichero, guarda una copia en la caché (.dvc/cache/), y crea el metafichero data/raw/clientes_churn.csv.dvc, además de añadir el CSV real a un .gitignore local para que Git no lo vea. El metafichero es así de humilde:
# data/raw/clientes_churn.csv.dvc (lo genera DVC; no se edita a mano) outs: - md5: 3f2a9c81d4be7a06c1e5f9a2b8d47e11 size: 2184730 hash: md5 path: clientes_churn.csv
Léelo como un resguardo de consigna: "existe un fichero llamado clientes_churn.csv, de 2,1 MB, cuyo contenido tiene esta huella MD5". Con ese hash, DVC puede localizar el contenido exacto en cualquier caché o remote. Cambia un solo byte del CSV y el hash cambia por completo — por eso sirve como identificador de versión. Lo que va a Git es el resguardo:
git add data/raw/clientes_churn.csv.dvc data/raw/.gitignore git commit -m "Versiona el dataset de churn (extracto de junio) con DVC"
Fíjate en el mensaje de commit: describir qué versión de los datos es esta ("extracto de junio") convierte el git log en el historial de datasets que nunca tuvimos.
El remote: compartir datos con dvc push y dvc pull
La caché es local: si Laura versiona el dataset en su máquina, Marc aún no puede obtenerlo. El remote es al dato lo que GitHub al código: el punto de intercambio. DVC soporta S3, GCS, Azure, SSH, carpetas locales o de red... y se configura una vez:
# Para el curso: un directorio local que simula el almacenamiento compartido. dvc remote add -d almacen /srv/dvc-remote-cineclick git add .dvc/config git commit -m "Configura el remote de datos"
El flag -d lo marca como remote por defecto, y la configuración queda en .dvc/config — texto, commiteado, compartido. En la vida real de CineClick esto apuntaría a un bucket (bastaría dvc remote add -d almacen s3://cineclick-mlops-datos más las credenciales, que nunca van a Git — DVC las guarda aparte con dvc remote modify --local). Para seguir el curso, la carpeta local simula el bucket a la perfección: los comandos son idénticos.
Y el intercambio:
Del otro lado, Marc:
git clone <repo> && cd cineclick-churn # ... crea el entorno como en 02-02 ... dvc pull # lee los .dvc del commit actual y descarga esos contenidos exactos
dvc pull mira qué hashes referencian los metaficheros presentes en el commit en el que está, los busca en el remote, los baja a su caché y materializa los ficheros de trabajo. Marc tiene ahora exactamente los mismos bytes con los que Laura entrenó — no "el CSV que hubiera ese día en la carpeta compartida". El gesto git pull + dvc pull se convierte en el nuevo "ponerse al día" del proyecto.
Viajar en el tiempo: una versión nueva del dataset
Llega el momento que justifica toda la maquinaria. Es julio, y el data engineer entrega un extracto nuevo con dos novedades: mes adicional de clientes y, atención, la columna horas_semana ya viene corregida en origen (arreglaron el bug de los valores negativos que Laura filtraba a mano). El fichero se llama igual y sustituye al anterior en data/raw/. Antes, esto era una pérdida irreversible; ahora:
# El nuevo extracto sobrescribe el fichero de trabajo dvc status # data/raw/clientes_churn.csv.dvc: changed <- DVC detecta que el contenido ya no casa con el hash dvc add data/raw/clientes_churn.csv # nuevo hash, nueva entrada en caché git add data/raw/clientes_churn.csv.dvc git commit -m "Datos de julio: nuevo mes y horas_semana corregida en origen" dvc push
El metafichero ahora contiene el hash del extracto de julio, y el commit lo deja escrito en la historia. Las dos versiones conviven en la caché y en el remote, cada una bajo su hash. Y aquí, el viaje en el tiempo — supongamos que el modelo entrenado con julio se comporta raro y queremos reproducir el entrenamiento de junio:
git log --oneline -- data/raw/clientes_churn.csv.dvc # localiza el commit de junio git checkout a1b2c3d # Git restaura el METAFICHERO de junio... dvc checkout # ...y DVC materializa LOS DATOS de junio desde la caché
git checkout solo mueve los ficheros que Git controla — es decir, el .dvc con el hash antiguo. dvc checkout lee ese hash y repone en data/raw/ el contenido correspondiente. Dos comandos, y el directorio de trabajo es idéntico, código y datos, al día del entrenamiento de junio. Para volver al presente: git checkout master && dvc checkout. Si el contenido no estuviera en la caché local (una máquina nueva), dvc pull lo traería del remote.
Esto es exactamente lo que el diagnóstico del módulo 1 echaba de menos en el problema nº 3: "no se puede reproducir el modelo actual ni comparar entrenamientos: el dataset de la v3 ya no existe". Ahora cada dataset existe para siempre, direccionable por commit, y la pregunta "¿con qué datos se entrenó este modelo?" tiene por fin una respuesta mecánica: los del commit del que salió.
Qué versiona cada uno: Git vs. DVC
La pareja queda repartida así:
| Aspecto | Git | DVC |
|---|---|---|
| Qué versiona | Código, configuración, tests, docs, metaficheros .dvc |
Datos y ficheros grandes/binarios (datasets, y pronto modelos) |
| Dónde guarda el contenido | En el propio repositorio (historia completa en cada clon) | Caché local + remote (se descarga solo lo que necesitas) |
| Unidad de cambio | Commit (diff línea a línea) | Hash del contenido completo del fichero |
| Cómo se comparte | git push / git pull (GitHub, etc.) |
dvc push / dvc pull (S3, GCS, carpeta de red...) |
| Cómo se viaja en el tiempo | git checkout <commit> |
dvc checkout (después del de Git) |
| Tamaño cómodo | KB–MB, texto | MB–TB, cualquier formato |
Y la regla mnemotécnica que gobierna el reparto: Git versiona las recetas; DVC, los ingredientes. Cada commit de Git es una foto completa del proyecto porque contiene el código, la configuración, el lock del entorno... y los resguardos que fijan los datos.
Errores Comunes y Consejos
- Error: commitear el metafichero sin hacer
dvc push(o al revés). Si Marc hacegit pully el hash que referencia el.dvcno está en el remote, sudvc pullfallará con un elocuente "file is missing". Los dos pushes van juntos, siempre; interiorízalo como un solo gesto (en el módulo 5, la CI lo verificará por nosotros). - Error: editar un fichero
.dvca mano. El hash es una huella calculada; escribir otro valor no cambia los datos, solo rompe la correspondencia. Los.dvclos escribe DVC (dvc add); tú solo los commiteas. - Error: hacer
git checkouta un commit antiguo y olvidar eldvc checkout. Quedas en un estado incoherente traicionero: código de junio con datos de julio en el directorio. Los pares van juntos:git checkout+dvc checkout, ida y vuelta. (Existedvc install, que engancha hooks de Git para automatizar el segundo paso; actívalo si te descubres olvidándolo.) - Error: versionar con DVC ficheros que deben ir en Git. Un
config.yamlde 20 líneas en DVC es contraproducente: pierdes el diff, que ahí sí es valioso. El criterio es tamaño + binariedad + ritmo de cambio, no "todo lo que hay en data/ por definición". - Consejo: un dataset nuevo merece un mensaje de commit informativo. "update data" no; "Datos de julio: nuevo mes y horas_semana corregida en origen" sí. El historial de
git log -- data/raw/*.dvces la documentación de la evolución de tus datos — gratis.
Ejercicios
Ejercicio 1
Ordena correctamente esta secuencia desordenada de comandos para incorporar un extracto nuevo del dataset y dejarlo disponible para todo el equipo, y explica qué hace cada paso: git commit -m "Datos de agosto", dvc push, dvc add data/raw/clientes_churn.csv, git add data/raw/clientes_churn.csv.dvc, (el fichero nuevo sobrescribe al viejo en data/raw/).
Ejercicio 2
Laura entrenó el "modelo bueno" hace tres semanas, en el commit f4e5d6a. Hoy el dataset de trabajo es el de julio. Escribe la secuencia exacta de comandos para: (1) dejar la máquina exactamente como el día de aquel entrenamiento (código y datos); (2) comprobar que los datos materializados están sincronizados con el metafichero; (3) volver al estado actual. Indica qué comando adicional necesitarías si lo hicieras en una máquina recién clonada.
Ejercicio 3
Un compañero propone: "en vez de DVC, subamos cada extracto a la carpeta compartida como clientes_churn_2026_06.csv, clientes_churn_2026_07.csv... y que el config.yaml apunte al que toque". Es un avance sobre sobrescribir, sin duda. Señala al menos tres garantías que da DVC y que este esquema de sufijos de fecha no da.
Soluciones
Solución 1: (0) el extracto nuevo sobrescribe el fichero de trabajo — el punto de partida; (1) dvc add data/raw/clientes_churn.csv — calcula el hash nuevo, guarda el contenido en la caché y actualiza el metafichero; (2) git add data/raw/clientes_churn.csv.dvc — prepara el resguardo actualizado para commit; (3) git commit -m "Datos de agosto" — fija en la historia la nueva versión de datos junto al código vigente; (4) dvc push — sube el contenido al remote para que el dvc pull de cualquier compañero funcione. El orden de 3 y 4 puede intercambiarse, pero ninguno puede faltar.
Solución 2: (1) git checkout f4e5d6a seguido de dvc checkout — Git repone el código y el metafichero de aquel día; DVC lee su hash y materializa aquel dataset desde la caché; (2) dvc status — debe responder que todo está al día (ninguna discrepancia entre ficheros de trabajo y metaficheros); (3) git checkout master y de nuevo dvc checkout. En una máquina recién clonada la caché está vacía, así que entre el checkout y el trabajo haría falta dvc pull para descargar del remote el contenido que el hash referencia.
Solución 3: entre otras: (1) verificación de integridad — el hash MD5 garantiza que el fichero es exactamente el original; con sufijos de fecha, nada impide que alguien edite o re-suba _2026_06.csv y nadie lo note jamás; (2) vínculo automático código↔datos — con DVC, cada commit fija qué datos le corresponden; con sufijos, el vínculo depende de que un humano actualice el config.yaml en el commit correcto y de que nadie lo toque después; (3) deduplicación y transporte eficiente — la caché por hash no guarda dos veces el mismo contenido y dvc pull trae solo lo que falta, mientras que la carpeta de ficheros con fecha crece sin control y se copia a mano; (4, propina) un solo flujo para cualquier tamaño y backend — el día que el CSV sean 40 GB en S3, los comandos no cambian.
Conclusión
El tercer artefacto ha entrado en vereda: con dvc init, dvc add y un remote configurado, cada versión de clientes_churn.csv queda fijada por su hash en un metafichero commiteado, compartida vía dvc push/dvc pull, y recuperable para siempre con la pareja git checkout + dvc checkout — el problema nº 3 del diagnóstico está resuelto. Ahora la foto es: código versionado, entorno versionado, datos versionados... pero el proceso que los une sigue siendo artesanal: ejecutar la limpieza, luego las features, luego el entrenamiento, luego la evaluación, a mano y en el orden correcto, acordándose de qué hay que repetir cuando algo cambia. Ese encadenamiento manual es frágil, y DVC tiene una segunda cara pensada justo para eliminarlo: los pipelines declarados en dvc.yaml, donde cada etapa conoce sus dependencias y dvc repro re-ejecuta solo lo necesario. Es la pieza que cierra el módulo, y con ella CineClick tocará por primera vez la reproducibilidad completa: la vemos en la próxima lección.
Curso de MLOps
Módulo 1: Fundamentos de MLOps
- Qué es MLOps y por qué los modelos mueren en el notebook
- El ciclo de vida de un modelo de ML en producción
- Niveles de madurez MLOps y roles del equipo
- El proyecto del curso: del notebook a producción
Módulo 2: Del notebook al código reproducible
- Estructura de un proyecto de ML: del notebook al paquete
- Entornos reproducibles y gestión de dependencias
- Versionado de datos con DVC
- Pipelines de entrenamiento reproducibles
Módulo 3: Experimentos y registro de modelos
- Tracking de experimentos con MLflow
- Model registry: versionar y promocionar modelos
- Feature stores: cuándo y para qué
Módulo 4: Servir modelos en producción
- Patrones de despliegue: batch, online y streaming
- Un servicio de predicción con FastAPI
- Empaquetado con Docker
- Escalado y despliegue: Kubernetes y serverless
- Optimización de la inferencia: latencia y coste
Módulo 5: Automatización: CI/CD y orquestación
- CI para ML: tests de código, datos y modelos
- CD: automatizar el despliegue del modelo
- Orquestación de pipelines de ML
- Estrategias de release: shadow, canary y A/B
