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:
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])
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:
print(len(df), len(df.dropna()))
print(len(df.dropna(subset=["salario"])))
print(len(df.dropna(subset=["idade", "cidade"], how="all")))
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:
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])
[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:
print(df["cidade"].fillna("Não informada").tolist())
['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:
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)
[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:
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"]])
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:
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])
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ção | Resultado |
|---|---|
sum(), mean(), max() | Ignoram os ausentes (skipna=True) |
NaN == NaN | False: um ausente nunca é igual a nada |
NaN > 3 | False, e por isso o filtro "descarta" ausentes sem avisar |
Coluna de inteiros com NaN | Vira float64 (capítulo 3) |
mean() de uma coluna toda ausente | NaN |
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))
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.