0
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-10-22

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

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

使用しているパッケージ

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

  • Pandas
  • NumPy
  • GeoPandas
  • Dash

使用するデータ

SSDSE-県別推移(SSDSE-B)

このデータセットのグラフ化・地図化を行いました。このデータは様々な分野の都道府県別・時系列データを集めたデータセットになっています。独立行政法人統計センターが公開しているものです。

47都道府県のポリゴン

japonyol.netで公開されているこのデータを使わせていただきました。

実装

demo.gif

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

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

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

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

鍵となる部分の解説

レイアウト

app.layout = html.Div([],id=XXX,style={'width':'100px'})

このような感じでhtmlのコンポーネントをPythonコードで並べることができます。コンポーネントをネストしていきたいときはPythonコードをネストしていきます。ここで、つけるIDは動的に変化させるときに利用します。

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

コールバック

@app.callback(Output('graph_title', 'children'),
    [Input('map', 'selectedData'), Input('data', 'value')])
def function(map, data):
    ....
    return children

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

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

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

GeoDataFrameのマップ化

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

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

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

全コード

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

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()
0
3
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?