No tópico anterior tivemos um intensivão sobre Pandas. Aqui não vai ser diferente, vamos usar bastante essa importante lib de Python para análise de dados. Iremos terminar o preparo dos nosso dados para enfim podermos criar mapas com nosso conjunto de dados. O post está GIGANTE.
Vamos iniciar pedindo um info de nosso conjunto de dados.
df_1o_turno.info()
# Retorna
# <class 'pandas.core.frame.DataFrame'>
# Int64Index: 72410 entries, 69664 to 55263
# Data columns (total 5 columns):
# SG_UF 72410 non-null object
# NM_MUNICIPIO 72410 non-null object
# CD_MUNICIPIO 72410 non-null int64
# NM_CANDIDATO 72410 non-null object
# QT_VOTOS_NOMINAIS 72410 non-null int64
# dtypes: int64(2), object(3)
# memory usage: 3.3+ MB
Vejamos, temos 72410 linhas, mas, novamente, o país tem 5570 municípios. Eu quero que cada linha do dataframe represente um único município, então vou precisar que meu dataframe mostre apenas, por linha, o candidato que obteve mais votos naquele município.
Mas primeiro, vamos entender porque isso acontece. Vamos realizar a busca por um município qualquer.
df_1o_turno[df_1o_turno['NM_MUNICIPIO'] == 'GOIÂNIA']

Observamos que a busca mostrou 117 linhas. Ué, se eu tenho 13 candidatos, porque 117 linhas foram mostradas?
Isso se deve ao fato da existência de zonas eleitorais. Municípios grandes apresentam mais de uma zona eleitoral, enquanto municípios menores podem apresentar somente uma zona eleitoral. Como eu quero apenas uma linha representando cada município, preciso me desfazer de todas as linhas excedentes. Primeiro, irei ter que me livrar da separação por zonas eleitorais, então irei agrupar o dataframe para que cada linha represente a quantidade de votos que cada candidato teve por município.
Aqui, irei apresentar uma função essencial do Pandas que é a função groupby. Irei pegar um exemplo do site da documentação pra ficar mais claro o que eu pretendo fazer:
>>> df = pd.DataFrame({'Animal': ['Falcon', 'Falcon',
... 'Parrot', 'Parrot'],
... 'Max Speed': [380., 370., 24., 26.]})
>>> df
Animal Max Speed
0 Falcon 380.0
1 Falcon 370.0
2 Parrot 24.0
3 Parrot 26.0
>>> df.groupby(['Animal']).mean()
Max Speed
Animal
Falcon 375.0
Parrot 25.0
Com a função groupby eu consigo agrupar meus dados e realizar algum tratamento matemático com a parte numérica dos dados. No caso acima, eu agrupei por Animal e pedi a média dos valores de 'Max Speed'. Irei fazer algo parecido no meu dataframe eleitoral, irei agrupar os candidatos usando uma lista de colunas e no final pedir a soma de votos de cada candidato por município:
df_1o_turno.groupby(['SG_UF', 'NM_MUNICIPIO', 'CD_MUNICIPIO', 'NM_CANDIDATO']).sum()

Observem que fiz algo parecido com o exemplo acima, dos animais. Agrupei meus dados por SG_UF, NM_MUNICIPIO, CD_MUNICIPIO e NM_CANDIDATO e somei todos os votos de cada candidato por município. Agora eu eliminei as linhas excedentes, que correspondiam às zonas eleitorais. Mas meu exemplo acima foi feito de forma demonstrativa, liberando um dataframe provisório. Meu dataframe original ainda não está agrupado, então o comando correto seria:
df_1o_turno = df_1o_turno.groupby(['SG_UF', 'NM_MUNICIPIO', 'CD_MUNICIPIO', 'NM_CANDIDATO']).sum().reset_index()
Adicionei um reset_index ao final. E notem o gif, meu dataframe agrupado não tem índice. Por meio da função reset_index eu crio um novo índice para meu dataframe agrupado. Após o agrupamento e o reset do índice, meu dataframe agora está assim:

Confirmamos que a interferência das zonas eleitorais foi eliminada agora com o groupby. Cada linha agora, por município, corresponde à quantidade de votos que cada candidato teve naquele município.
Agora eu preciso eliminar do dataframe os candidatos menos votados e deixar por município somente o candidato mais votado naquele local. Existe diversas formas de se fazer isso, mas uma bem prática é ordenar o dataframe pela quantidade de votos e eliminar as linhas abaixo da primeira linha. Para isso, irei usar a função sort_values, que irá ordenar pela quantidade de votos:
df_1o_turno.sort_values(by='QT_VOTOS_NOMINAIS', ascending=False, inplace=True) # ascending=False significa que a ordem será decrescente, caso fosse True, seria ordem crescente
Pedindo o head do dataframe ordenado:

Conseguimos ordenar, mas percebam que tem município duplicado ainda. São Paulo aparece 4 vezes, até por conta da população imensa. Mas quero que cada município apresente somente uma linha em nosso dataframe. Então irei pedir para o Pandas para eliminar as ocorrências duplicadas de municípios e deixar somente a primeira ocorrência. Agora que está tudo ordenado, só irá permanecer o candidato mais votado por município. Para isso, irei usar uma nova função chamada drop_duplicates, que recebe como argumentos um subset, que é a coluna que irá servir de alvo para a eliminação de dados duplicados, e o parâmetro keep recebe como argumento uma string que avisa a partir de qual linha irei dropar os dados duplicados. Por fim, criarei um novo dataframe chamado df_mais_votado_por_municipio:
df_mais_votado_por_municipio = df_1o_turno.drop_duplicates(subset='CD_MUNICIPIO', keep='first')
Visualizando agora nossos dados:

Vemos que agora somente uma ocorrência por município aparece em nosso dataframe. Perfeito. Pedindo o atributo shape de nosso dataframe, confirmamos que agora temos 5570 linhas e 5 colunas.
df_mais_votado_por_municipio.shape
# Retorna
# (5570, 5)
Pronto, o dataframe eleitoral está pronto. Agora iremos perceber que temos um problema. Eu preciso correlacionar o GeoDataFrame que possui o desenho de cada município com o DataFrame eleitoral.
Vamos observar os dados que temos nos nossos dois dataframes:

Vocês poderiam pensar que poderíamos correlacionar os dois dataframes pelo nome de município. Entretanto, essa abordagem é perigosa, haja vista que existem municípios com nomes duplicados em nosso país. Vamos pedir um dataframe só de municípios com nomes duplicados:
df_mais_votado_por_municipio[df_mais_votado_por_municipio.duplicated(subset='NM_MUNICIPIO')]
O resultado:

Vamos verificar um dos exemplos acima:
df_mais_votado_por_municipio[df_mais_votado_por_municipio['NM_MUNICIPIO'] == 'SANTA LUZIA']

Notem que só com o nome Santa Luzia há 4 municípios em 4 estados diferentes. Preciso usar outra abordagem para correlacionar os dois dataframes.
E a abordagem será por meio do código do município. O GeoDataFrame apresenta a coluna CD_GEOCMU, e o dataframe eleitoral apresenta a coluna CD_MUNICIPIO. O primeiro caso corresponde ao código do município no IBGE, e o segundo caso, ao código no TSE. Mas os dois códigos são distintos. Vamos verificar isso escolhendo um exemplo aleatório:
mapa_br[mapa_br['NM_MUNICIP'] == 'CAIUÁ']['CD_GEOCMU'].values[0] == df_mais_votado_por_municipio[df_mais_votado_por_municipio['NM_MUNICIPIO'] == 'CAIUÁ']['CD_MUNICIPIO'].values[0]
# Retorna
# False
Percebam que tentei comparar os dois códigos nos dois dataframes do município CAIUÁ, e os códigos não batem. Ou seja, código do IBGE é diferente do código no TSE. Preciso correlacionar os dois códigos para conseguir juntar meus dois dataframes e conseguir plotar meus mapas.
Por sorte, no GitHub do Estadão, há um repositório que correlacionou esses dados pra gente, e o link é este:
como-votou-sua-vizinhanca/data/votos at master · estadao/como-votou-sua-vizinhanca · GitHubO .csv acima apresenta essa correlação que precisamos. Irei criar um dataframe de equivalência e juntar nossos dois dataframes originais usando essa correlação do Estadão:
link_equivalencia_tse_ibge = 'h t t p s : / / r a w . g i t h u b u s e r c o n t e n t . c o m / e s t a d a o / c o m o - v o t o u - s u a - v i z i n h a n c a / m a s t e r / d a t a / v o t o s / c o r r e s p o n d e n c i a - t s e - i b g e . c s v'
df_equi_tse_ibge = pd.read_csv(link_equivalencia_tse_ibge)
Pedindo um head no dataframe de equivalência, vemos que há uma coluna do código no IBGE e outra do código no TSE:

Agora eu posso usar essa referência e dar "merge" dos meus dois dataframes nesse dataframe de equivalência, criando, enfim, nosso dataframe final.
Entretanto, pra dar merge em dois dataframes, uma das formas é escolher uma coluna que apresenta os mesmos dados nos dois e usar essa coluna como base. Por exemplo, se eu quero dar merge no dataframe de votação ao dataframe de equivalência acima, e fazer com que ACRELÂNDIA corresponda ao mesmo ACRELÂNDIA do dataframe de votação, eu uso como referência a coluna COD_TSE, e consigo correlacionar as duas linhas dos dois dataframes. Para isso, eu preciso que a coluna COD_TSE corresponda à coluna CD_MUNICIPIO no dataframe eleitoral, e que a coluna GEOCOD_IBGE corresponda à coluna CD_GEOCMU no dataframe dos mapas. Mudando os nomes das colunas para perfeita correlação:
df_mais_votado_por_municipio.rename(columns={'CD_MUNICIPIO': 'COD_TSE'}, inplace=True)
mapa_br.rename(columns={'CD_GEOCMU': 'GEOCOD_IBGE'}, inplace=True)
Nos nossos dois dataframes eu mudei os nomes das colunas para o mesmo nome do dataframe de equivalência. Agora ficou fácil dar merge. Vamos juntar o dataframe de votação ao dataframe de equivalência, usando a função merge, que recebe como argumento os dois dataframes e a coluna de referência para o merge:
df_final = pd.merge(df_equi_tse_ibge, df_mais_votado_por_municipio, on='COD_TSE')
Visualizando nosso df_final:

Percebam que juntei os dois dataframes num dataframe chamado de df_final. Mas tenho colunas que não me interessam aí, vamos dropá-las:
df_final.drop(columns=['chave', 'NOME', 'UF', 'AJUSTE'], inplace=True)
Agora preciso juntar o GeoDataFrame ao dataframe de equivalência. Mas temos um problema. Os dados de referência para o merge precisam ser do mesmo tipo. Vamos dar uma olhada nas duas colunas do código de IBGE dos dois conjuntos de dados:

Observem que no GeoDataFrame o tipo de GEOCOD_IBGE é object e no df_final é int64. Preciso que os dois tipos sejam iguais para o merge ter sucesso. Então, no mapa_br vou transformar os dados de GEOCOD_IBGE para int64, e por fim dar o merge:

Merge feito com sucesso. Mas novamente tenho colunas duplicadas, então vamos dropá-la:
df_final.drop(columns='NM_MUNICIP', inplace=True)
Meu df_final está quase perfeito para o plot dos mapas, mas tem um porém:

Observem o tipo do dataframe, pandas.core.frame.DataFrame. Para a plotagem correta dos mapas, eu preciso que meu conjunto de dados seja um GeoDataFrame e não um DataFrame. Não basta ter uma coluna geometry com figuras poligonais para o plot dos mapas, preciso que o tipo do conjunto de dados seja um GeoDataFrame, então iremos transformar de DF para GDF:
df_final = gpd.GeoDataFrame(df_final)
type(df_final)
# Retorna
# geopandas.geodataframe.GeoDataFrame
Uma última olhada em nosso conjunto de dados:

Ufa, agora temos um conjunto de dados pronto para a plotagem dos mapas. O próximo tópico será para a plotagem dos mapas, porque este já está gigantesco. Vou tentar trazer diversas abordagens de plot dos mapas. Até a parte 5, que talvez seja a última parte de nosso primeiro projeto.