← Blog

Prometheus, Grafana e drift detection em modelos em produção

2025-01-20MLOpsPrometheusGrafana

Por que monitorar modelos em produção

Deployar um modelo não é o fim — é o começo da fase em que ele enfrenta dados reais. Distribuições mudam: renda média dos clientes sobe, padrões de consumo se alteram, novos produtos entram no catálogo. Um modelo treinado em 2023 pode degradar silenciosamente em 2025 sem nenhum erro de código.

Monitoramento de ML cobre duas camadas:

  1. Operacional — latência, throughput, taxa de erro da API de predição
  2. De modelo — drift de features, degradação de métricas de negócio, distribuição de scores

Sem a segunda camada, você descobre o problema quando o churn sobe ou a inadimplência estoura — não quando o drift começou. Prometheus e Grafana são a stack padrão para coletar métricas e visualizar alertas em tempo real.

O que medir

Antes de configurar ferramentas, defina o que coletar:

| Categoria | Métricas | Por quê | |-----------|----------|---------| | Operacional | prediction_latency_seconds, predictions_total, errors_total | SLA e saúde da API | | Features | média, std, percentis por feature | detectar drift de entrada | | Modelo | distribuição de scores, taxa de classe positiva | detectar drift de saída | | Negócio | AUC rolling, KS, bad rate | conectar ML ao impacto real |

A regra prática: logue inputs e outputs de cada predição (ou amostra representativa) em um pipeline de métricas. Sem isso, drift detection é impossível.

Instrumentando a API com Prometheus

Prometheus coleta métricas via pull HTTP. Em uma API FastAPI de scoring:

from prometheus_client import Counter, Histogram, Gauge, generate_latest
from fastapi import FastAPI, Response
import time

app = FastAPI()

PREDICTIONS = Counter(
    "predictions_total", "Total predictions", ["model_version", "outcome"]
)
LATENCY = Histogram(
    "prediction_latency_seconds", "Prediction latency",
    buckets=[0.01, 0.05, 0.1, 0.25, 0.5, 1.0]
)
FEATURE_MEAN = Gauge(
    "feature_mean", "Rolling mean of input features", ["feature"]
)

@app.post("/predict")
async def predict(data: dict):
    start = time.time()

    features = extract_features(data)
    score = model.predict_proba([features])[0, 1]
    outcome = "high_risk" if score > 0.5 else "low_risk"

    for name, value in zip(FEATURE_NAMES, features):
        FEATURE_MEAN.labels(feature=name).set(value)

    PREDICTIONS.labels(model_version="v3", outcome=outcome).inc()
    LATENCY.observe(time.time() - start)

    return {"score": score}

@app.get("/metrics")
async def metrics():
    return Response(generate_latest(), media_type="text/plain")

O endpoint /metrics é o que o Prometheus scrapeia. Em produção, proteja com network policy — métricas não devem ser públicas.

Configuração do Prometheus (prometheus.yml):

scrape_configs:
  - job_name: "credit-score-api"
    scrape_interval: 15s
    static_configs:
      - targets: ["api:8000"]
    metrics_path: /metrics

Detecção de drift

Drift é a mudança na distribuição estatística dos dados entre treino (baseline) e produção (atual). O tipo mais comum em produção é covariate drift — as features de entrada mudam, mas o conceito (relação feature → target) pode ainda valer.

Detecção de drift
0.15
income_meanScore de drift: 0.00
Baseline (treino)Produção (atual)
credit_utilScore de drift: 0.00
Baseline (treino)Produção (atual)
tx_count_30dScore de drift: 0.00
Baseline (treino)Produção (atual)
age_meanScore de drift: 0.00
Baseline (treino)Produção (atual)
Distribuição estável

A demo acima ilustra o princípio: compare estatísticas de produção com o baseline de treino. Se a diferença ultrapassa um threshold, dispare alerta.

Em código, calcule o drift score por feature:

import numpy as np
from scipy import stats

