El mòdul 5 va deixar CineClick amb un sistema que es mou sol però que encara no mira: l'ombra s'activa a mà, i l'única manera de saber si el champion envelleix és que algú es recordi de comprovar-ho. Aquesta lliçó construeix els ulls del sistema. Veurem que un sistema de ML es vigila en dos plans diferents — el del servei (respon?) i el del model (encerta?) — i que el segon pot estar completament trencat mentre el primer llueix verd a tots els panells. Aprendràs a exposar mètriques del servei amb prometheus-client, a definir proxies operatius per vigilar el model quan encara no hi ha etiquetes, a persistir una taula de prediccions auditable i a tancar el cercle quan el ground truth per fi arriba.

Contingut

  1. Els dos plans del monitoratge
  2. Pla de servei: mètriques RED i /metrics
  3. Prometheus i Grafana en quatre pinzellades
  4. Pla de model sense etiquetes: proxies operatius
  5. La taula de prediccions: el log que es converteix en dada
  6. Quan arriben les etiquetes: mètriques reals
  7. Un dashboard, dos plans

Els dos plans del monitoratge

A la lliçó 01-02 vam fixar una idea que ara toca materialitzar: un sistema de ML té dues maneres independents de fallar.

  • Pla de servei: el sistema respon? Latència, taxa d'errors, saturació de recursos. És exactament el mateix que monitoraries a qualsevol microservei; res d'això no és específic de ML.
  • Pla de model: el sistema encerta? Li arriben dades semblants a les que va veure a l'entrenament? Això és exclusiu de ML, i és el pla que les eines d'infraestructura tradicionals no veuen.

La trampa és que són independents de debò:

Situació Pla servei Pla model Ho detecta un dashboard d'infraestructura?
Pod caigut, timeouts Vermell
p95 de 800 ms per un desplegament mal dimensionat Vermell Verd
L'equip de dades canvia la unitat de hores_setmana a minuts Verd Vermell No
Una promoció de màrqueting atreu una cohort de clients nova Verd Vermell (degradant-se) No

A les dues últimes files, l'API de CineClick retorna 200 OK en 38 ms, l'HPA està tranquil, Kubernetes reporta 3/3 rèpliques sanes… i el model està predient brossa. El servei "verd" amb el model trencat és el mode de fallada característic dels sistemes de ML, i la raó que aquest mòdul existeixi.

flowchart LR
    subgraph Pla de servei
        A["Respon?<br/>latència, errors, saturació"]
    end
    subgraph Pla de model
        B["Encerta?<br/>dades normals? prediccions normals?"]
    end
    A -->|"eines estàndard<br/>(Prometheus, Grafana)"| C[Dashboard]
    B -->|"taula de prediccions,<br/>proxies, ground truth"| C

Pla de servei: mètriques RED i /metrics

Per al pla de servei farem servir el patró RED, un estàndard de facto per a serveis de petició/resposta:

  • Rate: peticions per segon.
  • Errors: taxa de respostes amb error (4xx del client, 5xx nostres).
  • Duration: distribució de latències (p50, p95, p99 — no la mitjana, que amaga les cues).

La manera canònica d'exposar-les és un endpoint GET /metrics en format Prometheus. Afegim prometheus-client al servei FastAPI que vam construir a 04-02:

# src/cineclick_churn/api/metriques.py
from prometheus_client import Counter, Histogram

# Counter: valor que només creix. Prometheus en deriva taxes.
PREDICCIONS_TOTAL = Counter(
    "cineclick_prediccions_total",
    "Nombre de prediccions servides",
    ["resultat"],  # etiqueta: "risc" (p >= llindar) o "sense_risc"
)

# Histogram: reparteix observacions en cubetes (buckets) per calcular percentils.
# Les cubetes es trien al voltant del que ens importa: el pressupost de 300 ms.
LATENCIA_PREDICCIO = Histogram(
    "cineclick_latencia_prediccio_segons",
    "Latència de la predicció end-to-end",
    buckets=[0.01, 0.025, 0.05, 0.1, 0.2, 0.3, 0.5, 1.0],
)

