Machine Learning: Monitoramento em Produção e Drift de Modelo

Última atualização: 2026-08-26

Implantar um modelo é como um avião decolando — o verdadeiro desafio é monitorar o voo e lidar com o que vier pelo caminho.

1. O que você vai aprender


2. Uma história real de um engenheiro de operações

(1) O problema: depois do Double-11, toda previsão foi por água abaixo

O modelo SalesPredict do Bob estava rodando sem problemas, com MAPE estável em 8%. Mas depois da promoção do Double-11, o erro de previsão disparou para 35% — o comportamento dos usuários tinha mudado completamente (compras por impulso, mudanças nas preferências de categoria, ciclos de compra comprimidos), enquanto o modelo ainda fazia previsões baseadas em padrões pré-promoção. A degradação do modelo após a implantação é silenciosa — quando você percebe, o dano já está feito.

(2) A solução de monitoramento de drift

Monitore continuamente a distribuição dos dados de entrada (PSI) e a acurácia das previsões. No momento em que o drift for detectado, dispare o re-treinamento automatizado.

PYTHON
def check_drift(reference_data, current_data, threshold=0.2):
    psi = calculate_psi(reference_data, current_data)
    if psi > threshold:
        alert("Drift de dados detectado! PSI={:.3f}".format(psi))
        trigger_retraining()

(3) O resultado: drift detectado em 3 dias, corrigido em uma semana

Depois que Bob implantou o monitoramento de drift, a mudança de comportamento pós-Double-11 foi detectada em 3 dias (PSI=0,45). Em uma semana, o re-treinamento + validação A/B + rollout canário estavam completos, recuperando cerca de 300 mil USD em perdas de previsão.


3. O sistema de monitoramento de modelos

(1) Uma arquitetura de monitoramento em três camadas

100%
graph TB
    L1[Camada 1: Infraestrutura<br/>Latência / Vazão / Uptime] --> L2[Camada 2: Métricas de ML<br/>Distribuição de Previsões / Estatísticas de Features]
    L2 --> L3[Camada 3: Métricas de Negócio<br/>MAPE de Receita / Taxa de Conversão / Taxa de Churn]
    L3 --> ALERT{Anomalia Detectada?}
    ALERT -->|Sim| ACTION[Alerta → Investigar → Re-treinar]
    ALERT -->|Não| CONTINUE[Continuar Monitorando]

▶ Exemplo: Dados básicos de dashboard de monitoramento

PYTHON
import numpy as np
from datetime import datetime, timedelta

class ModelMonitor:
    def __init__(self):
        self.metrics_history = []

    def log_prediction(self, prediction, features, latency_ms):
        self.metrics_history.append({
            "timestamp": datetime.now(),
            "prediction": prediction,
            "features_mean": np.mean(features),
            "latency_ms": latency_ms,
        })

    def check_health(self, last_n=1000):
        if len(self.metrics_history) < last_n:
            return "Dados insuficientes"

        recent = self.metrics_history[-last_n:]
        predictions = [m["prediction"] for m in recent]
        latencies = [m["latency_ms"] for m in recent]

        report = {
            "media_previsoes": np.mean(predictions),
            "desvio_previsoes": np.std(predictions),
            "p95_latencia_ms": np.percentile(latencies, 95),
            "contagem_amostras": len(recent),
        }
        return report

monitor = ModelMonitor()
for _ in range(100):
    monitor.log_prediction(prediction=np.random.normal(200, 50),
                           features=np.random.randn(5),
                           latency_ms=np.random.uniform(10, 30))
print(monitor.check_health())

Saída:

TEXT 📖 Somente leitura
# Funções definidas com sucesso
Camada de Monitoramento Métrica Limite de Alerta Ferramenta
Infraestrutura Latência da API p95 > 100ms Prometheus
Infraestrutura Disponibilidade do serviço < 99,9% Grafana
Métricas de ML Mudança na distribuição de previsões PSI > 0,2 Personalizado
Métricas de ML Taxa de features ausentes > 5% Personalizado
Métricas de Negócio MAPE de previsão > 15% Relatórios de negócio
Métricas de Negócio Desvio de receita > 10% Relatórios de negócio

4. Detecção de drift de dados

