見出し画像

Dashを用いた人流データビジュアライゼーション Part.2

1. やりたいこと

G空間情報センターにて公開されている全国人流オープンデータを用いて、年度や時間帯ごとの滞在人口マップや該当地域に滞在した人の居住地情報をDashにて実装していきます。

画像1

2. 実装したい機能

①メッシュにカーソルを合わせると、インタラクティブに地図タイトルが変わる。
②都道府県/年度/月/平日休日区分/時間帯区分をプルダウンおよびラジオボタンで切り替えられる。
③時間帯区分のうち、深夜を選択すると、地図やグラフの背景が変わる。

ちなみに前回の続きとなるので、使用データの概要は省略します。

ディレクトリ構造はこんな感じ。

Agoop/
├── data
│   └── {pref_code}_from_to_city
│   │   └── 2019/{mm}/monthly_fromto_city.csv.zip2019年{mm}月の居住地別滞在人口
│   │   └── 2020/{mm}/monthly_fromto_city.csv.zip2020年{mm}月の居住地別滞在人口
│   └── {pref_code}_mesh1km
│   │   └── 2019/{mm}/monthly_mdp_mesh1km.csv.zip2019年{mm}月の1kmメッシュ滞在人口
│   │   └── 2020/{mm}/monthly_mdp_mesh1km.csv.zip2020年{mm}月の1kmメッシュ滞在人口
│   └── attribute
│   │   └── attribute_mesh1km_2019.csv.zip:1kmメッシュ属性_2019年ver
│   │   └── attribute_mesh1km_2020.csv.zip:1kmメッシュ属性_2020年ver
│   └── prefcode_citycode_master
│   │   └── prefcode_citycode_master_utf8_2019.csv.zip:都道府県・市区町村マスタ-_2019年ver
│   │   └── prefcode_citycode_master_utf8_2020.csv.zip:都道府県・市区町村マスタ-_2020年ver
└── src
   └── app.py

・monthly_fromto_city:市区町村別の滞在者のうち、どこから来た人なのかを示す居住地別滞在人口
・monthly_mdp_mesh1km:1kmメッシュ別の滞在人口
・attribute:1kmメッシュデータにおけるメッシュIDの座標を示すデータ
・prefcode_citycode_master:都道府県コード、市区町村コードのマスタデータ

3. Dashを用いたデータビジュアライゼーション

こちらが完成形です。(ドロップダウンやラジオボタンの機能を繰り返し書いているので少々長くなってしまった。。)

基本的には、選択した年度や時間帯でフィルタリングしたデータフレームをマッピングしたり、折線で表示しているだけです。

import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output

import ast
import json
import pandas as pd
from dfply import *
import geopandas as gpd
import plotly.express as px
import plotly.graph_objects as go
from shapely.geometry import Polygon

external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']
app = dash.Dash(__name__, external_stylesheets=external_stylesheets)

#--------------------------------------------------データ前処理--------------------------------------------------
attribute_mesh1km = pd.DataFrame() # 1kmメッシュ属性
prefcode_citycode_master = pd.DataFrame() # 都道府県・市区町村マスタ-
for year in [2019, 2020]:
   tmp_df01 = pd.read_csv(f'../data/attribute/attribute_mesh1km_{year}.csv.zip')
   tmp_df02 = pd.read_csv(f'../data/prefcode_citycode_master/prefcode_citycode_master_utf8_{year}.csv.zip')
   tmp_df01['year'] = year
   tmp_df02['year'] = year
   attribute_mesh1km = pd.concat([attribute_mesh1km, tmp_df01])
   prefcode_citycode_master = pd.concat([prefcode_citycode_master, tmp_df02])


# 滞在人口1kmメッシュデータ
dfs = []
for pref_code in [11, 12, 13, 14]:
   for mm in range(12):
       mm = str(mm+1).zfill(2)
       dfs.append(pd.read_csv(f'../data/{pref_code}_mesh1km/2019/{mm}/monthly_mdp_mesh1km.csv.zip'))
       dfs.append(pd.read_csv(f'../data/{pref_code}_mesh1km/2020/{mm}/monthly_mdp_mesh1km.csv.zip'))

