(+351) 21 24 10006  ·  info@bconcepts.pt
Carnaxide, Lisboa

Como detetar e remover outliers em Apache Spark: passo a passo

João Barros 03 de September de 2026 6 min de leitura

Este tutorial mostra como detetar e remover outliers em Apache Spark para melhorar a qualidade dos dados e o desempenho de modelos preditivos. Vamos explicar o porquê das abordagens IQR e Z-score, quando cada uma é mais apropriada e como aplicar essas técnicas de forma escalável em PySpark para conjuntos de dados com centenas de milhares ou milhões de linhas.

Pré-requisitos

  • Instalação do Spark e PySpark disponível (local ou cluster). Recomenda-se Spark 3.x para melhores funções e desempenho.
  • Ficheiro CSV ou Parquet com colunas numéricas para análise; para conjuntos de dados grandes (ex.: 1M–50M linhas) use Parquet para leitura mais rápida e menor I/O.
  • Noções básicas de DataFrame e funções do PySpark. Conhecimento básico sobre médias, desvio-padrão e percentis ajuda a interpretar resultados.
  • Configurações de recursos: se tiveres 10 executores com 4 cores cada, a aproximação de quantis (approxQuantile) é normalmente rápida; se trabalharem com 100M+ linhas ajusta o parâmetro relativeError.

Passo 1: Carregar os dados no PySpark

Começamos por ler os dados para um DataFrame. Usa Parquet sempre que possível (mais rápido e mantém tipos). Se carregares CSV, especifica schema para evitar inferência custosa. Verifica e trata nulls antes de calcular estatísticas — por exemplo, em muitos cenários 0.1–2% das linhas podem ter nulls em colunas numéricas e convém decidir se preenches ou descartas.

from pyspark.sql import SparkSession
from pyspark.sql.functions import col

spark = SparkSession.builder.appName("outliers-example").getOrCreate()
df = spark.read.csv("/caminho/dados.csv", header=True, inferSchema=True)
# Selecionar colunas numéricas de interesse
numeric_cols = ["valor1", "valor2"]
df = df.select([col(c) for c in numeric_cols])
df.printSchema()

Exemplo prático: num conjunto de dados com 5M linhas, df.count() pode demorar alguns segundos a minutos dependendo do cluster; evita count() desnecessários em produção.

Passo 2: Detetar outliers com IQR (Interquartile Range)

IQR é robusto a valores extremos e não assume normalidade. Calcula Q1 e Q3 por coluna, define limites [Q1 - 1.5*IQR, Q3 + 1.5*IQR] (1.5 é a regra clássica) e marca linhas com valores fora desses limites. Para conjuntos de dados muito grandes usa approxQuantile com relativeError tipicamente entre 0.01 e 0.001. Por exemplo, com 10M linhas e relativeError=0.01 normalmente obtemos quantis dentro de 1%.

from pyspark.sql.functions import expr

# Calcular quantis aproximados (mais rápido para grandes dados)
quantiles = df.approxQuantile(numeric_cols, [0.25, 0.75], 0.01)
# quantiles é lista de pares [q1, q3] por coluna
bounds = {}
for i, col_name in enumerate(numeric_cols):
    q1, q3 = quantiles[i]
    iqr = q3 - q1
    lower = q1 - 1.5 * iqr
    upper = q3 + 1.5 * iqr
    bounds[col_name] = (lower, upper)

# Criar expressão para filtrar outliers por coluna
outlier_cond = " OR ".join([f"{c} < {bounds[c][0]} OR {c} > {bounds[c][1]}" for c in numeric_cols])
outliers_iqr = df.filter(expr(outlier_cond))
non_outliers_iqr = df.filter(~expr(outlier_cond))

Na prática, em muitos casos apenas 0.1%–5% dos registos são marcados como outliers com IQR; esse valor depende do domínio (ex.: sensores têm mais ruído que transacções financeiras).