def population_stability_index(expected, actual, buckets=10):
    """PSI — métrica clássica de drift em crédito."""
    breakpoints = np.percentile(expected, np.linspace(0, 100, buckets + 1))
    breakpoints[0], breakpoints[-1] = -np.inf, np.inf

    expected_pct = np.histogram(expected, breakpoints)[0] / len(expected)
    actual_pct = np.histogram(actual, breakpoints)[0] / len(actual)

    expected_pct = np.clip(expected_pct, 0.001, None)
    actual_pct = np.clip(actual_pct, 0.001, None)

    return np.sum((actual_pct - expected_pct) * np.log(actual_pct / expected_pct))

# PSI < 0.1: estável | 0.1–0.25: atenção | > 0.25: drift significativo
psi = population_stability_index(baseline_income, production_income)

PSI (Population Stability Index) é amplamente usado em score de crédito. Alternativas incluem KS test (stats.ks_2samp) e distância de Wasserstein para distribuições contínuas.

Dashboards no Grafana

Grafana conecta ao Prometheus como data source e renderiza dashboards. Painéis essenciais para ML:

# Latência p95
histogram_quantile(0.95, rate(prediction_latency_seconds_bucket[5m]))

# Taxa de predições por minuto
rate(predictions_total[5m]) * 60

# Proporção de high_risk
sum(rate(predictions_total{outcome="high_risk"}[1h]))
/
sum(rate(predictions_total[1h]))

Configure alertas no Grafana ou Alertmanager:

# alertmanager: drift detectado
- alert: FeatureDriftHigh
  expr: abs(feature_mean{feature="credit_util"} - 0.45) > 0.15
  for: 30m
  labels:
    severity: warning
  annotations:
    summary: "Drift detected in credit_util feature"

Alertas devem ter for: 30m ou mais — evitar ruído de flutuações pontuais.

Pipeline de monitoramento completo

A arquitetura típica:

API de scoring → /metrics → Prometheus → Grafana (dashboards + alertas)
                    ↓
              batch job diário → compara baseline vs produção → PSI/KS → alerta se drift

O batch job roda fora da API — compara distribuições do dia anterior com o baseline de treino e envia métricas customizadas para o Prometheus ou alerta direto no Slack/PagerDuty.

# scripts/daily_drift_check.py
def run_drift_check():
    baseline = load_baseline_stats()      # salvo no treino
    production = load_production_stats()  # últimas 24h do data lake

    for feature in FEATURE_NAMES:
        psi = population_stability_index(
            baseline[feature], production[feature]
        )
        DRIFT_SCORE.labels(feature=feature).set(psi)

        if psi > 0.25:
            send_alert(f"Drift on {feature}: PSI={psi:.3f}")

Além do básico: pontos de atenção

Armadilhas comuns em monitoramento de ML:

  • Monitorar só latência — API saudável com modelo degradado é o cenário mais perigoso
  • Baseline desatualizado — atualize o baseline após cada retreino aprovado
  • Amostragem insuficiente — drift detection precisa de volume; com 10 predições/dia, espere falsos positivos
  • Alertas sem runbook — todo alerta precisa de ação definida: investigar, retreinar ou rollback
  • Logs com PII — features podem conter dados sensíveis; agregue métricas, não logue inputs brutos em texto

Boas práticas:

  • Congelar estatísticas do baseline (mean, std, percentis) como artefato do treino
  • Revisar dashboards semanalmente mesmo sem alertas — drift gradual é silencioso
  • Conectar métricas de negócio (bad rate, conversão) ao mesmo dashboard operacional

Conclusão

Monitoramento de ML em produção exige duas camadas: operacional (latência, erros) e de modelo (drift, distribuição de scores). Prometheus coleta, Grafana visualiza e alerta, e métricas como PSI quantificam drift de features.

Configure /metrics na API, dashboards com latência e taxa de outcomes, e um job diário de drift check contra o baseline de treino. O próximo passo é garantir que os dados que alimentam o modelo chegam limpos — pipelines de dados na AWS.

Referencias

  1. Prometheus — Documentação
  2. Prometheus — Python client
  3. Grafana — Documentação
  4. Grafana — Prometheus data source
  5. Evidently AI — Data drift concepts
  6. Scipy — ks_2samp
  7. Nubank — Monitoring ML models in production