Você aprendeu as peças. Esta aula as monta. Um projeto de ML de verdade não é uma sequência de células soltas de notebook — é um fluxo disciplinado que vai do dado bruto ao modelo servindo predições em produção, monitorado e auditável. Vamos construir esse fluxo de ponta a ponta em um único caso (previsão de churn de clientes), passando deliberadamente por cada módulo do curso, e depois cruzar a fronteira que a maioria dos cursos ignora: deploy e MLOps.

Integrar avaliação, engenharia de features, seleção, tuning, ensembles e explicabilidade em um pipeline único e sem vazamento; empacotar o modelo; servi-lo via API; e estabelecer o mínimo de MLOps (versionamento, monitoramento de drift, retreino) que separa um protótipo de um sistema real.

O caso e a disciplina do fluxo

Problema: prever quais clientes vão cancelar (churn) nos próximos 30 dias, para uma ação de retenção. É um problema desbalanceado (poucos cancelam), com custo assimétrico (perder um cliente custa mais que uma ligação de retenção) e que exige explicação (a equipe de negócio quer saber por quê).

A ordem correta das etapas — e onde cada módulo entra:

1. Definir alvo e métrica de negócio ........... Módulos 1, 2
2. Split honesto (temporal, se houver tempo) ... Módulo 1
3. Pipeline de features (sem vazamento) ........ Módulos 3, 4
4. Baseline forte .............................. Módulos 6, 7
5. Tuning com nested CV ........................ Módulo 5
6. Ensemble final .............................. Módulos 6, 7
7. Calibração + threshold por custo ............ Módulo 2
8. Explicabilidade ............................. Módulo 8
9. Empacotar, servir, monitorar ............... esta aula

A regra de ouro que atravessa tudo: o conjunto de teste só é tocado uma vez, no fim. Toda decisão (features, hiperparâmetros, threshold) sai da validação.

Split honesto primeiro

Antes de qualquer análise, separe o teste — idealmente respeitando o tempo, para simular produção.

import pandas as pd
from sklearn.model_selection import train_test_split

# df tem uma coluna de data de referência
df = df.sort_values("data_ref")

# Split temporal: treino no passado, teste no futuro (evita vazamento temporal)
corte = df["data_ref"].quantile(0.8)
treino_full = df[df["data_ref"] <= corte].copy()
teste       = df[df["data_ref"] >  corte].copy()

X_cols = [c for c in df.columns if c not in ["churn", "data_ref", "cliente_id"]]
X_tr, y_tr = treino_full[X_cols], treino_full["churn"]
X_te, y_te = teste[X_cols], teste["churn"]

print(f"Treino: {len(X_tr)} | Teste: {len(X_te)} | churn treino: {y_tr.mean():.1%}")

Pipeline de features sem vazamento (Módulos 3 e 4)

Tudo que aprende parâmetros vai dentro do pipeline, para ser reajustado por fold.

from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, TargetEncoder
from sklearn.impute import SimpleImputer
from sklearn.feature_selection import SelectKBest, mutual_info_classif

num = X_tr.select_dtypes("number").columns.tolist()
cat = X_tr.select_dtypes("object").columns.tolist()

pre = ColumnTransformer([
    ("num", Pipeline([
        ("imp", SimpleImputer(strategy="median")),
        ("sc", StandardScaler()),
    ]), num),
    ("cat", Pipeline([
        ("imp", SimpleImputer(strategy="most_frequent")),
        ("te", TargetEncoder(smooth="auto", cv=5)),   # out-of-fold, sem vazamento
    ]), cat),
])

# Seleção de features também dentro do pipeline
selecao = SelectKBest(mutual_info_classif, k=30)

Baseline forte + tuning com nested CV (Módulos 5, 6, 7)

from lightgbm import LGBMClassifier
from sklearn.model_selection import RandomizedSearchCV, StratifiedKFold, cross_val_score
from scipy.stats import randint, uniform, loguniform

modelo = Pipeline([
    ("pre", pre),
    ("sel", selecao),
    ("clf", LGBMClassifier(class_weight="balanced", random_state=42, verbose=-1)),
])

espaco = {
    "clf__n_estimators": randint(200, 1500),
    "clf__learning_rate": loguniform(1e-3, 0.3),
    "clf__num_leaves": randint(15, 200),
    "clf__subsample": uniform(0.6, 0.4),
    "clf__reg_lambda": loguniform(1e-3, 10),
}