(1) PSI (Population Stability Index)

▶ Exemplo: Calculando o PSI

PYTHON
import numpy as np

def calculate_psi(reference, current, n_bins=10):
    """Calcula o Population Stability Index.

    PSI < 0,1: Sem mudança significativa
    PSI 0,1-0,2: Mudança moderada, investigar
    PSI > 0,2: Mudança significativa, ação necessária
    """
    breakpoints = np.percentile(reference, np.linspace(0, 100, n_bins + 1))
    breakpoints[0] = -np.inf
    breakpoints[-1] = np.inf

    ref_counts = np.histogram(reference, bins=breakpoints)[0]
    cur_counts = np.histogram(current, bins=breakpoints)[0]

    ref_pct = ref_counts / len(reference)
    cur_pct = cur_counts / len(current)

    # Evitar log(0)
    ref_pct = np.clip(ref_pct, 1e-6, None)
    cur_pct = np.clip(cur_pct, 1e-6, None)

    psi = np.sum((cur_pct - ref_pct) * np.log(cur_pct / ref_pct))
    return psi

# Simular: dados normais vs. com drift
rng = np.random.default_rng(42)
reference = rng.normal(100, 20, 10000)

# Sem drift
current_no_drift = rng.normal(100, 20, 5000)
psi_no_drift = calculate_psi(reference, current_no_drift)

# Drift moderado (média deslocada em 10)
current_moderate = rng.normal(110, 20, 5000)
psi_moderate = calculate_psi(reference, current_moderate)

# Drift severo (média deslocada em 25, efeito Double-11)
current_severe = rng.normal(125, 30, 5000)
psi_severe = calculate_psi(reference, current_severe)

print(f"Sem drift:  PSI = {psi_no_drift:.3f} (OK)")
print(f"Moderado:   PSI = {psi_moderate:.3f} (Investigar)")
print(f"Severo:     PSI = {psi_severe:.3f} (Ação Necessária!)")

Saída:

TEXT 📖 Somente leitura
# Funções definidas com sucesso

(2) Teste KS e Divergência JS

▶ Exemplo: Comparando múltiplas métricas de drift

PYTHON
from scipy import stats
import numpy as np

def calculate_js_divergence(reference, current, n_bins=50):
    """Divergência de Jensen-Shannon (simétrica, limitada em [0, 1])."""
    bins = np.linspace(min(reference.min(), current.min()),
                       max(reference.max(), current.max()), n_bins + 1)
    p = np.histogram(reference, bins=bins, density=True)[0]
    q = np.histogram(current, bins=bins, density=True)[0]
    m = (p + q) / 2
    js = 0.5 * stats.entropy(p, m) + 0.5 * stats.entropy(q, m)
    return js

rng = np.random.default_rng(42)
ref = rng.normal(100, 20, 10000)
cur_drifted = rng.normal(115, 25, 5000)

# Teste KS
ks_stat, ks_pvalue = stats.ks_2samp(ref, cur_drifted)

# PSI
psi = calculate_psi(ref, cur_drifted)

# Divergência JS
js = calculate_js_divergence(ref, cur_drifted)

print(f"Estatística KS: {ks_stat:.4f}, valor-p: {ks_pvalue:.6f}")
print(f"PSI:           {psi:.4f}")
print(f"Divergência JS: {js:.4f}")

Saída:

TEXT 📖 Somente leitura
# Funções definidas com sucesso
Métrica Intervalo Limite Sem-Drift Limite de Drift Características
PSI [0, ∞) < 0,1 > 0,2 Padrão da indústria
Estatística KS [0, 1] < 0,05 > 0,1 Teste não paramétrico
Divergência JS [0, 1] < 0,05 > 0,1 Simétrica, interpretável
Divergência KL [0, ∞) < 0,1 > 0,2 Assimétrica

5. Drift de conceito e re-treinamento automatizado

(1) Drift de dados vs. Drift de conceito

Tipo Definição Método de Detecção Exemplo
Drift de dados P(X) muda — a distribuição de entrada muda PSI/KS nas features Gasto do usuário dobra durante o Double-11
Drift de conceito P(Y X) muda — a relação entrada-saída muda Aumento do erro de previsão
Drift de rótulo P(Y) muda — a distribuição de saída muda Monitoramento da distribuição de previsões Taxa de churn sobe de 8% para 15%

