Esse notebook vai tratar das Transformações nos dados e Personalização de gráficos possíveis utilizando Altair. Transformações de dados são parte fundamental da Visualização da Informação, escolher o nível de detalhe é tão importante quando as variáveis visuais. E a visualização se torna inútil se ninguém entende o que ela transmite. Em adição as variáveis visuais, um gráfico precisa de elementos de referência, contexto ou guias para ajudar a entender o gráfico. Eixos e legendas são parte fundamental de uma visualização eficiente!
Então, começamos importando as bibliotecas necesárias.
import altair as alt
import pandas as pd
E carregando nosso arquivo local `dados.csv`, com os dados de sensores climáticos no ano de 2019 do estado do Pará por semana. Os valores estão agrupados pela média.
df = pd.read_csv("https://raw.githubusercontent.com/tiagodavi70/vl-altair-tutorial/master/datasets/dados.csv")
df.head()
Cidade | Data | Precipitação | Pressão Atmosférica ao nível da estação | Pressão Atmosférica máxima | Pressão Atmosférica mínima | Radiação Global | Temperatura do ar - bulbo seco | Temperatura do ponto de orvalho | Temperatura máxima | Temperatura mínima | Temperatura orvalho máxima | Temperatura orvalho mínima | Umidade Relativa máxima | Umidade Relativa mínima | Umidade Relativa do Ar | Direção Horária do Vento | Rajada Máxima de Vento | Velocidade Horária do Vento | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | Altamira | 2019-01-06 00:00:00+00:00 | 0.280556 | 989.965278 | 990.274306 | 989.647917 | 894.125641 | 25.822917 | 22.869444 | 26.330556 | 25.337500 | 23.331944 | 22.501389 | 87.416667 | 82.006944 | 84.631944 | 110.173611 | 3.634028 | 1.013889 |
1 | Altamira | 2019-01-13 00:00:00+00:00 | 0.604762 | 990.213690 | 990.492262 | 989.927381 | 823.916484 | 24.428571 | 22.095833 | 24.861905 | 24.016071 | 22.498214 | 21.716667 | 90.005952 | 85.172619 | 87.684524 | 126.190476 | 2.862500 | 0.835714 |
2 | Altamira | 2019-01-20 00:00:00+00:00 | 0.103571 | 990.877381 | 991.207143 | 990.555357 | 828.782418 | 24.779762 | 22.383333 | 25.240476 | 24.330357 | 22.810714 | 21.984524 | 89.684524 | 84.553571 | 87.190476 | 137.708333 | 2.890476 | 0.848214 |
3 | Altamira | 2019-01-27 00:00:00+00:00 | 0.304762 | 990.875595 | 991.179762 | 990.569048 | 882.498901 | 25.006548 | 22.736905 | 25.505357 | 24.580357 | 23.158333 | 22.370238 | 90.214286 | 85.214286 | 87.803571 | 126.994048 | 3.219643 | 0.882738 |
4 | Altamira | 2019-02-03 00:00:00+00:00 | 0.094048 | 989.825595 | 990.120238 | 989.521429 | 956.195604 | 25.751786 | 22.675595 | 26.236905 | 25.324405 | 23.132143 | 22.314286 | 86.642857 | 81.321429 | 84.029762 | 107.065476 | 3.315476 | 0.951190 |
Vamos começar com um idioma visual de agregação conhecido: o histograma. O histograma apresenta a frequência de determinados grupos em uma distribuição.
Vamos começar sem agregar nada: um scatterplot da temperatura máxima pela data.
alt.Chart(df).mark_circle().encode(
alt.X("Temperatura máxima", scale=alt.Scale(zero=False)),
alt.Y("Data:T")
)
O modo mais simples para agregar os pontos na distribuição é adicionando o argumento bin
na variável visual.
alt.Chart(df).mark_circle().encode(
alt.X("Temperatura máxima", scale=alt.Scale(zero=False), bin=True),
alt.Y("Data:T")
)
Conseguimos ver algumas divisões bem definidas, mas alterando um pouco o argumento `bin` podemos ajustar o número de grupos. Vamos tentar 20 grupos.
alt.Chart(df).mark_circle().encode(
alt.X("Temperatura máxima", scale=alt.Scale(zero=False), bin=alt.BinParams(maxbins=20)),
alt.Y("Data:T")
)
Parece bom, mas como fica com 40 divisões? 5? É fácil ver as diferenças usando esse scatterplot? Adicione opacidade e veja se a frequência fica mais explícita.
Parece que dividimos bem. Agora temos muitos pontos, vamos contar o número de pontos por cada intervalo com a função count()
.
alt.Chart(df).mark_circle().encode(
alt.X("Temperatura máxima", scale=alt.Scale(zero=False), bin=alt.BinParams(maxbins=20)),
alt.Y("count()")
)
Para aproveitar o espação vamos trocar a marca de point
para bar
e pronto, temos um histograma.
alt.Chart(df).mark_bar(color="cadetblue").encode(
alt.X("Temperatura máxima", scale=alt.Scale(zero=False), bin=alt.BinParams(maxbins=20)),
alt.Y("count()")
)
As temperaturas ficam concentradas no intervalo de 25 até 29 graus. Qual o intervalo das temperaturas mínimas?
Ainda contando frequências, podemos contar um campo em função de outro usando bin nos dois mapeamentos visuais. Vamos ver a relação entre a Pressão Atmosférica máxima e a Temperatura do ar mapeando quantas vezes aparecem juntos pelo tamanho.
alt.Chart(df).mark_circle().encode(
alt.X("Pressão Atmosférica máxima", scale=alt.Scale(zero=False),bin=alt.BinParams(maxbins=20)),
alt.Y("Temperatura do ar - bulbo seco",scale=alt.Scale(zero=False),bin=alt.BinParams(maxbins=20)),
alt.Size("count()"),
alt.Color("count()")
)
Como fica o scatterplot? Tire os parâmetros bin, cor e o tamanho para ver como fica e veja se dá pra enxergar o mesmo padrão.
Para aproveitar o espaço, ao mesmo tempo que alinha as divisões dos grupos, podemos tirar o tamanho e trocar a marca de círculos para retângulos.
alt.Chart(df).mark_rect().encode(
alt.X("Pressão Atmosférica máxima", scale=alt.Scale(zero=False),bin=alt.BinParams(maxbins=20)),
alt.Y("Temperatura do ar - bulbo seco",bin=alt.BinParams(maxbins=20)),
alt.Color("count()"),
)
A mudança no esquema de cor faz saltar melhor as diferenças mesmo usando a área e a cor na primeira versão. Mais abaixo vamos aprender a manipular essas decisões automáticas e ajustar os padrões conforme for melhor para o domínio do problema.
Contagem é um tipo de agregação, e Altair tem outras funções que permitem agregações estatísticas e em função do tempo.
Momentos são funções que descrevem uma distribuição, como média, mediana, variância e desvio padrão. Com Altair também podemos agrupar somando e utilizar percentis.
Agora vamos mostrar a média das temperaturas máximas por cidade.
alt.Chart(df).mark_point().encode(
alt.X("Cidade"),
alt.Y("average(Temperatura máxima)", scale=alt.Scale(zero=False))
)
Substitua a média (average) pela mediana (median) e veja o que acontece com o gráfico. Nessa distribuição fica muita diferente?
Importante: As barras ficam melhor para mostrar essa distribuição, mas não é recomendável tirar a base 0 quando o mapeamento é feito com barras. A comparação entre barras é normalmente feita com uma base comum, já que é feito em função de razão.
E se não tem uma base comum? Vamos ver como fica mais abaixo.As barras estão ordenadas pelo nome, e podemos ordenar pelo valor das médias.
alt.Chart(df).mark_point().encode(
alt.X("Cidade", sort=alt.EncodingSortField(op='average', field='Temperatura máxima', order='ascending')),
alt.Y("average(Temperatura máxima)",scale=alt.Scale(zero=False))
)
Trocando ascending por descending inverte a distribuição.
Temos valores de umidade relativa mínima e máxima, e podemos agrupar pelos quartis para mostrar a distribuição.
alt.Chart(df).mark_bar().encode(
alt.X("Cidade"),
alt.Y("q3(Temperatura mínima)", scale=alt.Scale(zero=False)),
alt.Y2("q1(Temperatura mínima)")
)
Usando os quartis inferior e superior dá pra ver que o tamanho das barras é mais ou menos igual, com algumas barras bem grandes, mostrando uma variação de temperatura mínima grande nesses casos.
Tiramos a base 0, mas como as barras não tem a mesma base, podemos comparar só os intervalos
Usando as funções de agregação de tempo do Altair podemos contextualizar os nossos dados temporais.
Até aqui temos utilizado um arquivo local com os dados agregados por semana, o `dados.csv`. Agora vamos carregar um arquivo com mais dados, o completo.csv, assim poderemos ver também agregação por hora e dia. Esse arquivo tem os dados de cada sensor por hora ao invés de agregado por semana.
Como o arquivo é grande, vamos carregar pela URL do repositório para evitar erros de memória.
url = "https://raw.githubusercontent.com/tiagodavi70/altair_tutorial/master/datasets/completo.csv"
Agora conseguimos fazer umas análises a nível de hora, usando a função hour(<campo>)
:
alt.Chart(url).mark_area().encode(
alt.X("hours(Data):T"),
alt.Y("average(Temperatura máxima):Q", scale=alt.Scale(zero=False))
)
Apesar de simples e óbvio, conseguimos ver que os dias são quentes e as noites um pouco mais frias, isso confirma que os sensores de temperatura estão em bom funcionamento.
Usando month(), como fica a distribuição? E mapeando a temperatura mínima para Y2?
Usando Altair, ainda é possíevl criar campos novos baseado em operações, ou modificar campos já existens com parâmetros de busca.
Vamos ver a diferença entre as temperaturas máxima e mínima ao longo do do dia, criando um novo campo chamado 'Diferença' e adicionando ao conjunto de dados.
alt.Chart(url).mark_bar().transform_calculate(
Diferença="datum['Temperatura máxima'] - datum['Temperatura mínima']"
).encode(
alt.X("hours(Data):T"),
alt.Y("average(Diferença):Q", scale=alt.Scale(zero=False))
)
O termo datum é como Vega-Lite se refere a cada linha dos dados (datum é singular de data em inglês), e a grafia do nome dos campos varia entre duas formas: datum[campo] e datum.campo. A forma [ ] deve ser usada quando o atributo tem espaços no nome. As operações matemáticas e lógicas seguem a sintaxe do Javascript, que pode ser vista no documento de referência.
Durante a noite pouco tem mudança de temperatura, e durante o dia oscila um pouco mais.
Conseguimos observar um grau de oscilação grande em alguns horários, talvez em função das estações do ano. Vamos fazer uma transformação de filtro e ver todas as cidades em no mês das férias, julho.
alt.Chart(url).mark_rect().transform_calculate(
Diferença="datum['Temperatura máxima'] - datum['Temperatura mínima']"
).transform_filter(
"month(datum.Data) == 6"
).encode(
alt.X("hours(Data):T"),
alt.Y("Cidade:N"),
alt.Color("average(Diferença):Q"),
)
Novo repartimento tem pouca variação em Julho, podemos detalhar um pouco mais.
alt.Chart(url).mark_line().transform_calculate(
Diferença="datum['Temperatura máxima'] - datum['Temperatura mínima']"
).transform_filter(
"(month(datum.Data) == 6) && (datum.Cidade == 'Novo Repartimento')"
).encode(
alt.X("hours(Data):T"),
alt.Y("average(Diferença):Q", scale=alt.Scale(zero=False))
)
Explicando melhor as transformações que fizemos nos dados para esse gráfico:
transform_calculate
criamos um novo campo da diferença das temperaturas mínimas e máximas de uma medição.transform_filter
aplicamos dois filtros: o primeiro (month(datum.Data) == 6)
para indicar que só queremos o mês de Julho e (&&
) apenas a cidade de Novo Repartimento (datum.Cidade == 'Novo Repartimento')
.Podemos ver uma varição muito pequena durante os dias. Essas trocas de temperatura são importantes para depósitos de alimento e bebidas, e mostra que durante as férias essa variação é pequena e uniforme.
Experimente trocar de Julho para o mês de Março, e verifique se existe outra cidade do mesmo jeito.
Descobrimos a menor diferença olhando para os gráficos, mas como são muitos, ainda é um esforço visual. Como uma cidade se destacou bem mais que as outras, também foi mais fácil identificar. Aplicando mais transformações podemos ordenar para mostrar as 10 primeiras cidades que apresentam menos diferença, e mudar a variável visual mapeando a diferença de cor para tamanho da barra:
alt.Chart(url).mark_bar().transform_calculate(
Diferença="datum['Temperatura máxima'] - datum['Temperatura mínima']"
).transform_filter(
"month(datum.Data) == 6"
).transform_aggregate(
Diferença_Cidade='average(Diferença)',
groupby=['Cidade'],
).transform_window(
Rank='rank()',
sort=[alt.SortField('Diferença_Cidade', order='ascending')]
).transform_filter(
"datum.Rank < 10"
).encode(
alt.X("Diferença_Cidade:Q"),
alt.Y("Cidade:N", sort=alt.EncodingSortField(
op='max', field='Diferença_Cidade', order='ascending'
))
)
Sobre as transformações pra esse gráfico:
transform_calculate
e o primeiro transform_filter
estão como no heatmaptransform_aggregate
cria uma agregação de um campo pelo outro através de uma função, nesse caso agrupamos a média da diferença de cada cidade.transform_window
aplica uma função aos dados da agregação. Rank=rank()
cria uma nova coluna nos novos dados com a posição de cada um baseado no valor da agregação.transform_filter
indica que só queremos as 10 primeiras cidades.Procure Pacajá e Soure no heatmap acima e veja como é muito mais simples comparar depois das transformações.
Aperte nos três pontinhos ao lado do gráfico e veja no Editor Web do Vega como os dados ficam depois de transformados.
Para nós que estamos construindo esses gráficos, fica simples de entender o que está acontecendo olhando o código, mas o público alvo de uma visualização nem sempre tem esse contexto. Vamos continuar agora vendo meios de deixar adequar esses os gráficos para ambientes que não são de programação.
Até aqui temos muitas ferramentas de exploração, manipulação de dados e mapeamento, e agora vamos ver ferramentas de manipulação de estilo. Afinal, se ninguém entende o que a visualização está tentando comunicar ela não serve para nada!
Vamos ver alguns exemplos novos e melhorar alguns gráficos que já fizemos aqui mesmo.
Para mostrar como alterar escalas e eixos vamos usar um campo da nossa
alt.Chart(df).mark_point().encode(
alt.X("Precipitação:Q"),
)
Podemos ver que tem muitos valores na borda do 0, então vamos mexer um pouco na escala e esticar um pouco o eixo. Vamos mudar a escala da padrão linear para logarítimica, que destaca as bordas da distribuição, ao mesmo tempo que aumentamos o alcance (`range`), começando no ponto normal mas seguindo até 850 pixels.
alt.Chart(df).mark_point().transform_filter(
"datum['Precipitação'] > 0" # para evitar erros com a função log
).encode(
alt.X("Precipitação:Q",scale=alt.Scale(type="log",range=[0,850])),
)
Antes do 0.1 temos alguns valores soltos, mostrando que o começo não é uniforme mas depois de 0.1 até 1.5 passa a ser.
Podemos comparar com outra váriavel também ambas. Podemos ver abaixo a relação da umidade com as medidas de precipítação.
alt.Chart(df).mark_circle().transform_filter(
"datum['Precipitação'] > 0"
).encode(
alt.Y("Precipitação:Q",
scale=alt.Scale(type="log"),
sort="descending"
),
alt.X("Umidade Relativa máxima:Q",
scale=alt.Scale(zero=False)),
)
Conseguimos ver um aglomerado, onde tem medições de Umidade máxima perto do limite superior também tem precipitação.
Acabamos de mexer com a escala de uma váriavel e não indicamos isso em lugar nenhum. Vamos adicionar um título, e descrever melhor os eixos, indicando as mudanças na escala. Aproveitando, também vamos mexer nas marcações da escala e mudar a opacidade e tamanho dos círculos pra mostrar melhor o aglomerado. Por fim, aumentar o tamanho.
alt.Chart(df, title="Relação da Umidade Relativa Máxima do Ar com a Precipitação").mark_circle(
opacity=0.45,
size=50,
color="cadetblue"
).transform_filter(
"datum['Precipitação'] > 0"
).transform_calculate(
perc_umid="datum['Umidade Relativa máxima'] / 100" # criar nova coluna para as etiquetas
).encode(
alt.Y("Precipitação:Q",
scale=alt.Scale(type="log"),
axis=alt.Axis(orient="right", offset=10), # mover um pouco a escala e colocar á direita
sort="descending", # inverter a escala
title="Precipitação (mm) - escala logarítimica invertida"
),
alt.X("perc_umid:Q", # usando a nova coluna, só foi escalado, sem danos ao mapeamento visual
scale=alt.Scale(zero=False),
title="Umidade Relativa Máxima",
axis=alt.Axis(tickCount=5, format="%") # recuperando o estilo
),
tooltip=[alt.Tooltip("Data:T"),
alt.Tooltip("Cidade:N")]
).properties(width=450, height=450)
Tente criar esse gráfico uma mudança de cada vez e veja como ele vai se transformando.
Agora que conseguimos manipular os elementos do gráfico, ainda podemos melhorar o estilo das cores e legendas nos nossos gráficos.
Para mostrar isso, vamos escolher duas cidades, Capitão Poço e Altamira para comparar temperaturas máximas e mínimas.
Podemos aplicar o filtro diretamente no dataframe separando as duas cidades, ou usar a transformação no Altair.
df_completo = pd.read_csv(url)
df_cidades = df[(df["Cidade"] == "Capitão Poço") | (df["Cidade"] == "Altamira")]
Vamos primeiro fazer um gráfico de linhas pra comparar durante os meses.
alt.Chart(url).mark_line(strokeWidth=4).transform_filter(
"datum.Cidade == 'Capitão Poço' || datum.Cidade == 'Altamira'"
).encode(
alt.X("month(Data):T"),
alt.Y("average(Temperatura máxima):Q",scale=alt.Scale(zero=False)),
alt.Color("Cidade:N")
)
Agora vamos mudar as linhas para as área com valores mínimo e máximo, e tirar o mapeamento automático. Vamos escolher cores próximas das bandeiras dos municípios alterando o domain
e o range
.
alt.Chart(url, title="Comparação de temperatura de Capitão Poço e Altamira").mark_area(opacity=.65).transform_filter(
"(datum.Cidade == 'Capitão Poço' || datum.Cidade == 'Altamira')"
).encode(
alt.X("month(Data):T",title="Meses do Ano"),
alt.Y("average(Temperatura máxima):Q",scale=alt.Scale(zero=False),title="Média das temperaturas"),
alt.Y2("average(Temperatura mínima):Q"),
alt.Color("Cidade:N", scale=alt.Scale(
domain=['Capitão Poço','Altamira'],
range=['DodgerBlue',"MediumSeaGreen"]))
)
Quanto ao heatmap anterior, podemos mudar o esquema de cor dele para um mais familiar em relação a temperaturas, e algumas mudanças de estilo em relação ao título usando a função configure_title
.
alt.Chart(url, title="Diferença das Temperaturas Mínima e Máxima em Julho").mark_rect().transform_calculate(
Diferença="datum['Temperatura máxima'] - datum['Temperatura mínima']"
).transform_filter(
"month(datum.Data) == 6"
).encode(
alt.X("hours(Data):T", title="Hora do dia"),
alt.Y("Cidade:N"),
alt.Color("average(Diferença):Q",
scale=alt.Scale(scheme="yelloworangebrown"),
# scale=alt.Scale(range=["yellow", "red"]), # alternativa para definir cores manualmente, mas na dúvida use uma do Altair
legend=alt.Legend(title="Média das diferenças",
gradientThickness=30,
labelFontSize=15)
),
tooltip=[alt.Tooltip("Cidade:N"), alt.Tooltip("average(Diferença):Q", title="Diferença")]
).configure_title(
fontSize=20,
color='gray',
dy=-20 # espaço vertical
)
Como fica a distribuição por mês? Por semana? Experimente os esquemas de cor orangered
, magma
Nossas análises até aqui estão restritas a uma única visão, e temos um limite de quanta informação é possível entender desse jeito. No próximo notebook vamos ver como ampliar as visões e usar interação para maximizar o uso das variáveis visuais.
Mude o agregação de horas para dia da semana no último heatmap. Mude também o esquema de cor para um tom azulado.
Crie um heatmap do mês em relação ao dia, com a cor mapeada para a média das temperaturas. Esse heatmap deve ser aplicado somente para uma cidade, que deve ser indicada no título.
Crie um gráfico de barras divergentes, cada lado deve ter uma cor diferente.
Escolha um atributo e crie um gráfico com dois histogramas lado a lado, um para abril e outro para maio.
Crie um gráfico de inclinação, com inclinações para cima em verde e para baixo em vermelho.
Crie um gráfico de barras empilhado mapeando três atributos da base. Perceba que a base não tem dois atributos categóricos, então um deles ser gerado por uma transformação.
Adicione inverno e verão com cores diferentes em um histograma.
Crie uma célula com o conteúdo abaixo, e use x
para resolver os exercícios 8 e 9:
import pandas as pd
import numpy as np
x = pd.DataFrame({'x': np.linspace(-5, 5)})
Crie um gráfico de linhas utilizando esse DataFrame com seno e coseno da distribuição x
usando transform_calculate
.
Use transform_filter
e remova as regiões do gráfico onde a curva do coseno é menor que a curva do seno.