Como detetar desvios de distribuição com Kolmogorov-Smirnov em Machine Learning
Este tutorial mostra como detetar desvios de distribuição (data drift) em Machine Learning usando o teste Kolmogorov-Smirnov em Python. Saber quando as features mudam ao longo do tempo é útil para manter modelos robustos e evitar degradação da performance. Vou explicar o porquê do teste, dar um exemplo prático com código e mostrar como interpretar e agir sobre os resultados.
Pré-requisitos
- Python 3.8+ e bibliotecas: pandas, scipy, matplotlib (instalar com pip).
- Um conjunto de dados dividido em duas amostras: reference (treino) e current (produção).
- Conhecimentos básicos de pandas e interpretação de estatísticas.
- Para produção, idealmente ter amostras com tamanhos representativos: p.ex. 1k-10k para referência e 200-5k para corrente, dependendo do caso.
Passo 1: Entender o propósito do teste Kolmogorov-Smirnov
O teste Kolmogorov-Smirnov (KS) compara duas amostras para verificar se provêm da mesma distribuição contínua. O resultado devolve a estatística KS (a maior diferença entre as funções de distribuição empíricas, no intervalo [0,1]) e um p-valor que quantifica a evidência contra a hipótese nula de que ambas as amostras vêm da mesma distribuição. O KS é não paramétrico e sensível a diferenças na localização e na forma da distribuição. Isto significa que detecta mudanças de média, variância e mesmo alterações mais subtis nas caudas.
Passo 2: Preparar dados de exemplo
Crie duas amostras: reference (treino) e current (produção) — com pequenas alterações para simular drift. Mantém os nomes das colunas iguais. No exemplo abaixo usamos 1000 pontos na referência e 500 na produção; alterámos a escala de 'income' para simular drift e acrescentámos alguns outliers e valores missing para aproximar cenários reais.
import numpy as np
import pandas as pd
np.random.seed(42)
# reference: distribuição normal e exponencial
reference = pd.DataFrame({
'age': np.random.normal(40, 10, 1000),
'income': np.random.exponential(40000, 1000)
})
# introduzir alguns missing e outliers
reference.loc[::200, 'income'] = np.nan
reference.loc[5, 'age'] = 120 # outlier
# current: simula drift na 'income' (mudança de escala) e pequena mudança na idade
current = pd.DataFrame({
'age': np.random.normal(41, 11, 500),
'income': np.random.exponential(60000, 500)
})
current.loc[::150, 'income'] = np.nan
Passo 3: Implementar o teste KS por feature
Usamos scipy.stats.ks_2samp para calcular a estatística KS e o p-valor. Interpretação simples: p-valor baixo (ex.: < 0.05) indica diferença significativa entre distribuições. Atenção: com muitas features temos múltiplos testes; aplicar correção (p.ex. Bonferroni) reduz falsos positivos. Também considere o tamanho das amostras: com grandes n pequenos desvios podem ser significativos estatisticamente mas sem relevância prática.
from scipy.stats import ks_2samp
def detect_drift_ks(ref, cur, alpha=0.05, bonferroni=False):
results = []
numeric_cols = ref.select_dtypes(include=[np.number]).columns
m = len(numeric_cols)
for col in numeric_cols:
a = ref[col].dropna()
b = cur[col].dropna()
stat, pvalue = ks_2samp(a, b)
if bonferroni:
pvalue_adj = min(pvalue * m, 1.0)
else:
pvalue_adj = pvalue
drift = pvalue_adj < alpha
results.append({
'feature': col,
'ks_stat': float(stat),
'p_value': float(pvalue),
'p_value_adj': float(pvalue_adj),
'drift': bool(drift)
})
return pd.DataFrame(results)
results = detect_drift_ks(reference, current, alpha=0.05, bonferroni=True)
print(results)
Passo 4: Visualizar diferenças (exemplo)
Visualizações ajudam a confirmar o que o teste indica. Plotar histogramas ou KDE para comparar shapes das distribuições entre reference e current. Se o KS indicar drift em 'income', o histograma normalmente mostra deslocamento da cauda ou aumento da dispersão. Inclui boxplots para ver outliers e diferenças na mediana.
import matplotlib.pyplot as plt
for col in ['age', 'income']:
plt.figure(figsize=(6,3))
plt.hist(reference[col].dropna(), bins=40, alpha=0.5, label='reference', density=True)
plt.hist(current[col].dropna(), bins=40, alpha=0.5, label='current', density=True)
plt.title(f'Comparação de distribuições: {col}')
plt.legend()
plt.tight_layout()
plt.show()
Passo 5: Lidar com drift detectado
Se o drift for significativo, ações possíveis: re-treinar o modelo com dados atualizados, aplicar re-calibração, usar normalizações/transformações que sejam estáveis (p.ex. quantile transformer treinado na referência), ou implementar monitorização contínua. Prioriza features com maior importância no modelo e quantifica o impacto reavaliando métricas de desempenho (AUC, RMSE, etc.). Em muitas equipas, um workflow prático é: 1) alertar se 1-3 features críticas têm drift; 2) correr teste A/B ou retreinar em janela móvel; 3) validar performance em validação holdout.
Verificar o resultado
Confirma que o pipeline funciona ao observar o DataFrame de resultados: colunas com 'drift' = True identificam features com p-valor < alpha. Valida visualmente com os histogramas; se ambos indicam mudança, há drift real. Testa com diferentes seeds/amostras e verifica a estabilidade do teste: por vezes pequenas amostras produzem p-valores instáveis. Considera também métricas de magnitude do drift (ks_stat) para priorizar intervenções: valores próximos de 0.1 são fracos, acima de 0.2-0.3 já são alterações relevantes em muitos contextos.
Conclusão
O teste Kolmogorov-Smirnov é uma ferramenta simples e eficaz para detetar desvios de distribuição em features numéricas e ajudar a decidir se um modelo precisa de manutenção. Para produção, automatiza este check, define thresholds por feature e combina com testes para variáveis categóricas (ex.: Chi-square) e com monitorização de performance do modelo. Dica prática: começa por monitorizar as 5-10 features com maior importance no modelo e regista métricas como ks_stat e p_value ao longo do tempo para identificar tendências antes de agir.