Práctica: Containeriza un script de preprocesamiento de datos

Lectura
30 min~10 min lectura

Práctica: Containeriza un script de preprocesamiento de datos

En esta lección práctica, aplicarás los fundamentos de Docker para encapsular un flujo de trabajo fundamental en ciencia de datos: el preprocesamiento de datos. Moverás un script de Python, con sus dependencias específicas, desde un entorno local frágil a un contenedor portable, reproducible y autónomo. Este es el primer paso crucial para construir pipelines de ML robustos que puedan ejecutarse en cualquier lugar, desde la laptop de un desarrollador hasta un clúster de producción en la nube. Abandonarás la mentalidad de "funciona en mi máquina" para adoptar la garantía de "funciona en cualquier contenedor".

Concepto clave: El contenedor como entorno de ejecución autónomo

Imagina que tu script de preprocesamiento de datos es una receta de cocina compleja que requiere ingredientes muy específicos (bibliotecas de Python como pandas 1.5.3, scikit-learn 1.2.0) y utensilios especiales (un intérprete de Python 3.9, ciertas herramientas del sistema). Si intentas cocinar esa receta en una cocina diferente (otra computadora, un servidor), es probable que falten ingredientes, que las versiones sean distintas o que los utensilios no se comporten igual, arruinando el plato. Docker soluciona esto proporcionando una cocina portátil y pre-equipada (el contenedor). Tú defines exactamente qué ingredientes y utensilios incluir en una lista (el Dockerfile). Luego, Docker construye una cocina completa y aislada (la imagen) que puedes llevar a cualquier parte. Cuando ejecutas la receta (tu script) dentro de esa cocina, el resultado es idéntico, sin importar si estás en Windows, macOS o Linux.

La analogía se extiende al proceso de construcción. El Dockerfile es la lista de compras y las instrucciones de montaje de la cocina. El comando docker build toma esa lista, adquiere los ingredientes base (una imagen base como `python:3.9-slim`) y ejecuta los pasos para instalar todo lo necesario. El resultado final es una imagen Docker, una plantilla inmutable y lista para usar. Al ejecutar docker run, creas una instancia viva y aislada de esa imagen: un contenedor, que es donde finalmente se ejecuta tu script. Este enfoque encapsula no solo el código, sino todo su entorno de ejecución, eliminando una fuente masiva de errores en proyectos de ciencia de datos.

Tip del instructor: No pienses en Docker solo como una herramienta de despliegue. Para el científico de datos, es primero una herramienta de reproducibilidad y colaboración. Containerizar incluso los scripts más simples garantiza que cualquier colega, sistema CI/CD o servidor pueda reproducir exactamente tus transformaciones de datos.

Cómo funciona en la práctica: Paso a paso desde el script hasta el contenedor

El flujo de trabajo comienza con un script de Python autónomo. Supongamos que tienes un archivo `preprocesar.py` que lee un CSV crudo, limpia valores nulos, escala características numéricas y guarda el dataset procesado. Este script probablemente dependa de pandas y scikit-learn. En tu máquina local, lo ejecutas dentro de un entorno virtual. Para containerizarlo, el primer paso es aislar estas dependencias en un archivo `requirements.txt`. Luego, debes definir el entorno de construcción en un Dockerfile.

El Dockerfile es el plano de construcción. Partirás de una imagen base oficial y ligera de Python. Luego, copiarás los archivos de tu proyecto (el script y el archivo de requisitos) al sistema de archivos del contenedor. El siguiente paso crítico es instalar las dependencias usando pip y el archivo `requirements.txt`. Finalmente, especificarás el comando que se ejecutará por defecto cuando el contenedor se inicie, que en este caso será `python preprocesar.py`. Con el Dockerfile definido, ejecutas `docker build -t preprocesador-datos .` para crear la imagen. Docker ejecutará cada instrucción del Dockerfile en capas, creando una imagen lista. Para ejecutarlo, usas `docker run preprocesador-datos`. El contenedor se crea, ejecuta tu script y luego finaliza, dejando los resultados (si los guardaste en un volumen) disponibles.

Un aspecto vital en ciencia de datos es el manejo de los datos mismos. Los contenedores son efímeros: cualquier archivo creado dentro del contenedor se perderá cuando este se detenga. Por lo tanto, debes usar volúmenes Docker para mapear directorios de tu máquina anfitriona al contenedor. Así, tu script puede leer el CSV de entrada desde un directorio local y escribir el CSV de salida en otro directorio local, persistente más allá del ciclo de vida del contenedor. Este patrón separa claramente la lógica de procesamiento (dentro del contenedor) de los datos (fuera del contenedor).

Código en acción: Un ejemplo completo y funcional

