O Pipeline Que Todo Mundo Escreve
Se você já usou scikit-learn por um tempo, provavelmente já escreveu este pipeline: colunas numéricas são imputadas e escalonadas, o resto é imputado e one-hot encoded, e as colunas são roteadas por dtype. Funciona em quase qualquer tabela sem você olhar.
É curto, reutilizável e fácil de confiar. Esse é o problema.
Quis descobrir o que esse pipeline erra silenciosamente, então tentei quebrá-lo de propósito. O resultado: um modelo com 0.979 na validação cruzada 5-fold e 0.564 em clientes novos. Nada crashou. Nenhum warning apareceu. Todos os números pareciam de um modelo forte.
Essa é a história de quatro armadilhas que um pipeline baseado em dtype não enxerga — e da automação auto-verificável que construí para pegá-las. Para uma discussão mais ampla sobre por que guardrails de runtime não bastam para sistemas de IA, veja nossa análise sobre a camada de confiança que falta para agentes de IA.

Quatro Armadilhas, Uma Falha Silenciosa
Gerei dados sintéticos de churn (8.000 linhas por dataset, 5 seeds) e plantei quatro armadilhas:
- Região como número.
region_codeguarda inteiros de 1 a 12, mas cada região afeta o churn de forma diferente e fora de ordem. O pipeline escala como se fosse uma medida. - Valores ausentes informativos.
support_callsestá ausente para ~45% dos churners e 8% dos não-churners. Imputação pela mediana apaga esse sinal. - Uma data guardada como texto.
signup_dateé uma stringYYYY-MM-DD, então vira uma coluna one-hot por data única (1.931 valores únicos em 6.000 linhas). - Um ID que vaza o label. No treino,
customer_idfoi atribuído mais ou menos na ordem do desfecho. No holdout limpo, IDs não têm relação com churn.
A Pontuação Que Mentiu
| Variante | AUC CV | AUC Holdout |
|---|---|---|
| Pipeline automático ingênuo | 0.979 | 0.564 |
| + região como categoria | 0.982 | 0.624 |
| + indicador de ausente | 0.984 | 0.670 |
| + parse da data | 0.980 | 0.585 |
| + remover coluna ID | 0.640 | 0.632 |
| Todas as quatro correções | 0.833 | 0.826 |
Dentro da regressão logística da seed 0, a feature mais forte entre 1.940 era customer_id (|coeficiente| 5.05). As seguintes eram colunas one-hot de datas específicas. O modelo leu o label do ID e memorizou ruído por data.
Ensinando o Pipeline a Se Checar
Se você constrói automação para pessoas que não programam, "lembre de checar vazamentos" não é um plano. Cada armadilha acima deixa uma impressão digital estatística. Aqui vai um scanner que roda antes da modelagem:
import pandas as pd
import numpy as np
def scan_for_traps(df, target_col, id_like=('_id', 'id_'), date_like=('date', '_at', 'time')):
"""Imprime avisos em linguagem simples para armadilhas comuns de vazamento."""
warnings = []
y = df[target_col]
for col in df.columns:
if col == target_col:
continue
s = df[col]
# Armadilha 4: coluna tipo ID correlacionada com o target
if any(tok in col.lower() for tok in id_like) and s.dtype.kind in 'iu':
corr = np.corrcoef(s.fillna(-1), y)[0, 1]
if abs(corr) > 0.3:
warnings.append(f"'{col}' parece um ID mas correlaciona com o target (r={corr:.2f}). Possível vazamento.")
# Armadilha 3: coluna de texto com muitos valores únicos (provavelmente data)
if s.dtype == object and s.nunique() > 0.1 * len(s):
warnings.append(f"'{col}' é texto com {s.nunique()} valores únicos. Faça parse antes do one-hot.")
# Armadilha 2: ausência informativa
if s.isna().any():
miss_rate = y[s.isna()].mean()
present_rate = y[s.notna()].mean()
if abs(miss_rate - present_rate) > 0.1:
warnings.append(f"A ausência em '{col}' é informativa (taxa do target {miss_rate:.2f} vs {present_rate:.2f}). Adicione um indicador de ausente.")
# Armadilha 1: coluna numérica com baixa cardinalidade (provavelmente categoria)
if s.dtype.kind in 'iu' and s.nunique() < 20 and col.lower().endswith(('code', 'id', 'zip')):
warnings.append(f"'{col}' é numérica mas parece categórica ({s.nunique()} valores). Trate como categoria.")
for w in warnings:
print('⚠️', w)
return warnings
def gap_warning(cv_auc, holdout_auc, threshold=0.1):
"""Sinaliza gaps suspeitos entre CV e holdout."""
gap = cv_auc - holdout_auc
if gap > threshold:
print(f"⚠️ Validação cruzada ({cv_auc:.2f}) está muito acima do holdout ({holdout_auc:.2f}). Suspeite de vazamento ou memorização.")
return gap
Na seed 0, isso sinalizou exatamente as quatro colunas plantadas (customer_id, region_code, support_calls, signup_date) e deixou tenure_months, monthly_spend e plan_type em paz.