interno = StratifiedKFold(5, shuffle=True, random_state=1)
externo = StratifiedKFold(5, shuffle=True, random_state=2)

busca = RandomizedSearchCV(modelo, espaco, n_iter=40, cv=interno,
                           scoring="average_precision", random_state=42, n_jobs=-1)

# Estimativa IMPARCIAL do desempenho (nested CV)
nested = cross_val_score(busca, X_tr, y_tr, cv=externo, scoring="average_precision")
print(f"PR-AUC nested (imparcial): {nested.mean():.3f} ± {nested.std():.3f}")

# Modelo final: ajusta a busca em todo o treino
busca.fit(X_tr, y_tr)
melhor = busca.best_estimator_

O número que você comunica é o do nested CV — não o best_score_. Este é o ponto do curso que mais separa amadores de profissionais.

Calibração e threshold por custo (Módulo 2)

import numpy as np
from sklearn.calibration import CalibratedClassifierCV

# Calibra as probabilidades (importante para decidir por custo)
calibrado = CalibratedClassifierCV(melhor, method="isotonic", cv=5)
calibrado.fit(X_tr, y_tr)

# Threshold que minimiza custo esperado (validação, não teste)
from sklearn.model_selection import cross_val_predict
proba_val = cross_val_predict(calibrado, X_tr, y_tr, cv=5, method="predict_proba")[:, 1]

custo_fn, custo_fp = 500, 20   # perder cliente vs. ligação de retenção
thr = np.linspace(0.01, 0.99, 99)
custos = [((proba_val >= t).astype(int) == 0) & (y_tr == 1)  # FN
          for t in thr]
custos = [np.sum(((proba_val>=t)==0)&(y_tr==1))*custo_fn +
          np.sum(((proba_val>=t)==1)&(y_tr==0))*custo_fp for t in thr]
threshold_otimo = float(thr[np.argmin(custos)])
print(f"Threshold de menor custo: {threshold_otimo:.2f}")

Avaliação final no teste — uma única vez (Módulo 1)

from sklearn.metrics import average_precision_score, classification_report

proba_te = calibrado.predict_proba(X_te)[:, 1]
pred_te = (proba_te >= threshold_otimo).astype(int)

print(f"PR-AUC teste: {average_precision_score(y_te, proba_te):.3f}")
print(classification_report(y_te, pred_te, digits=3))

# Intervalo de confiança via bootstrap (Módulo 1)
rng = np.random.default_rng(42)
aps = [average_precision_score(y_te.values[i], proba_te[i])
       for i in (rng.integers(0, len(y_te), len(y_te)) for _ in range(1000))
       if len(np.unique(y_te.values[i])) == 2]
print(f"IC95% PR-AUC: [{np.percentile(aps,2.5):.3f}, {np.percentile(aps,97.5):.3f}]")

Explicabilidade para a equipe de negócio (Módulo 8)

import shap

# Explicação global e local sobre o modelo de árvore subjacente
explainer = shap.TreeExplainer(busca.best_estimator_.named_steps["clf"])
# (aplicar o pré-processamento antes de passar ao explainer)

# Global: quais fatores mais dirigem o churn
# shap.summary_plot(shap_values, X_transformado)

# Local + acionável: por que ESTE cliente é risco, e o que mudaria (contrafactual)
# -> entrega para o time de retenção o "porquê" de cada alerta

Entregue dois artefatos ao negócio: um resumo global ("tempo de contrato curto e suporte acionado com frequência são os maiores fatores de churn") e explicações locais por cliente ("este cliente foi sinalizado por X e Y").

Empacotando o modelo

Salve o pipeline inteiro (pré-processamento + modelo + threshold), não só o classificador. O pré-processamento faz parte do modelo.

import joblib, json, sklearn, lightgbm

artefato = {
    "modelo": calibrado,
    "threshold": threshold_otimo,
    "colunas": X_cols,
    "versoes": {"sklearn": sklearn.__version__, "lightgbm": lightgbm.__version__},
    "metrica_teste": float(average_precision_score(y_te, proba_te)),
    "treinado_em": pd.Timestamp.now().isoformat(),
}
joblib.dump(artefato, "modelo_churn_v1.joblib")

Registrar versões de bibliotecas evita o pesadelo de "funcionava na minha máquina": um pipeline serializado com uma versão de sklearn pode falhar em outra.

Servindo o modelo via API

O padrão mais comum: uma API REST que recebe features e devolve a predição. FastAPI é o padrão em Python.