A continuación, se presenta un ejemplo real y funcional de un script de preprocesamiento y su Dockerfile correspondiente. Este script simula una tarea común: cargar datos, realizar limpieza y codificación, y guardar el resultado.

Script de preprocesamiento (preprocesar.py)

import pandas as pd
import numpy as np
import argparse
import sys
import os
from sklearn.preprocessing import StandardScaler

def main():
    # Configurar argumentos de línea de comandos
    parser = argparse.ArgumentParser(description='Preprocesar dataset de entrada.')
    parser.add_argument('--input', type=str, required=True, help='Ruta al archivo CSV de entrada.')
    parser.add_argument('--output', type=str, required=True, help='Ruta donde guardar el CSV procesado.')
    args = parser.parse_args()

    # Validar que el archivo de entrada exista
    if not os.path.exists(args.input):
        print(f"ERROR: El archivo de entrada '{args.input}' no existe.")
        sys.exit(1)

    print(f"[INFO] Cargando datos desde {args.input}")
    df = pd.read_csv(args.input)

    print(f"[INFO] Dataset crudo: {df.shape[0]} filas, {df.shape[1]} columnas")

    # 1. Manejo de valores nulos
    # Para columnas numéricas, imputar con la mediana
    numeric_cols = df.select_dtypes(include=[np.number]).columns
    for col in numeric_cols:
        if df[col].isnull().sum() > 0:
            median_val = df[col].median()
            df[col].fillna(median_val, inplace=True)
            print(f"[INFO] Imputados {df[col].isnull().sum()} valores nulos en '{col}' con mediana {median_val:.2f}")

    # Para columnas categóricas, imputar con 'Desconocido'
    categorical_cols = df.select_dtypes(include=['object']).columns
    for col in categorical_cols:
        if df[col].isnull().sum() > 0:
            df[col].fillna('Desconocido', inplace=True)
            print(f"[INFO] Imputados {df[col].isnull().sum()} valores nulos en '{col}' con 'Desconocido'")

    # 2. Codificación de variables categóricas (One-Hot Encoding para pocas categorías)
    df_processed = pd.get_dummies(df, columns=categorical_cols, drop_first=True, dtype=int)

    # 3. Estandarización de características numéricas
    if len(numeric_cols) > 0:
        scaler = StandardScaler()
        df_processed[numeric_cols] = scaler.fit_transform(df_processed[numeric_cols])
        print(f"[INFO] Estandarizadas columnas numéricas: {list(numeric_cols)}")

    print(f"[INFO] Dataset procesado: {df_processed.shape[0]} filas, {df_processed.shape[1]} columnas")

    # Guardar el dataset procesado
    df_processed.to_csv(args.output, index=False)
    print(f"[INFO] Dataset procesado guardado en {args.output}")

if __name__ == "__main__":
    main()

Archivo de dependencias (requirements.txt)

pandas==1.5.3
scikit-learn==1.2.0
numpy==1.24.3

Dockerfile para containerizar el script

# Usar una imagen base oficial de Python ligera
FROM python:3.9-slim

# Establecer el directorio de trabajo dentro del contenedor
WORKDIR /app

# Copiar el archivo de requisitos primero (mejora el caching de capas de Docker)
COPY requirements.txt .

# Instalar las dependencias de Python especificadas
RUN pip install --no-cache-dir -r requirements.txt

# Copiar el script de preprocesamiento al contenedor
COPY preprocesar.py .

# Definir el comando por defecto al ejecutar el contenedor
# Se espera que el usuario proporcione los argumentos --input y --output en el 'docker run'
ENTRYPOINT ["python", "preprocesar.py"]

Comandos para construir y ejecutar el contenedor

Con los tres archivos anteriores (`preprocesar.py`, `requirements.txt`, `Dockerfile`) en el mismo directorio, ejecuta los siguientes comandos en tu terminal.

# Construir la imagen Docker. El punto (.) indica el contexto de construcción (directorio actual).
docker build -t preprocesador-ml:1.0 .

# Ejecutar el contenedor, mapeando volúmenes para entrada/salida de datos.
# Asume que tienes un archivo 'datos_crudos.csv' en un directorio './datos' local.
# El contenedor leerá desde '/datos_entrada' y escribirá en '/datos_salida' (dentro del contenedor).
# Usamos volúmenes para conectar estos paths a directorios locales.
docker run --rm \
  -v $(pwd)/datos:/datos_entrada \
  -v $(pwd)/resultados:/datos_salida \
  preprocesador-ml:1.0 \
  --input /datos_entrada/datos_crudos.csv \
  --output /datos_salida/datos_procesados.csv

Errores comunes y cómo evitarlos

Al comenzar con Docker, es fácil cometer ciertos errores que frustran el proceso. Aquí hay cinco comunes en el contexto de ciencia de datos y cómo solucionarlos.

