Mapeamento de médias do ENEM por estado com Folium

Wendel Marques
7 min readAug 8, 2020

cerca de 3 meses comecei minha jornada de aprendizado em tópicos de Data Science. Para nortear meus estudos, criei um repositório no GitHub com os links de cursos e livros (gratuitos e pagos; em português e em inglês) que encontrei enquanto buscava por materiais de estudos.

Materiais de estudos sobre Data Science e Machine/ Deep Learnin | Clique aqui para acessá-lo.

Para aplicar o que aprendi no curso Python Fundamentos para Análise de Dados (Data Science Academy), resolvi criar um projeto. Ao procurar por dados abertos, encontrei os microdados do ENEM no site do INEP. Esses microdados estão inteiramentes ligados ao vestgeek.com, um projeto que idealizei em 2015 e que mantenho desde então…

Arquivos

Os arquivos utilizados nesse artigo e todo o código estão disponíveis em: github.com/WendelMarques/mapeamento-medias-enem-folium

O que queremos?

Mapear todas as médias do ENEM por escola, isto é, vamos colocar um pin em cada localização de uma unidade escolar a sua respectiva média. Além disso, vamos fazer esse mapeamento levando em consideração os limites estaduais. Haverá 27 grupos de escolas (26 estados + Distrito Federal).

Ficará assim:

Primeiro passo

O primeiro passo é importar as bibliotecas necessárias.

import pandas as pd
import numpy as np
import json
import folium
from folium import Marker
from folium.plugins import MarkerCluster
from jinja2 import Template
from branca.element import Template, MacroElement

Datasets

Para alcançar o nosso objetivo, precisamos de dois datasets. O primeiro (microdados-enem-escolas.csv) contém os microdados do ENEM e o segundo (tabela-lista-escolas-detalhado.csv) contém a lagitude e longitude de cada escola. Inicialmente, trabalharemos com o primeiro.

Conhecendo e tratando os dados do ENEM

Leitura e visualização

Armazenaremos o dataset microdados-enem-escolas.csv no dataframe df.

Remoção de colunas

Precisamos remover algumas colunas que não serão necessárias. Podemos saber o nome das colunas que queremos remover a partir da consulta do resultado de list(df.columns) no passo anterior.

Tratamento de dados faltantes

Por n fatores, alguns bancos de dados não são alimentados corretamente. Dentre outras consequências, uma delas é a ausência de dados. Existem muitas formas de lidar com essa questão. Aqui, vamos simplesmente eliminar as linhas nas quais faltam dados. Entretanto, poderíamos, por exemplo, substituir cada NaN pela respectiva mediana da escola.

Adicinando média total

Como o objetivo é plotar a média simples por escola, o próximo passo é realizar esse calculo, porque o dataset original não contém essa informação. Logo, adicionamos a coluna Média Total e então, para cada linha, somamos as notas das provas de RED, CH, CN, LP e MT; por fim, dividimos por cinco.

Em seguida, limparemos ainda mais nosso dataframe df. Cada escola possui no máximo 7 linhas de dados (2009 a 2015), as quais possuem uma média total por ano (calculada no item anterior). Para realizar a mesclagem, as linhas são agrupadas de acordo com o código Educacenso (código da instiruição de ensino), então a média é calculada para cada agrupamento.

Recapitulando…

Até o momento, temos um dataframe df que armazena o código da escola e sua respectiva média total simples dos anos de 2009 a 2015. O próximo passo é a manipulação do dataset que contém os dados de localização.

Conhecendo e tratando os dados de localização

Do dataset tabela-lista-escolas-detalhado.csv, nos interessa apenas as colunas referentes ao código da escola, ao nome da escola e aos dados de localização (longitude, latitude, município e estado).

Merge dos dataframes

Com os dataframes manipulados, a próxima etapa é realizar o merge entre os dois. Dessa forma, um dataframe é criado com todos os dados relevantes. A última célula serve para salvar o trabalho realizado até o momento.

Plotagem

Sobre o Folium

Folium é uma biblioteca que facilita a visualização de dados em um mapa. Utilizando o Python para manipular os dados em conjunto com a biblioteca JavaScript Leaflet, colocar pontos em um mapa se torna um tarefarelativamente simples.

Alterações na classe Marker

É importante ressaltar que foi meu primeiro contato com o Folium, então algumas características ainda são obscuras.

Aparentemente, por padrão, não é possível substituir o valor do cluster, mas as seguintes alterações resolvem esse problema.

“Agora chega a vez de passar propriedades personalizadas via marcador. No Folium o marcador padrão não o suporta, mas a seguinte classe de marcador pode ser introduzida, estendendo a classe Marker:”

(fonte: Vadim Gremyachev/Stack OverFlow)

class MarkerWithProps(Marker):   _template = Template(u"""
{% macro script(this, kwargs) %}
var {{this.get_name()}} = L.marker(
[{{this.location[0]}}, {{this.location[1]}}],
{
icon: new L.Icon.Default(),
{%- if this.draggable %}
draggable: true,
autoPan: true,
{%- endif %}
{%- if this.props %}
props : {{ this.props }}
{%- endif %}
}
).addTo({{this._parent.get_name()}});
{% endmacro %}
""")

No código a seguir, em className adicionei uma função que, de acordo com o valor da média, retorna: marker-cluster-small (marcador verde), marker-cluster-medium (marcador amarelo) ou marker-cluster-large (marcador laranja).