Explicació peça a peça:

  • Un Counter només suma. No guardem "peticions per segon": guardem el total acumulat, i Prometheus en deriva la taxa amb rate(). Això el fa robust davant de reinicis i scrapes perduts.
  • L'etiqueta resultat ens dona de franc una mètrica de model dins del pla de servei: la taxa de positius (proporció de prediccions "risc"), que farem servir com a proxy més endavant.
  • Un Histogram no guarda cada latència individual, sinó quantes observacions han caigut a cada cubeta. Per això les cubetes han d'envoltar els valors crítics: amb el p95 real a 92 ms i el pressupost a 300 ms, posem cubetes fines per sota de 300 ms i gruixudes per sobre.

Instrumentem l'endpoint de 04-02 sense tocar-ne la lògica:

# src/cineclick_churn/api/enrutament.py (fragment)
import time
from .metriques import PREDICCIONS_TOTAL, LATENCIA_PREDICCIO

@router.post("/predir")
def predir(client: ClientEntrada):
    inici = time.perf_counter()
    probabilitat = model.predict_proba(construir_features(client))[0, 1]
    resultat = "risc" if probabilitat >= LLINDAR_ABANDONAMENT else "sense_risc"

    PREDICCIONS_TOTAL.labels(resultat=resultat).inc()
    LATENCIA_PREDICCIO.observe(time.perf_counter() - inici)
    # ... el logging JSON de 04-02 continua igual ...
    return PrediccioSortida(probabilitat=probabilitat, resultat=resultat)

I exposem l'endpoint a main.py:

# src/cineclick_churn/api/main.py (fragment)
from prometheus_client import make_asgi_app

app.mount("/metrics", make_asgi_app())

Amb això, curl http://localhost:8000/metrics retorna text pla amb els comptadors i les cubetes de l'histograma. No cal enviar res enlloc: és Prometheus qui ve a llegir.

Prometheus i Grafana en quatre pinzellades

