Capítulo 25, Intermediário
Dados ausentes: NaN e arrays mascarados
Dados reais têm buracos: um sensor que falhou, uma pergunta não respondida. O NumPy os representa com `nan`, e você precisa saber como ele se comporta, porque ele contamina quase tudo.
Código deste capítulo: intermediario/cap25_dados_ausentes.py
O `nan` contamina
O nan ("não é um número") é um valor decimal especial. Qualquer conta que o envolva dá nan, e essa propagação é o que o torna perigoso: um único buraco estraga uma média inteira. As versões "nan-aware" (nansum, nanmean, nanmax...) ignoram os buracos:
import numpy as np
x = np.array([1.0, np.nan, 3.0, 4.0])
print(x.sum(), np.nansum(x), np.nanmean(x), np.nanmax(x))
nan 8.0 2.6666666666666665 4.0
Como detectar (e a pegadinha da igualdade)
O nan é diferente de si mesmo: nan == nan é falso. Por isso x == np.nan nunca encontra nada, e a detecção correta é com np.isnan:
print(np.isnan(x), np.isnan(x).sum())
print(np.nan == np.nan, np.isnan(np.nan))
[False True False False] 1
False True
Remover ou preencher
Duas estratégias comuns: descartar as posições com nan, ou preencher com um valor razoável (a média, a mediana, o valor anterior). Qual escolher é uma decisão sobre o seu problema, e não uma regra do NumPy. Descartar perde informação, e preencher a inventa:
limpo = x[~np.isnan(x)]
preenchido = np.where(np.isnan(x), np.nanmean(x), x)
print(limpo, preenchido.round(2))
print(x > 2)
[1. 3. 4.] [1. 2.67 3. 4. ]
[False False True True]
Repare no último resultado: uma comparação com nan dá sempre False, e por isso uma máscara como x > 2 descarta os buracos em silêncio, sem avisar.
Só decimais têm `nan`
Os inteiros não têm representação para "ausente". Um array de inteiros não pode guardar nan, e colocar um nan em uma lista de inteiros a converte para decimal:
print(np.array([1, 2, np.nan]).dtype)
try:
np.array([1, 2, 3])[0] = np.nan
except ValueError as erro:
print(erro)
float64
cannot convert float NaN to integer
Arrays mascarados
O np.ma oferece uma alternativa: um array com uma máscara que marca quais posições devem ser ignoradas, funcionando também para inteiros. Ele é útil quando os dados ausentes vêm marcados por um valor especial, como -999:
m = np.ma.masked_invalid(x)
print(m, m.mean(), m.mask)
sentinela = np.ma.masked_equal([1, -999, 3], -999)
print(sentinela, sentinela.mean())
[1.0 -- 3.0 4.0] 2.6666666666666665 [False True False False]
[1 -- 3] 2.0
O -- marca a posição ignorada, e a média considera só os valores válidos.
Valores sentinela são uma armadilha
Valores como
-999ou0para "sem dado" são um perigo: se alguém esquecer de tratá-los, entram nas contas como números de verdade. Converta-os paranan(ou mascare-os) na entrada dos dados, uma única vez, e não confie na memória de quem usar depois.
Exercício 1
Média por coluna ignorando buracos
Escreva media_por_coluna(m) que calcule a média de cada coluna ignorando os nan.