▶ Exemplo: Detecção de drift do Double-11 do Bob

PYTHON
import numpy as np

rng = np.random.default_rng(42)

# Período normal: relação gasto com anúncios → receita
n_normal = 10000
ad_spend_normal = rng.uniform(10, 100, n_normal)
revenue_normal = 50 + 0.8 * ad_spend_normal + rng.normal(0, 10, n_normal)

# Período Double-11: comportamento completamente diferente
n_promo = 5000
ad_spend_promo = rng.uniform(50, 200, n_promo)  # Gasto com anúncios mais alto
revenue_promo = 200 + 1.5 * ad_spend_promo + rng.normal(0, 30, n_promo)  # Coeficientes diferentes!

# Detectar drift na distribuição de gasto com anúncios
psi = calculate_psi(ad_spend_normal, ad_spend_promo)
ks_stat, ks_p = stats.ks_2samp(ad_spend_normal, ad_spend_promo)

print("=== Detecção de Drift de Dados ===")
print(f"PSI do gasto com anúncios: {psi:.3f} {'⚠️ DRIFT' if psi > 0.2 else 'OK'}")
print(f"KS do gasto com anúncios:  {ks_stat:.3f} (p={ks_p:.6f}) {'⚠️ DRIFT' if ks_p < 0.05 else 'OK'}")

# Detectar drift de conceito: mesmo X, relação Y|X diferente
# Usar um modelo treinado em dados normais para prever dados promo
from sklearn.linear_model import LinearRegression
model_normal = LinearRegression()
model_normal.fit(ad_spend_normal.reshape(-1, 1), revenue_normal)

pred_promo = model_normal.predict(ad_spend_promo.reshape(-1, 1))
mape_normal_on_normal = np.mean(np.abs((revenue_normal - model_normal.predict(ad_spend_normal.reshape(-1, 1))) / revenue_normal)) * 100
mape_normal_on_promo = np.mean(np.abs((revenue_promo - pred_promo) / revenue_promo)) * 100

print(f"\n=== Detecção de Drift de Conceito ===")
print(f"MAPE (normal→normal): {mape_normal_on_normal:.1f}%")
print(f"MAPE (normal→promo):  {mape_normal_on_promo:.1f}%")
print(f"Degradação de desempenho: {mape_normal_on_promo - mape_normal_on_normal:.1f}% ⚠️ DRIFT DE CONCEITO")

Saída:

TEXT 📖 Somente leitura
=== Detecção de Drift de Dados ===

(2) O pipeline automatizado de re-treinamento

▶ Exemplo: Framework de detecção de drift + re-treinamento automatizado

PYTHON
import numpy as np
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_absolute_percentage_error

class AutoRetrainPipeline:
    def __init__(self, drift_threshold_psi=0.2, mape_threshold=0.15):
        self.drift_threshold_psi = drift_threshold_psi
        self.mape_threshold = mape_threshold
        self.model = None
        self.reference_data = None

    def train(self, X, y):
        self.model = LinearRegression()
        self.model.fit(X, y)
        self.reference_data = X.copy()

    def check_drift(self, X_current):
        drifts = []
        for i in range(X_current.shape[1]):
            psi = calculate_psi(self.reference_data[:, i], X_current[:, i])
            drifts.append({"feature": f"feat_{i}", "psi": psi, "drift": psi > self.drift_threshold_psi})
        return drifts

    def check_performance(self, X, y):
        y_pred = self.model.predict(X)
        mape = mean_absolute_percentage_error(y, y_pred)
        return mape < self.mape_threshold, mape

    def auto_retrain_if_needed(self, X_current, y_current):
        # Passo 1: Verificar drift
        drifts = self.check_drift(X_current)
        any_drift = any(d["drift"] for d in drifts)

        # Passo 2: Verificar desempenho
        performance_ok, mape = self.check_performance(X_current, y_current)

        # Passo 3: Decisão
        if not any_drift and performance_ok:
            return "OK - Nenhum re-treinamento necessário"

        if any_drift and not performance_ok:
            # Re-treinar com dados recentes
            self.model.fit(X_current, y_current)
            self.reference_data = X_current.copy()
            new_mape = mean_absolute_percentage_error(y_current, self.model.predict(X_current))
            return f"RE-TREINADO - Drift de PSI detectado, MAPE {mape:.1%} → {new_mape:.1%}"

        if any_drift:
            return f"ALERTA DE DRIFT - Drift de PSI mas desempenho ainda OK (MAPE {mape:.1%})"

        return f"ALERTA DE DESEMPENHO - Sem drift mas MAPE degradou para {mape:.1%}"

