Agrupando e Resumindo

Autor

Douglas Braga

A seção anterior fechou com alugueis inteira, as 10.692 linhas mantidas, e uma coluna nova, aluguel_por_m2, dividindo o aluguel pela área de cada imóvel. O que falta é olhar para essa tabela não imóvel a imóvel, mas cidade a cidade: quanto custa alugar em cada uma, o que muda de uma para outra, e como trazer para dentro dela o que outras tabelas sabem sobre essas cinco cidades.

alugueis = pd.read_csv("dados/alugueis.csv", na_values=["-"])
alugueis["andar"] = alugueis["andar"].fillna(0)
alugueis["aluguel_por_m2"] = alugueis["aluguel"] / alugueis["area_m2"]
alugueis.shape
(10692, 14)

Duas formas de resumir, dois tipos de coluna

describe() resume o que é contínuo — média, desvio, quartis:

alugueis.describe()
area_m2 quartos banheiros vagas andar condominio aluguel iptu seguro_incendio total aluguel_por_m2
count 10692.000000 10692.000000 10692.000000 10692.000000 10692.000000 1.069200e+04 10692.000000 10692.000000 10692.000000 1.069200e+04 10692.000000
mean 149.217920 2.506079 2.236813 1.609147 5.067995 1.174022e+03 3896.247194 366.704358 53.300879 5.490487e+03 33.426097
std 537.016942 1.171266 1.407198 1.589521 6.069050 1.559231e+04 3408.545518 3107.832321 47.768031 1.648473e+04 22.831340
min 11.000000 1.000000 1.000000 0.000000 0.000000 0.000000e+00 450.000000 0.000000 3.000000 4.990000e+02 0.125668
25% 56.000000 2.000000 1.000000 0.000000 1.000000 1.700000e+02 1530.000000 38.000000 21.000000 2.061750e+03 18.571429
50% 90.000000 2.000000 2.000000 1.000000 3.000000 5.600000e+02 2661.000000 125.000000 36.000000 3.581500e+03 26.666667
75% 182.000000 3.000000 3.000000 2.000000 8.000000 1.237500e+03 5000.000000 375.000000 68.000000 6.768000e+03 40.714286
max 46335.000000 13.000000 10.000000 12.000000 301.000000 1.117000e+06 45000.000000 313700.000000 677.000000 1.120000e+06 300.000000

cidade não é uma dessas colunas. É categórica, e a pergunta que cabe a ela não é “qual é a média”, é “quantos de cada”:

alugueis["cidade"].value_counts()
cidade
São Paulo         5887
Rio de Janeiro    1501
Belo Horizonte    1258
Porto Alegre      1193
Campinas           853
Name: count, dtype: int64
round(100 * alugueis["cidade"].value_counts()["São Paulo"] / len(alugueis), 1)
np.float64(55.1)

São Paulo sozinha responde por 55,1% dos 10.692 imóveis — mais da metade da tabela é de uma cidade só. É a mesma distinção que a seção 6.1 fez entre dado quantitativo e qualitativo: describe() não teria o que fazer com cidade, e value_counts() não teria o que fazer com aluguel.

Mediana contra média, cidade por cidade

A seção anterior deixou de propósito o imóvel de R$ 45.000 mensais e o condomínio de R$ 1.117.000 dentro de alugueis, porque descartá-los seria decidir por um lado sem prova. O preço desse adiamento aparece agora: qualquer média calculada sobre aluguel carrega esses valores junto.

mediana_media = alugueis.groupby("cidade")["aluguel"].agg(["median", "mean"]).round(2)
mediana_media
median mean
cidade
Belo Horizonte 2300.0 3664.13
Campinas 1500.0 2364.29
Porto Alegre 1650.0 2337.70
Rio de Janeiro 2300.0 3232.90
São Paulo 3400.0 4652.79
(mediana_media["mean"] > mediana_media["median"]).all()
np.True_

Nas cinco cidades, sem exceção, a média fica acima da mediana. Em São Paulo, a cidade com mais imóveis na tabela, a distância é de R$ 1.252,79 — mediana de R$ 3.400 contra média de R$ 4.652,79:

round(mediana_media.loc["São Paulo", "mean"] - mediana_media.loc["São Paulo", "median"], 2)
np.float64(1252.79)

R$ 1.252,79 de diferença, numa cidade só. É por isso que o resumo por cidade, daqui em diante, usa a mediana: ela não se importa com os poucos imóveis que puxam a média para cima.

Uma tabela por cidade

agg com várias funções e várias colunas de uma vez produz, numa chamada, o que seria preciso montar coluna a coluna: contagem, mediana do aluguel e mediana do aluguel por metro quadrado, para cada cidade.

