Visualizações cartográficas mapeiam dados em posições reais no globo. Aplicações comuns incluem localizações, rotas e trajetórias na superfície do planeta. Para "planificar" uma esfera tridimensional em um plano bidimensional, nós devemos mapear pares de (longitude,latitude) em coordenadas (x,y).
Projeções são como uma escala, mapeando um domínio espacial para uma variável visual, comumente posição. Vamos aprender como relacionar esse domínio novo com os dados que estão relacionados com eles utilizando Altair.
Nesse notebook vamos focar em Técnicas de Geo-Visualização, como pontos, símbolos e Mapas Choropleth.
import pandas as pd
import altair as alt
url = "https://raw.githubusercontent.com/tiagodavi70/vl-altair-tutorial/master/datasets/completo.csv"
meta_url = "https://raw.githubusercontent.com/tiagodavi70/vl-altair-tutorial/master/datasets/metadados.csv"
df = pd.read_csv("https://raw.githubusercontent.com/tiagodavi70/vl-altair-tutorial/master/datasets/dados.csv")
meta_df = pd.read_csv(meta_url)
para_geo = "https://raw.githubusercontent.com/tiagodavi70/vl-altair-tutorial/master/datasets/para_geo.json"
para_topo = "https://raw.githubusercontent.com/tiagodavi70/vl-altair-tutorial/master/datasets/para_topo.json"
Até agora vimos dados em formatos tabulares, descrito em termo de linhas e colunas, que não é um formato comum para mapas. Para criar mapas vamos utilizar formatos específicos que descrevem as regiões geográficas e trajetórias. GeoJSON e TopoJSON são formatos que descrevem características geográficas, extendendo o formato JSON, utilizando campos como features, latitude e longitude.
Vamos usar um arquivo TopoJSON, do repositório TopoJSON - Brasil, com as cidades do estado do Pará. Abra o arquivo e veja a estrutura dele, como organiza em arcos. Procure o atribute objects, e dentro dele tem a informação sobre cidades no atributo 15 (referente ao código do estado). Vamos precisar desse atributo para criar nosso mapa.
Vamos criar um mapa usando mark_geoshape()
. No lugar dos dados, vamos utilizar a função alt.topo_feature
para utilizar a especificação no formato correto. Vamos indicar o 15
como o atributo que contém nossas cidades.
alt.Chart(alt.topo_feature(para_topo,"15")).mark_geoshape()
Abrindo esse arquivo vemos que não tem informação alguma sobre as cidades, só a descrição da topologia. Enquanto é plausível usar ele para visualizar o mapa como está, vamos usar um GeoJSON para conseguir ligar nossos dados pelo nome das cidades. O GeoJSON que vamos usar é do repositório Geodata BR - Brasil, que além da informação regional, também tem o nome das cidades.
Usando esse GeoJSON não precisamos fazer nenhuma modificação extra para carregar nosso mapa.
alt.Chart(para_geo).mark_geoshape()
Não temos as bordas municipais, mas vamos mexer um pouco no estilo com stroke
e fill
para fazer tudo ficar vísivel.
alt.Chart(para_geo).mark_geoshape(
stroke="#ddd",
strokeWidth=0.3,
color="darkgrey",
)
Além dos variáveis visuais que já vimos, Altair conta com as variáveis (latitude,longitude) que respeita a projeção do mapa, e podem ser mapeados sem a necessidade de uma marca visual específica. Mesmo utilizando mapas descritos em formatos geográficos, muitos conjuntos de dados tabulares contém informação geográfica na forma de coordenadas (latitude,longitude) ou referências para regiões geográficas, como nome de países, nome de cidades, código postal, que podem ser mapeados para coordenadas usando um serviço de geolocalização.
Em alguns casos, é suficiente projetar os dados tabelados, sem um mapa servindo como base.Vamos mostrar as cidades sem carregar o arquivo com a topologia dos mapas.
alt.Chart(meta_df).mark_circle(size=50).encode(
latitude="LATITUDE:Q",
longitude="LONGITUDE:Q"
)
Usando os dois tipos de mapas que já vimos, conseguimos juntar ambos por meio de camadas, criando um mapa de símbolos.
alt.layer(
alt.Chart(para_geo).mark_geoshape(
stroke="#ddd",
strokeWidth=0.3,
color="darkgrey",
),
alt.Chart(meta_df).mark_circle(size=50).encode(
latitude="LATITUDE:Q",
longitude="LONGITUDE:Q"
)
).configure_view(
strokeWidth=0 # adicionando pra evitar que o frame da visualização apareça
)
O nosso símbolo de exemplo aqui é um círculo, e com os dois mapas conectados, vamos tirar o arquivo de metadados e usar o nosso conjunto de sensores. Para isso, vamos usar uma transformação que conecta dois conjunto de dados.
Usando transform_lookup
podemos definir um campo (lookup) na lista de cidades com suas posições, que serve como chave primária para conectar em outro conjunto de dados, o nosso conjutno de sensores. A ordem das transformações importa muito, usando a agregação primeiro e a ligação depois.
Vamos aplicar essas transformações e ver nosso novo mapa conectado.
alt.concat(
alt.Chart(para_geo).mark_geoshape(
stroke="#ddd", strokeWidth=0.3, color="darkgrey",
),
alt.Chart(url).mark_circle().transform_filter(
"datum.Cidade !== 'MINA DO PALITO' && datum.Cidade !== 'SERRA DOS CARAJAS'" # tirando da análise lugares que não são cidades
).transform_aggregate(
groupby=["Cidade"],
valor="average(Precipitação)"
).transform_lookup(
lookup="Cidade",
from_=alt.LookupData(data=meta_url, key='ESTAÇÃO', fields=list(meta_df.columns))
).encode(
latitude="LATITUDE:Q",
longitude="LONGITUDE:Q",
tooltip=['Cidade:N', 'valor:Q']
)
).configure_view(
strokeWidth=0
)
Agora podemos escolher um atributo e mapear para o tamanho dos nossos círculos.
alt.layer(
alt.Chart(para_geo).mark_geoshape(
stroke="#ddd", strokeWidth=0.3, color="darkgrey",
),
alt.Chart(url).mark_circle().transform_filter(
"datum.Cidade !== 'MINA DO PALITO' && datum.Cidade !== 'SERRA DOS CARAJAS'"
).transform_aggregate(
groupby=["Cidade"],
valor="average(Precipitação)"
).transform_lookup(
lookup="Cidade",
from_=alt.LookupData(data=meta_url, key='ESTAÇÃO', fields=list(meta_df.columns))
).encode(
latitude="LATITUDE:Q",
longitude="LONGITUDE:Q",
tooltip=['Cidade:N', 'valor:Q'],
size=alt.Size('valor:Q', scale=alt.Scale(range=[0, 1000]), legend=None)
)
).configure_view(
strokeWidth=0
)
Conseguimos ver alguns pontos maiores no nordeste paraense, indicando maior precipitação.
Um Mapa Choropleth usa core ou texturas diretamente sobre regiões para mapear valores. Mesmo que um mapa de símbolos na maioria das vezes seja mais eficiente, mapas choropleth são muito populares, e úteis quando muitos há muitos símbolos no mapa atrapalhando a percepção.
O modo em que o Altair trata os formatos geográficos pede que os dados passem por processamento antes de criar a especificação. Vamos criar um novo dataframe, já com as médias dos valores calculadas.
df_completo = pd.read_csv(url)
df_aggr = df_completo.groupby(["Cidade"]).mean() # agrupar pela média
df_aggr.reset_index(level=0, inplace=True) # criar uma coluna de cidade, tirando cidade do indíce
Temos que adicionar informação em como o GeoJSON tem que ser interpretado, adicionando no cabeçaçho dos dados o formato correto de leitura. Criamos um atalho também, usando transform_calculate
, para indicar o nome da cidade dentro do GeoJSON.
Vamos apresentar o nosso Mapa Choropleth.
base = alt.Chart(alt.Data(url=para_geo, format=alt.DataFormat(property='features',type='json'))).mark_geoshape(
stroke="#555", strokeWidth=0.3, color="darkgrey",
).transform_calculate(
nome="datum.properties.name"
).transform_lookup(
lookup="nome",
from_=alt.LookupData(data=df_aggr, key='Cidade', fields=list(df_aggr.columns))
).transform_filter(
"datum.Cidade !== 'MINA DO PALITO' && datum.Cidade !== 'SERRA DOS CARAJAS'"
).encode(
tooltip=alt.Tooltip(["nome:N"]),
).properties(
width=650,
height=500
)
alt.layer(
base, base.encode(color='Precipitação:Q')
).configure_view(
strokeWidth=0
)
Um dos maiores problemas com mapas choropleth é a escolha de cores. No mapa acima não mudamos nada em relação ao esquema padrão de cores (yellowgreenblue
) do Altair, mas na utilização de mapas essa é uma questão importante.
Vamos refazer o mapa, agora trocando esquemas de cores. Vamos testar um esquema de matiz única (teals
), que varia somente a iluminação, um esquema multi matiz (viridis
), que varia matiz e iluminação e um esquema divergente (blueorange
), que usa um ponto médio.
base_cor = alt.Chart().mark_geoshape(
stroke="#555",
strokeWidth=0.3,
color="darkgrey"
).encode(
alt.Tooltip(["nome:N"])
).properties(width=280)
def mapa(esquema):
return base_cor + base_cor.encode(alt.Color('Precipitação:Q', scale=alt.Scale(scheme=esquema), legend=None))
alt.concat(
mapa('tealblues'), mapa('viridis'), mapa('blueorange'),
data=alt.Data(url=para_geo, format=alt.DataFormat(property='features',type='json'))
).transform_calculate(
nome="datum.properties.name"
).transform_lookup(
lookup="nome",
from_=alt.LookupData(data=df_aggr, key='Cidade', fields=list(df_aggr.columns))
).transform_filter(
"datum.Cidade !== 'MINA DO PALITO' && datum.Cidade !== 'SERRA DOS CARAJAS'"
).configure_view(
strokeWidth = 0
).resolve_scale(
color='independent'
)
Qual escala de cor mostra apresenta melhor a precipitação nesse mapa?