Error 1: No usar volúmenes para los datos. El error más frecuente es copiar los datos dentro de la imagen con `COPY datos.csv .`. Esto infla la imagen, hace que los datos sean estáticos (no se pueden cambiar sin reconstruir) y los resultados se pierden al terminar el contenedor. Solución: Siempre usa el flag `-v` en `docker run` para montar volúmenes. Separa la lógica (imagen) de los datos (volúmenes).

Error 2: Imágenes gigantescas. Partir de una imagen base como `python:3.9` (que incluye herramientas de compilación) o instalar paquetes del sistema innecesarios genera imágenes de varios GB. Solución: Usa imágenes base "slim" o "alpine". Limpia la caché de apt o pip en la misma línea RUN (ej: `RUN pip install --no-cache-dir -r requirements.txt && rm -rf /var/lib/apt/lists/*`).

Error 3: Ejecutar como usuario root. Por defecto, los contenedores se ejecutan como root, lo que es un riesgo de seguridad y puede causar problemas de permisos al escribir archivos en volúmenes montados. Solución: En el Dockerfile, crea un usuario no privilegiado y cambia a él con `USER`. Añade `RUN useradd -m -u 1000 cientifico && chown -R cientifico /app` y luego `USER cientifico`.

Error 4: Dependencias no especificadas. Un `requirements.txt` vago con `pandas>=1.0` puede llevar a diferentes versiones en distintas construcciones, rompiendo la reproducibilidad. Solución: Fija las versiones exactas (`pandas==1.5.3`). Usa `pip freeze` en tu entorno de desarrollo probado para generarlo. Considera herramientas como `pipenv` o `poetry` para un manejo más robusto.

Error 5: Confundir CMD con ENTRYPOINT. Usar solo `CMD ["python", "preprocesar.py"]` permite sobrescribir el comando fácilmente con `docker run imagen /bin/bash`, lo que a veces no es deseable. Solución: Usa la combinación `ENTRYPOINT` para el ejecutable fijo y `CMD` para los argumentos por defecto. En nuestro ejemplo, usar solo `ENTRYPOINT` es adecuado porque siempre queremos ejecutar ese script, pero los argumentos (rutas de archivos) deben ser proporcionados en el `docker run`.

Checklist de dominio

Antes de considerar esta lección completa, asegúrate de poder verificar cada uno de los siguientes puntos. Demuestran una comprensión práctica y aplicada de la containerización para scripts de datos.

  • Puedo escribir un Dockerfile funcional que parta de una imagen base Python adecuada, copie un script y sus dependencias, y defina un punto de entrada.
  • Comprendo la importancia de fijar versiones exactas en `requirements.txt` y cómo esto garantiza la reproducibilidad de la imagen.
  • Sé construir una imagen Docker con `docker build -t nombre:etiqueta .` y explicar qué sucede en cada capa durante la construcción.
  • Puedo ejecutar un contenedor a partir de mi imagen, mapeando correctamente volúmenes de host para la entrada y salida de datos usando el flag `-v`.
  • Entiendo la diferencia entre el ciclo de vida de un contenedor (efímero) y la persistencia de los datos (a través de volúmenes) y diseño mis scripts en consecuencia.
  • Soy capaz de inspeccionar imágenes creadas (`docker images`) y contenedores en ejecución o detenidos (`docker ps -a`).
  • Puedo explicar por qué es una mala práctica copiar conjuntos de datos grandes directamente en la imagen Docker y conozco la alternativa correcta (volúmenes).
  • He containerizado con éxito mi propio script de preprocesamiento o análisis de datos y lo he ejecutado de forma aislada, sin depender de las bibliotecas instaladas en mi máquina local.

Al dominar esta práctica, has dado el salto de usar Docker conceptualmente a aplicarlo de manera concreta a tu flujo de trabajo de ciencia de datos. El contenedor que acabas de crear es una unidad atómica, confiable y despliegable de tu lógica de preprocesamiento. Este es el cimiento sobre el que construirás pipelines más complejos, donde múltiples contenedores (para preprocesamiento, entrenamiento, evaluación) pueden orquestarse con herramientas como Kubernetes, tema de módulos posteriores. La portabilidad y confiabilidad que introduces hoy justifican plenamente la inversión inicial en aprender Docker.

De lección a portfolio

Convertí esta lección en una habilidad visible para entrevistas.

Guardá el curso, completá los ejercicios y conectá esta habilidad con una ruta de empleo, data, IA, programación o marketing.

Newsletter Cursalo

Recibí rutas y cursos nuevos

Sumate para recibir recursos orientados a empleo y portfolio.

  • Rutas de empleo
  • Cursos prácticos
  • Portfolio y entrevistas

Sin spam. También podés entrar con tu cuenta para guardar progreso. Iniciá sesión