df = pd.concat(dfs).reset_index(drop=True)
df = df >> inner_join(attribute_mesh1km, by=['mesh1kmid', 'prefcode', 'citycode', 'year']) >> inner_join(prefcode_citycode_master[['citycode', 'cityname', 'year']], by=['citycode', 'year'])
df['cityname'] = df['cityname'].apply(lambda x : x[5:] if '東京23区' in x else x)
df['geometry'] = df.apply(lambda x: Polygon([(x['lon_min'], x['lat_min']), (x['lon_min'], x['lat_max']), (x['lon_max'], x['lat_max']), (x['lon_max'], x['lat_min'])]), axis=1)
df = df.set_index('mesh1kmid')

# 滞在人口From-Toデータ
dfs = []
for pref_code in [11, 12, 13, 14]:
   for mm in range(12):
       mm = str(mm+1).zfill(2)
       dfs.append(pd.read_csv(f'../data/{pref_code}_from_to_city/2019/{mm}/monthly_fromto_city.csv.zip'))
       dfs.append(pd.read_csv(f'../data/{pref_code}_from_to_city/2020/{mm}/monthly_fromto_city.csv.zip'))

df_fromtocity = pd.concat(dfs).reset_index(drop=True)
df_fromtocity = df_fromtocity >> inner_join(prefcode_citycode_master[['citycode', 'cityname', 'year']], by=['citycode', 'year'])
df_fromtocity['cityname'] = df_fromtocity['cityname'].apply(lambda x : x[5:] if '東京23区' in x else x)
df_fromtocity['year_mm'] = df_fromtocity['year'].astype(str) + '/' + df_fromtocity['month'].astype(str)
#--------------------------------------------------------------------------------------------------------------

