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.

Data scientist reviewing cross-validation vs holdout AUC gap on a dashboard, illustrating silent ML pipeline leakage Coding Session Visual

Quatro Armadilhas, Uma Falha Silenciosa

Gerei dados sintéticos de churn (8.000 linhas por dataset, 5 seeds) e plantei quatro armadilhas:

  1. Região como número. region_code guarda 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.
  2. Valores ausentes informativos. support_calls está ausente para ~45% dos churners e 8% dos não-churners. Imputação pela mediana apaga esse sinal.
  3. Uma data guardada como texto. signup_date é uma string YYYY-MM-DD, então vira uma coluna one-hot por data única (1.931 valores únicos em 6.000 linhas).
  4. Um ID que vaza o label. No treino, customer_id foi 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

VarianteAUC CVAUC Holdout
Pipeline automático ingênuo0.9790.564
+ região como categoria0.9820.624
+ indicador de ausente0.9840.670
+ parse da data0.9800.585
+ remover coluna ID0.6400.632
Todas as quatro correções0.8330.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.

Python code snippet on laptop showing scikit-learn ColumnTransformer with dtype-based routing and missing indicator logic Development Concept Image

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

  1. Existe um holdout parecido com os dados futuros? Validação cruzada no treino não pega vazamento que existe no treino.
  2. Em qual coluna o modelo mais se apoia? Se um ID, número de linha ou timestamp está no topo, pare.
  3. Uma coluna numérica é realmente categoria? Códigos, CEPs e IDs de região são números que não são quantidades.
  4. Os valores ausentes são aleatórios? Se a taxa do target difere onde a coluna está ausente, adicione um indicador.
  5. Existem colunas de texto com milhares de valores únicos? Datas, nomes e texto livre precisam de tratamento próprio, não one-hot.

Server room representing production holdout data that reveals model failure after passing cross-validation Algorithm Concept Visual

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

Este conteúdo foi elaborado com o auxílio de ferramentas de IA, com base em fontes confiáveis, e revisado pela nossa equipe editorial antes da publicação. Não substitui o aconselhamento de um profissional especializado.