Onde Essa Abordagem Falha
- Uma única fonte de dados sintéticos. Todos os resultados vêm de um gerador que eu escrevi. A direção dos efeitos deve se manter, mas os tamanhos vão diferir em dados reais. Correção: rode as mesmas variantes numa tabela real que você conhece bem.
- O vazamento de ID é plantado e exagerado. Vazamentos reais costumam ser mais fracos. Correção: cheque toda coluna tipo ID ou timestamp pela relação com o target antes de modelar.
- Só duas famílias de modelos. Outros modelos podem reagir diferente. Correção: repita com seu próprio modelo, usando um holdout construído como seus dados futuros vão chegar.
- Dispersão em alguns resultados. Os números de gradient boosting para indicador de ausente e região são ruidosos. Correção: use mais seeds ou CV repetida antes de tirar conclusões sobre uma mudança isolada.
- O checker foi testado nos mesmos dados sintéticos. Dados reais vão disparar avisos que precisam de decisão humana. Correção: trate avisos como convites para olhar, não como correções automáticas.
Cinco Perguntas Antes de Confiar na Automação
- Existe um holdout parecido com os dados futuros? Validação cruzada no treino não pega vazamento que existe no treino.
- Em qual coluna o modelo mais se apoia? Se um ID, número de linha ou timestamp está no topo, pare.
- Uma coluna numérica é realmente categoria? Códigos, CEPs e IDs de região são números que não são quantidades.
- Os valores ausentes são aleatórios? Se a taxa do target difere onde a coluna está ausente, adicione um indicador.
- Existem colunas de texto com milhares de valores únicos? Datas, nomes e texto livre precisam de tratamento próprio, não one-hot.

Automação Não É Verificação
Um pipeline de 8 linhas é um bom ponto de partida e um péssimo ponto de parada. Neste experimento ele produziu um 0.98 que caiu para 0.56 em dados novos, e foram necessárias quatro correções separadas — não uma — para chegar a um 0.83 honesto.
Automação economizou tempo, mas não fez a checagem, então adicionei a checagem à automação. Se você constrói ferramentas para pessoas que não conseguem enxergar esses problemas sozinhas, é essa parte que precisa ser construída.
Próximos passos: embale o scanner acima num passo de pré-processamento do seu Pipeline, logue os avisos no seu experiment tracker e bloqueie a promoção de modelos com base num check de gap CV/holdout. Para um padrão relacionado em sistemas de produção, veja como CSS @function traz funções customizadas nativas sem Sass — um lembrete de que conveniência sem verificação é dívida técnica.
Fonte: Towards Data Science — My Machine Learning Pipeline Cheated, and Cross-Validation Never Noticed