Python Dashで地図とグラフのダッシュボードを作る

## Dashを使ってダッシュボードを作る

ローコードでブラウザでの表示が可能なダッシュボードを作成できるDashを使って選択したデータの時系列グラフと指定した年度の都道府県別マップを作成してみました。

## 使用しているパッケージ

今回使用しているパッケージは以下の通りです。
- Pandas
- NumPy
- GeoPandas
- Dash

## 使用するデータ

### [SSDSE-県別推移(SSDSE-B)](https://www.nstac.go.jp/use/literacy/ssdse/?doing_wp_cron=1697961570.8732249736785888671875#SSDSE-B)
このデータセットのグラフ化・地図化を行いました。このデータは様々な分野の都道府県別・時系列データを集めたデータセットになっています。独立行政法人統計センターが公開しているものです。

### [47都道府県のポリゴン](https://japonyol.net/editor/article/47-prefectures-geojson.html)
[japonyol.net](https://japonyol.net/)で公開されているこのデータを使わせていただきました。

## 実装

![demo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/773995/d71a2bb8-a584-1247-6ee8-157dc0cb04a8.gif)

年度を選択すると地図の表示が変化します。

地図上で特定の都道府県を選択すると時系列データのグラフが都道府県の時系列データに変化し、
もう一度、同じ都道府県をクリックすると全国のデータに戻るようになっています。

表示するデータを変えることもできます。

全コードは下にあります。

## 鍵となる部分の解説

### レイアウト

``` python
app.layout = html.Div([],id=XXX,style={'width':'100px'})
```
このような感じでhtmlのコンポーネントをPythonコードで並べることができます。コンポーネントをネストしていきたいときはPythonコードをネストしていきます。ここで、つけるIDは動的に変化させるときに利用します。

レイアウトについてはここにまとめられています。
https://dash.plotly.com/layout

### コールバック

``` python
@app.callback(Output('graph_title', 'children'),
    [Input('map', 'selectedData'), Input('data', 'value')])
def function(map, data):
    ....
    return children
```
`Input()`を使うとドロップダウンなどから値を取ってくることができます。この値を関数に渡して処理します。`Input()`の１番目の引数がhtmlのIDで、ここで指定したIDのコンポーネントから値を取得します。2番目の引数でコンポーネントの中のどのプロパティを利用するかを指定します。`value`がよく使われますが、図の中の要素から値を取得したい場合には`selectedData`としてあげることで、クリックした要素の値を取ってくることができます。これを利用して、選択した都道府県の時系列グラフを表示するようにしています。

また、`Output()`で書き換えるコンポーネントとその中のプロパティを指定しています。

図からの要素の取得はここにまとめられています。
https://dash.plotly.com/interactive-graphing

### GeoDataFrameのマップ化

地図を作るのに`choropleth()`を利用しています。

GeoPandasで作成した`GeoDataFrame`を地図化する際に、このデータでは'都道府県'列をインデックスにセットする必要があります。インデックスにセットしたら`choropleth()`の初めの引数には`GeoDataFrame`、２番目の引数`geojson`には`geometory`、３番目の引数`locations`には`index`を渡します。

色分けに使用する列は引数`color`に列名を渡してあげればよいです。

## 全コード

GitHub上にも置いておきます。
https://github.com/HKimura787/blog/blob/main/Qiita/Dash/interactive_app1.py

``` python
import pandas as pd
import geopandas as gpd
import os
from dash import Dash, html, dcc, Input, Output
import plotly.express as px
import numpy as np

def data_handring():

    # the function to get absolute path 
    def get_absolute_path(path):
        this_dir = os.path.dirname(os.path.abspath(__file__))
        return os.path.join(this_dir, path)

    # read geojson
    gdf = gpd.read_file(get_absolute_path('data\prefectures.geojson'))

    # read csv
    df = pd.read_csv(get_absolute_path('data\SSDSE-B-2023.csv'), encoding='shift_jis', skiprows=1)

    # merge csv and geojson
    merged_df = gpd.GeoDataFrame(df.merge(gdf, left_on='都道府県', right_on='name', how = 'left'))
    print(merged_df.head())
    
    # plot map
    # merged_df.query('年度==2020').plot(column='総人口', legend=True, figsize=(10, 10))
    # plt.show()

    return merged_df

def main():
    df = data_handring()

    app = Dash(__name__)

    # レイアウトの作成
    app.layout = html.Div([
        html.H1('都道府県別統計データの推移'),
        html.Div([
            html.Div([
                html.H2('データを選択'),
                dcc.Dropdown(
                    id='data',
                    options=[{'label': i, 'value': i} for i in df.columns[3:]],
                    value='総人口',
                    style={'width': '50%'}
                )
            ]),
            html.Div([
                html.H2('地図に表示する年度を選択'),
                dcc.RadioItems(
                    id='year',
                    options=[{'label': i, 'value': i} for i in np.arange(df['年度'].min(), df['年度'].max()+1)],
                    value=df['年度'].max(),
                    style={'display': 'flex'})
            ]),
        ]),
        html.Div([
            html.Div([
                html.H2(id='graph_title',),
                dcc.Graph(id='graph',style={'height': '70vh'}),
            ]),
            html.Div([
                html.H2(id='map_title'),
                dcc.Graph(id='map',style={'height': '70vh'})
            ]),
        ], style={'display': 'flex'})])
    
    # 時系列グラフのタイトルを変更するためのコールバック関数
    @app.callback(Output('graph_title', 'children'),
        [Input('map', 'selectedData'), Input('data', 'value')])
    def data_graph_title(selectedData, data):
        if selectedData is None:
            return f'全国の{data}の推移'
        else:
            location = selectedData['points'][0]['location']
            return f'{location}の{data}の推移'
    
    # 時系列グラフを更新するためのコールバック関数
    @app.callback(Output('graph', 'figure'),
        [Input('map', 'selectedData'), Input('data', 'value')])
    def display_selected_data(selectedData, data):
        if selectedData is None:
            filtered_df = df.groupby('年度').sum().reset_index()
        else:
            location = selectedData['points'][0]['location']
            filtered_df = df.query('都道府県 == @location')
        fig = px.line(filtered_df, x='年度', y=data)
        return fig
    
    # 地図のタイトルを変更するためのコールバック関数
    @app.callback(
        Output('map_title', 'children'),
        [Input('year', 'value'), Input('data', 'value')])
    def map_title(year, data):
        return f'{year}年度の{data}の地図'
    
    # 地図を更新するためのコールバック関数
    @app.callback(
        Output('map', 'figure'),
        [Input('year', 'value'), Input('data', 'value')])
    def update_map(year,data):
        filtered_df = df.query(f'年度 == {int(year)}').set_index('都道府県')       
        fig = px.choropleth(
            filtered_df, 
            geojson=filtered_df.geometry, 
            locations=filtered_df.index, 
            color=data,
            projection="mercator",
            fitbounds="locations",
            range_color=(df[data].min(), df[data].max()),
            color_continuous_scale="Reds",
            basemap_visible=False)
        fig.update_layout(clickmode='event+select')        
        return fig
    
    # サーバーの起動
    app.run_server(debug=True, use_reloader=False)

if __name__ == '__main__':
    main()
```