Pular para o conteúdo

    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:

    intermediario/cap25_dados_ausentes.pylinhas 10 a 13
    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))
    
    Saída
    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:

    intermediario/cap25_dados_ausentes.pylinhas 18 a 19
    print(np.isnan(x), np.isnan(x).sum())
    print(np.nan == np.nan, np.isnan(np.nan))
    
    Saída
    [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:

    intermediario/cap25_dados_ausentes.pylinhas 24 a 27
    limpo = x[~np.isnan(x)]
    preenchido = np.where(np.isnan(x), np.nanmean(x), x)
    print(limpo, preenchido.round(2))
    print(x > 2)
    
    Saída
    [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:

    intermediario/cap25_dados_ausentes.pylinhas 32 a 36
    print(np.array([1, 2, np.nan]).dtype)
    try:
        np.array([1, 2, 3])[0] = np.nan
    except ValueError as erro:
        print(erro)
    
    Saída
    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:

    intermediario/cap25_dados_ausentes.pylinhas 41 a 45
    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())
    
    Saída
    [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 -999 ou 0 para "sem dado" são um perigo: se alguém esquecer de tratá-los, entram nas contas como números de verdade. Converta-os para nan (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.