Detecção de Outliers em Python: Guia Estatístico com Pandas e Scikit-learn (2026)

Guia prático para detectar outliers em Python com pandas e scikit-learn: IQR, Z-score modificado, Isolation Forest e LOF, com código pronto para produção.

Detecção de Outliers em Python: Guia 2026

Atualizado: 22 de junho de 2026

A detecção de outliers em Python consiste em identificar observações cujos valores se afastam significativamente do comportamento típico do restante dos dados, usando métodos estatísticos como IQR e Z-score, ou algoritmos de aprendizado de máquina como Isolation Forest e Local Outlier Factor. Com pandas 2.2+ e scikit-learn 1.5, esse processo é rápido, reprodutível e estatisticamente rigoroso. Neste guia eu mostro o raciocínio por trás de cada método, quando aplicá-lo e como implementá-lo em poucas linhas de código.

  • Outliers não são erros por padrão. Antes de remover, decida se representam ruído de medição ou um sinal raro porém válido.
  • Para variáveis univariadas e aproximadamente simétricas, o método IQR de Tukey continua sendo o ponto de partida mais defensável em 2026.
  • Em distribuições assimétricas, prefira o Z-score modificado baseado em mediana e MAD, que resiste bem à contaminação.
  • Para dados multivariados ou de alta dimensão, use Isolation Forest ou Local Outlier Factor do scikit-learn.
  • Nunca remova outliers no conjunto de teste, pois o vazamento de dados invalida métricas de avaliação.
  • Sempre documente o critério usado: reprodutibilidade vence cosmética estatística.

O que são outliers e por que eles importam

Um outlier é uma observação que se desvia tanto das demais que levanta a suspeita de ter sido gerada por um mecanismo diferente. Essa definição clássica vem de Hawkins (1980) e ainda guia toda a literatura moderna sobre detecção de anomalias. É importante distinguir três conceitos que costumam aparecer juntos: outlier (ponto extremo em uma variável conhecida), anomalia (padrão que viola a estrutura do problema, geralmente multivariada) e novidade (observação legítima de uma classe ainda não vista).

Outliers afetam praticamente todo modelo paramétrico. A média e o desvio-padrão amostrais têm ponto de ruptura zero, ou seja, basta um único valor extremo para distorcê-los arbitrariamente. Regressão linear por mínimos quadrados, PCA e k-means herdam essa fragilidade. Por isso, antes de qualquer modelagem, faço uma triagem de outliers seguindo o protocolo: (1) inspeção visual, (2) detecção estatística univariada, (3) detecção multivariada quando há correlações relevantes e (4) decisão informada de remoção, transformação ou modelagem robusta.

Em 2026, a combinação pandas + scikit-learn cobre praticamente todos os métodos clássicos e modernos. Se você ainda não está familiarizado com a API de DataFrames moderna, recomendo ler antes o nosso guia de migração do Pandas para o Polars, que aborda diferenças semânticas importantes em operações de filtragem.

Método IQR (Intervalo Interquartil) com pandas

O método do Intervalo Interquartil, popularizado por John Tukey, define como outlier qualquer ponto fora de [Q1 − 1.5·IQR, Q3 + 1.5·IQR], onde IQR = Q3 − Q1. A grande vantagem é estatística: quartis têm ponto de ruptura de 25%, ou seja, até um quarto dos dados pode estar contaminado sem comprometer a estimativa dos limites. Para distribuições gaussianas, o intervalo cobre aproximadamente 99,3% da massa, o que define um limiar conservador adequado para a maioria dos pipelines analíticos.

import pandas as pd
import numpy as np

def iqr_bounds(series: pd.Series, k: float = 1.5) -> tuple[float, float]:
    """Retorna (limite_inferior, limite_superior) pelo método de Tukey."""
    q1, q3 = series.quantile([0.25, 0.75])
    iqr = q3 - q1
    return q1 - k * iqr, q3 + k * iqr

df = pd.read_csv("vendas_2026.csv")
low, high = iqr_bounds(df["ticket_medio"])
mask_outlier = (df["ticket_medio"] < low) | (df["ticket_medio"] > high)
print(f"Encontrados {mask_outlier.sum()} outliers em {len(df)} linhas")
df_clean = df.loc[~mask_outlier].copy()

Use k = 3.0 em vez de 1.5 quando precisar identificar apenas outliers extremos (a convenção em boxplots de Tukey). Para variáveis estritamente positivas com cauda longa, como receita, tempo de resposta ou latência, aplique a transformação np.log1p antes de calcular o IQR. Isso evita marcar como outlier valores que são apenas grandes em uma escala assimétrica.

Z-score modificado: a alternativa robusta para dados assimétricos

O Z-score modificado, proposto por Iglewicz e Hoaglin (1993), substitui a média pela mediana e o desvio-padrão pela Median Absolute Deviation (MAD), produzindo uma estatística robusta a contaminação. A fórmula é M_i = 0.6745 · (x_i − mediana) / MAD, onde a constante 0,6745 ajusta a escala para que MAD estime o desvio-padrão sob normalidade. O limiar canônico é |M_i| > 3.5.

