Hay un momento que todo data scientist conoce bien: el modelo funciona. Las métricas son buenas, el notebook corre de principio a fin sin errores, y la demo impresiona en la reunión. Y entonces alguien dice la frase inevitable: "perfecto, ahora desplegadlo". Ahí empieza el verdadero trabajo.
La distancia entre un notebook que funciona y un sistema de ML en producción no es solo técnica. Es conceptual. Un notebook es un entorno de exploración: lineal, controlado, lleno de decisiones implícitas que el autor recuerda pero que el código no documenta. Producción es lo contrario: asíncrono, distribuido, hostil, y sin ninguna paciencia para las suposiciones no escritas.
Este artículo recorre los diferentes pasos para hacer ese salto sin romperse en el intento.
El problema del notebok perfecto
El notebook es el hábitat natural del científico de datos. Es interactivo, permite iterar rápido, visualizar resultados al momento y explorar sin comprometerse. Es exactamente lo que necesitas cuando no sabes qué estás buscando.
El problema es que esas mismas características que lo hacen ideal para explorar lo hacen pésimo para producción. Un notebook típico acumula una serie de problemas silenciosos.
Las celdas se ejecutan en orden arbitrario durante el desarrollo. Es habitual tener una variable definida en la celda 12 que se usa en la celda 5, porque en algún momento reorganizaste el notebook y olvidaste actualizar las dependencias. El código funciona si ejecutas las celdas en el orden correcto, pero ese orden no está escrito en ningún sitio.
Los datos son estáticos. El CSV que usaste para entrenar estaba en tu escritorio, o en una ruta absoluta específica de tu máquina. El código que "carga los datos" asume una estructura que no existirá en ningún otro entorno.
Las dependencias están implícitas. Instalaste una librería hace seis meses y ya no recuerdas cuándo ni por qué. El notebook funciona en tu máquina. En la de nadie más.
Los hiperparámetros están hardcodeados. El learning rate que elegiste está en la celda 7. Si quieres cambiarlo, tienes que editar el código.
Nada de esto es un defecto del data scientist. Es simplemente lo que ocurre cuando usas la herramienta correcta para la fase incorrecta. El notebook es para explorar. Producción necesita software.
Antes de pensar en deployment, ¿el modelo está listo?
Antes de tocar una sola línea de código de infraestructura, hay que responder una pregunta más fundamental: ¿el modelo merece ser desplegado?
Esta pregunta tiene dos dimensiones que a menudo se confunden.
La primera es la dimensión técnica. ¿Las métricas son correctas? ¿Se evaluó el modelo en un conjunto de test que no contaminó el entrenamiento? ¿Se hizo validación cruzada cuando era pertinente? ¿Los datos de entrenamiento son representativos de los datos que el modelo verá en producción? Un modelo con 95% de accuracy en un dataset de juguete puede tener un rendimiento desastroso en datos reales si la distribución es distinta.
La segunda es la dimensión de negocio. Las métricas técnicas no siempre se traducen a valor real. Un modelo de detección de fraude puede tener una precisión del 92% y aun así ser inaceptable si el 8% de errores son falsos negativos que cuestan millones. Antes de desplegar, hay que definir qué métrica de negocio importa y cuál es el umbral mínimo aceptable. Precisión, recall, AUC, F1: ninguna de estas métricas tiene sentido en el vacío.
También es el momento de hablar de los datos de entrenamiento desde la perspectiva de sesgos. Un modelo entrenado con datos históricos puede perpetuar patrones discriminatorios. Detectar este problema en producción, cuando ya está afectando a usuarios reales, es exponencialmente más costoso que detectarlo antes del despliegue.
Refactorizar el código
El primer paso técnico es transformar el notebook en código que pueda ejecutarse de forma reproducible en cualquier entorno. Esto no significa reescribir todo desde cero, pero sí implica un cambio de mentalidad.
La modularización es el principio más importante. El código debe dividirse en funciones y módulos con responsabilidades claras: carga de datos, preprocesamiento, entrenamiento, evaluación, inferencia. Cada función debe tener inputs y outputs definidos, sin dependencia de estado global. Si una función necesita una variable que no recibe como argumento, es una señal de que hay un problema de diseño.
La reproducibilidad requiere atención explícita. Las semillas aleatorias deben fijarse de forma sistemática: en NumPy, en Python, en TensorFlow o PyTorch si se usan. Las versiones de las librerías deben estar fijadas de forma determinista: uv genera un lockfile exacto que garantiza que cualquier persona que clone el proyecto instale exactamente las mismas versiones, en cualquier máquina y en cualquier momento.
La configuración debe separarse del código. Los hiperparámetros, rutas de archivos, nombres de modelos y cualquier otro parámetro que pueda cambiar entre ejecuciones deben vivir en archivos de configuración (YAML o JSON) o en variables de entorno, nunca hardcodeados. Herramientas como Hydra o simple archivos de configuración permiten cambiar parámetros sin tocar el código.
Los experimentos deben ser rastreados. Herramientas como MLflow, Weights & Biases o DVC permiten registrar automáticamente los parámetros, métricas y artefactos de cada experimento. Cuando alguien pregunta "¿cuál era el learning rate del modelo que está en producción?", la respuesta no debería ser "déjame buscar en los comentarios del notebook".
El pipeline de datos
En un notebook de desarrollo, los datos llegan de algún sitio y ya están limpios, o casi. En producción, los datos llegan de sistemas externos, en tiempo real o en batch, con formatos que cambian, con valores nulos inesperados, con filas duplicadas y con distribuciones que evolucionan con el tiempo.
El pipeline de datos en producción tiene que resolver varios problemas que en desarrollo no existían.
El primero es la validación de inputs. Cada dato que entra al sistema debe validarse antes de ser procesado. Si el modelo espera una variable numérica y recibe un string, el comportamiento debe ser predecible: lanzar un error claro, no producir una predicción silenciosamente incorrecta. Librerías como Great Expectations o Pydantic permiten definir esquemas de datos y validarlos automáticamente.
El segundo es el feature engineering reproducible. Las transformaciones que se aplican a los datos durante el entrenamiento deben aplicarse exactamente igual durante la inferencia. Si durante el entrenamiento se normalizaron las features con la media y desviación estándar del conjunto de entrenamiento, esos mismos valores deben usarse en producción. El error más común aquí es recalcular la normalización con los datos de producción, introduciendo un data leakage inverso. La solución es serializar el objeto de preprocesamiento (por ejemplo, el StandardScaler de scikit-learn) junto con el modelo.
# Serializar preprocesador y modelo juntos tras el entrenamiento
import joblib
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.ensemble import RandomForestClassifier
pipeline = Pipeline([
("scaler", StandardScaler()),
("model", RandomForestClassifier())
])
pipeline.fit(X_train, y_train)
joblib.dump(pipeline, "modelo_v1.joblib")
# En producción, cargar el pipeline completo
pipeline = joblib.load("modelo_v1.joblib")
prediccion = pipeline.predict(X_nuevo) # El scaler se aplica automáticamenteEl tercero es el data drift. Los datos que el modelo ve en producción cambian con el tiempo. La distribución de los inputs puede desviarse de la distribución con la que se entrenó, degradando el rendimiento silenciosamente. Detectar este problema requiere monitorización activa, no solo de las métricas del modelo sino de las distribuciones de los datos de entrada.
Empaquetar y servir el modelo
Una vez que el código es limpio y el pipeline está definido, llega la pregunta de cómo hacer que el modelo esté disponible para ser usado.
La respuesta más común para modelos que necesitan respuestas en tiempo real es una API REST. FastAPI es actualmente la opción más popular en el ecosistema Python por su rendimiento, su integración con Pydantic para validación de datos y su documentación automática. Un endpoint típico de inferencia recibe un JSON con los features de entrada y devuelve las predicciones como JSON.
from fastapi import FastAPI
from pydantic import BaseModel
import joblib, os
import numpy as np
app = FastAPI()
pipeline = joblib.load(os.environ.get("MODEL_PATH", "modelo_v1.joblib"))
class InputData(BaseModel):
feature1: float
feature2: float
feature3: float
class Prediction(BaseModel):
score: float
label: str
@app.post("/predict", response_model=Prediction)
def predict(data: InputData):
X = np.array([[data.feature1, data.feature2, data.feature3]])
score = float(pipeline.predict_proba(X)[0][1])
label = "positivo" if score >= 0.5 else "negativo"
return Prediction(score=score, label=label)
# Para levantar el servidor: uvicorn main:app --host 0.0.0.0 --port 8080La containerización es el siguiente paso obligatorio. Docker permite empaquetar el modelo, el código y todas sus dependencias en una imagen que se ejecutará de forma idéntica en cualquier entorno. Esto elimina el problema de "funciona en mi máquina" de forma definitiva. Una imagen Docker bien construida debe ser lo más pequeña posible (usando imágenes base slim o alpine), debe construirse de forma reproducible y debe ser inmutable: la misma imagen debe producir los mismos resultados siempre.
FROM python:3.11-slim
WORKDIR /app
# Copiar dependencias primero para aprovechar la caché de capas
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copiar solo el código, nunca el modelo
COPY main.py .
# Crear usuario sin privilegios (nunca correr ML en producción como root)
RUN useradd --create-home appuser
USER appuser
EXPOSE 8080
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"]# Construir la imagen y ejecutar montando el modelo como volumen
docker build -t mi-modelo:v1 .
docker run \
-v /ruta/local/modelos:/app/modelos \
-e MODEL_PATH=/app/modelos/modelo_v1.joblib \
-p 8080:8080 \
mi-modelo:v1Para casos de uso con volúmenes muy altos o con necesidades de escalado automático, existen frameworks especializados en model serving: TorchServe para modelos de PyTorch, TensorFlow Serving para modelos de TF, BentoML o Ray Serve para un enfoque más agnóstico del framework.
La elección entre inferencia en batch y en tiempo real es crítica y depende completamente del caso de uso. Si el modelo alimenta una recomendación que aparece en la homepage de un usuario, necesita tiempo real. Si el modelo clasifica documentos para un proceso de negocio nocturno, batch es más eficiente, más barato y más fácil de operar.
Monitorización
Un modelo en producción sin monitorización es como un coche sin indicadores: puede funcionar perfectamente durante meses, o puede llevar semanas fallando sin que nadie lo sepa.
La monitorización de un sistema de ML tiene varias capas.
La capa de infraestructura es la más básica: latencia de las peticiones, throughput, uso de CPU y memoria, tasas de error. Estas son las métricas que cualquier sistema de software monitoriza, y deben estar instrumentadas desde el primer día.
La capa de datos monitoriza la distribución de los inputs. Si el modelo recibe datos con una distribución muy diferente a la de entrenamiento, sus predicciones serán menos fiables. Herramientas como Evidently o WhyLabs permiten calcular métricas de drift automáticamente y generar alertas cuando los inputs se alejan de lo esperado.
import pandas as pd
from evidently.report import Report
from evidently.metric_preset import DataDriftPreset
from evidently.metrics import DatasetDriftMetric
df_referencia = pd.read_parquet("datos_entrenamiento.parquet")
df_produccion = pd.read_parquet("datos_produccion_semana_actual.parquet")
report = Report(metrics=[
DataDriftPreset(),
DatasetDriftMetric()
])
report.run(
reference_data=df_referencia,
current_data=df_produccion
)
report.save_html("drift_report.html")
result = report.as_dict()
try:
drift_metric = next(
m for m in result["metrics"]
if "DatasetDriftMetric" in m["metric"]
)
if drift_metric["result"]["dataset_drift"]:
n_features = drift_metric["result"]["number_of_drifted_columns"]
print(f"ALERTA: drift detectado en {n_features} features — considerar reentrenamiento")
else:
share = drift_metric["result"]["share_of_drifted_columns"]
print(f"Sin drift significativo ({share:.1%} de features afectadas)")
except StopIteration:
raise RuntimeError("No se encontró DatasetDriftMetric en el reporte. Verifica la versión de Evidently.")La capa del modelo monitoriza el rendimiento real de las predicciones. Esto es más complejo porque requiere ground truth: necesitas saber si las predicciones del modelo fueron correctas. En algunos casos esto es inmediato (un modelo que predice si un correo es spam puede validarse en segundos). En otros, el feedback llega con retraso (un modelo que predice la probabilidad de churn puede tardar semanas en saber si acertó). Diseñar el sistema de feedback desde el principio es fundamental.
Finalmente, el sistema debe tener una estrategia definida de reentrenamiento. ¿Cuándo se reentrena el modelo? ¿En un schedule fijo? ¿Cuando las métricas caen por debajo de un umbral? ¿Con qué datos? ¿Cómo se valida el nuevo modelo antes de reemplazar al anterior? Estas preguntas deben responderse antes de llegar a producción, no después.
El factor humano y organizativo
La parte más subestimada del MLOps no es técnica. Es organizativa.
Llevar un modelo a producción requiere coordinación entre perfiles que raramente hablan el mismo idioma: data scientists, ingenieros de software, DevOps, y stakeholders de negocio. Cada uno tiene prioridades y perspectivas distintas, y esas diferencias crean fricción.
Los data scientists optimizan para la experimentación rápida. Los ingenieros de software optimizan para la robustez y la mantenibilidad. DevOps optimiza para la estabilidad operacional. El negocio optimiza para el impacto. Un proyecto de ML exitoso necesita que todos estos objetivos estén alineados desde el principio.
MLOps, en su sentido más profundo, no es un conjunto de herramientas. Es una cultura y un conjunto de prácticas que acercan los mundos del desarrollo de modelos y la operación de sistemas. Implica definir quién es responsable de cada parte del sistema, cómo se gestionan los cambios, cómo se comunican los incidentes y cómo se toman decisiones sobre reentrenamiento o rollback.
Una práctica concreta que ayuda mucho es el model card: un documento que describe el modelo, sus capacidades, sus limitaciones, los datos con los que fue entrenado, sus métricas de rendimiento y las consideraciones éticas relevantes. Es un artefacto de comunicación tanto técnico como organizativo, y fuerza a hacer explícitas muchas suposiciones que de otro modo permanecerían implícitas.
Errores más comunes
Después de ver muchos proyectos de ML ir a producción, hay un conjunto de errores que se repiten con una regularidad casi reconfortante.
El primero es asumir que el entorno de producción se parece al de desarrollo. No se parece. Los datos tienen formatos distintos, los volúmenes son diferentes, la latencia importa, y los errores inesperados ocurren. La única forma de estar preparado es invertir tiempo en testing: tests unitarios de las funciones de preprocesamiento, tests de integración del pipeline completo, y tests de carga para validar el rendimiento bajo presión.
El segundo es no versionar el modelo. Si el modelo en producción produce un resultado inesperado y no sabes exactamente qué versión del código y qué datos se usaron para entrenarlo, estás en un problema grave. El versionado de modelos no es opcional.
El tercero es desplegar directamente a producción. Una estrategia de despliegue gradual, como canary releases o A/B testing, permite validar el comportamiento del nuevo modelo con un subconjunto de tráfico real antes de comprometerse completamente. Si algo falla, el impacto es limitado y el rollback es inmediato.
El cuarto es olvidarse del caso de error. ¿Qué hace el sistema si el modelo falla? ¿Si la latencia supera el umbral aceptable? ¿Si los datos de entrada son inválidos? Un sistema robusto tiene respuestas explícitas a todas estas preguntas, generalmente en forma de valores por defecto, fallbacks a reglas heurísticas o mensajes de error informativos.
El quinto, y quizás el más común, es subestimar el tiempo que todo esto lleva. Llevar un modelo a producción correctamente cuesta más tiempo que entrenarlo. Eso no es un fracaso del proceso. Es simplemente la realidad del software en el mundo real.
Conclusión
La brecha entre el notebook y producción no es un problema técnico que se resuelve con la herramienta adecuada. Es un cambio de paradigma. El notebook es para descubrir. Producción es para entregar valor de forma sostenible, confiable y medible.
Los equipos que hacen bien esta transición no son necesariamente los que tienen las mejores herramientas. Son los que entienden que el modelo es solo una parte de un sistema más grande, que ese sistema debe ser diseñado con el mismo rigor que el modelo, y que la monitorización, el testing y la colaboración no son opcionales sino los pilares sobre los que se sostiene cualquier proyecto de ML a largo plazo.
Vale la pena recordar también que este trabajo nunca termina. Un modelo en producción no es un proyecto cerrado: es un sistema vivo que necesita atención continua. Los datos cambian, los usuarios cambian, el negocio cambia. Lo que funcionaba hace seis meses puede estar degradándose silenciosamente hoy. La diferencia entre un equipo de ML maduro y uno que no lo es suele medirse exactamente ahí: no en si saben entrenar un buen modelo, sino en si tienen los sistemas y la cultura para detectar cuándo ese modelo deja de serlo.
El objetivo final no es tener un modelo en producción. Es tener un sistema de ML que genere valor mañana, la semana que viene, y dentro de un año, aunque los datos cambien, el equipo rote y los requisitos evolucionen.
Y hasta aquí este nuevo artículo. Espero que te haya gustado, y de paso, te dejo algunos recursos y lecturas de interés por si quieres echarle un ojo.
Te espero en el siguiente artículo!! :)
Recursos de interés
Herramientas mencionadas en el artículo:
MLflow (tracking de experimentos y model registry): https://mlflow.org/docs/latest/
FastAPI (API REST para servir modelos): https://fastapi.tiangolo.com/
Evidently (monitorización y detección de drift): https://docs.evidentlyai.com/
scikit-learn Pipelines (preprocesamiento reproducible): https://scikit-learn.org/stable/modules/compose.html
Hydra (gestión de configuración): https://hydra.cc/docs/intro/
Docker (containerización): https://docs.docker.com/get-started/
Lecturas recomendadas:
"Model Cards for Model Reporting" — paper original de Google (Mitchell et al., 2019): https://arxiv.org/abs/1810.03993
Documentación oficial de scikit-learn sobre persistencia de modelos con joblib: https://scikit-learn.org/stable/model_persistence.html
