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

  1. Por qué Git no sirve para datos
  2. El modelo mental de DVC: metaficheros + caché + remote
  3. Manos a la obra: dvc init y dvc add
  4. El remote: compartir datos con dvc push y dvc pull
  5. Viajar en el tiempo: una versión nueva del dataset
  6. 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 diff de 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.csv es (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" --> C

La 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:

dvc add data/raw/clientes_churn.csv

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:

dvc push    # sube al remote los contenidos de la caché que falten allí

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 hace git pull y el hash que referencia el .dvc no está en el remote, su dvc pull fallará 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 .dvc a mano. El hash es una huella calculada; escribir otro valor no cambia los datos, solo rompe la correspondencia. Los .dvc los escribe DVC (dvc add); tú solo los commiteas.
  • Error: hacer git checkout a un commit antiguo y olvidar el dvc 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. (Existe dvc 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.yaml de 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/*.dvc es 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.

© Copyright 2026. Todos los derechos reservados