# app.py
from fastapi import FastAPI
from pydantic import BaseModel
import joblib, pandas as pd

app = FastAPI(title="API de Churn")
artefato = joblib.load("modelo_churn_v1.joblib")

class Cliente(BaseModel):
    tempo_contrato: float
    mensalidade: float
    chamados_suporte: int
    # ... demais features

@app.post("/prever")
def prever(c: Cliente):
    X = pd.DataFrame([c.dict()])
    proba = float(artefato["modelo"].predict_proba(X)[:, 1][0])
    decisao = int(proba >= artefato["threshold"])
    return {"probabilidade_churn": round(proba, 4),
            "alerta_retencao": decisao}
# Rodar: uvicorn app:app --host 0.0.0.0 --port 8000
# Empacotar em container para deploy reproduzível:
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py modelo_churn_v1.joblib .
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]

O container garante que o ambiente de produção seja idêntico ao de treino — a mesma disciplina de reprodutibilidade das primeiras aulas, agora aplicada à infraestrutura.

MLOps: o que acontece depois do deploy

Deploy não é o fim — é o começo do ciclo de vida. Os quatro pilares mínimos:

Versionamento. Versione três coisas, não só o código: dados (ex.: DVC), modelos (ex.: MLflow Model Registry) e experimentos (métricas, hiperparâmetros, artefatos). Sem isso, você não consegue reproduzir nem auditar um modelo em produção.

# Rastreamento de experimentos com MLflow
import mlflow

with mlflow.start_run():
    mlflow.log_params(busca.best_params_)
    mlflow.log_metric("pr_auc_teste", artefato["metrica_teste"])
    mlflow.sklearn.log_model(calibrado, "modelo")

Monitoramento de drift. O mundo muda; o modelo não. Data drift (a distribuição das features de entrada muda) e concept drift (a relação features→alvo muda) degradam o desempenho silenciosamente. Monitore as distribuições de entrada e, quando o alvo chega (com atraso), o desempenho real.

# Detecção simples de drift: teste estatístico entre treino e produção
from scipy.stats import ks_2samp

def checar_drift(ref, atual, features, alpha=0.05):
    alertas = {}
    for f in features:
        stat, p = ks_2samp(ref[f].dropna(), atual[f].dropna())
        if p < alpha:
            alertas[f] = round(p, 4)   # distribuição mudou
    return alertas

Retreino. Defina o gatilho: por tempo (mensal), por drift detectado, ou por queda de métrica. Automatize o pipeline de retreino — mas valide o novo modelo antes de promover (nunca substitua um modelo em produção sem comparar com o atual em dados recentes).

Observabilidade e governança. Logue predições, latência, taxa de erro; guarde as explicações (SHAP) das decisões para auditoria; documente o modelo (um "model card" com dados de treino, métricas, limitações e vieses conhecidos). Em domínios regulados (crédito, saúde), isso é obrigação legal, não opcional.

O ciclo completo

   dados → features → treino → validação (nested CV) → modelo
     ↑                                                    ↓
  retreino ← gatilho ← monitoramento ← produção (API) ← deploy
     └──────────── drift / queda de métrica ─────────────┘

Um sistema de ML maduro é esse loop fechado, não uma linha reta. O modelo é um organismo vivo que precisa de manutenção — e a disciplina metodológica do curso (evitar vazamento, avaliar sem viés, explicar decisões) se aplica a cada volta do ciclo.

Um projeto real segue uma ordem disciplinada, e o teste é tocado uma única vez, no fim — toda decisão pelo caminho sai da validação. O pré-processamento, a seleção e o modelo vão encapsulados num pipeline só, sem vazamento, e o desempenho se estima com validação cruzada aninhada: é esse número que se reporta. Depois vem calibrar as probabilidades e escolher o limiar pelo custo de negócio, avaliando no teste com intervalo de confiança por bootstrap. O que se entrega ao negócio não é só a métrica: explicabilidade global e local fazem parte do produto. Na hora de publicar, empacote o pipeline inteiro com as versões travadas e sirva por API, em container, para que o resultado seja reproduzível. E o trabalho não acaba no deploy — MLOps é o laço seguinte: versionar dados, modelos e experimentos, monitorar drift, retreinar com validação antes de promover, e manter observabilidade e governança.