#--------------------------------------------------レイアウト----------------------------------------------------
app.layout = html.Div(
   [
       html.H3('人流オープンデータ(国土交通省)', style={'textAlign': 'center'}),
       html.Div(
           [
               html.Div(
                   [
                       html.H5('都道府県の選択'),
                       dcc.Dropdown(
                           id='pref-dropdown',
                           options=[
                               {'label': '埼玉県', 'value': '11埼玉県'},
                               {'label': '千葉県', 'value': '12千葉県'},
                               {'label': '東京都', 'value': '13東京都'},
                               {'label': '神奈川県', 'value': '14神奈川県'}
                           ],
                           value='13東京都',
                       ),
                   ],style={'padding': '1%', 'width': '10%'},
               ),
               html.Div(
                   [
                       html.H5('年度'),
                       dcc.Dropdown(
                           id='year-dropdown',
                           options=[
                               {'label': '2019年', 'value': 2019},
                               {'label': '2020年', 'value': 2020}
                           ],
                           value=2019,
                       ),
                   ],style={'padding': '1%', 'width': '10%'},
               ),
               html.Div(
                   [
                       html.H5('月'),
                       dcc.Dropdown(
                           id='month-dropdown',
                           options=[{'label': f'{mm+1}月', 'value': mm+1} for mm in range(12)],
                           value=1,
                       ),
                   ],style={'padding': '1%', 'width': '10%'},
               ),
               html.Div(
                   [
                       html.H5('平日休日区分'),
                       dcc.Dropdown(
                           id='dayflag-dropdown',
                           options=[
                               {'label': '休日', 'value': '0休日'},
                               {'label': '平日', 'value': '1平日'},
                               {'label': '全日', 'value': '2全日'}
                           ],
                           value='0休日',
                       ),
                   ],style={'padding': '1%', 'width': '10%'},
               ),
               html.Div(
                   [
                       html.H5('時間帯区分'),
                       dcc.Dropdown(
                           id='timezone-dropdown',
                           options=[
                               {'label': '昼(11時台〜14時台)', 'value': '0昼(11時台〜14時台)'},
                               {'label': '深夜(1時台〜4時台)', 'value': '1深夜(1時台〜4時台)'},
                               {'label': '終日(0時台〜23時台)', 'value': '2終日(0時台〜23時台)'}
                           ],
                           value='0昼(11時台〜14時台)',
                       ),
                   ],style={'padding': '1%', 'width': '10%'},
               ),
           ],style={'margin': 'auto', 'display': 'flex'},
       ),
       html.Div(
           [
               html.H3(id='map-titile', style={'textAlign': 'center'}),
               html.H5(id='hoverdata-h5', style={'textAlign': 'center'}),
               html.Div(dcc.Loading(id='loading', type='circle', children=dcc.Graph(id='population-map')), style={'width': '80%', 'margin': 'auto'}),
           ],
       ),
       html.Div(
           [
               html.Div(
                   [
                       html.H5('都道府県の選択'),
                       dcc.RadioItems(
                           id='pref-radioitems',
                           options=[
                               {'label': '埼玉県', 'value': '11埼玉県'},
                               {'label': '千葉県', 'value': '12千葉県'},
                               {'label': '東京都', 'value': '13東京都'},
                               {'label': '神奈川県', 'value': '14神奈川県'}
                           ],
                           value='13東京都',
                       ),
                   ],style={'padding': '1%', 'width': '10%'},
               ),
               html.Div(
                   [
                       html.H5('平日休日区分'),
                       dcc.RadioItems(
                           id='dayflag-radioitems',
                           options=[
                               {'label': '休日', 'value': '0休日'},
                               {'label': '平日', 'value': '1平日'},
                               {'label': '全日', 'value': '2全日'}
                           ],
                           value='0休日',
                       ),
                   ],style={'padding': '1%', 'width': '10%'},
               ),
               html.Div(
                   [
                       html.H5('時間帯区分'),
                       dcc.RadioItems(
                           id='timezone-radioitems',
                           options=[
                               {'label': '昼(11時台〜14時台)', 'value': '0昼(11時台〜14時台)'},
                               {'label': '深夜(1時台〜4時台)', 'value': '1深夜(1時台〜4時台)'},
                               {'label': '終日(0時台〜23時台)', 'value': '2終日(0時台〜23時台)'}
                           ],
                           value='0昼(11時台〜14時台)',
                       ),
                   ],style={'padding': '1%', 'width': '10%'},
               ),
           ],style={'margin': 'auto', 'display': 'flex'},
       ),
       html.Div(
           [
               html.H5('市区町村選択'),
               dcc.Dropdown(
                   id='city-dropdown',
                   options=[{'label': cityname, 'value': cityname} for cityname in df_fromtocity[df_fromtocity['prefcode']==13].cityname.unique()],
                   value='千代田区',
               ),
           ],style={'padding': '1%', 'width': '10%'},
       ),
       html.Div(
           [
               html.H3(id='line-titile', style={'textAlign': 'center'}),
               html.Div(dcc.Graph(id='population-line-graph'), style={'width': '80%', 'margin': 'auto'}),
           ],
       ),
   ],
)
#--------------------------------------------------------------------------------------------------------------

#--------------------------------------------------コールバック関数-----------------------------------------------
@app.callback(Output('map-titile', 'children'),  Input('pref-dropdown', 'value'), Input('year-dropdown', 'value'), Input('month-dropdown', 'value'), Input('dayflag-dropdown', 'value'), Input('timezone-dropdown', 'value'))
def update_title(pref, year, month, dayflag, timezone):
   return f'{pref[2:]} {year}{month}月 {dayflag[1:]}/{timezone[1:]}の滞在人口'

@app.callback(Output('population-map', 'figure'), Input('pref-dropdown', 'value'), Input('year-dropdown', 'value'), Input('month-dropdown', 'value'), Input('dayflag-dropdown', 'value'), Input('timezone-dropdown', 'value'))
def update_map(pref, year, month, dayflag, timezone):
   df_target = df >> mask(X.prefcode==int(f'{pref[:2]}'), X.year==year, X.month==month, X.dayflag==int(f'{dayflag[0]}'), X.timezone==int(f'{timezone[0]}'))
   if timezone=='1深夜(1時台〜4時台)':
       mapbox_style = 'carto-darkmatter'
   else:
       mapbox_style = 'open-street-map'
   fig = px.choropleth_mapbox(
       df_target, geojson=gpd.GeoSeries(df_target['geometry']).__geo_interface__, locations=df_target.index, color='population',
       color_continuous_scale='Jet', center={'lat': df_target['lat_center'].mean(), 'lon': df_target['lon_center'].mean()},
       hover_data=['cityname'], mapbox_style=mapbox_style,opacity=0.5,zoom=9, height=800,
   )
   return fig

