Limpando e Transformando

Autor

Douglas Braga

A seção anterior fechou com aluguel já corretamente tipado como int64 e, mesmo assim, escondendo um imóvel isolado a R$ 45.000 — um problema que não é mais de tipo, mas do que os valores contêm. andar é o próximo caso, e não é bem o mesmo problema: o marcador "-" já virou nulo com na_values, então a coluna está tipada; o que falta decidir é o que fazer com esses nulos, com as linhas repetidas que ainda não foram olhadas, e com os valores extremos que describe() revela.

alugueis = pd.read_csv("dados/alugueis.csv", na_values=["-"])
alugueis.shape
(10692, 13)

Onde estão os nulos

Com andar já tipado, isna().sum() mostra o que sobrou:

alugueis.isna().sum()[lambda s: s > 0]
andar    2461
dtype: int64

andar, com 2.461 nulos — o mesmo número que a seção anterior contou como traços. Nenhuma outra coluna do arquivo tem valor faltando.

Nulo não é uma coisa só

O reflexo depois de ver uma coluna com nulo é dropna(). Antes de aplicá-lo, vale perguntar o que esse nulo está dizendo — e a própria tabela responde, se for comparada consigo mesma. condominio e area_m2 entre as linhas em que andar é nulo e as linhas em que não é:

alugueis.groupby(alugueis["andar"].isna())[["condominio", "area_m2"]].median()
condominio area_m2
andar
False 750.0 80.0
True 0.0 190.0

Nas linhas com andar nulo, a mediana de condominio é 0 e a de area_m2 é 190 m². Nas linhas com andar preenchido, a mediana de condominio sobe para 750 e a de area_m2 cai para 80 m². A proporção de imóveis sem condomínio nenhum aprofunda o mesmo padrão:

mascara = alugueis["andar"].isna()
zero_nulo = round(100 * (alugueis.loc[mascara, "condominio"] == 0).sum() / mascara.sum(), 1)
zero_preenchido = round(100 * (alugueis.loc[~mascara, "condominio"] == 0).sum() / (~mascara).sum(), 1)
zero_nulo, zero_preenchido
(np.float64(84.4), np.float64(3.6))

84,4% das linhas com andar nulo têm condominio igual a zero, contra 3,6% das linhas com andar preenchido. Um imóvel sem condomínio e maior que a mediana é o perfil de uma casa, não de um apartamento. O arquivo não traz uma coluna de tipo de imóvel — o que ele traz é esse padrão, repetido em duas variáveis diferentes, e é ele, não uma afirmação de cabeça, que sustenta a leitura de que andar nulo aqui não é o dado faltando: é o imóvel não ter andar, porque é uma casa. Vale a ressalva: isso é evidência forte, não prova — ninguém confere linha por linha se cada uma dessas 2.461 é mesmo uma casa. É a mesma leitura que se faz de qualquer dado sem rótulo direto: somar sinais indiretos até que só uma explicação os explique todos ao mesmo tempo.

Isso muda a decisão. Descartar essas 2.461 linhas jogaria fora informação que não estava ausente — o imóvel existe, o aluguel existe, só o andar não faz sentido para ele. Preencher com 0 é a escolha coerente com o que o dado significa: 0 é o andar de quem não tem andar, o térreo, que é exatamente o que uma casa é.

sem_nulos = alugueis.dropna()
com_zero = alugueis.fillna({"andar": 0})
alugueis.shape, sem_nulos.shape, com_zero.shape
((10692, 13), (8231, 13), (10692, 13))
round(100 * (alugueis.shape[0] - sem_nulos.shape[0]) / alugueis.shape[0])
23

dropna() sai de 10.692 para 8.231 linhas — 23% da tabela, por uma coluna em que o nulo nunca foi ausência. fillna({"andar": 0}) mantém as 10.692 e escreve, em número, a mesma coisa que o traço já dizia em texto. É essa a versão que fica:

alugueis["andar"] = alugueis["andar"].fillna(0)
alugueis["andar"].isna().sum()
np.int64(0)