import numpy as np
import pandas as pd
from scipy.stats import median_abs_deviation

def modified_zscore(series: pd.Series) -> pd.Series:
    median = series.median()
    mad = median_abs_deviation(series, scale="normal")  # já inclui 1/0.6745
    if mad == 0:
        # Fallback para MAD zero (constantes ou quase-constantes)
        return pd.Series(np.zeros(len(series)), index=series.index)
    return (series - median) / mad

df["mz"] = modified_zscore(df["latencia_ms"])
df["is_outlier"] = df["mz"].abs() > 3.5
print(df["is_outlier"].value_counts())

Honestamente, em meus projetos com séries de latência de API e métricas de fraude, o Z-score modificado consistentemente supera o Z-score clássico, porque a mediana não é puxada por valores extremos. O parâmetro scale="normal" do SciPy aplica o fator 1,4826 (= 1/0,6745) que torna MAD consistente com o desvio-padrão sob a hipótese de normalidade. É um detalhe que muitas implementações artesanais esquecem, e que já me custou algumas horas de depuração.

Isolation Forest com scikit-learn

Para dados multivariados, métodos univariados aplicados coluna a coluna falham: um ponto pode estar dentro do intervalo aceitável em cada eixo individualmente, mas ser um outlier conjunto pelo padrão de correlação. O Isolation Forest (Liu, Ting & Zhou, 2008) ataca esse problema construindo árvores que isolam pontos por divisões aleatórias. Outliers exigem menos divisões para serem isolados e recebem pontuações de anomalia mais altas.

from sklearn.ensemble import IsolationForest
import pandas as pd

features = ["valor_transacao", "frequencia_diaria", "tempo_sessao"]
X = df[features].dropna()

iso = IsolationForest(
    n_estimators=200,
    contamination=0.02,   # 2% esperado de outliers
    max_samples="auto",
    random_state=42,
    n_jobs=-1,
)
iso.fit(X)

# -1 = outlier, 1 = inlier
df.loc[X.index, "iso_label"] = iso.predict(X)
df.loc[X.index, "iso_score"] = iso.decision_function(X)
print(df["iso_label"].value_counts())

Três parâmetros merecem atenção. contamination define a fração esperada de outliers; se você não tem essa estimativa, use "auto", que aplica o limiar original do artigo. n_estimators=200 é um bom equilíbrio entre estabilidade e custo computacional para conjuntos com até alguns milhões de linhas. random_state é obrigatório para reprodutibilidade. Sem ele, dois runs consecutivos produzem rotulagens diferentes, o que vira pesadelo em revisão de código (peguei esse bug em produção uma vez, e não vou esquecer).

Local Outlier Factor (LOF) para densidade local

O Local Outlier Factor (Breunig et al., 2000) compara a densidade local de um ponto com a de seus k vizinhos mais próximos. É a melhor escolha quando o dataset tem aglomerados de densidades distintas, algo comum em dados de comportamento de usuário, sensores IoT e séries financeiras agregadas. Um ponto recebe LOF próximo de 1 se sua densidade é compatível com a vizinhança, e LOF significativamente maior que 1 indica isolamento relativo.

from sklearn.neighbors import LocalOutlierFactor
from sklearn.preprocessing import StandardScaler

scaler = StandardScaler()
Xs = scaler.fit_transform(X)

lof = LocalOutlierFactor(
    n_neighbors=20,
    contamination="auto",
    novelty=False,        # detecção em dados de treino
    n_jobs=-1,
)
labels = lof.fit_predict(Xs)
df.loc[X.index, "lof_label"] = labels
df.loc[X.index, "lof_score"] = -lof.negative_outlier_factor_

O parâmetro mais sensível é n_neighbors. Valores muito pequenos (≤ 5) tornam o LOF instável; valores muito grandes (≥ 50) aproximam o método de uma medida global e perdem a vantagem da densidade local. Como regra prática, comece com n_neighbors = max(20, log2(n)) e ajuste com base em curvas de score. Se você precisar avaliar dados novos em produção, treine com novelty=True e use predict() nas amostras de inferência. Esse é o modo de novelty detection documentado no guia oficial de detecção de outliers do scikit-learn.

Como remover outliers no Pandas sem vazar dados

O erro mais comum em pipelines de machine learning é calcular limiares de outliers no conjunto completo antes do split de treino/teste. Isso é data leakage: o conjunto de teste influencia indiretamente os parâmetros do filtro. A regra é simples: ajuste no treino, aplique no teste.

from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(
    df[features], df["target"], test_size=0.2, random_state=42, stratify=df["target"]
)

# Limiar IQR calculado APENAS no treino
low, high = iqr_bounds(X_train["valor_transacao"])
mask_train = X_train["valor_transacao"].between(low, high)
X_train_clean = X_train.loc[mask_train]
y_train_clean = y_train.loc[mask_train]