No convertirem això en un tutorial de Prometheus (per a això hi ha l'operació de plataforma); només el mínim perquè el sistema quedi connectat. El model mental és pull: Prometheus visita el /metrics de cada pod cada pocs segons (scrape), emmagatzema les sèries temporals, i Grafana les dibuixa.

A Kubernetes, amb el Prometheus Operator que ja fa servir la plataforma de CineClick, n'hi ha prou de declarar què ha de scrapejar:

# 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òdul 4
  endpoints:
    - port: http
      path: /metrics
      interval: 15s           # freqüència de scrape

Les dues alertes del pla de servei surten directament de les mètriques de guàrdia que vam definir a 05-04 per als canaries — aquí les fem permanents per al 100% del trànsit:

# deploy/k8s/alertes-servei.yaml (fragment de PrometheusRule)
groups:
  - name: churn-api-servei
    rules:
      - alert: LatenciaP95Excedida
        # histogram_quantile calcula el percentil a partir de les cubetes
        expr: histogram_quantile(0.95,
                rate(cineclick_latencia_prediccio_segons_bucket[5m])) > 0.3
        for: 10m
        labels: {severity: warning, canal: alertes-ml}
      - alert: TaxaErrors5xx
        expr: (sum(rate(http_requests_total{status=~"5.."}[10m]))
               / sum(rate(http_requests_total[10m]))) > 0.01
        for: 10m
        labels: {severity: critical, canal: alertes-ml}

Lectura: si el p95 supera els 300 ms de pressupost durant 10 minuts, o els 5xx superen l'1% del trànsit, arriba un avís a #alertes-ml. Són exactament els mateixos llindars que disparaven el rollback automàtic del canary: la coherència entre "el que vigila un release" i "el que vigila el dia a dia" evita tenir dues veritats diferents.

Pla de model sense etiquetes: proxies operatius

Aquí comença l'específic de ML, i amb això el concepte clau d'aquesta lliçó: el retard del ground truth (ground truth delay). Per saber si una predicció de churn va encertar, cal esperar que el client es doni de baixa… o no. A CineClick això són setmanes. Avui no podem calcular el recall de les prediccions d'avui; només ho podrem fer quan el futur arribi.

Llavors el pla de model queda cec mentrestant? No: vigilem proxies operatius — senyals que no mesuren l'encert però que, si es mouen, indiquen que passa alguna cosa estranya abans que el ground truth ho confirmi:

Proxy Què vigila Senyal d'alarma a CineClick
Distribució de probabilitats emeses La "forma" del que opina el model L'histograma de scores canvia de forma d'una setmana a l'altra
Taxa de positius (p ≥ 0.5) El volum de clients marcats en risc Fora de la banda esperada 10%–25% (la mateixa banda de guàrdia de 05-04)
Volum per pla i metode_pagament La mescla d'entrada El pla basic passa del 40% al 70% de les peticions
% de valors atípics a les features La qualitat de la dada d'entrada hores_setmana > 100, antiguitat_mesos = 0 amb tiquets_suport = 50…

Fixa't en la lògica: cap d'aquests proxies no requereix saber qui es va donar de baixa. La taxa de positius ja la tenim a Prometheus gràcies a l'etiqueta resultat del counter. Els altres requereixen mirar les prediccions amb més detall — i per a això cal persistir-les.

Un aclariment d'abast: si un proxy es mou, quant s'ha mogut i és estadísticament rellevant? Aquesta pregunta — la deriva formal, amb PSI i tests — és la lliçó 06-02. Aquí només deixem els senyals encesos.

La taula de prediccions: el log que es converteix en dada

A 04-02 el servei ja escriu una línia JSON per predicció (hash_entrada, probabilitat, versio_model, latencia_ms, i des de 05-04 els camps d'ombra/canary). Fins ara aquell log només servia per depurar. El salt d'aquesta lliçó és tractar-lo com a dada de primera classe: la taula de prediccions és la matèria primera de tot el monitoratge de model, de l'anàlisi de deriva de 06-02 i de l'avaluació real que veurem a l'apartat següent.

On va a parar? Un col·lector (a CineClick, el mateix stack de logs de la plataforma) recull les línies JSON dels pods i les materialitza com a Parquet particionat per data — el mateix format que ja fa servir el batch setmanal:

magatzem/prediccions/online/data=2026-07-06/prediccions.parquet

Què ha de contenir cada fila perquè d'aquí a tres mesos puguem auditar i avaluar?

Camp Per a què serveix després
id_prediccio, timestamp Identificar i ordenar; la finestra temporal del join amb etiquetes
hash_entrada Detectar duplicats i auditar sense guardar el payload cru
features (les que van entrar al model, inclosa ratio_tickets) Reproduir la predicció; anàlisi de deriva per feature
probabilitat Distribució de scores, taxa de positius, avaluació real
versio_model, alias Atribuir cada predicció a un model concret (v2 o el challenger?)
probabilitat_ombra, versio_model_ombra Comparar champion vs. challenger offline
grup (canary/control/estable) No barrejar poblacions en avaluar
latencia_ms Correlacionar degradacions de servei amb canvis de model

Regla d'or: guarda les features tal com van entrar al model, no només l'input cru. Si demà features.py canvia, l'input cru ja no reprodueix el que el model va veure.

Quan arriben les etiquetes: mètriques reals

Setmanes després, el ground truth arriba: el sistema de facturació de CineClick sap quins clients es van donar de baixa. Ara podem respondre la pregunta que cap proxy no respon: quin recall i quina precision REALS està donant el champion en producció?

El mecanisme és un job (a CineClick, una task més dins de l'ecosistema Prefect que ja orquestra el scoring dels dilluns) que casa prediccions amb baixes:

# flows/avaluacio_real.py (fragment de la task principal)
import pandas as pd

def avaluar_setmana(prediccions: pd.DataFrame, baixes: pd.DataFrame,
                    finestra_dies: int = 30) -> dict:
    """Casa prediccions amb baixes reals i calcula mètriques de producció.

    prediccions: taula de prediccions d'UNA setmana (id_client, timestamp,
                 probabilitat, versio_model, grup).
    baixes: baixes confirmades (id_client, data_baixa).
    """
    df = prediccions.merge(baixes, on="id_client", how="left")

    # Etiqueta real: es va donar de baixa DINS de la finestra posterior a la
    # predicció. La finestra importa: una baixa 6 mesos després no valida la
    # predicció d'avui.
    df["abandonament_real"] = (
        (df["data_baixa"].notna())
        & (df["data_baixa"] > df["timestamp"])
        & (df["data_baixa"] <= df["timestamp"] + pd.Timedelta(days=finestra_dies))
    ).astype(int)

    df["prediccio"] = (df["probabilitat"] >= 0.5).astype(int)

    vp = int(((df.prediccio == 1) & (df.abandonament_real == 1)).sum())
    fp = int(((df.prediccio == 1) & (df.abandonament_real == 0)).sum())
    fn = int(((df.prediccio == 0) & (df.abandonament_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_prediccions": len(df),
        "n_baixes": int(df.abandonament_real.sum()),
    }

Tres detalls que fan o desfan aquest job:

  1. El join és per id I per finestra temporal. Sense la finestra, qualsevol baixa històrica "valida" qualsevol predicció i les mètriques surten inflades.
  2. Filtra per grup. Els clients del grup de control permanent (05-04) no reben el descompte de retenció: són l'única població on l'etiqueta no està contaminada per la nostra pròpia intervenció. Les mètriques "netes" es calculen sobre ells.
  3. Compara sempre amb la referència offline. El champion v2 prometia recall 0.68 i precision 0.55 al test set. Si producció dona 0.62/0.51 de manera estable, és la degradació normal del salt offline→online. Si el recall real cau setmana rere setmana, el model està envellint — i aquesta tendència és l'entrada de la política de reentrenament de 06-03.

El resultat de cada execució s'escriu com a sèrie temporal (recall_real, precision_real per setmana i per versio_model), a punt per pintar-se al costat de la resta.

Un dashboard, dos plans

L'error clàssic és tenir el dashboard de servei a Grafana i "allò del model" en un notebook que algú executa quan se'n recorda. CineClick munta un únic dashboard amb els dos plans, perquè els incidents reals creuen la frontera (un pic de latència coincideix amb un canvi de mescla d'entrada; una caiguda de taxa de positius coincideix amb un desplegament). Esbós de panells:

Fila Panells Font
Servei req/s, taxa 5xx, p50/p95/p99 vs. línia de 300 ms, rèpliques/HPA Prometheus
Model (proxies) histograma de probabilitats per dia, taxa de positius amb banda 10–25%, mescla per pla/mètode de pagament, % atípics Taula de prediccions
Model (ground truth) recall i precision reals per setmana vs. línies offline (0.68 / 0.55), amb anotacions de desplegaments Job d'avaluació real
Context versió de model activa (/versio), esdeveniments de release (ombra/canary) Registry + CD

La fila de context és la que converteix el dashboard en eina de diagnòstic: quan alguna cosa es torça, la primera pregunta sempre és "què va canviar i quan?".

Errors Comuns i Consells

  • Monitorar només el pla de servei. És l'error número u: l'equip de plataforma "ja monitora" l'API… i ningú no mira si el model encerta. Recorda la taula del principi: el mode de fallada típic de ML és servei verd, model vermell.
  • Fer servir la latència mitjana en lloc de percentils. Amb p50 38 ms i p95 92 ms, la mitjana dirà ~45 ms i amagarà que el 5% dels teus usuaris espera el doble. Alerta sobre p95/p99, sempre.
  • Loguejar el payload cru "per si de cas". A més del problema de privadesa (el reprendrem a 06-04), infla l'emmagatzematge. El hash_entrada + les features processades cobreixen auditoria i anàlisi.
  • No guardar versio_model a cada predicció. Quan conviuen champion, ombra i canary, una taula sense versió és inservible: no sabràs a qui atribuir cada encert o errada.
  • Casar prediccions amb etiquetes sense finestra temporal. Infla recall i precision de manera silenciosa. Defineix la finestra segons el negoci (en churn, l'horitzó de la predicció) i documenta-la.
  • Alertar de tot. Si #alertes-ml rep deu avisos al dia, deixarà de llegir-se. Comença amb poques alertes accionables (les dues de servei + taxa de positius fora de banda) i afegeix-ne només quan una absència t'hagi fet mal.

Exercicis

  1. Instrumentació del batch. El flow scoring-setmanal-churn (05-03) també fa prediccions, però no és un servei HTTP: Prometheus no pot scrapejar-lo de manera contínua. Quines mètriques del pla de model registraries a cada execució setmanal i on les persistiries perquè apareguin al mateix dashboard?

  2. Diagnòstic amb la taula de prediccions. Un dilluns observes: p95 = 85 ms, 5xx = 0%, taxa de positius = 4% (portava mesos entre el 12% i el 16%). L'equip de plataforma diu que "tot està verd". Enumera, en ordre, les tres primeres comprovacions que faries fent servir la taula de prediccions i l'endpoint /versio.

  3. Finestra d'avaluació. El job de ground truth fa servir finestra_dies=30. Marc proposa abaixar-la a 7 per "tenir mètriques reals abans". Què els passaria a recall_real i precision_real amb aquesta finestra i per què? Quin compromís proposaries?

Solucions

  1. A cada execució del flow: distribució de probabilitats del lot (percentils del score), taxa de positius sobre el total puntuat, nombre de files puntuades (una davallada indica problema aigües amunt), i % d'atípics per feature. Es persisteixen com a files a la mateixa taula de prediccions (el batch ja escriu data/prediccions_churn.parquet; n'hi ha prou d'afegir els mateixos camps que l'online: versió, timestamp, features) i/o s'empenyen a Prometheus via Pushgateway, que existeix precisament per a jobs efímers. Així el dashboard únic mostra online i batch junts, distingits per una etiqueta origen.

  2. Primer, GET /versio: ha canviat versio_model o alias recentment? Una promoció o un rollback accidental és la causa més ràpida de descartar. Segon, a la taula de prediccions, comparar l'histograma de probabilitats d'aquesta setmana amb el de les anteriors: si tota la distribució s'ha desplaçat cap avall, el model "opina diferent" — probable canvi a les dades d'entrada. Tercer, comparar la mescla d'entrada (distribució de pla, metode_pagament, i estadístics de hores_setmana, antiguitat_mesos, ratio_tickets) contra setmanes prèvies per localitzar quina feature s'ha mogut; un valor constant o nul delata una fallada del productor de dades. Nota: el servei està sa — el problema és de dades o de model, exactament l'escenari "verd per fora".

  3. Amb 7 dies, moltes baixes que la predicció va anticipar correctament caurien fora de la finestra i comptarien com a falsos positius: precision_real baixaria artificialment, i recall_real es calcularia sobre moltes menys baixes (només les molt immediates), i es tornaria sorollós i inestable. El compromís habitual: mantenir la finestra alineada amb l'horitzó de negoci (30 dies) per a les mètriques oficials, i publicar a més una mètrica "parcial a 7 dies" clarament etiquetada com a avanç provisional, útil com a senyal primerenc però mai com a criteri de decisió.

Conclusió

CineClick ja té ulls. El pla de servei queda cobert amb mètriques RED exposades a /metrics, scrapejades per Prometheus i alertades amb els mateixos llindars de guàrdia de 05-04 (p95 > 300 ms, 5xx > 1%). El pla de model — el que cap eina d'infraestructura no veu — queda cobert en dos temps: proxies operatius mentre el ground truth no arriba (distribució de scores, taxa de positius en banda 10–25%, mescla d'entrada, atípics) i mètriques reals quan arriba, gràcies al job que casa la taula de prediccions amb les baixes confirmades i compara el recall/precision de producció amb els 0.68/0.55 promesos offline. La peça central és aquesta taula de prediccions: el log JSON de 04-02 elevat a dada de primera classe, amb features, versió i grup a cada fila. Però fins ara només hem dit que els proxies "es mouen" — no quant ni amb quina significació. Posar números rigorosos a aquest moviment, distingir un dilluns estrany d'un canvi real a la població, i automatitzar aquesta vigilància amb Evidently és exactament el tema de la lliçó següent: deriva de dades i deriva de concepte.

© Copyright 2026. Tots els drets reservats