resultado = alugueis.groupby("cidade").agg(
    contagem=("aluguel", "count"),
    aluguel_mediano=("aluguel", "median"),
    aluguel_por_m2_mediano=("aluguel_por_m2", "median"),
).round(2).sort_values("aluguel_por_m2_mediano")
resultado
contagem aluguel_mediano aluguel_por_m2_mediano
cidade
Campinas 853 1500.0 18.33
Belo Horizonte 1258 2300.0 20.00
Porto Alegre 1193 1650.0 22.22
Rio de Janeiro 1501 2300.0 29.91
São Paulo 5887 3400.0 30.56

A coluna contagem é o mesmo número que value_counts() já tinha mostrado, agora ao lado do preço. Ordenada pelo aluguel por metro quadrado, a tabela por si só responde onde o metro quadrado é mais barato e onde é mais caro:

fig, ax = plt.subplots()
ax.barh(resultado.index, resultado["aluguel_por_m2_mediano"])
ax.set_xlabel("aluguel por m² (R$, mediana)")
plt.tight_layout()
plt.show()
Figura 32.1: Mediana do aluguel por m² em cada cidade, da mais barata à mais cara

Juntando outras tabelas: o merge que não pode multiplicar linha

alugueis não sabe em que estado cada cidade fica, nem quantas pessoas vivem lá — isso está em dados/cidades.csv e dados/estados.csv. Um merge costura essas tabelas pela coluna que elas têm em comum, e é exatamente aí que mora um erro fácil de cometer e fácil de não perceber: se a chave do merge se repetir do lado que está sendo trazido, cada repetição gera uma cópia da linha original. A tabela cresce, e nada na tela avisa — o merge roda, devolve algo, e o algo é silenciosamente errado. A defesa é olhar o shape antes e depois de todo merge, sempre.

cidades = pd.read_csv("dados/cidades.csv")
shape_antes = alugueis.shape
alugueis = alugueis.merge(cidades, on="cidade")
shape_antes, alugueis.shape
((10692, 14), (10692, 16))

De (10692, 14) para (10692, 16): as mesmas 10.692 linhas, com sigla e regiao a mais. cidades tem uma linha por cidade — a chave não se repete do lado trazido, e por isso o merge não multiplicou nada.

dados/estados.csv tem mais colunas que isso — nome do estado, taxa de homicídios —, mas só a população interessa a uma tabela de aluguéis, e é só ela que entra no merge:

estados = pd.read_csv("dados/estados.csv")[["sigla", "populacao"]]
shape_antes = alugueis.shape
alugueis = alugueis.merge(estados, on="sigla")
shape_antes, alugueis.shape
((10692, 16), (10692, 17))

De novo, a contagem de linhas não muda: (10692, 16) vira (10692, 17), só com populacao a mais. estados também tem uma linha por sigla, então cada linha de alugueis encontra exatamente uma correspondência do outro lado.

Nenhum dos dois merge multiplicou linha — mas isso não quer dizer que a chave usada identifique a cidade. sigla identifica o estado, e duas das cinco cidades, São Paulo e Campinas, dividem o mesmo estado:

alugueis.loc[alugueis["cidade"].isin(["São Paulo", "Campinas"]), ["cidade", "sigla", "populacao"]].drop_duplicates()
cidade sigla populacao
0 São Paulo SP 45973194
11 Campinas SP 45973194

As duas linhas trazem populacao igual — 45.973.194 —, porque é a população do estado de São Paulo inteiro, não da cidade. Um merge correto pela contagem de linhas ainda pode juntar a coisa errada com a coisa certa: populacao, a partir daqui, descreve o estado onde o imóvel está, não o município.

Um merge de muitos-para-um — muitas linhas de um lado, uma correspondência só do outro — não muda o número de linhas: cada linha do lado “muitos” encontra exatamente uma parceira e sai dali com as colunas novas coladas. Se a contagem de linhas depois do merge for maior do que antes, a chave se repete no lado que deveria ser único, e cada repetição multiplicou uma linha que já existia. É por isso que conferir o shape antes e depois não é cautela de sobra: é o único jeito de saber, olhando, se o merge fez o que parecia fazer.

São Paulo e Campinas mostram o outro lado do mesmo cuidado: o merge pela sigla não multiplicou nenhuma linha — e mesmo assim juntou uma informação de estado a uma tabela de cidades, fazendo duas cidades bem diferentes em tamanho carregarem o mesmo número de população. A contagem de linhas certa não garante que a coluna trazida signifique o que parece significar para cada linha.

alugueis termina esta seção com 10.692 linhas e 17 colunas — a mesma tabela do início, mais o que cidades.csv e estados.csv acrescentaram. Como cada .qmd roda no seu próprio kernel, a próxima seção volta a ler dados/alugueis.csv do zero, sem essas colunas: o que decide o que vira X e y não depende de estado nem de população.