dropna() e fillna() não são um mecânico melhor que o outro — são duas respostas a perguntas diferentes sobre o que o nulo significa. dropna() presume que a linha está incompleta e que o certo é não contar com ela. fillna(valor) presume que o nulo é, ele mesmo, informação, e escolhe o número que a representa. A segunda presunção só se sustenta quando alguém conhece o domínio dos dados o bastante para saber que “sem andar” e “casa” são a mesma coisa — o pandas não tem como adivinhar isso sozinho, e a linha fillna({"andar": 0}) é, por si só, muda sobre o motivo.

Duplicatas

alugueis.duplicated().sum()
np.int64(358)

358 linhas repetem, em todas as 13 colunas, uma linha que já apareceu antes. Contando também as primeiras ocorrências de cada grupo repetido, e como esses grupos se distribuem por tamanho:

repetidos = alugueis[alugueis.duplicated(keep=False)]
tamanhos = repetidos.groupby(list(alugueis.columns), dropna=False).size()

print("linhas repetidas:", len(repetidos))
print("grupos:", len(tamanhos))
print("pares:", (tamanhos == 2).sum())
print("de três ou mais:", (tamanhos >= 3).sum(), "grupos,", tamanhos[tamanhos >= 3].sum(), "linhas")
print("maior grupo:", tamanhos.max())
linhas repetidas: 604
grupos: 246
pares: 206
de três ou mais: 40 grupos, 192 linhas
maior grupo: 22

Dos 246 grupos, 206 são pares. Os outros 40 têm de três a 22 cópias, e são esses 40 grupos, sozinhos, que respondem pelas 192 linhas que sobram das 604. O maior — 22 anúncios idênticos em todas as 13 colunas — leva ao extremo a mesma pergunta que qualquer duplicata levanta: é um imóvel (ou um anúncio) republicado vinte e duas vezes, ou vinte e dois apartamentos realmente iguais? A tabela não traz o que decidiria entre as duas. Um exemplo, de um grupo menor:

repetidos.sort_values(["cidade", "area_m2"]).head(3)
cidade area_m2 quartos banheiros vagas andar aceita_animal mobiliado condominio aluguel iptu seguro_incendio total
340 Belo Horizonte 15 1 1 0 3.0 não sim 0 1100 0 15 1115
4384 Belo Horizonte 15 1 1 0 3.0 não sim 0 1100 0 15 1115
10425 Belo Horizonte 15 1 1 0 3.0 não sim 0 1100 0 15 1115

Três anúncios de Belo Horizonte, 15 m², um quarto, idênticos até o último real de condomínio e seguro-incêndio.

Uma linha repetida pode ser duas coisas bem diferentes: o mesmo imóvel anunciado mais de uma vez — pela imobiliária e pelo dono, ou reenviado depois de expirar —, ou dois apartamentos realmente iguais, no mesmo prédio, com a mesma planta e o mesmo condomínio. A tabela não guarda um identificador de anúncio nem um endereço, e sem isso não há como distinguir as duas situações a partir do que está nela. Remover as duplicatas presumiria a primeira explicação; mantê-las presumiria a segunda. Nenhuma das duas é sustentada pelo dado disponível — e é por isso que esta seção não decide: alugueis segue com as 10.692 linhas, duplicadas incluídas.

Outliers

alugueis[["area_m2", "condominio"]].describe()
area_m2 condominio
count 10692.000000 1.069200e+04
mean 149.217920 1.174022e+03
std 537.016942 1.559231e+04
min 11.000000 0.000000e+00
25% 56.000000 1.700000e+02
50% 90.000000 5.600000e+02
75% 182.000000 1.237500e+03
max 46335.000000 1.117000e+06

Em area_m2, a mediana é 90 m² contra um máximo de 46.335 m². Em condominio, a mediana é R$ 560 contra um máximo de R$ 1.117.000. Como razão entre máximo e mediana:

razao_area = round(alugueis["area_m2"].max() / alugueis["area_m2"].median())
razao_condominio = round(alugueis["condominio"].max() / alugueis["condominio"].median())
razao_area, razao_condominio
(515, 1995)

