Validação Cruzada em Python: K-Fold, Estratificada, LOOCV
Aprenda validação cruzada em Python: K-Fold, Stratified K-Fold, Leave-One-Out, CV aninhada e pipelines com scikit-learn — com exemplos executáveis.
A validação cruzada é a forma padrão de estimar o quão bem um modelo de machine learning se comportará com dados não vistos. Em vez de depender de uma única divisão treino/teste — que pode produzir uma pontuação excessivamente otimista ou pessimista dependendo de quais amostras acabam onde — a validação cruzada treina e avalia o modelo várias vezes em diferentes partições dos dados e calcula a média dos resultados.
Esta página cobre:
- Por que uma única divisão treino/teste não é suficiente
- Validação cruzada K-Fold e como escolher
k - Stratified K-Fold para distribuições de classes desequilibradas
- Validação cruzada Leave-One-Out (LOO) para conjuntos de dados pequenos
- Avaliação de múltiplas métricas com
cross_validate - Uso de um
Pipelinedentro da validação cruzada para prevenir vazamento de dados - Validação cruzada aninhada para ajuste de hiperparâmetros sem viés
Todos os exemplos utilizam scikit-learn e o conjunto de dados Iris integrado, para que você possa executá-los imediatamente sem baixar nada.
Por que a Validação Cruzada é Importante
Um fluxo de avaliação ingênuo divide os dados uma vez, treina em uma parte e testa na outra. A pontuação obtida depende muito de quais amostras foram para cada parte — uma divisão favorável pode fazer um modelo fraco parecer bom; uma desfavorável pode fazer um modelo forte parecer ruim.
A validação cruzada resolve isso repetindo o processo de treino/teste k vezes, usando uma porção diferente dos dados como conjunto de teste a cada vez. A pontuação final é a média sobre todos os folds, o que é muito mais estável do que uma única medição.
A validação cruzada também aproveita ao máximo dados limitados: cada amostra é usada tanto para treinamento quanto para avaliação ao longo de todo o experimento.
Veja a página divisão treino/teste para conhecer a técnica de referência mais simples que a validação cruzada aprimora.
Validação Cruzada K-Fold
K-Fold é a estratégia de validação cruzada mais amplamente utilizada. Os dados são divididos em k folds de tamanho igual. Em cada uma das k iterações:
- Um fold é reservado como conjunto de teste.
- Os
k - 1folds restantes formam o conjunto de treinamento. - O modelo é treinado do zero e avaliado no fold de teste.
Após k iterações você tem k pontuações. A média delas é a estimativa de desempenho validada cruzadamente; o desvio padrão indica o quão consistente é esse desempenho em diferentes fatias de dados.
Para k = 5 e um conjunto de dados de 150 amostras, cada fold contém 30 amostras (20%) para teste e 120 amostras (80%) para treinamento.
Exemplo Básico de K-Fold
from sklearn.model_selection import KFold, cross_val_score
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import load_iris
# Load the built-in Iris dataset (150 samples, 4 features, 3 classes)
iris = load_iris()
X, y = iris.data, iris.target
model = LogisticRegression(max_iter=200)
kfold = KFold(n_splits=5, shuffle=True, random_state=42)
scores = cross_val_score(model, X, y, cv=kfold, scoring='accuracy')
print("Fold scores:", scores.round(4))
# Fold scores: [1. 1. 0.9333 0.9667 0.9667]
print("Mean accuracy: %.4f Std: %.4f" % (scores.mean(), scores.std()))
# Mean accuracy: 0.9733 Std: 0.0249A função cross_val_score gerencia toda a iteração internamente. Parâmetros principais:
| Parâmetro | Finalidade |
|---|---|
estimator | Qualquer modelo scikit-learn (ou Pipeline) |
X, y | Matriz de features e vetor alvo |
cv | Objeto cross-validator ou inteiro (ex.: cv=5) |
scoring | String de métrica — 'accuracy', 'f1_macro', 'roc_auc', etc. |
Escolhendo k
- k = 5 ou k = 10 é recomendado para a maioria dos conjuntos de dados. Esses valores oferecem um bom tradeoff viés-variância na estimativa.
- k maior (ex.: 10) produz menor viés, mas maior variância na estimativa, e é mais custoso computacionalmente.
- k menor (ex.: 3) é mais rápido, mas a estimativa é mais sensível à forma como os dados foram divididos.
- Para conjuntos de dados muito pequenos (menos de ~100 amostras), considere usar Leave-One-Out.
Inspecionando os Folds Manualmente
Você pode iterar sobre os folds por conta própria quando precisar inspecionar o que entra em cada divisão ou quando quiser executar lógica personalizada por fold:
from sklearn.model_selection import KFold
from sklearn.datasets import load_iris
import numpy as np
iris = load_iris()
X, y = iris.data, iris.target
kfold = KFold(n_splits=5, shuffle=True, random_state=42)
for fold, (train_idx, test_idx) in enumerate(kfold.split(X), start=1):
X_train, X_test = X[train_idx], X[test_idx]
y_train, y_test = y[train_idx], y[test_idx]
print(f"Fold {fold}: train={len(train_idx)} samples, test={len(test_idx)} samples")Saída:
Fold 1: train=120 samples, test=30 samples
Fold 2: train=120 samples, test=30 samples
Fold 3: train=120 samples, test=30 samples
Fold 4: train=120 samples, test=30 samples
Fold 5: train=120 samples, test=30 samplesValidação Cruzada Stratified K-Fold
O K-Fold simples divide os dados pela ordem de índice. Com distribuições de classes desequilibradas, isso pode fazer com que alguns folds contenham muito poucos exemplos de uma classe minoritária, tornando a pontuação não confiável.
O Stratified K-Fold garante que cada fold contenha aproximadamente a mesma proporção de cada classe que o conjunto de dados completo. Use StratifiedKFold sempre que seu alvo for categórico:
from sklearn.model_selection import StratifiedKFold, cross_val_score
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import load_iris
iris = load_iris()
X, y = iris.data, iris.target
model = LogisticRegression(max_iter=200)
skfold = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
scores = cross_val_score(model, X, y, cv=skfold, scoring='accuracy')
print("Fold scores:", scores.round(4))
# Fold scores: [1. 0.9667 0.9333 1. 0.9333]
print("Mean accuracy: %.4f Std: %.4f" % (scores.mean(), scores.std()))
# Mean accuracy: 0.9667 Std: 0.0298StratifiedKFold é o cross-validator padrão usado dentro de GridSearchCV e RandomizedSearchCV para problemas de classificação — você obtém estratificação automaticamente nesses contextos.
Validação Cruzada Leave-One-Out
A validação cruzada Leave-One-Out (LOO) é o caso extremo: k é igual ao número de amostras. Em cada iteração, uma amostra é o conjunto de teste e todas as demais formam o conjunto de treinamento. Para um conjunto de dados de 150 amostras, isso significa 150 ciclos de treinamento/avaliação.
from sklearn.model_selection import LeaveOneOut, cross_val_score
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import load_iris
iris = load_iris()
X, y = iris.data, iris.target
model = LogisticRegression(max_iter=200)
loocv = LeaveOneOut()
scores = cross_val_score(model, X, y, cv=loocv, scoring='accuracy')
print(f"Number of folds: {len(scores)}") # 150
print(f"Mean accuracy: {scores.mean():.4f}") # 0.9667
print(f"Std deviation: {scores.std():.4f}") # 0.1795Quando usar LOO:
- Seu conjunto de dados tem menos de ~100 amostras e você não pode reservar nenhum dado para teste.
- Você deseja a estimativa de menor viés do desempenho do modelo.
Desvantagens do LOO:
- Custo computacional muito alto — o modelo é retreinado
nvezes. - Alta variância na estimativa: a pontuação de teste de cada fold é 0 ou 1 (classificação binária) ou um único ponto, portanto o desvio padrão não é significativo para folds individuais.
Para a maioria dos conjuntos de dados, K-Fold com k=5 ou k=10 é um melhor tradeoff.
Avaliando Múltiplas Métricas de Uma Vez
cross_val_score pode calcular apenas uma métrica por chamada. Use cross_validate para calcular várias métricas simultaneamente e também recuperar pontuações de treinamento para verificar overfitting:
from sklearn.model_selection import KFold, cross_validate
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import load_iris
import numpy as np
iris = load_iris()
X, y = iris.data, iris.target
model = LogisticRegression(max_iter=200)
kfold = KFold(n_splits=5, shuffle=True, random_state=42)
cv_results = cross_validate(
model, X, y,
cv=kfold,
scoring=['accuracy', 'f1_macro'],
return_train_score=True,
)
print("Test accuracy: ", cv_results['test_accuracy'].round(4))
# Test accuracy: [1. 1. 0.9333 0.9667 0.9667]
print("Train accuracy:", cv_results['train_accuracy'].round(4))
# Train accuracy: [0.975 0.9583 0.9833 0.975 0.9833]
print("Test F1-macro: ", cv_results['test_f1_macro'].round(4))
# Test F1-macro: [1. 1. 0.9259 0.9691 0.971 ]Comparar as pontuações de treino e teste nos folds é uma maneira rápida de identificar overfitting: se a acurácia de treinamento for consistentemente muito maior do que a acurácia de teste, o modelo está memorizando os dados de treinamento. Veja a discussão sobre viés e variância para mais contexto.
Usando um Pipeline Dentro da Validação Cruzada
Um erro comum é ajustar etapas de pré-processamento (como escalonamento de features ou imputação) em todo o conjunto de dados antes da validação cruzada. Isso vaza informações do fold de teste para o processo de treinamento, levando a uma pontuação excessivamente otimista.
O padrão correto é encapsular o pré-processamento e o modelo juntos em um Pipeline e passar o pipeline para cross_val_score. O scikit-learn reajusta o pipeline inteiro — incluindo o scaler — de forma independente dentro de cada fold:
from sklearn.model_selection import StratifiedKFold, cross_val_score
from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.datasets import load_iris
iris = load_iris()
X, y = iris.data, iris.target
# Correct: preprocessing is fitted only on training folds
pipe = Pipeline([
('scaler', StandardScaler()),
('clf', LogisticRegression(max_iter=200)),
])
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
scores = cross_val_score(pipe, X, y, cv=skf, scoring='accuracy')
print("Fold scores:", scores.round(4))
# Fold scores: [1. 0.9667 0.9 1. 0.9 ]
print("Mean accuracy: %.4f Std: %.4f" % (scores.mean(), scores.std()))
# Mean accuracy: 0.9533 Std: 0.0452Sempre use um Pipeline quando seu fluxo de trabalho incluir qualquer etapa que aprende com os dados (escalonamento, PCA, codificação, imputação).
Validação Cruzada Aninhada
Quando você usa validação cruzada tanto para ajustar hiperparâmetros quanto para avaliar o desempenho do modelo nos mesmos dados, corre o risco de overfitting nos folds de validação — os hiperparâmetros escolhidos são aqueles que por acaso obtiveram a melhor pontuação nessas partições específicas, tornando a pontuação reportada otimista.
A validação cruzada aninhada separa as duas preocupações:
- Loop interno: seleciona hiperparâmetros via busca em grade nos folds de treinamento.
- Loop externo: avalia o melhor modelo encontrado pelo loop interno em um fold de teste reservado.
from sklearn.model_selection import GridSearchCV, StratifiedKFold, cross_val_score
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import load_iris
iris = load_iris()
X, y = iris.data, iris.target
# Inner CV: hyperparameter selection
inner_cv = StratifiedKFold(n_splits=3, shuffle=True, random_state=2)
param_grid = {'C': [0.01, 0.1, 1, 10]}
gs = GridSearchCV(
LogisticRegression(max_iter=300),
param_grid,
cv=inner_cv,
scoring='accuracy',
)
# Outer CV: unbiased performance estimation
outer_cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=1)
nested_scores = cross_val_score(gs, X, y, cv=outer_cv, scoring='accuracy')
print("Nested CV fold scores:", nested_scores.round(4))
# Nested CV fold scores: [0.9667 1. 0.9333 1. 0.9 ]
print("Mean accuracy: %.4f" % nested_scores.mean())
# Mean accuracy: 0.9600A abordagem aninhada fornece uma estimativa sem viés do desempenho do modelo final implantado. Use-a sempre que estiver reportando resultados em um contexto de pesquisa ou comparando algoritmos. Para um fluxo de trabalho de implantação simples onde você vai retreinar em todos os dados disponíveis de qualquer forma, um único loop externo com GridSearchCV geralmente é suficiente. Veja a página de busca em grade para um guia detalhado sobre ajuste de hiperparâmetros.
Armadilhas Comuns
Pré-processamento fora do fold
Ajustar um scaler no conjunto de dados completo antes de chamar cross_val_score — em vez de dentro de um Pipeline — vaza as estatísticas do fold de teste para o treinamento. A solução é sempre usar um Pipeline.
Usando random_state incorretamente
Se você definir shuffle=True sem um random_state, cada execução produz uma divisão diferente e seus resultados não são reproduzíveis. Sempre defina random_state como um inteiro fixo ao reportar números.
Interpretando o desvio padrão
Um desvio padrão alto entre os folds não é sempre ruim — pode refletir variabilidade genuína no conjunto de dados (por exemplo, alguns folds são mais fáceis do que outros). Examine as pontuações individuais dos folds antes de tirar conclusões.
Validação cruzada em dados de séries temporais
K-Fold embaralha os dados aleatoriamente, o que misturaria informações futuras nas janelas de treinamento passadas para problemas de séries temporais. Em vez disso, use TimeSeriesSplit do scikit-learn, que respeita a ordem temporal.
Referência Rápida
| Técnica | Quando usar | Classe scikit-learn |
|---|---|---|
| K-Fold | Escolha padrão para a maioria das tarefas de regressão/classificação | KFold |
| Stratified K-Fold | Classificação com classes desequilibradas | StratifiedKFold |
| Leave-One-Out | Conjuntos de dados muito pequenos (< ~100 amostras) | LeaveOneOut |
| CV Aninhada | Reportar pontuações sem viés com ajuste de hiperparâmetros | GridSearchCV dentro de cross_val_score |
| CV de séries temporais | Dados com ordenação temporal | TimeSeriesSplit |
Tópicos Relacionados
- Divisão Treino/Teste — a linha de base mais simples que a validação cruzada aprimora
- Busca em Grade — ajuste de hiperparâmetros, frequentemente combinado com validação cruzada
- Regressão Logística — um dos classificadores usados nos exemplos acima
- Regressão Linear — contrapartida de regressão, também avaliada com validação cruzada
- Matriz de Confusão — detalhamento do desempenho por classe para complementar as pontuações de acurácia
- Curva AUC-ROC — outra métrica de avaliação que você pode passar para
scoringemcross_val_score