# Teste
pipeline = AutoRetrainPipeline()
rng = np.random.default_rng(42)
X_train = rng.uniform(10, 100, (5000, 3))
y_train = 50 + 0.8 * X_train[:, 0] + rng.normal(0, 5, 5000)
pipeline.train(X_train, y_train)

# Simular dados do Double-11
X_promo = rng.uniform(50, 200, (2000, 3))
y_promo = 200 + 1.5 * X_promo[:, 0] + rng.normal(0, 10, 2000)

result = pipeline.auto_retrain_if_needed(X_promo, y_promo)
print(result)

Saída:

TEXT 📖 Somente leitura
# Funções definidas com sucesso

❓ Perguntas Frequentes

P: PSI > 0,2 significa sempre que precisamos re-treinar? R: Não necessariamente. O PSI só detecta mudança de distribuição, não degradação de desempenho. A distribuição pode mudar enquanto o modelo permanece acurado (por exemplo, mais usuários, mas os mesmos padrões). É melhor disparar o re-treinamento com uma condição dupla: PSI + MAPE.

P: Com que frequência devemos verificar drift? R: Depende do seu ritmo de negócio — verifique de hora em hora para serviços em tempo real, diariamente para jobs em lote, e semanalmente para previsões mensais. Sempre verifique imediatamente após eventos importantes (como o Double-11).

P: Que dados devemos usar para re-treinamento? R: Recomenda-se uma janela deslizante (os 3-6 meses mais recentes de dados), ou treinamento ponderado (dando peso maior aos dados recentes). Evite usar todos os dados históricos — dados mais antigos podem refletir um conceito desatualizado.

P: Qual é mais perigoso, drift de conceito ou drift de dados? R: Drift de conceito é mais perigoso — com drift de dados apenas a distribuição de entrada muda, então o modelo ainda pode estar acurado; com drift de conceito a relação entrada-saída muda, então o modelo está garantidamente errado. Dito isso, drift de dados é frequentemente um sinal de alerta precoce de drift de conceito.

P: Re-treinamento automatizado é arriscado? R: Sim — 1) os novos dados podem ter problemas de qualidade de rótulo; 2) re-treinamento automatizado pode introduzir novos bugs; 3) ele deve passar por validação A/B antes de ir para produção. Fluxo recomendado: re-treinamento automatizado + revisão humana + validação A/B, em três etapas.

P: Como distinguimos flutuação sazonal de drift real? R: Use comparação ano a ano (este Q4 vs. Q4 do ano passado) em vez de período a período (Q4 vs. Q3). Sazonalidade é periódica (se repete todo ano); drift é estrutural (uma mudança sustentada que não se reverte).


📖 Resumo


📝 Exercícios

  1. Básico (Dificuldade ⭐): Use PSI para detectar mudança de distribuição entre dois conjuntos de dados e compare os valores de PSI para o caso sem drift versus um deslocamento de média de 10. Dica: consulte a função de cálculo de PSI na Seção 4.
  2. Intermediário (Dificuldade ⭐⭐): Implemente monitoramento em três camadas — para dados de previsão simulados, detecte: 1) drift de PSI; 2) degradação de MAPE; 3) anomalias de latência (p95 > 100ms). Dica: três funções de verificação independentes + um relatório combinado.
  3. Desafio (Dificuldade ⭐⭐⭐): Implemente um AutoRetrainPipeline completo — gere dados do período normal + período de promoção, treine um modelo inicial, detecte drift e re-treine automaticamente, compare o MAPE antes e depois do re-treinamento e produza um relatório completo de monitoramento. Dica: consulte o framework de re-treinamento automatizado na Seção 5.

← Anterior: Teste A/B | Próximo: Design do Projeto →

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%