O máximo de area_m2 é 515 vezes a mediana da própria coluna; o de condominio, 1.995 vezes. E o topo de area_m2 não é um ponto isolado — os cinco maiores:

alugueis["area_m2"].sort_values(ascending=False).head(5).tolist()
[46335, 24606, 12732, 2000, 2000]

46.335, 24.606 e 12.732 m² são os três imóveis acima de dez mil metros quadrados. O quarto e o quinto maior já caem bem abaixo disso — e empatam entre si, os dois em exatos 2.000 m². Em condominio, o maior valor não é um ponto isolado no outro sentido: ele se repete.

alugueis[alugueis["condominio"] == alugueis["condominio"].max()]
cidade area_m2 quartos banheiros vagas andar aceita_animal mobiliado condominio aluguel iptu seguro_incendio total
255 Belo Horizonte 155 1 4 0 4.0 não não 1117000 2790 64 38 1120000
6979 Belo Horizonte 155 1 4 0 4.0 não não 1117000 2790 64 38 1120000

As duas linhas com condomínio de R$ 1.117.000 são o mesmo par que a seção de duplicatas já contou: mesma cidade, mesma área, mesmo aluguel, tudo igual — um imóvel (ou um anúncio) registrado duas vezes, não dois imóveis diferentes que coincidem em ter condomínio alto.

p99_area = round(alugueis["area_m2"].quantile(0.99))
p99_area
650

Noventa e nove por cento dos imóveis têm até 650 m².

fig, axs = plt.subplots(1, 2, figsize=(11, 4.5))

axs[0].boxplot(alugueis["area_m2"])
axs[0].set_title("todos os imóveis")
axs[0].set_ylabel("área (m²)")

axs[1].boxplot(alugueis["area_m2"])
axs[1].set_ylim(0, p99_area)
axs[1].set_title(f"eixo cortado no percentil 99 ({p99_area} m²)")

plt.tight_layout()
plt.show()
Figura 31.1: Área dos imóveis: o boxplot da esquerda comprime tudo perto de zero por causa dos poucos imóveis muito grandes; cortando o eixo no percentil 99, a distribuição do restante aparece

À esquerda, a caixa inteira do boxplot — onde mora metade dos imóveis — vira uma linha reta colada no zero: os imóveis de 46.335, 24.606 e 12.732 m² esticam o eixo inteiro para caber. À direita, com o eixo cortado em 650 m², a caixa aparece, e são esses mesmos imóveis — e outros acima do corte — que ficam fora do que o gráfico mostra.

Nenhuma dessas linhas sai da tabela. O imóvel de 46.335 m² e o condomínio de R$ 1.117.000 continuam em alugueis até o fim desta seção e da próxima: são o motivo pelo qual a seção seguinte, ao resumir valores por cidade, prefere a mediana à média — um valor deste tamanho desloca uma média inteira, e a mediana não se importa com ele.

Uma coluna derivada

alugueis["aluguel_por_m2"] = alugueis["aluguel"] / alugueis["area_m2"]
alugueis["aluguel_por_m2"].describe()
count    10692.000000
mean        33.426097
std         22.831340
min          0.125668
25%         18.571429
50%         26.666667
75%         40.714286
max        300.000000
Name: aluguel_por_m2, dtype: float64

aluguel_por_m2 divide, linha a linha, o aluguel pela área — uma conta que o arquivo não trazia pronta, e que agora é só mais uma coluna de alugueis. Ela também mostra o efeito de um outlier de área por um ângulo diferente: qual é o imóvel do menor valor da coluna, e de onde vem esse valor:

linha_minima = alugueis.loc[alugueis["aluguel_por_m2"].idxmin()]
round(linha_minima["aluguel_por_m2"], 2), round(linha_minima["area_m2"])
(np.float64(0.13), 12732)

R$ 0,13 por metro quadrado — não porque o aluguel seja baixo, mas porque a área é 12.732 m², um dos três imóveis extremos já vistos, grande o bastante para que qualquer aluguel divida em quase nada. É essa coluna que a próxima seção compara entre as cinco cidades.