@app.callback(Output('hoverdata-h5', 'children'), Input('population-map', 'hoverData'))
def update_map_title(hoverData):
   try:
       title = ast.literal_eval(json.dumps(hoverData, ensure_ascii=False))
       meshcode = title['points'][0]['location']
       population = title['points'][0]['z']
       location = title['points'][0]['customdata'][0]
       return f'{location}(地域メッシュコード:{meshcode}{population}人'
   except:
       return 'NULL'

@app.callback(Output('city-dropdown', 'options'), Input('pref-radioitems', 'value'))
def update_cityname(pref):
   tmp = df_fromtocity >> mask(X.prefcode==int(f'{pref[:2]}'))
   return [{'label': cityname, 'value': cityname} for cityname in tmp['cityname'].unique()]

@app.callback(Output('population-line-graph', 'figure'), Input('city-dropdown', 'value'), Input('dayflag-radioitems', 'value'), Input('timezone-radioitems', 'value'))
def update_line_graph(cityname, dayflag, timezone):
   df_target = df_fromtocity >> mask(X.cityname==cityname, X.dayflag==int(f'{dayflag[0]}'), X.timezone==int(f'{timezone[0]}'))
   if timezone=='1深夜(1時台〜4時台)':
       template = 'plotly_dark'
   else:
       template = 'plotly_white'
   fig = go.Figure()
   fig.add_trace(go.Scatter(x=df_target[df_target['from_area']==0].year_mm, y=df_target[df_target['from_area']==0].population, mode='lines+markers',name='自市区町村'))
   fig.add_trace(go.Scatter(x=df_target[df_target['from_area']==1].year_mm, y=df_target[df_target['from_area']==1].population, mode='lines+markers',name='県内他市区町村'))
   fig.add_trace(go.Scatter(x=df_target[df_target['from_area']==2].year_mm, y=df_target[df_target['from_area']==2].population, mode='lines+markers',name='地方ブロック内他県'))
   fig.add_trace(go.Scatter(x=df_target[df_target['from_area']==3].year_mm, y=df_target[df_target['from_area']==3].population, mode='lines+markers',name='他の地方ブロック'))
   fig.update_layout(title=f'{cityname} {dayflag[1:]}/{timezone[1:]}の居住地別滞在人口', template=template, font_size=15)
   return fig

#--------------------------------------------------------------------------------------------------------------

if __name__ == '__main__':
   app.run_server(debug=True)

3-1. 人流データ前処理

2019/1〜2020/12までの滞在人口1kmメッシュデータと滞在人口From-Toデータの2つのデータフレームを作成→外部マスタデータ(attribute_mesh1kmprefcode_citycode_master)を紐付けて、画面に反映したいカラム(citynameなど)を追加する。

attribute_mesh1km = pd.DataFrame() # 1kmメッシュ属性
prefcode_citycode_master = pd.DataFrame() # 都道府県・市区町村マスタ-
for year in [2019, 2020]:
   tmp_df01 = pd.read_csv(f'../data/attribute/attribute_mesh1km_{year}.csv.zip')
   tmp_df02 = pd.read_csv(f'../data/prefcode_citycode_master/prefcode_citycode_master_utf8_{year}.csv.zip')
   tmp_df01['year'] = year
   tmp_df02['year'] = year
   attribute_mesh1km = pd.concat([attribute_mesh1km, tmp_df01])
   prefcode_citycode_master = pd.concat([prefcode_citycode_master, tmp_df02])


