Criação de Mapas Interativos e Painéis de Controlo de dados georreferenciados
Com este tutorial, poderás aprender como:
- Desenvolver aplicações de web interativas: Criar uma web app interativa utilizando o framework
dash
em Python. - Visualizar dados geoespaciais: Importar e manipular dados geoespaciais usando as bibliotecas
pandas
egeopandas
. Além disso, aprenderá a plotar dados geoespaciais em mapas interativos utilizando as funcionalidades doplotly
. - Manipular dados: Realizar operações de manipulação de dados, como filtragem, agregação e cálculos de novas colunas, utilizando as funcionalidades do
pandas
. - Criar gráficos interativos: Criar gráficos interativos utilizando o
plotly
e oplotly.express
. Criar gráficos de dispersão, gráficos de linha, mapas de calor e outros tipos de visualizações interativas. - Integrar de elementos de interface: Adicionar elementos de interface como botões, sliders e tabelas interativas ao aplicativo
dash
. Esses elementos irão permitir a interação com os dados e ajustar as visualizações conforme as necessidades. - Aplicar na prática a análise de alertas de derramamentos de óleo: O tutorial concentra-se na análise de dados de alertas de derramamentos de óleo, permitindo visualizar a localização dos derramamentos, explorar os dados por meio de filtros de tempo e área, e visualizar informações detalhadas sobre os alertas selecionados.
Esta aplicação é a original, no entanto, os elementos usados não estarão completamente disponíveis, sendo um desafio tentar reproduzir esta visualização com recurso aos dois materiais disponibilizados.
1 – Importar os pacotes necessários
Neste capítulo, estamos a importar as bibliotecas e módulos necessários para a aplicação. As bibliotecas importadas são as seguintes:
numpy
(abreviação:np
): Biblioteca para suporte a arrays multidimensionais e funções matemáticas de alto desempenho.dash
: Biblioteca para criação de aplicações web interativas baseadas em Python.html
edcc
(componentes do Dash): Módulos para criação de elementos HTML e componentes interativos em Dash.plotly.graph_objs
(abreviação:go
): Módulo contendo objetos gráficos do Plotly para criação de visualizações interativas.pandas
(abreviação:pd
): Biblioteca para manipulação e análise de dados em formato de tabela.geopandas
(abreviação:gpd
): Biblioteca para manipulação e análise de dados geoespaciais em formato de tabela.plotly.express
(abreviação:px
): Módulo que simplifica a criação de visualizações interativas usando o Plotly.
# Importação dos pacotes necessários
import numpy as np
import dash
from dash import html, dcc, Input, Output, dash_table
import plotly.graph_objs as go
import pandas as pd
import geopandas as gpd
import plotly.express as px
2 – Carregar e pré-processar os dados
Os dados são carregados a partir dos arquivos “pickle” data_oil.pickle
e stations.pickle
.
Um “pickle” é um formato em Python usado para converter objetos Python numa representação binária e vice-versa. Permite salvar objetos num arquivo ou enviá-los por uma rede e, em seguida, recuperá-los posteriormente, mantendo sua estrutura e estado original.
# Carregar os dados do pickle
data = pd.read_pickle(r'oil_app\data_oil.pickle')
# Calcular 'width' baseado na 'area' e 'length'
data['width'] = 4 * data.area / (np.pi * data.length)
# Criar a coluna 'registry_nr' com valores de 1 até ao número de entradas
data['registry_nr'] = [i for i in range(1, len(data) + 1)]
# Converter a coluna 'date' para datetime format do pandas
data['date'] = pd.to_datetime(data.date)
# Extrair o 'year' from the 'date' column
data['year'] = [date.year for date in data.date]
# Carregar as estações e portos onde poderão possivelmente serem postos drones para a verificação (estes contem a autonomia prevista)
stations = pd.read_pickle(r'oil_app\stations.pickle')
# define color
stations['colors'] = ['red' if vehicle=='Aéreo' else 'blue' for vehicle in stations['Veículo não Tripulado']]
# Cria um dicionário com os números e nomes dos meses
meses_nomes = {1: 'Jan', 2: 'Fev', 3: 'Mar', 4: 'Abr', 5: 'Mai', 6: 'Jun',
7: 'Jul', 8: 'Ago', 9: 'Set', 10: 'Out', 11: 'Nov', 12: 'Dez'}
# Obtém as datas mínima e máxima do conjunto de dados
data_minima = min(data['date'])
data_maxima = max(data['date'])
# Cria uma lista de datas para serem usadas como marcações no slider
datas = pd.date_range(start=data_minima, end=data_maxima, freq='MS').tolist()
# Cria uma lista de números de mês correspondentes às datas
numeros_meses = [data.month for data in datas]
# Cria uma lista de nomes de mês correspondentes aos números de mês
nomes_meses = [meses_nomes[numero] for numero in numeros_meses]
# Cria um dicionário de marcações para o slider
marcas = {numero: nome for numero, nome in zip(numeros_meses, nomes_meses)}
# Obtém a lista de anos e meses únicos no conjunto de dados
anos_unicos = data['date'].dt.year.unique()
Neste código, os dados são carregados a partir dos arquivos “pickle” usando as funções pd.read_pickle()
. De seguida, são realizadas as etapas de pré-processamento conforme descrito.
Para o DataFrame data
, é feito o cálculo da coluna ‘width’ com base nas colunas ‘area’ e ‘length’, assumindo que as manchas são elipsoidais. A coluna ‘registry_nr’ é criada com valores sequenciais de 1 até o comprimento dos dados, pois é permitido disponibilizar o dia do mês nem a hora do registo. A coluna ‘date’ é convertida para o formato de data e hora usando pd.to_datetime()
. A partir da coluna ‘date’, é extraído o ano e as colunas ‘year’.
No DataFrame stations
, a coluna ‘colors’ foi criada com base nos valores da coluna ‘Veículo não Tripulado’, onde ‘Aéreo’ recebe a cor ‘red’ e os demais valores recebem a cor ‘blue’, para os destinguir a posteriori.
Posteriormente, criamos uma variedade de listas úteis para a definição da aplicação.
Estas etapas de carregamento e pré-processamento de dados preparam os dados para serem usados posteriormente na construção do aplicativo Dash.
Nota: Podes descarregar aqui (oil_spill_alerts) a versão oficial dos alertas e aqui (stations) das estações onde possivelmente se poderiam localizar drones para a verificação dos derramamentos de óleo, em csv. Podes alterar o código de acordo com o que realmente será necessário.
Para o que vamos fazer será necessário criar uma função que desenha os círculos
from draw_circle_on_map import draw_circle_on_map
Aqui podes ver o conteúdo do ficheiro draw_circle_on_map.py
import numpy as np
import pandas as pd
import plotly.express as px
def draw_circle_on_map(lat, lon, radius_km, color):
# Convert radius from km to degrees of longitude using Haversine formula
R_earth = 6371 # Earth's radius in km
dlon = radius_km / (R_earth * np.cos(np.radians(lat)))
dlat = radius_km / R_earth
# Construct a circle polygon
bearings = np.arange(0, 361)
circle_lon = lon + np.degrees(dlon) * np.cos(np.radians(bearings))
circle_lat = lat + np.degrees(dlat) * np.sin(np.radians(bearings))
circle_df = pd.DataFrame({'lat': circle_lat, 'lon': circle_lon})
# Create a scatter mapbox plot with the circle as a filled polygon
fig = px.scatter_mapbox(circle_df, lat='lat', lon='lon',
mapbox_style='carto-positron', zoom=8,
center={'lat': lat, 'lon': lon},
hover_data={'lat': True, 'lon': True},
color_discrete_sequence=[color])
fig.update_traces(mode='lines+markers', marker=dict(size=3), line = dict(width=3))
return fig
3 – Definição do layout da aplicação Dash
Para começar, vamos desconstruir uma criação simples de uma aplicação onde se apenas se visualizam os locais dos alertas e a sua legenda, desta forma:
Para tal podemos proceder da seguinte forma:
# Cria a aplicação Dash
app = dash.Dash(__name__)
# Define o layout da aplicação
app.layout = html.Div(children=[
# Título da página
html.H1('Mapa de Cluster de Alertas de Derramamento de Óleo', style={'text-align': 'center'}),
# Mapa para exibir as localizações dos alertas
dcc.Graph(id='oil_map', style={'height': '600px', 'width': '1000px'}) #nome do mapa é oil_map
])
# Define o callback da aplicação para atualizar o mapa
@app.callback(
Output('oil_map', 'figure'), #queremos o oil_map
Input('oil_map', 'id') #no local do oil_map definido anteriormente
)
def update_map(_):
# Cria um gráfico de dispersão no mapa com marcadores de cluster
fig = px.scatter_mapbox(data, lat='lat', lon='lon', zoom=4, height=600)
fig.update_layout(mapbox_style="open-street-map")
fig.update_layout(margin={"r":0,"t":0,"l":0,"b":0})
return fig
if __name__ == '__main__':
# Executa o aplicativo Dash em modo de debugging
app.run_server(debug=True)
Esse código cria uma aplicação de Dash simples que exibe um mapa interativo com marcadores de cluster representando os alertas. O mapa é atualizado automaticamente sempre que houver alguma alteração no elemento com o ID ‘oil_map’. Ao executar o código, a aplicação inicia-se num servidor local e poderá ser acedido no navegador para visualizar o mapa interativo.
Habitualmente é numa localização como esta:
Na secção de código fornecida, o layout da aplicação Dash.
app.layout
é um objetohtml.Div
que contém todos os elementos visuais do aplicativo.html.H1
cria um elemento de cabeçalho de nível 1 (H1) com o texto “Mapa de Cluster de Derramamentos de Óleo”. O estilo definido comstyle={'text-align': 'center'}
centraliza o texto.dcc.Graph
cria um elemento de gráfico interativo com o ID ‘oil_map’. Este gráfico será usado para exibir as localizações dos derramamentos de óleo. O estilo definido comstyle={'height': '600px', 'width': '1000px'}
define a altura e largura do gráfico.
4 – Definição dos callbacks
A definição das funções de callback que atualizam os componentes do aplicativo com base nas interações do usuário.
Nessa secção do código, define-se o callback responsável por atualizar o mapa quando necessário.
@app.callback
é usado para definir um callback. Especifica a saída esperada e as entradas que ativam o callback.Output('oil_map', 'figure')
especifica que o retorno do callback será atribuído à figura do gráfico com o ID ‘oil_map’.Input('oil_map', 'id')
especifica a entrada que aciona o callback. Neste caso, é o ID do gráfico ‘oil_map’, usado como um espaço reservado para a entrada, mas não é usado no corpo do callback.
Dentro do callback, temos a lógica que atualiza o gráfico:
fig = px.scatter_mapbox(data, lat='lat', lon='lon', zoom=4, height=600)
cria um gráfico de dispersão no mapa usando os dados fornecidos (data
). As colunas ‘lat’ e ‘lon’ são usadas para as coordenadas das localizações no mapa. O zoom e a altura do gráfico também são definidos.fig.update_layout(mapbox_style="open-street-map")
define o estilo do mapa como “open-street-map“.fig.update_layout(margin={"r":0,"t":0,"l":0,"b":0})
define as margens do layout do gráfico para remover o espaço em branco ao redor do mapa.
O retorno do callback é a figura atualizada (fig
), que será exibida no gráfico ‘oil_map’.
O restante do código diz respeito à inicialização do aplicativo Dash e a sua execução em modo de debugging.
5 – Aumentar a complexidade da aplicação
O que foi apresentado antes é uma versão ‘simpática’ da aplicação, no entanto, de interativa não tem nada. Para tal, faz uma leitura cuidada dos passos seguintes de uma aplicação muito mais avançada que permite a exploração dos dados.
# Cria a aplicação
app = dash.Dash(__name__, external_stylesheets=['style.css'])
# Define o layout do aplicativo
app.layout = html.Div(children=[
# Título da página
html.H1('Derramamentos de Óleo Potenciais', style={'text-align': 'center'}),
# Abas
dcc.Tabs(id='tabs', value='map_tab', children=[
# Aba para o mapa interativo
dcc.Tab(label='Mapa', value='map_tab', children=[
# Botão para selecionar área ou comprimento
html.H4('Escolha entre círculos representando a Área do derramamento de
óleo ou representando a proporção entre a Área real e a Área assumindo que o derramamento de óleo é uma elipse'),
dcc.RadioItems(
id='area_ratio',
options=[{'label': 'Área', 'value': 'area'},
{'label': 'Proporção', 'value': 'ratio'}],
value='area',
labelStyle={'display': 'inline-block', 'margin-right': '20px'}
),
# Slider para selecionar faixa de mês e ano
html.H3('Escolha faixas de mês e ano'),
html.Div([
dcc.RangeSlider(
id='year_slider',
min=data['date'].dt.year.min(),
max=data['date'].dt.year.max(),
value=[data['date'].dt.year.min(), data['date'].dt.year.max()],
marks={str(year): str(year) for year in data['date'].dt.year.unique()},
step=None,
vertical=True,
className='black-slider'
),
dcc.RangeSlider(
id='month_slider',
min=min(month_numbers),
max=max(month_numbers),
value=[min(month_numbers), max(month_numbers)],
marks=marks,
step=None,
vertical=True,
className='black-slider',
),
# Mapa para exibir locais de derramamento de óleo
html.Div([html.H3('Localização dos alertas de derramamento de óleo'),
html.H4('Escolha entre mapa estático e mapa animado por ano'),
dcc.RadioItems(id='toggle-animation',
options=[
{'label': 'Nenhum', 'value': 'None'},
{'label': 'Ano', 'value': 'year'}],
value='None',
labelStyle={'display': 'inline-block', 'margin-right': '10px'}
),
html.P('Neste gráfico, você pode escolher um conjunto de pontos para ver a tabela de dados abaixo!'),
dcc.Graph(id='oil_map', style={'height': '600px', 'width': '1200px'}),
], style={"marginTop": "20px", 'backgroundColor': '#90EE90'}),
], style={'display': 'flex'}),
]),
# Aba para tabela
dcc.Tab(label='Tabela de dados selecionados', value='stations_table', children=[
# Tabela para exibir os dados selecionados
dash_table.DataTable(
id='selected_data_table',
columns=[{"name": i, "id": i} for i in data.drop(columns=['date']).columns],
page_size=10
)
]),
# Aba para tabela de estações
dcc.Tab(label='Estações', children=[
# Tabela de estações
html.Div([
# Mapa para exibir locais de possíveis estações
html.Div([html.H3('Localização de possíveis estações'),
html.P('Na tabela abaixo, você pode escolher as estações de UAV e USV a serem representadas no mapa'),
dcc.Graph(id='stations_map', style={'height': '500px', 'width': '1000px'})]),
dash_table.DataTable(
id='stations_table',
columns=[{"name": i, "id": i} for i in stations.columns],
data=stations.to_dict('records'),
row_selectable='multi',
style_table={'height': '200px', 'overflowY': 'auto', 'whiteSpace': 'normal',
'width': '600px'},
selected_rows=[1, 4, 7, 9, 11, 16]
),
]),
]),
])
])
# Define o callback da aplicação
@app.callback(
Output('oil_map', 'figure'),
Input('area_ratio', 'value'),
Input('year_slider', 'value'),
Input('month_slider', 'value'),
Input('toggle-animation', 'value')
)
def update_map(area_ratio, year_range, month_range, toggle):
o_df = data[['area', 'lat', 'lon', 'length', 'date', 'ratio', 'year']]
if toggle == 'None':
toggle = None
# Filtra os dados com base na seleção do ano e mês
df = data[['area', 'lat', 'lon', 'length', 'date', 'ratio', 'year']]
df = df[(df['date'].dt.year >= year_range[0]) & (df['date'].dt.year <= year_range[1])] df = df[(df['date'].dt.month >= month_range[0]) & (df['date'].dt.month <= month_range[1])] # Cria um gráfico de dispersão no mapa if max(data[area_ratio]) > 300:
max_range = 200
tickvals_ar_ra = [0, 50, 100, 150, 200]
ticktext_ar_ra = ['0', '50', '100', '150', '>200']
else:
max_range = 1
tickvals_ar_ra = [0, 0.2, 0.4, 0.6, 0.8, 1]
ticktext_ar_ra = ['0', '0.2', '0.4', '0.6', '0.8', '>1']
df['marker_size'] = df[area_ratio]
o_df['marker_size'] = o_df[area_ratio]
min_marker_size = o_df['marker_size'].min()
max_marker_size = o_df['marker_size'].max()
fig = px.scatter_mapbox(df,
lat='lat',
lon='lon',
hover_data=['lat', 'lon', 'area', 'ratio', 'length'],
size='marker_size',
color=area_ratio,
color_continuous_scale='jet',
animation_frame=toggle,
range_color=[0, max_range],
zoom=4.1,
center={'lat': 37.5, 'lon': -18.0})
fig.update_layout(coloraxis_colorbar=dict(
title=area_ratio,
tickvals=tickvals_ar_ra,
ticktext=ticktext_ar_ra,
))
if toggle == 'year':
fig.layout.updatemenus[0].buttons[0].args[1]['frame']['duration'] = 1000
fig.layout.updatemenus[0].buttons[0].args[1]['transition']['duration'] = 250
fig.update_layout(mapbox_style="open-street-map")
fig.update_layout(
plot_bgcolor='#90EE90',
paper_bgcolor='#90EE90'
)
return fig
@app.callback(
Output('selected_data_table', 'data'),
Input('month_slider', 'value'),
Input('year_slider', 'value'),
Input('oil_map', 'selectedData')
)
def update_table(month_range, year_range, selectedData):
if selectedData:
selected_indices = [point['pointIndex'] for point in selectedData['points']]
selected_df = data.iloc[selected_indices].copy()
else:
selected_df = data.copy()
selected_data = selected_df[
(selected_df['date'].dt.month >= month_range[0]) & (selected_df['date'].dt.month <= month_range[1]) & (selected_df['date'].dt.year >= year_range[0]) & (selected_df['date'].dt.year <= year_range[1])]
selected_data = selected_data.to_dict('records')
return selected_data
@app.callback(
Output('stations_map', 'figure'),
Input('stations_table', 'data'),
Input('stations_table', 'selected_rows')
)
def update_stations_map(data_s, selected_rows):
# Cria um dataframe com as linhas selecionadas
selected_data = pd.DataFrame(data_s).iloc[selected_rows, :]
# Cria um mapa vazio
fig = px.scatter_mapbox(selected_data, lat='lat', lon='lon', zoom=3.5, height=600,
color='Veículo não Tripulado',
color_discrete_sequence=['red', 'blue'])
fig.update_layout(mapbox_style="open-street-map")
fig2 = px.scatter_mapbox(data,
lat='lat',
lon='lon',
hover_data=['lat', 'lon', 'area', 'ratio', 'length'],
zoom=4.1,
center={'lat': 37.5, 'lon': -18.0})
for i, row in selected_data.iterrows():
circle_fig = draw_circle_on_map(row['lat'], row['lon'], row['usv_uav_autonomy_km'], row['colors'])
fig.add_trace(circle_fig.data[0])
fig.update_layout(mapbox_style="open-street-map")
fig.add_trace(fig2.data[0])
fig.update_layout(coloraxis_showscale=False)
return fig
if __name__ == '__main__':
app.run_server(debug=True)
O código apresentado é resumidamente contem:
- Definição do layout do aplicativo, incluindo 3 guias (tabs), gráficos em duas das guias, tabelas em duas das guias e elementos que permitem a interação.
- Definição dos callbacks do aplicativo, que são funções que são acionadas quando ocorrem interações do utilizador, como alterar filtros ou selecionar pontos no mapa.
- Callbacks responsáveis por atualizar os gráficos e tabelas com base nas interações do usuário.
- A execução do aplicativo num servidor local para visualização e interação.
Obtendo-se uma app como aceder num repositório gratuito como o render https://potential-oil-spills-in-portugal.onrender.com/ que oferece a possibilidade de publicar uma página de Dash.
Esta aplicação é meramente ilustrativa de todas as possibilidades ao nosso alcance para se obter uma aplicação que auxilie um investigador na exploração dos dados e para começar por delinear estratégias de ação para apurar padrões, para planeamento da ação a tomar para a localização dos meios de verificação bem como a previsão do local ‘futuro’ da mancha para otimizar o tempo de ação para a sua verificação.
Existem várias opções de serviços cloud onde se pode implantar Dash apps. Alguns mais populares são:
- Microsoft Azure: O Azure oferece uma variedade de serviços, como Azure App Service, Azure Virtual Machines e Azure Kubernetes Service (AKS), que podem ser usados para estas apps.
- Google Cloud Platform (GCP): O GCP oferece serviços como Compute Engine e App Engine que podem ser usados para hospedar estas apps. Além disso, o Google Kubernetes Engine (GKE) permite aplicações Dash em containers usando o Kubernetes.
- Heroku: Heroku é uma plataforma cloud popular para aplicativos da web.
- PythonAnywhere: PythonAnywhere é uma cloud especializada em hospedar aplicações Python. Oferecem suporte ao Dash e permitem implementar e executar aplicações Dash facilmente.
Esses são apenas alguns exemplos de provedores populares, mas existem muitos outros disponíveis. Cada provedor tem as suas próprias vantagens, preços e recursos, portanto, é recomendável pesquisar e comparar as opções para encontrar aquela que melhor satisfaça as necessidades e orçamento.
5 – Utilidade da visualização interativa.
A visualização interativa desempenha um papel fundamental na análise de dados e na comunicação de informações complexas.
Exploração de dados: A visualização interativa permite explorar os dados em detalhe, permitindo a manipulação e filtragem dos dados de acordo com o que se pretende. Isso possibilita a descoberta de insights ocultos, identificação de padrões e tendências e a compreensão mais profunda dos dados.
Comunicação eficaz: Torna a comunicação da informação complexa mais fácil e compreensível. Um utilizador pode interagir com os gráficos e visualizações, ajustar parâmetros, explorar diferentes perspectivas e obter respostas imediatas às suas perguntas, facilitando a transmissão de conhecimento.
Análise de cenários: Com visualizações interativas, podemos analisar diferentes cenários ajustando variáveis, modificando parâmetros e observando as mudanças contribuindo na análise de sensibilidade e na compreensão dos impactos potenciais das decisões tomadas.
Envolvimento do utilizador: A visualização interativa envolve os utilizadores ativamente, tornando a experiência mais envolvente e estimulante. Isso aumenta o interesse e a compreensão dos dados, incentivando a exploração e descoberta.
Tomada de decisões orientada por dados: Esta visualização fornece uma base sólida para a tomada de decisões orientada por dados. Podemos explorar diferentes perspectivas, comparar opções e avaliar os potenciais impactos antes de tomar decisões informadas e embasadas nos dados.