Para personalizar a função icon_create_function do cluster de marcadores, o exemplo a seguir demonstra como substituir o rótulo do marcador para exibir o valor personalizado em vez do padrão (contagem de marcadores em um cluster):

(fonte: Vadim Gremyachev/Stack OverFlow)

icon_create_function = '''
function(cluster) {
var markers = cluster.getAllChildMarkers();
var sum = 0;

for (var i = 0; i < markers.length; i++) {
sum += markers[i].options.props.mediaTotal;
}
var avg = sum/cluster.getChildCount();
avg=avg.toFixed(2);

function verifica_Media(media) {
if (media < 450) {
return 'marker-cluster marker-cluster-large'
}
else if (media >= 450 && media <= 500) {
return 'marker-cluster marker-cluster-medium'
}
else {
return 'marker-cluster marker-cluster-small'
}
}


return L.divIcon({
html:
'<div style="display:flex;justify-
content:center;align-items:center;font-
size:7pt;">'+ avg +'</div>',
className: verifica_Media(avg),
iconSize: new L.Point(40, 40)
});
}

*Minhas alterações estão em negrito.

Criando o mapa

Para criar o mapa, informamos o parâmetro location, que recebe uma latitute e uma longitude.

map = folium.Map(location=[-9.026078, -70.441312], zoom_start=4)
folium.TileLayer(name='ENEM MÉDIAS').add_to(map)
Brasil

Transferimos nossos dados do dataframe mergeEscolaLocalizacao para a lista maker_data_list, em seguida a convertemos para uma tupla.

maker_data_list = []for index, row in mergeEscolaLocalizacao.iterrows():
maker_data_list.append({
'location':[row['Latitude'], row['Longitude']],
'mediaTotal': round(row['MEDIA TOTAL'], 2),
'nomeEscola': row['Escola'],
'UF': row['UF']
})
marker_data = tuple(maker_data_list)

O conjunto de todas as escolas de um estado estará atrelado a uma UF, o qual é armazenado em estados[uf]. O dicionário grupos[uf] cria uma camada para cada UF.

estados = {}
grupos = {}
estados_unique = np.unique(mergeEscolaLocalizacao['UF'])
for uf in estados_unique:
estados[uf] = MarkerCluster(
icon_create_function=icon_create_function,
name=uf
)
grupos[uf] = folium.FeatureGroup(name=uf)

O trecho abaixo adiciona um pin no lugar em que cada escola armazenada no nosso dataset está localizada.

for uf in estados:
for marker_item in marker_data:
if(marker_item['UF'] == uf):
MarkerWithProps(
location=marker_item['localizacao'],
props = {'mediaTotal': marker_item['mediaTotal']},
popup = marker_item['nomeEscola']
+ ":<br>" + '<b>' +
str(marker_item['mediaTotal']) + '</b>',
icon = (
folium.Icon(color='red',icon_color='#f5f6fa',
icon='university', prefix='fa')
if marker_item['mediaTotal'] < 600

else folium.Icon(color='blue',
icon_color='#f5f6fa', icon='university',
prefix='fa')
),
).add_to(estados[uf])
grupos[uf].add_child(estados[uf])
estados[uf].add_to(map)

Os pins ficam assim:

Legenda no mapa

O código a seguir encontrei em TileMill Documentations/Advanced legends. Apesar de existirem outras formas de adiconar uma legenda ao mapa, essa pareceu ser a melhor opção.

(Removi o script que permitia arrastar a caixinha de legenda).

Legendas

O último passo…

Por fim, configuramos o Layer Control e salvamos o mapa.

folium.LayerControl('topright', collapsed=False).add_to(map)

Layer Control na lateral direita:

Layer Control
map.save('mapeamento-medias-enem.html')
O mapa completo

Vale ressaltar que o dataframe mergeEscolaLocalizacao possui mais de 20 mil escolas, dessa forma o desempenho foi prejudicado (leva cerca de 40seg para o mapa ser carregado). Como houve a necessidade de alterar o marcador de cluster padrão (para exibir a média), usar o FastMarkerCluster pareceu não ser a solução (talvez esse notebook seja útil). Além disso, tentei também utilizar o prefer_canvas, mas não funcionou.

Desafio

Que tal colocar a mão na massa? Tente salvar um mapa para cada estado ou um mapa para uma região do Brasil (o desempenho melhora). É possível também, para uma determinada cidade, dividir a plotagem por bairro. Perceba que nos dois últimos casos, novos dados de localização serão necessários.

Autoavaliação

Dificuldades

  • Foi o meu primeiro contato com o Folium, então tive que pesquisar absolutamente tudo;
  • Mesmo que eu tenha iniciado o projeto logo após estudar Python e Pandas, tive que pesquisar novamente sobre alguns comandos porque os esqueci.

Facilidades

  • O alto número de usuários na comunidade facilitou as minhas buscas por respostas para as minhas dúvidas (Stackover Flow é uma dádiva).

Local de donwload dos Datasets

Links úteis

Créditos

Algoritmo para alterar o marcador padrão do Folium

Legenda no mapa

TileMill Documentations/Advanced legends

Fontes

--

--

Wendel Marques

Computer Science (UFG) | Data Science Enthusiast | Creator of Vestgeek