El módulo 5 dejó a CineClick con un sistema que se mueve solo pero que todavía no mira: la sombra se activa a mano, y la única forma de saber si el champion envejece es que alguien se acuerde de comprobarlo. Esta lección construye los ojos del sistema. Veremos que un sistema de ML se vigila en dos planos distintos — el del servicio (¿responde?) y el del modelo (¿acierta?) — y que el segundo puede estar completamente roto mientras el primero luce verde en todos los paneles. Aprenderás a exponer métricas del servicio con prometheus-client, a definir proxies operativos para vigilar el modelo cuando aún no hay etiquetas, a persistir una tabla de predicciones auditable y a cerrar el círculo cuando la verdad terreno por fin llega.
Contenido
- Los dos planos de la monitorización
- Plano de servicio: métricas RED y
/metrics - Prometheus y Grafana en una pincelada
- Plano de modelo sin etiquetas: proxies operativos
- La tabla de predicciones: el log que se convierte en dato
- Cuando llegan las etiquetas: métricas reales
- Un dashboard, dos planos
Los dos planos de la monitorización
En la lección 01-02 fijamos una idea que ahora toca materializar: un sistema de ML tiene dos formas independientes de fallar.
- Plano de servicio: ¿el sistema responde? Latencia, tasa de errores, saturación de recursos. Es exactamente lo mismo que monitorizarías en cualquier microservicio; nada aquí es específico de ML.
- Plano de modelo: ¿el sistema acierta? ¿Le están llegando datos parecidos a los que vio en entrenamiento? Esto es exclusivo de ML, y es el plano que las herramientas de infraestructura tradicionales no ven.
La trampa está en que son independientes de verdad:
| Situación | Plano servicio | Plano modelo | ¿Lo detecta un dashboard de infraestructura? |
|---|---|---|---|
| Pod caído, timeouts | Rojo | — | Sí |
| p95 de 800 ms por un despliegue mal dimensionado | Rojo | Verde | Sí |
El equipo de datos cambia la unidad de horas_semana a minutos |
Verde | Rojo | No |
| Promoción de marketing atrae una cohorte de clientes nueva | Verde | Rojo (degradándose) | No |
En las dos últimas filas, la API de CineClick devuelve 200 OK en 38 ms, el HPA está tranquilo, Kubernetes reporta 3/3 réplicas sanas… y el modelo está prediciendo basura. El servicio "verde" con el modelo roto es el modo de fallo característico de los sistemas de ML, y la razón de que este módulo exista.
flowchart LR
subgraph Plano de servicio
A["¿Responde?<br/>latencia, errores, saturación"]
end
subgraph Plano de modelo
B["¿Acierta?<br/>¿datos normales? ¿predicciones normales?"]
end
A -->|"herramientas estándar<br/>(Prometheus, Grafana)"| C[Dashboard]
B -->|"tabla de predicciones,<br/>proxies, verdad terreno"| CPlano de servicio: métricas RED y /metrics
Para el plano de servicio usaremos el patrón RED, un estándar de facto para servicios de petición/respuesta:
- Rate: peticiones por segundo.
- Errors: tasa de respuestas con error (4xx del cliente, 5xx nuestras).
- Duration: distribución de latencias (p50, p95, p99 — no la media, que esconde las colas).
La forma canónica de exponerlas es un endpoint GET /metrics en formato Prometheus. Añadimos prometheus-client al servicio FastAPI que construimos en 04-02:
# src/cineclick_churn/api/metricas.py
from prometheus_client import Counter, Histogram
# Counter: valor que solo crece. Prometheus calcula tasas a partir de él.
PREDICCIONES_TOTAL = Counter(
"cineclick_predicciones_total",
"Número de predicciones servidas",
["resultado"], # etiqueta: "riesgo" (p >= umbral) o "sin_riesgo"
)
# Histogram: reparte observaciones en cubetas (buckets) para calcular percentiles.
# Los buckets se eligen alrededor de lo que nos importa: el presupuesto de 300 ms.
LATENCIA_PREDICCION = Histogram(
"cineclick_latencia_prediccion_segundos",
"Latencia de la predicción end-to-end",
buckets=[0.01, 0.025, 0.05, 0.1, 0.2, 0.3, 0.5, 1.0],
)Explicación pieza a pieza:
- Un
Countersolo suma. No guardamos "peticiones por segundo": guardamos el total acumulado, y Prometheus deriva la tasa conrate(). Esto lo hace robusto ante reinicios y scrapes perdidos. - La etiqueta
resultadonos da gratis una métrica de modelo dentro del plano de servicio: la tasa de positivos (proporción de predicciones "riesgo"), que usaremos como proxy más adelante. - Un
Histogramno guarda cada latencia individual, sino cuántas observaciones cayeron en cada cubeta. Por eso los buckets deben rodear los valores críticos: con el p95 real en 92 ms y el presupuesto en 300 ms, ponemos cubetas finas por debajo de 300 ms y gruesas por encima.
Instrumentamos el endpoint de 04-02 sin tocar su lógica:
# src/cineclick_churn/api/enrutado.py (fragmento)
import time
from .metricas import PREDICCIONES_TOTAL, LATENCIA_PREDICCION
@router.post("/predecir")
def predecir(cliente: ClienteEntrada):
inicio = time.perf_counter()
probabilidad = modelo.predict_proba(construir_features(cliente))[0, 1]
resultado = "riesgo" if probabilidad >= UMBRAL_ABANDONO else "sin_riesgo"
PREDICCIONES_TOTAL.labels(resultado=resultado).inc()
LATENCIA_PREDICCION.observe(time.perf_counter() - inicio)
# ... el logging JSON de 04-02 sigue igual ...
return RespuestaPrediccion(probabilidad=probabilidad, resultado=resultado)Y exponemos el endpoint en main.py:
# src/cineclick_churn/api/main.py (fragmento)
from prometheus_client import make_asgi_app
app.mount("/metrics", make_asgi_app())Con esto, curl http://localhost:8000/metrics devuelve texto plano con los contadores y las cubetas del histograma. No hay que enviar nada a ningún sitio: es Prometheus quien viene a leer.
Prometheus y Grafana en una pincelada
No vamos a convertir esto en un tutorial de Prometheus (para eso está la operación de plataforma); solo lo justo para que el sistema quede conectado. El modelo mental es pull: Prometheus visita /metrics de cada pod cada pocos segundos (scrape), almacena las series temporales, y Grafana las dibuja.
En Kubernetes, con el Prometheus Operator que ya usa la plataforma de CineClick, basta con declarar qué debe scrapear:
# deploy/k8s/servicemonitor.yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: churn-api
namespace: cineclick
spec:
selector:
matchLabels:
app: churn-api # el Service del módulo 4
endpoints:
- port: http
path: /metrics
interval: 15s # frecuencia de scrapeLas dos alertas del plano de servicio salen directamente de las métricas de guardia que definimos en 05-04 para los canaries — aquí las hacemos permanentes para el 100% del tráfico:
# deploy/k8s/alertas-servicio.yaml (fragmento de PrometheusRule)
groups:
- name: churn-api-servicio
rules:
- alert: LatenciaP95Excedida
# histogram_quantile calcula el percentil a partir de las cubetas
expr: histogram_quantile(0.95,
rate(cineclick_latencia_prediccion_segundos_bucket[5m])) > 0.3
for: 10m
labels: {severity: warning, canal: alertas-ml}
- alert: TasaErrores5xx
expr: (sum(rate(http_requests_total{status=~"5.."}[10m]))
/ sum(rate(http_requests_total[10m]))) > 0.01
for: 10m
labels: {severity: critical, canal: alertas-ml}Lectura: si el p95 supera los 300 ms de presupuesto durante 10 minutos, o los 5xx superan el 1% del tráfico, llega un aviso a #alertas-ml. Son exactamente los mismos umbrales que disparaban el rollback automático del canary: la coherencia entre "lo que vigila un release" y "lo que vigila el día a día" evita tener dos verdades distintas.
Plano de modelo sin etiquetas: proxies operativos
Aquí empieza lo específico de ML, y con ello el concepto clave de esta lección: el retraso de la verdad terreno (ground truth delay). Para saber si una predicción de churn acertó, hay que esperar a que el cliente se dé de baja… o no. En CineClick eso son semanas. Hoy no podemos calcular el recall de las predicciones de hoy; solo podremos hacerlo cuando el futuro llegue.
¿Entonces el plano de modelo queda ciego mientras tanto? No: vigilamos proxies operativos — señales que no miden el acierto pero que, si se mueven, indican que algo raro pasa antes de que la verdad terreno lo confirme:
| Proxy | Qué vigila | Señal de alarma en CineClick |
|---|---|---|
| Distribución de probabilidades emitidas | La "forma" de lo que opina el modelo | El histograma de scores cambia de forma de una semana a otra |
| Tasa de positivos (p ≥ 0.5) | El volumen de clientes marcados en riesgo | Fuera de la banda esperada 10%–25% (la misma banda de guardia de 05-04) |
Volumen por plan y metodo_pago |
La mezcla de entrada | El plan basico pasa del 40% al 70% de las peticiones |
| % de valores atípicos en features | La calidad del dato de entrada | horas_semana > 100, antiguedad_meses = 0 con tickets_soporte = 50… |
Fíjate en la lógica: ninguno de estos proxies requiere saber quién se dio de baja. La tasa de positivos ya la tenemos en Prometheus gracias a la etiqueta resultado del counter. Los demás requieren mirar las predicciones con más detalle — y para eso necesitamos persistirlas.
Una aclaración de alcance: si un proxy se mueve, ¿cuánto se ha movido y es estadísticamente relevante? Esa pregunta — la deriva formal, con PSI y tests — es la lección 06-02. Aquí solo dejamos las señales encendidas.
La tabla de predicciones: el log que se convierte en dato
En 04-02 el servicio ya escribe una línea JSON por predicción (hash_entrada, probabilidad, version_modelo, latencia_ms, y desde 05-04 los campos de sombra/canary). Hasta ahora ese log solo servía para depurar. El salto de esta lección es tratarlo como dato de primera clase: la tabla de predicciones es la materia prima de toda la monitorización de modelo, del análisis de deriva de 06-02 y de la evaluación real que veremos en el apartado siguiente.
¿A dónde va? Un colector (en CineClick, el propio stack de logs de la plataforma) recoge las líneas JSON de los pods y las materializa como Parquet particionado por fecha — el mismo formato que ya usa el batch semanal:
¿Qué debe contener cada fila para que dentro de tres meses podamos auditar y evaluar?
| Campo | Para qué sirve después |
|---|---|
id_prediccion, timestamp |
Identificar y ordenar; la ventana temporal del join con etiquetas |
hash_entrada |
Detectar duplicados y auditar sin guardar el payload crudo |
features (las que entraron al modelo, incluida ratio_tickets) |
Reproducir la predicción; análisis de deriva por feature |
probabilidad |
Distribución de scores, tasa de positivos, evaluación real |
version_modelo, alias |
Atribuir cada predicción a un modelo concreto (¿v2 o el challenger?) |
probabilidad_sombra, version_modelo_sombra |
Comparar champion vs. challenger offline |
grupo (canary/control/estable) |
No mezclar poblaciones al evaluar |
latencia_ms |
Correlar degradaciones de servicio con cambios de modelo |
Regla de oro: guarda las features tal y como entraron al modelo, no solo el input crudo. Si mañana features.py cambia, el input crudo ya no reproduce lo que el modelo vio.
Cuando llegan las etiquetas: métricas reales
Semanas después, la verdad terreno llega: el sistema de facturación de CineClick sabe qué clientes se dieron de baja. Ahora podemos responder la pregunta que ningún proxy responde: ¿qué recall y precision REALES está dando el champion en producción?
El mecanismo es un job (en CineClick, una task más dentro del ecosistema Prefect que ya orquesta el scoring de los lunes) que casa predicciones con bajas:
# flows/evaluacion_real.py (fragmento de la task principal)
import pandas as pd
def evaluar_semana(predicciones: pd.DataFrame, bajas: pd.DataFrame,
ventana_dias: int = 30) -> dict:
"""Casa predicciones con bajas reales y calcula métricas de producción.
predicciones: tabla de predicciones de UNA semana (id_cliente, timestamp,
probabilidad, version_modelo, grupo).
bajas: bajas confirmadas (id_cliente, fecha_baja).
"""
df = predicciones.merge(bajas, on="id_cliente", how="left")
# Etiqueta real: se dio de baja DENTRO de la ventana tras la predicción.
# La ventana importa: una baja 6 meses después no valida la predicción de hoy.
df["abandono_real"] = (
(df["fecha_baja"].notna())
& (df["fecha_baja"] > df["timestamp"])
& (df["fecha_baja"] <= df["timestamp"] + pd.Timedelta(days=ventana_dias))
).astype(int)
df["prediccion"] = (df["probabilidad"] >= 0.5).astype(int)
vp = int(((df.prediccion == 1) & (df.abandono_real == 1)).sum())
fp = int(((df.prediccion == 1) & (df.abandono_real == 0)).sum())
fn = int(((df.prediccion == 0) & (df.abandono_real == 1)).sum())
return {
"recall_real": vp / (vp + fn) if (vp + fn) else None,
"precision_real": vp / (vp + fp) if (vp + fp) else None,
"n_predicciones": len(df),
"n_bajas": int(df.abandono_real.sum()),
}Tres detalles que hacen o rompen este job:
- El join es por id Y por ventana temporal. Sin la ventana, cualquier baja histórica "valida" cualquier predicción y las métricas salen infladas.
- Filtra por
grupo. Los clientes del grupo de control permanente (05-04) no reciben el descuento de retención: son la única población donde la etiqueta no está contaminada por nuestra propia intervención. Las métricas "limpias" se calculan sobre ellos. - Compara siempre con la referencia offline. El champion v2 prometía recall 0.68 y precision 0.55 en el test set. Si producción da 0.62/0.51 de forma estable, es la degradación normal del salto offline→online. Si el recall real cae semana tras semana, el modelo está envejeciendo — y esa tendencia es la entrada de la política de reentrenamiento de 06-03.
El resultado de cada ejecución se escribe como serie temporal (recall_real, precision_real por semana y por version_modelo), lista para pintarse junto al resto.
Un dashboard, dos planos
El error clásico es tener el dashboard de servicio en Grafana y "lo del modelo" en un notebook que alguien ejecuta cuando se acuerda. CineClick monta un único dashboard con los dos planos, porque los incidentes reales cruzan la frontera (un pico de latencia coincide con un cambio de mezcla de entrada; una caída de tasa de positivos coincide con un despliegue). Esbozo de paneles:
| Fila | Paneles | Fuente |
|---|---|---|
| Servicio | req/s, tasa 5xx, p50/p95/p99 vs. línea de 300 ms, réplicas/HPA | Prometheus |
| Modelo (proxies) | histograma de probabilidades por día, tasa de positivos con banda 10–25%, mezcla por plan/método de pago, % atípicos | Tabla de predicciones |
| Modelo (verdad terreno) | recall y precision reales por semana vs. líneas offline (0.68 / 0.55), con anotaciones de despliegues | Job de evaluación real |
| Contexto | versión de modelo activa (/version), eventos de release (sombra/canary) |
Registry + CD |
La fila de contexto es la que convierte el dashboard en herramienta de diagnóstico: cuando algo se tuerce, la primera pregunta siempre es "¿qué cambió y cuándo?".
Errores Comunes y Consejos
- Monitorizar solo el plano de servicio. Es el error número uno: el equipo de plataforma "ya monitoriza" la API… y nadie mira si el modelo acierta. Recuerda la tabla del principio: el modo de fallo típico de ML es servicio verde, modelo rojo.
- Usar la latencia media en lugar de percentiles. Con p50 38 ms y p95 92 ms, la media dirá ~45 ms y esconderá que el 5% de tus usuarios espera el doble. Alerta sobre p95/p99, siempre.
- Loguear el payload crudo "por si acaso". Además del problema de privacidad (lo retomamos en 06-04), infla el almacenamiento. El
hash_entrada+ las features procesadas cubren auditoría y análisis. - No guardar
version_modeloen cada predicción. Cuando conviven champion, sombra y canary, una tabla sin versión es inservible: no sabrás a quién atribuir cada acierto o fallo. - Casar predicciones con etiquetas sin ventana temporal. Infla recall y precision de forma silenciosa. Define la ventana según el negocio (en churn, el horizonte de la predicción) y documéntala.
- Alertar de todo. Si
#alertas-mlrecibe diez avisos al día, dejará de leerse. Empieza con pocas alertas accionables (las dos de servicio + tasa de positivos fuera de banda) y añade solo cuando una ausencia te haya dolido.
Ejercicios
-
Instrumentación del batch. El flow
scoring-semanal-churn(05-03) también hace predicciones, pero no es un servicio HTTP: Prometheus no puede scrapearlo de forma continua. ¿Qué métricas del plano de modelo registrarías en cada ejecución semanal y dónde las persistirías para que aparezcan en el mismo dashboard? -
Diagnóstico con la tabla de predicciones. Un lunes observas: p95 = 85 ms, 5xx = 0%, tasa de positivos = 4% (llevaba meses entre 12% y 16%). El equipo de plataforma dice que "todo está verde". Enumera, en orden, las tres primeras comprobaciones que harías usando la tabla de predicciones y el endpoint
/version. -
Ventana de evaluación. El job de verdad terreno usa
ventana_dias=30. Marc propone bajarla a 7 para "tener métricas reales antes". ¿Qué les pasaría arecall_realyprecision_realcon esa ventana y por qué? ¿Qué compromiso propondrías?
Soluciones
-
En cada ejecución del flow: distribución de probabilidades del lote (percentiles del score), tasa de positivos sobre el total puntuado, número de filas puntuadas (un desplome indica problema upstream), y % de atípicos por feature. Se persisten como filas en la misma tabla de predicciones (el batch ya escribe
data/predicciones_churn.parquet; basta añadir los mismos campos que el online: versión, timestamp, features) y/o se empujan a Prometheus vía Pushgateway, que existe precisamente para jobs efímeros. Así el dashboard único muestra online y batch juntos, distinguidos por una etiquetaorigen. -
Primero,
GET /version: ¿cambióversion_modelooaliasrecientemente? Una promoción o rollback accidental es la causa más rápida de descartar. Segundo, en la tabla de predicciones, comparar el histograma de probabilidades de esta semana con el de las anteriores: si toda la distribución se desplazó hacia abajo, el modelo "opina distinto" — probable cambio en los datos de entrada. Tercero, comparar la mezcla de entrada (distribución deplan,metodo_pago, y estadísticos dehoras_semana,antiguedad_meses,ratio_tickets) contra semanas previas para localizar qué feature se movió; un valor constante o nulo delata un fallo del productor de datos. Nota: el servicio está sano — el problema es de datos o de modelo, exactamente el escenario "verde por fuera". -
Con 7 días, muchas bajas que la predicción anticipó correctamente caerían fuera de la ventana y contarían como falsos positivos:
precision_realbajaría artificialmente, yrecall_realse calcularía sobre muchas menos bajas (solo las muy inmediatas), volviéndose ruidoso e inestable. El compromiso habitual: mantener la ventana alineada con el horizonte de negocio (30 días) para las métricas oficiales, y publicar además una métrica "parcial a 7 días" claramente etiquetada como adelanto provisional, útil como señal temprana pero nunca como criterio de decisión.
Conclusión
CineClick ya tiene ojos. El plano de servicio queda cubierto con métricas RED expuestas en /metrics, scrapeadas por Prometheus y alertadas con los mismos umbrales de guardia de 05-04 (p95 > 300 ms, 5xx > 1%). El plano de modelo — el que ninguna herramienta de infraestructura ve — queda cubierto en dos tiempos: proxies operativos mientras la verdad terreno no llega (distribución de scores, tasa de positivos en banda 10–25%, mezcla de entrada, atípicos) y métricas reales cuando llega, gracias al job que casa la tabla de predicciones con las bajas confirmadas y compara el recall/precision de producción con los 0.68/0.55 prometidos offline. La pieza central es esa tabla de predicciones: el log JSON de 04-02 elevado a dato de primera clase, con features, versión y grupo en cada fila. Pero hasta ahora solo hemos dicho que los proxies "se mueven" — no cuánto ni con qué significancia. Poner números rigurosos a ese movimiento, distinguir un lunes raro de un cambio real en la población, y automatizar esa vigilancia con Evidently es exactamente el tema de la siguiente lección: deriva de datos y deriva de concepto.
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