Exercícios

  1. Em um projeto integrado, em que momento o conjunto de teste pode ser usado, e por quê essa regra é tão rígida?

    ✓ Resposta:

    O conjunto de teste deve ser usado uma única vez, no final, apenas para produzir a estimativa de desempenho que será reportada — depois que todas as decisões (features, seleção, hiperparâmetros, threshold, ensemble) já foram tomadas usando exclusivamente treino/validação. A regra é rígida porque cada vez que você olha o teste e ajusta algo com base nele, você "vaza" informação do teste para o processo de modelagem, reintroduzindo viés de seleção e tornando a estimativa final otimista. Se o teste for consultado repetidamente durante a experimentação, ele deixa de medir generalização e passa a medir o quão bem você se ajustou àquele conjunto específico — exatamente o que ele deveria evitar.

  2. Por que é essencial salvar o pipeline inteiro (pré-processamento + modelo) e não apenas o classificador treinado?

    ✓ Resposta:

    Porque o pré-processamento faz parte do modelo: ele aprende parâmetros dos dados de treino (medianas de imputação, médias/desvios de escala, categorias e encodings, features selecionadas). Em produção, os dados brutos precisam passar exatamente pelas mesmas transformações, com os mesmos parâmetros aprendidos no treino. Se você salvar só o classificador, terá de reconstruir o pré-processamento manualmente na inferência, com enorme risco de aplicar transformações ligeiramente diferentes (outra escala, outra ordem de colunas, outro tratamento de categoria nova) — o que causa "training-serving skew" e predições erradas. Salvar o pipeline completo garante que a mesma sequência de passos, com os mesmos parâmetros, seja aplicada de forma idêntica em treino e produção.

  3. Diferencie data drift de concept drift e explique como cada um degrada um modelo em produção.

    ✓ Resposta:

    Data drift (ou covariate shift) é a mudança na distribuição das features de entrada ao longo do tempo, sem que necessariamente a relação entre features e alvo mude — por exemplo, o perfil de novos clientes muda (mais jovens, outra região). O modelo passa a receber entradas diferentes das que viu no treino, e como foi calibrado para a distribuição antiga, seu desempenho pode cair. Concept drift é a mudança na própria relação entre features e alvo — o que antes indicava churn deixa de indicar (mudança de comportamento do mercado, nova concorrência). Aqui, mesmo com a mesma distribuição de entrada, o mapeamento aprendido fica desatualizado e as predições ficam sistematicamente erradas. Ambos degradam o modelo silenciosamente (as métricas caem sem erro explícito), por isso é preciso monitorar distribuições de entrada (para data drift) e o desempenho real quando os rótulos chegam (para concept drift), disparando retreino quando necessário.

  4. Você detectou drift e retreinou o modelo automaticamente. Por que não se deve promover o novo modelo direto para produção?

    ✓ Resposta:

    Porque um retreino automático não garante que o novo modelo seja melhor — ele pode ter sido treinado sobre dados recentes ruidosos, corrompidos, com vazamento introduzido pela mudança de pipeline, ou simplesmente ter desempenho pior. Promover às cegas arrisca substituir um modelo funcional por um pior, degradando o sistema. O correto é tratar o modelo retreinado como candidato e validá-lo antes de promover: comparar seu desempenho com o do modelo atual em dados recentes e representativos (idealmente um conjunto de validação temporal ou via teste A/B / shadow deployment, onde o novo modelo roda em paralelo sem afetar decisões reais). Só se o candidato superar o incumbente de forma estatisticamente convincente é que ele é promovido — a mesma disciplina de avaliação imparcial do curso, aplicada continuamente.

  5. Cite três coisas que devem ser versionadas em um projeto de MLOps além do código, e por que cada uma importa.

    ✓ Resposta:

    (1) Dados — versionar o dataset (ou seu snapshot/hash) usado em cada treino é essencial para reproduzir um modelo e para diagnosticar problemas (um modelo só é reproduzível se você souber exatamente com quais dados foi treinado); ferramentas como DVC ajudam. (2) Modelos — cada versão do modelo treinado deve ser registrada com seus metadados (métricas, data, dados de origem), permitindo rollback para uma versão anterior se a nova falhar em produção e mantendo um histórico auditável (ex.: MLflow Model Registry). (3) Experimentos — registrar hiperparâmetros, métricas, sementes e artefatos de cada execução permite comparar abordagens, entender por que um modelo foi escolhido e reconstruir o processo de decisão. Juntos, esses três garantem reprodutibilidade, auditabilidade e governança — sem eles, um modelo em produção vira uma caixa-preta impossível de reproduzir, comparar ou corrigir.

Fontes e leituras recomendadas