Do projeto mesmo, até o momento fizemos a obtenção do mapa do Brasil por meio da base de dados do IBGE. Os dados geográficos estão em formato shapefile, e com o uso do shapefile conseguimos plotar o mapa do país usando a lib GeoPandas.
Agora iremos fazer um bom uso da lib Pandas, pois iremos baixar a base de dados (do site do TSE) dos votos da última eleição. Como o site e política não se misturam, no meio do passo a passo iremos trocar nomes de candidatos por nomes genéricos. Claro que quem quer bullshitar sobre política vai tentar de um jeito ou de outro, então qualquer possível comentário sobre política será apagado sem dó nem piedade

Então, precisarei da lib Pandas. Até o momento, nosso projeto tem os seguintes imports:
# Importando libs
%matplotlib inline
import matplotlib.pyplot as plt
import geopandas as gpd
import pandas as pd
Da mesma forma que usei "alias" ou apelidos para matplotlib e geopandas, usei agora para pandas, chamando o módulo como "pd".
A base de dados está dentro do site do TSE (
Tribunal Superior Eleitoral):
Acima tem todo o caminho de navegação necessário para baixarmos o arquivo do link "Votação nominal por município e zona (formato ZIP)", ele é um zip com dados da votação por estado e os dados do país inteiro. Percebam que no site do TSE tem todo o histórico de todas as eleições feitas no país até então. Dá pra brincar de reproduzir o mapa do nosso projeto com dados de eleições passadas como forma de treino.

Baixado e deszipado nosso arquivo, aqui no meu computador eu criei uma pasta chamada "votacao" e adicionei o zip nela. Diversos arquivos no formato .csv irão ser deszipados nesta pasta. O que me interessa aqui é o "votacao_candidato_munzona_2018_BR.csv".
Arquivos .csv são um formato de arquivo bastante usado para análise de dados, e significam "comma separated values", ou arquivos separados por vírgulas. Aqui um pouco mais sobre csv a título de curiosidade
Comma-separated values - WikipediaAssim como carreguei a base de dados dos mapas usando o GeoPandas, irei carregar a base e dados eleitorais usando Pandas:
# Carregando a massa de dados de votação
df = pd.read_csv('votacao/votacao_candidato_munzona_2018_BR.csv', sep=';', encoding='latin1')
df.head(10).T
Aqui algumas informações a mais. No carregamento dos dados, às vezes precisamos dizer o separador dos dados para o Pandas. No caso dessa base de dados eleitorais, o separador é um ponto e vírgula, ou seja, cada dado está separado por um ponto e vírgula na base de dados, e preciso dizer para o Pandas que o separador é esse para ele criar corretamente nosso DataFrame. Também preciso dizer qual foi a codificação usada para a criação do arquivo, e no caso dessa base de dados foi usada a codificação "latin1". Caso eu não informe isso, o pandas irá gerar um erro ao parsear os dados e verificar que a codificação que ele espera não é a que está lá.
Assim como no GeoPandas, posso usar a função do DataFrame "head" para verificar as primeiras linhas do dataframe. Mas nossa base de dados têm muitas colunas, então eu teria que usar o scroll para a direita para ver todas as colunas (tentem aí). Para visualizar dados com muitas colunas no Pandas às vezes é mais interessante usar a forma transposta dos dados, na qual colunas viram linhas e linhas viram colunas. O gif abaixo mostra as duas formas de visualização:

Percebam que a forma transposta facilita a visualização de dados com muitas colunas.
Podemos listar todas as colunas de nosso dataframe chamando o atributo "columns" de um dataframe do Pandas:
df.columns
# Retorna
# Index(['DT_GERACAO', 'HH_GERACAO', 'ANO_ELEICAO', 'CD_TIPO_ELEICAO',
# 'NM_TIPO_ELEICAO', 'NR_TURNO', 'CD_ELEICAO', 'DS_ELEICAO', 'DT_ELEICAO',
# 'TP_ABRANGENCIA', 'SG_UF', 'SG_UE', 'NM_UE', 'CD_MUNICIPIO',
# 'NM_MUNICIPIO', 'NR_ZONA', 'CD_CARGO', 'DS_CARGO', 'SQ_CANDIDATO',
# 'NR_CANDIDATO', 'NM_CANDIDATO', 'NM_URNA_CANDIDATO',
# 'NM_SOCIAL_CANDIDATO', 'CD_SITUACAO_CANDIDATURA',
# 'DS_SITUACAO_CANDIDATURA', 'CD_DETALHE_SITUACAO_CAND',
# 'DS_DETALHE_SITUACAO_CAND', 'TP_AGREMIACAO', 'NR_PARTIDO', 'SG_PARTIDO',
# 'NM_PARTIDO', 'SQ_COLIGACAO', 'NM_COLIGACAO', 'DS_COMPOSICAO_COLIGACAO',
# 'CD_SIT_TOT_TURNO', 'DS_SIT_TOT_TURNO', 'ST_VOTO_EM_TRANSITO',
# 'QT_VOTOS_NOMINAIS'],
# dtype='object')
Cada elemento da lista acima é uma coluna de nosso dataframe.
Uma das colunas é a que diz qual o turno da eleição. Escolhi, em nosso projeto, usar os dados do primeiro turno. Então vou filtrar nosso dataframe somente pelas linhas correspondentes a dados do primeiro turno e excluir valores de segundo turno.
Algo bem comum em Pandas é a chamada de uma coluna (ou uma Series) pelo nome da coluna, da seguinte forma:
df['NR_TURNO']
O comando acima irá mostrar duas colunas, uma com o índice e a outra com todos os dados da coluna, ou seja, 1 ou 2, 1 correspondendo ao primeiro turno e 2 ao segundo turno.
Dessa forma, eu posso selecionar somente os dados nos quais NR_TURNO == 1, dessa forma:
df[df['NR_TURNO'] == 1]
Fazendo isso, o Pandas irá mostrar nosso DataFrame somente com as linhas nos quais NR_TURNO for igual a 1. Dessa forma, eu posso criar um novo dataframe a partir desses valores, o que excluirá valores de segundo turno:
# Selecionando somente as linhas correspondentes ao primeiro turno
df_1o_turno = df[df['NR_TURNO'] == 1]
df_1o_turno.sample(4).T
A função sample retorna uma amostra selecionada de forma aleatória. No caso acima, eu pedi uma amostra de 4 itens do novo dataframe usando a forma transposta dos dados:

Percebam que os 4 itens da nossa amostragem apresentam NR_TURNO == 1.
Eu acho que tem colunas demais em nosso dataframe que são inúteis, que não importam pra gente agora, para nossa plotagem do mapa de resultado. Vou mostrar duas formas de sumir com essas colunas desnecessárias, uma é usando a função drop e a outra criando um novo dataframe filtrado como feito acima, mas selecionando várias colunas:
df.drop(columns=['DT_GERACAO', 'HH_GERACAO', 'NM_TIPO_ELEICAO', 'CD_ELEICAO', 'TP_ABRANGENCIA', 'CD_CARGO', 'SQ_CANDIDATO', 'NR_CANDIDATO', 'NM_URNA_CANDIDATO', 'NM_SOCIAL_CANDIDATO', 'CD_SITUACAO_CANDIDATURA', 'DS_SITUACAO_CANDIDATURA', 'CD_DETALHE_SITUACAO_CAND', 'TP_AGREMIACAO', 'NR_PARTIDO', 'SQ_COLIGACAO', 'NM_COLIGACAO', 'DS_COMPOSICAO_COLIGACAO', 'CD_SIT_TOT_TURNO', 'DS_SIT_TOT_TURNO', 'ST_VOTO_EM_TRANSITO', 'CD_TIPO_ELEICAO', 'DS_ELEICAO', 'DS_CARGO', 'DS_DETALHE_SITUACAO_CAND',
'SG_PARTIDO', 'NM_PARTIDO', 'DT_ELEICAO'], inplace=True)
A forma acima é extremamente verbosa, mas trouxe pra cá pra mostrar uma função a mais do Pandas. Por meio da função drop, eu apago as colunas selecionadas acima (vejam o argumento columns, no qual passei uma lista de colunas). Tive que usar o parâmetro inplace=True para que as colunas sejam apagadas de fato. Caso isso não seja feito, o Pandas apenas irá nos mostrar um dataframe temporário com as colunas apagadas, mas o dataframe original ainda estará intocado. Usando o inplace, eu aplico a mudança ao dataframe original.
Mas, como dito, a forma acima foi mais a título didático. Por ser extremamente verbosa, não se aplica aqui. Quando queremos eliminar várias colunas, é mais interessante selecionar somente a que queremos:
df_1o_turno = df_1o_turno[['SG_UF', 'CD_MUNICIPIO', 'NM_MUNICIPIO',
'NM_CANDIDATO', 'QT_VOTOS_NOMINAIS']]
Caso eu peça um df_1o_turno[['SG_UF', 'CD_MUNICIPIO', 'NM_MUNICIPIO', 'NM_CANDIDATO', 'QT_VOTOS_NOMINAIS']] ao pandas, ele irá me mostrar um dataframe temporário com essas colunas citadas. Ao sobrepor a variável com o dataframe temporário, eu criei um dataframe permanente somente com as colunas de interesse. Portanto, fiquei somente com os dados de sigla de cada UF, código do município no TSE, nome do município, nome do candidato e quantidade de votos nominais.
Ao dar um comando sample no novo dataframe:

Agora não precisamos transpor os dados. Com poucas colunas, somente com o que interessa, fica mais simples a visualização dos dados.
Mas eu ainda tou vendo um grande potencial de bullshitagem política ali. Tem diversos nomes de candidatos. Vou mudar o nome de todos esses candidatos por nomes genéricos.
Aqui vou introduzir uma nova função de dataframe chama unique. Ela retorna um array da lib numpy (eu sei, não falei de numpy ainda, mas saibam que é uma lib bastante usada pelo Pandas para gerenciar arrays, ou sequências de dados) com os valores únicos de uma sequência de dados. Então, posso pedir para o Pandas um array com os nomes de candidatos possíveis em nossa base de dados.
df_1o_turno['NM_CANDIDATO'].unique()
# Retorna
# array(['JOSE MARIA EYMAEL', 'FERNANDO HADDAD',
# 'VERA LUCIA PEREIRA DA SILVA SALGADO',
# 'JOÃO DIONISIO FILGUEIRA BARRETO AMOEDO', 'CIRO FERREIRA GOMES',
# 'GUILHERME CASTRO BOULOS', 'JOÃO VICENTE FONTELLA GOULART',
# 'BENEVENUTO DACIOLO FONSECA DOS SANTOS', 'ALVARO FERNANDES DIAS',
# 'GERALDO JOSÉ RODRIGUES ALCKMIN FILHO', 'JAIR MESSIAS BOLSONARO',
# 'MARIA OSMARINA MARINA DA SILVA VAZ DE LIMA',
# 'HENRIQUE DE CAMPOS MEIRELLES'], dtype=object)
Eita, um prato cheio para bullshit político. Quero mudar o nome de todos esses daí. Aqui vou fazer de uma forma elegante usando list comprehension. Eu poderia simplesmente fazer algo do tipo:
lista_candidatos = list(df_1o_turno['NM_CANDIDATO'].unique())
lista_candidatos_nova = [lista_candidatos[0] = 'CANDIDATO A', lista_candidatos[1] = 'CANDIDATO B'] ...
# E assim sucessivamente
Ou até criar um laço for, com uma lista vazia antes, ir dando append na lista vazia, etc. Mas list comprehensions são poderosas e nos ajudam a criar soluções elegantes. Posso substituir tudo isso da seguinte forma:
In [1]: from string import ascii_uppercase
In [2]: ascii_uppercase
Out[2]: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
In [3]: lista_candidatos_nova = [f'CANDIDATO {x}' for x in ascii_uppercase[:13]]
In [4]: lista_candidatos_nova
Out[4]:
['CANDIDATO A',
'CANDIDATO B',
'CANDIDATO C',
'CANDIDATO D',
'CANDIDATO E',
'CANDIDATO F',
'CANDIDATO G',
'CANDIDATO H',
'CANDIDATO I',
'CANDIDATO J',
'CANDIDATO K',
'CANDIDATO L',
'CANDIDATO M']
13 foi um número que chamamos em programação de número mágico. Parece que surgiu ali do nada, mas eu sei que são 13 candidatos que concorreram na eleição passada.
Números mágicos costumam ser uma má prática de programação. O ideal é linkar esse número com algo já existente em nosso código, como por exemplo o length da lista de candidatos originais:
from string import ascii_uppercase
lista_candidatos = list(df_1o_turno['NM_CANDIDATO'].unique())
lista_candidatos_nova = [f'CANDIDATO {x}' for x in ascii_uppercase[:(len(lista_candidatos))]]
Parece bem elegante pra mim esse tipo de solução.
Agora iremos trocar os candidatos pela nova lista de candidatos. Pandas tem uma função chamada replace que faz isso que queremos. Ela pode receber dois iteráveis como argumento e trocar os valores correspondentes do primeiro iterável com os valores correspondentes do segundo iterável. Aqui novamente irei precisar pedir um inplace=True para aplicar as mudanças dentro de nosso dataframe em vez de criar um dataframe temporário. Nosso código de troca de nomes de candidatos fica assim:
# Trocando os nomes dos candidatos para evitar polêmica
from string import ascii_uppercase
lista_candidatos = list(df_1o_turno['NM_CANDIDATO'].unique())
lista_candidatos_nova = [f'CANDIDATO {x}' for x in ascii_uppercase[:(len(lista_candidatos))]]
df_1o_turno.replace(lista_candidatos, lista_candidatos_nova, inplace=True)
Nossos nomes de candidato agora são CANDIDATO A, CANDIDATO B, etc
Nosso conjunto de dados está quase limpo o suficiente. Agora iremos investigar a quantidade de linhas em nosso DataFrame. Irei usar uma variação da função unique, que é a nunique, que retorna a quantidade de valores únicos em nosso DataFrame. Irei verificar quantos municípios aparecem em nosso DataFrame:
# Contabilizando a quantidade de municípios
df_1o_turno['CD_MUNICIPIO'].nunique()
# Retorna
# 5741
Ué, o Brasil tem 5570 municípios. Temos municípios excedentes aí. Vamos dar uma investigada vendo a quantidade de valores únicos de unidade da federação:
df_1o_turno['SG_UF'].unique()
# Retorna
# array(['MG', 'MA', 'SC', 'RS', 'BA', 'SP', 'AL', 'GO', 'RN', 'PR', 'PB',
# 'TO', 'MT', 'RJ', 'CE', 'PE', 'ES', 'ZZ', 'PA', 'PI', 'MS', 'AM',
# 'SE', 'AP', 'RO', 'RR', 'DF', 'AC'], dtype=object)
Tem um tal de ZZ aí. Não conheço nenhum estado com a sigla de UF como ZZ. Pesquisando, vi que esse ZZ corresponde aos eleitores que votaram em outro país. Podemos investigar isso da seguinte forma:

Vejam os nomes de município acima. Guatemala, Bissau, Hanói, Belmopan, Paramaribo, etc. Nomes bem esquisitos para municípios do BR, mas bastante conhecidos mundo afora, como Montevidéu por exemplo.
Verificamos agora com certeza que ZZ corresponde aos eleitores que votaram em outros países. Vamos excluir esses valores, pois meu mapa será só do BR:
# Observem o 'ZZ' acima, corresponde aos eleitores de fora do país
# Retirando os eleitores de fora do país
df_1o_turno = df_1o_turno[df_1o_turno['SG_UF'] != 'ZZ']
df_1o_turno['CD_MUNICIPIO'].nunique()
# Retorno
# 5570
Agora temos a quantidade de municípios correta.
Falta mais um pouquinho para terminarmos de limpar nossos dados, mas este talvez seja o maior post que já criei aqui na área, então ficamos por aqui e continuaremos na parte 4 deste projeto. Até mais.