# Mesmos limiares aplicados ao teste, sem recalcular
mask_test = X_test["valor_transacao"].between(low, high)
X_test_clean = X_test.loc[mask_test]
y_test_clean = y_test.loc[mask_test]

Para integração com pipelines do scikit-learn, encapsule a lógica em um FunctionTransformer ou construa um transformador customizado herdando de BaseEstimator e TransformerMixin. Se você usa DuckDB para etapas pesadas de ETL, dê uma olhada em nosso guia de DuckDB com Python, onde mostro como aplicar filtros baseados em quantis diretamente em SQL antes de carregar para o pandas.

Visualização: boxplots, violin plots e scatter matrix

Antes de aplicar qualquer método automatizado, gosto de inspecionar visualmente as distribuições. Boxplots tornam o IQR explícito, violin plots revelam multimodalidade, e a scatter matrix evidencia outliers multivariados que escapam a análises univariadas.

import matplotlib.pyplot as plt
import seaborn as sns

fig, axes = plt.subplots(1, 2, figsize=(12, 4))
sns.boxplot(data=df, x="categoria", y="ticket_medio", ax=axes[0])
sns.violinplot(data=df, x="categoria", y="ticket_medio",
               inner="quartile", ax=axes[1])
axes[0].set_title("Boxplot por categoria")
axes[1].set_title("Violin plot por categoria")
plt.tight_layout()
plt.savefig("outliers_inspecao.png", dpi=150)

Para datasets com mais de duas variáveis, gere uma pair plot colorida pela rotulagem do Isolation Forest. Esse diagnóstico é o que separa um relatório profissional de uma análise apressada. Para projeção visual de outliers em alta dimensão, considere UMAP ou t-SNE, ambos disponíveis na biblioteca umap-learn, que se integra naturalmente ao pandas.

Quando NÃO remover outliers

Esta é a seção que poupa carreiras. Há três cenários em que remover outliers é estatisticamente incorreto. Primeiro, quando o outlier é o evento de interesse: em detecção de fraude, falhas mecânicas, doenças raras ou eventos extremos de mercado, remover anomalias significa apagar o sinal que você quer modelar. Segundo, em distribuições com cauda pesada genuína (lei de potência, log-normal com sigma alto): o que parece outlier sob a lente gaussiana é comportamento esperado da distribuição real. Terceiro, em amostras pequenas (n < 50): a remoção introduz viés desproporcional e infla artificialmente medidas de ajuste.

Nesses casos, prefira alternativas como transformação (log, Box-Cox, Yeo-Johnson via PowerTransformer do scikit-learn), winsorização (limita valores extremos a um percentil sem descartá-los) ou modelos robustos (HuberRegressor, RANSACRegressor, regressão quantílica). Documentar a decisão é tão importante quanto executá-la. Em revisões de papers e auditorias, "removi os outliers" sem critério explícito é o caminho mais curto para uma rejeição. A documentação oficial do pandas sobre dados ausentes também oferece orientações úteis sobre como tratar valores extremos via imputação controlada.

Perguntas frequentes

Qual o melhor método para detectar outliers em Python?

Depende da distribuição e da dimensionalidade. Para variáveis univariadas simétricas, comece com IQR. Para variáveis assimétricas, use Z-score modificado com MAD. Para problemas multivariados ou de alta dimensão, Isolation Forest e Local Outlier Factor do scikit-learn são as opções mais confiáveis em 2026.

Como remover outliers no Pandas sem distorcer o modelo?

Calcule os limiares apenas no conjunto de treino e aplique os mesmos valores ao conjunto de teste. Use series.between(low, high) para criar a máscara booleana e nunca recalcule percentis no teste, pois isso configura data leakage e invalida as métricas de avaliação.

Qual a diferença entre outlier e anomalia?

Outlier é um termo estatístico para observações extremas em uma ou poucas variáveis. Anomalia é mais amplo: refere-se a qualquer padrão que viole a estrutura esperada dos dados, geralmente em contextos multivariados ou temporais. Toda anomalia é um outlier em algum sentido, mas nem todo outlier é anômalo.

Posso usar Z-score em distribuições assimétricas?

O Z-score clássico baseado em média e desvio-padrão é frágil em distribuições assimétricas, porque ambas as estatísticas são puxadas pela cauda. Prefira o Z-score modificado de Iglewicz e Hoaglin, que usa mediana e MAD e mantém validade sob assimetria moderada a alta.

O Isolation Forest funciona em datasets pequenos?

O Isolation Forest precisa de pelo menos algumas centenas de observações para produzir scores estáveis. Em datasets com menos de 200 linhas, prefira métodos baseados em distância como LOF com n_neighbors reduzido, ou análises univariadas combinadas com inspeção visual via boxplot.

Dr. Elena Vasquez
Sobre o Autor Dr. Elena Vasquez

Data scientist with a PhD in computational statistics. Translates papers into pandas one notebook at a time.