Passo 3: Detetar outliers com Z-score (assunção de normalidade)

Z-score é útil quando a distribuição é aproximadamente normal. Calcula média e desvio-padrão, transforma valores em Z e marca pontos com |Z| > threshold. Um threshold comum é 3 (aproximadamente 0.3% dos pontos numa distribuição normal). Atenção: se o desvio-padrão for zero, Z-score não é aplicável — normalmente implica coluna constante ou dados incorrectos.

from pyspark.sql.functions import mean, stddev

stats = df.agg(*[mean(c).alias(c+"_mean") for c in numeric_cols], *[stddev(c).alias(c+"_std") for c in numeric_cols]).collect()[0]

z_exprs = []
threshold = 3.0
for c in numeric_cols:
    mu = stats[c+"_mean"]
    sigma = stats[c+"_std"] if stats[c+"_std"] is not None else 0.0
    if sigma == 0.0:
        # Não é possível calcular Z-score se desvio for zero; evitar divisão por zero
        z_exprs.append(f"false")
    else:
        z_exprs.append(f"abs(({c} - {mu}) / {sigma}) > {threshold}")

outlier_cond_z = " OR ".join(z_exprs)
outliers_z = df.filter(expr(outlier_cond_z))
non_outliers_z = df.filter(~expr(outlier_cond_z))

Comparar IQR vs Z-score: em distribuições com caudas longas o IQR tende a marcar menos pontos falsos; com distribuições quase gaussianas Z-score é sensível e interpretável (|Z|>3).

Passo 4: Escolher estratégia e remover outliers

Decide se queres remover todos os outliers identificados por qualquer método ou usar apenas um. Estratégias comuns: (a) remover pela união de métodos (mais conservador), (b) remover pela intersecção (apenas se ambos concordarem), (c) aplicar clipping ou winsorization em vez de apagar linhas. Por exemplo, se removes 2% de 1M de linhas ficas com 980k; essa perda pode ser aceitável, mas se removes 20% convém rever thresholds.

# Remover outliers IQR (exemplo)
df_clean = non_outliers_iqr
# Alternativa: remover união de outliers IQR e Z-score
# outliers_union = outliers_iqr.union(outliers_z).dropDuplicates()
# df_clean = df.join(outliers_union, on=numeric_cols, how='left_anti')

# Gravar resultado
df_clean.write.mode("overwrite").parquet("/caminho/dados_clean.parquet")

Se preferires não perder registos, podes aplicar clipping: por exemplo truncar valores abaixo do lower para lower e acima do upper para upper, o que mantém o número de linhas mas reduz o impacto dos extremos.

Verificar o resultado

Confirma que os outliers foram removidos mostrando contagens e estatísticas antes/depois. Observa percentis chave (1%, 50%, 99%) para confirmar redução de extremos. É importante validar também o impacto no modelo: treina o modelo antes e depois e compara métricas (RMSE, AUC, etc.) — às vezes a remoção melhora desempenho, noutras ocasiões podes perder sinal útil.

print("Total original:", df.count())
print("Total limpo:", df_clean.count())
# Estatísticas antes e depois
print("Estatísticas originais:")
df.describe().show()
print("Estatísticas limpas:")
df_clean.describe().show()
# Ver quantis para confirmar remoção de extremos
print("Quantis limpos:", df_clean.approxQuantile(numeric_cols, [0.01, 0.5, 0.99], 0.01))

Conclusão

Apagaste ou trataste outliers usando IQR e Z-score em Apache Spark. Estas técnicas ajudam a melhorar análises e modelos quando aplicadas com cuidado. Próximos passos práticos: experimentar thresholds (ex.: 1.5→3 para IQR, 3→4 para Z), testar clipping vs remoção, e validar impacto no teu modelo com dados de validação. Dica: regista quantos registos são removidos e mantém uma cópia dos outliers para investigação posterior.