# 滞在人口1kmメッシュデータ
dfs = []
for pref_code in [11, 12, 13, 14]:
   for mm in range(12):
       mm = str(mm+1).zfill(2)
       dfs.append(pd.read_csv(f'../data/{pref_code}_mesh1km/2019/{mm}/monthly_mdp_mesh1km.csv.zip'))
       dfs.append(pd.read_csv(f'../data/{pref_code}_mesh1km/2020/{mm}/monthly_mdp_mesh1km.csv.zip'))

df = pd.concat(dfs).reset_index(drop=True)
df = df >> inner_join(attribute_mesh1km, by=['mesh1kmid', 'prefcode', 'citycode', 'year']) >> inner_join(prefcode_citycode_master[['citycode', 'cityname', 'year']], by=['citycode', 'year'])
df['cityname'] = df['cityname'].apply(lambda x : x[5:] if '東京23区' in x else x) # 2019年と2020年のcitynameを統一
df['geometry'] = df.apply(lambda x: Polygon([(x['lon_min'], x['lat_min']), (x['lon_min'], x['lat_max']), (x['lon_max'], x['lat_max']), (x['lon_max'], x['lat_min'])]), axis=1)
df = df.set_index('mesh1kmid')

# 滞在人口From-Toデータ
dfs = []
for pref_code in [11, 12, 13, 14]:
   for mm in range(12):
       mm = str(mm+1).zfill(2)
       dfs.append(pd.read_csv(f'../data/{pref_code}_from_to_city/2019/{mm}/monthly_fromto_city.csv.zip'))
       dfs.append(pd.read_csv(f'../data/{pref_code}_from_to_city/2020/{mm}/monthly_fromto_city.csv.zip'))

df_fromtocity = pd.concat(dfs).reset_index(drop=True)
df_fromtocity = df_fromtocity >> inner_join(prefcode_citycode_master[['citycode', 'cityname', 'year']], by=['citycode', 'year'])
df_fromtocity['cityname'] = df_fromtocity['cityname'].apply(lambda x : x[5:] if '東京23区' in x else x)
df_fromtocity['year_mm'] = df_fromtocity['year'].astype(str) + '/' + df_fromtocity['month'].astype(str) # 時系列のx軸として可視化するためのカラム

以下の計算処理が結構時間がかかると思いますので、必要に応じて、データ量を減らして頂ければと。

df['geometry'] = df.apply(lambda x: Polygon([(x['lon_min'], x['lat_min']), (x['lon_min'], x['lat_max']), (x['lon_max'], x['lat_max']), (x['lon_max'], x['lat_min'])]), axis=1)

3-2. レイアウト

dcc.Dropdowndcc.RadioItemsで切り替えたい項目を設置しました。
dcc.Loadingは地図が表示するまでの待機時間でクルクルアイコンを表示させるために追加してます。

3-3. 一つ目のコールバック関数

pref-dropdown, month-dropdown, dayflag-dropdown, timezone-dropdownの各valueを受け取って、html.H3(id='map-titile')に渡す。

@app.callback(Output('map-titile', 'children'),  Input('pref-dropdown', 'value'), Input('year-dropdown', 'value'), Input('month-dropdown', 'value'), Input('dayflag-dropdown', 'value'), Input('timezone-dropdown', 'value'))
def update_title(pref, year, month, dayflag, timezone):
   return f'{pref[2:]} {year}年{month}月 {dayflag[1:]}/{timezone[1:]}の滞在人口'

3-4. 二つ目のコールバック関数

pref-dropdown, month-dropdown, dayflag-dropdown, timezone-dropdownの各valueでフィルタリング&描画し、dcc.Graph(id='population-map')に渡します。
※ただし、深夜帯を選択した場合は地図の背景を切り替える。

