Validação cruzada (CV, do inglês cross-validation): medir uma receita sem depender da sorte de uma divisão
| treino | o modelo aprende seus parâmetros aqui | tocado sempre |
| validação | escolhe entre modelos e hiperparâmetros | tocado muitas vezes |
| teste | simula o futuro | tocado uma vez |
Dados de puro ruído: 20 colunas aleatórias, alvo no cara ou coroa. Nenhum modelo pode passar de 50%. Treine 100 árvores com hiperparâmetros aleatórios e fique com a melhor na validação.
Preveja. O vencedor marca acima de 50% na validação. Quanto a mesma árvore marca em dados intocados? A diferença cresce ou diminui quando você compara mais configurações?
É a maldição do vencedor: o viés cresce com o número de coisas que você testou.
Preveja. Só muda o random_state da divisão. Quão distantes podem ficar duas acurácias reportadas? E duas médias de uma CV 5-fold?
Mesmo modelo, mesmos dados, 300 sementes de uma divisão 80/20:
| um hold-out | 93,0% a 100,0% |
| média da CV 5-fold | 96,8% a 98,4% |
A CV reduz muito a sorte da divisão; ela não remove a sorte da amostra — todos os folds saem das mesmas linhas.
Preveja. Com \(n = 80\) linhas (\(n\) = número de linhas), aperte Sortear 100: qual faixa é mais larga, a do hold-out ou a da média da CV? Depois aperte Novo conjunto algumas vezes: a faixa da CV fica sobre a linha branca?
Particione os dados de treino em \(k\) folds; cada rodada treina nos outros \(k-1\) e avalia no que ficou de fora; reporte a média e o desvio-padrão dos \(k\) escores.
A CV roda só em X_train: o teste continua trancado fora, como no diagrama.
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
scores = cross_val_score(pipe, X_train, y_train, cv=cv, scoring='f1')
print(f"{scores.mean():.3f} ± {scores.std():.3f}")
Os divisores diferem em quais linhas podem cair juntas num fold de validação.
| método | quando usar | cuidado com |
|---|---|---|
| K-Fold | regressão, dados balanceados (com shuffle=True) | folds sem nenhuma linha de uma classe rara |
| Stratified K-Fold | classificação, principalmente desbalanceada | só estratifica pelo alvo |
| Leave-One-Out | conjuntos muito pequenos | \(n\) ajustes (\(n\) = número de linhas); uma linha por fold: sem desvio por fold |
| TimeSeriesSplit | dados temporais | os primeiros folds treinam com pouco; gap se o alvo olha à frente |
| Group K-Fold | dados agrupados (pacientes, usuários) | exige uma coluna de grupo |
Preveja. 100 linhas, 8 positivas, KFold embaralhado em 5 folds: com que frequência algum fold fica sem nenhuma positiva?
Preveja. Se o scaler for ajustado em todo o X antes da CV, o escore sai mais alto, mais baixo ou quase igual? E se o passo fosse um seletor de atributos que olha o y?
# ERRADO: as estatísticas das linhas de validação vazam para o treino
X_scaled = StandardScaler().fit_transform(X)
cross_val_score(LogisticRegression(), X_scaled, y, cv=cv)
# CERTO: cada fold ganha o próprio scaler
pipe = make_pipeline(StandardScaler(), LogisticRegression())
cross_val_score(pipe, X, y, cv=cv)
Pipeline, todos os passos são ajustados só na porção de treino do fold e aplicados congelados à de validação.stratify se for classificação.StratifiedKFold(5, shuffle=True, random_state=...).Pipeline, com todo passo ajustado dentro.cv em todos os candidatos — a comparação fica pareada.Ritmo: passos 1 a 3 em 10 min; passo 4 e a simulação em 12 min; passos 5 e 6 com a simulação em 13 min.