Pular para o conteúdo

    Capítulo 9, Básico

    Dados ausentes

    Dado ausente não é um detalhe, é uma decisão: descartar a linha, preencher o valor ou deixar como está. Cada escolha muda o resultado, e uma escolha automática (o `dropna()` sem argumentos) esconde um estrago.

    Encontrar

    O isna() devolve uma máscara booleana, e somar a máscara conta os ausentes por coluna. O isnull() é o mesmo método com outro nome. A média da máscara dá a proporção:

    basico/cap09_ausentes.pylinhas 10 a 17
    import pandas as pd
    
    pd.set_option("display.width", 170)
    pd.set_option("display.max_columns", 20)
    df = pd.read_csv("dados/funcionarios_15.csv")
    
    print(df.isna().sum()[lambda s: s > 0])
    print((df.isna().mean() * 100).round(1)[lambda s: s > 0])
    
    Saída
    departamento    1
    cidade          2
    idade           1
    salario         2
    dtype: int64
    departamento     6.7
    cidade          13.3
    idade            6.7
    salario         13.3
    dtype: float64
    

    O perigo do `dropna()` sem argumentos

    Descartar toda linha que tenha qualquer ausente parece simples. Mas basta uma coluna problemática para apagar linhas que estavam perfeitas em todas as outras:

    basico/cap09_ausentes.pylinhas 22 a 24
    print(len(df), len(df.dropna()))
    print(len(df.dropna(subset=["salario"])))
    print(len(df.dropna(subset=["idade", "cidade"], how="all")))
    
    Saída
    15 9
    13
    15
    

    O dropna() descartou 6 de 15 funcionários (40% dos dados) por causa de buracos em colunas diferentes. O subset restringe a decisão às colunas que importam para a sua pergunta: se a análise é sobre salário, só os salários ausentes justificam descartar. E how="all" só descarta a linha se todas as colunas do subset estiverem vazias.

    Preencher: média ou mediana

    O fillna troca os ausentes por um valor. Para colunas numéricas, a mediana é quase sempre a escolha mais segura, porque valores extremos arrastam a média para longe do que é típico, e a mediana mal se mexe:

    basico/cap09_ausentes.pylinhas 29 a 33
    print(df["idade"].fillna(df["idade"].median()).tolist())
    
    salarios = pd.Series([3000, 3200, 3100, 2900, 50000, None])
    print(salarios.mean(), salarios.median())
    print(salarios.fillna(salarios.mean()).iloc[-1], salarios.fillna(salarios.median()).iloc[-1])
    
    Saída
    [24.0, 25.0, 26.0, 27.0, 28.0, 29.0, 27.0, 31.0, 32.0, 33.0, 24.0, 25.0, 26.0, 27.0, 28.0]
    12440.0 3100.0
    12440.0 3100.0
    

    Um único salário de 50 mil faz a média passar de 12 mil, e preencher o ausente com ela inventaria um salário que ninguém tem. A mediana (3.100) é uma estimativa muito mais honesta.

    Para colunas de texto, o preenchimento é um rótulo explícito, e não um cálculo:

    basico/cap09_ausentes.pylinha 35
    print(df["cidade"].fillna("Não informada").tolist())
    
    Saída
    ['São Paulo', 'Belo Horizonte', 'Porto Alegre', 'Rio de Janeiro', 'Não informada', 'São Paulo', 'Belo Horizonte', 'Porto Alegre', 'Rio de Janeiro', 'Curitiba', 'São Paulo', 'Não informada', 'Porto Alegre', 'Rio de Janeiro', 'Curitiba']
    

    Preencher para frente e para trás

    Em dados que têm ordem (uma série ao longo do tempo), o valor anterior (ffill) ou o seguinte (bfill) costuma ser uma boa estimativa. O antigo fillna(method="ffill") foi removido no Pandas 3:

    basico/cap09_ausentes.pylinhas 40 a 47
    medicao = pd.Series([20.0, None, None, 23.0, None])
    print(medicao.ffill().tolist())
    print(medicao.bfill().tolist())
    
    try:
        medicao.fillna(method="ffill")
    except TypeError as erro:
        print("TypeError:", erro)
    
    Saída
    [20.0, 20.0, 20.0, 23.0, 23.0]
    [20.0, 23.0, 23.0, 23.0, nan]
    TypeError: NDFrame.fillna() got an unexpected keyword argument 'method'
    

    Guardar a informação de que faltava

    Preencher apaga a evidência de que o dado não existia. Se a ausência pode ser informativa (quem não informou a idade pode ser diferente dos demais), guarde uma coluna indicadora antes de preencher:

    basico/cap09_ausentes.pylinhas 52 a 55
    tratado = df.copy()
    tratado["idade_informada"] = tratado["idade"].notna()
    tratado["idade"] = tratado["idade"].fillna(tratado["idade"].median())
    print(tratado.loc[[5, 6, 7], ["nome", "idade", "idade_informada"]])
    
    Saída
                 nome  idade  idade_informada
    5   Felipe Araújo   29.0             True
    6  Gabriela Nunes   27.0            False
    7  Henrique Costa   31.0             True
    

    O `inplace=True` e o encadeamento

    Você vai ver, em muito material, df["idade"].fillna(0, inplace=True). Esse padrão não funciona mais no Pandas 3: o df["idade"] é uma cópia temporária, e o inplace altera só ela. O Pandas avisa, e a tabela fica como estava:

    basico/cap09_ausentes.pylinhas 60 a 66
    import warnings
    
    teste = df.copy()
    with warnings.catch_warnings(record=True) as avisos:
        warnings.simplefilter("always")
        teste["idade"].fillna(0, inplace=True)
    print(int(teste["idade"].isna().sum()), [a.category.__name__ for a in avisos])
    
    Saída
    1 ['ChainedAssignmentError']
    

    A idade continua ausente (1 valor), e um aviso foi emitido. A forma correta é reatribuir: df["idade"] = df["idade"].fillna(0). Eu evito o inplace em geral: ele não economiza memória na prática e quebra o encadeamento de métodos (capítulo 30).

    Como os ausentes se comportam

    OperaçãoResultado
    sum(), mean(), max()Ignoram os ausentes (skipna=True)
    NaN == NaNFalse: um ausente nunca é igual a nada
    NaN > 3False, e por isso o filtro "descarta" ausentes sem avisar
    Coluna de inteiros com NaNVira float64 (capítulo 3)
    mean() de uma coluna toda ausenteNaN
    basico/cap09_ausentes.pylinhas 71 a 73
    import numpy as np
    
    print(np.nan == np.nan, pd.Series([1.0, None, 3.0]).sum(), pd.Series([1.0, None, 3.0]).sum(skipna=False))
    
    Saída
    False 4.0 nan
    

    Exercício 1

    Preencher sem mutar

    Escreva preencher_idade(tabela), que devolva uma cópia com a idade preenchida pela mediana e uma coluna idade_informada, sem alterar a tabela recebida. Confira que o original ainda tem 1 ausente.