@app.callback(Output('population-map', 'figure'), Input('pref-dropdown', 'value'), Input('year-dropdown', 'value'), Input('month-dropdown', 'value'), Input('dayflag-dropdown', 'value'), Input('timezone-dropdown', 'value'))
def update_map(pref, year, month, dayflag, timezone):
   df_target = df >> mask(X.prefcode==int(f'{pref[:2]}'), X.year==year, X.month==month, X.dayflag==int(f'{dayflag[0]}'), X.timezone==int(f'{timezone[0]}'))
   if timezone=='1深夜(1時台〜4時台)':
       mapbox_style = 'carto-darkmatter'
   else:
       mapbox_style = 'open-street-map'
   fig = px.choropleth_mapbox(
       df_target, geojson=gpd.GeoSeries(df_target['geometry']).__geo_interface__, locations=df_target.index, color='population',
       color_continuous_scale='Jet', center={'lat': df_target['lat_center'].mean(), 'lon': df_target['lon_center'].mean()},
       hover_data=['cityname'], mapbox_style=mapbox_style,opacity=0.5,zoom=9, height=800,
   )
   return fig

3-5. 三つ目のコールバック関数

地図上のメッシュにカーソルを合わせたときに、該当メッシュの地名と地域メッシュコード、滞在人口をhtml.H5(id='hoverdata-h5')に渡す。

@app.callback(Output('hoverdata-h5', 'children'), Input('population-map', 'hoverData'))
def update_map_title(hoverData):
   try:
       title = ast.literal_eval(json.dumps(hoverData, ensure_ascii=False))
       meshcode = title['points'][0]['location']
       population = title['points'][0]['z']
       location = title['points'][0]['customdata'][0]
       return f'{location}(地域メッシュコード:{meshcode}{population}人'
   except:
       return 'NULL'

3-6. 四つ目のコールバック関数

pref-radioitemsのvalue(埼玉県, 千葉県, 東京都, 神奈川県)によって、dcc.Dropdown(id='city-dropdown')optionsを変更する。

@app.callback(Output('city-dropdown', 'options'), Input('pref-radioitems', 'value'))
def update_cityname(pref):
   tmp = df_fromtocity >> mask(X.prefcode==int(f'{pref[:2]}'))
   return [{'label': cityname, 'value': cityname} for cityname in tmp['cityname'].unique()]

3-7. 五つ目のコールバック関数

pref-dropdown, month-dropdown, dayflag-dropdown, timezone-dropdownの各valueでフィルタリングし、居住地区分ごとの滞在人口を描画して、dcc.Graph(id='population-line-graph')に渡す。
※ただし、深夜帯を選択した場合はグラフの背景を切り替える。

@app.callback(Output('population-line-graph', 'figure'), Input('city-dropdown', 'value'), Input('dayflag-radioitems', 'value'), Input('timezone-radioitems', 'value'))
def update_line_graph(cityname, dayflag, timezone):
   df_target = df_fromtocity >> mask(X.cityname==cityname, X.dayflag==int(f'{dayflag[0]}'), X.timezone==int(f'{timezone[0]}'))
   if timezone=='1深夜(1時台〜4時台)':
       template = 'plotly_dark'
   else:
       template = 'plotly_white'
   fig = go.Figure()
   fig.add_trace(go.Scatter(x=df_target[df_target['from_area']==0].year_mm, y=df_target[df_target['from_area']==0].population, mode='lines+markers',name='自市区町村'))
   fig.add_trace(go.Scatter(x=df_target[df_target['from_area']==1].year_mm, y=df_target[df_target['from_area']==1].population, mode='lines+markers',name='県内他市区町村'))
   fig.add_trace(go.Scatter(x=df_target[df_target['from_area']==2].year_mm, y=df_target[df_target['from_area']==2].population, mode='lines+markers',name='地方ブロック内他県'))
   fig.add_trace(go.Scatter(x=df_target[df_target['from_area']==3].year_mm, y=df_target[df_target['from_area']==3].population, mode='lines+markers',name='他の地方ブロック'))
   fig.update_layout(title=f'{cityname} {dayflag[1:]}/{timezone[1:]}の居住地別滞在人口', template=template, font_size=15)
   return fig


一度にやろうとせずに、一つ一つの機能を着実にコーディングしていくことをお勧めします!

4. 参考書籍

Python インタラクティブ・データビジュアライゼーション入門 ―Plotly/Dashによるデータ可視化とWebアプリ構築

この記事が気に入ったらサポートをしてみませんか?