Dashとは

DashはPythonでダッシュボードを作ることができるWebフレームワークです。Pythonでグラフを書く際にはPlotlyやmatplotlibなどのライブラリが有名ですが、Dashはグラフを描画するためのライブラリではなく、グラフをダッシュボードに載せてWebアプリ化するためのフレームワークです。そのため、裏ではflaskが動いています。また、グラフはPlotly製なので、見た目が綺麗なグラフを簡潔に書くことができるのが特徴です。

基本的な書き方

少し特徴的な書き方ですが、アプリのインスタンスを作成し、そこにlayoutを追記していきます。WebフレームワークなのでHTMLを書く必要があり、dash_html_componentsというHTMLパーツを組み合わせて書いていくイメージです。

import dash
# グラフやインプットに使うパーツ
import dash_core_components as dcc
# HTMLを記述するためのパーツ
import dash_html_components as html
# コールバックを実装するためのもの
from dash.dependencies import Input, Output, State
app = dash.Dash(__name__)
app.layout = html.Div(
    children=[
        html.H1("Hello World!")
    ]
)
if __name__ == '__main__':
    app.run_server(debug=False)

上記を記入したファイルを実行すると「Hello World!」と書かれたページが表示されるかと思います。

簡単なグラフを描画してみる

試しに実際にグラフを描画してみたいと思います。今回はコロナウイルスの都道府県別の累計感染者数のデータが公開されていたのでそれを使っていきます。
データの前処理については今回の本筋とは外れてしまうので割愛し、前処理後の以下のようなデータを使用します。

df.head()

file

インプットとアウトプットを用意する

2月からデータがあるため、期間を指定して表示するようにします。期間の指定はdcc.DatePickerRangeというパーツを使ってインプットのフィールドを設置することができます。
また、入力を確定するボタンとグラフを描画する場所も同時に用意しておきます。ボタンはhtml.Buttonを使って設置することができます。グラフを描画する場所はhtml.Divで作っておきます。
これらを先ほど書いたapp.layoutに追加します。
dashではインプットとアウトプットを1対1で関連付けする必要があります。そのため、インプットとアウトプットの識別子として、id属性を指定します。今回はインプットであるdcc.DatePickerRangeとhtml.Buttonに「date-range」と「show-button」という属性値を指定しています。アウトプットであるhtml.Divには「show-graph」という属性値を指定しています。

# 後の方で使う都道府県名のリスト
prefecture_options = [{"label":prefecture, "value":prefecture} for prefecture in df['prefectureNameJ'].unique().tolist()]
max_date = df['year_month_date'][len(df)-1].date()
min_date = df['year_month_date'][0].date()
app = dash.Dash(__name__)
app.layout = html.Div(
    children=[
        html.Div([
            dcc.DatePickerRange(
                id='date-range',
                min_date_allowed=min_date,
                max_date_allowed=max_date,
                initial_visible_month=max_date,
                end_date=max_date,
                start_date=(max_date - datetime.timedelta(weeks=4))
            ),
            html.Button(
                '表示する',
                id='show-button',
                n_clicks=0,
            ),
        ]),
        html.Div(
            id = 'show-graph'
        ),
    ]
)

上記を実行すると期間を指定するインプットフィールドと「表示する」というボタンが表示されます。

インプットに応じてグラフを表示する(コールバック)

先ほど用意したインプットに対応したグラフが表示されるようにしていきます。インプットに応じてアウトプットを返す処理をdashではコールバックと言います。
今回は都道府県ごとのデータなので1日の感染者数を地図上に描画してみます。このままだと累積の感染者数のデータなので、期間で絞り込んだ後に都道府県ごとに累計感染者数の1日ごとの差分をとって1日の感染者数に変換する必要があります。
コールバックは以下のように書きます。

@app.callback(
    Output('show-graph', 'children'),
    Input('show-button', 'n_clicks'),
    [State('date-range', 'start_date'),
     State('date-range', 'end_date')],
)
def update_output(n_clicks, start_date, end_date):
    # 日付を条件に抽出したデータフレームはグローバル変数にしておく
    global df_sum
    df_selected = df.query('year_month_date >= @start_date & year_month_date <= @end_date')
    df_sum = pd.DataFrame(columns=df_selected.columns)
    for _, group in df_selected.groupby('prefectureNameJ'):
        df_group = pd.DataFrame(group)
        df_group['testedPositive_diff'] = df_group.sort_values('year_month_date').testedPositive.diff()
        df_group = df_group.dropna()
        df_group['testedPositive_diff'] = df_group.testedPositive_diff.apply(lambda x: 0 if x < 0 else x)
        df_group['year_month_date'] = df_group['year_month_date'].astype(str)
        df_sum = pd.concat([df_sum, df_group])
    fig = px.scatter_geo(
        df_sum,
        lat='lat',
        lon='lon',
        hover_name='prefectureNameJ',
        size='testedPositive_diff',
        animation_frame='year_month_date',
        projection='natural earth',
    )
    fig.update_layout(
        height=600,
        title={
            'text': '全国の感染者分布',
            'font':{
                'size':25
            },
            'y':0.95,
            'x':0.5,
            'xanchor': 'center',
            'yanchor': 'top'},
        geo = dict(
            resolution = 50,
            landcolor = 'rgb(204, 204, 204)',
            lataxis = dict(
                range = [28, 47],
            ),
            lonaxis = dict(
                range = [125, 150],
            ),
        )
    )
    return dcc.Graph(
                figure = fig
            )

Output()にid属性値を用いてグラフを描画する場所を指定し、Input()にも同様にしてインプットのトリガーを指定します。また、State()にはインプットのトリガーが発火した際に渡される状態を保持しておきます。今回は「表示する」ボタンがインプットのトリガーで、選択された期間がStateに保持されます。
インプットのトリガーが発火すると、Inputの値とStateに保持された値を引数としてupdate_output(n_clicks, start_date, end_date)も発火します。
今回はstart_dateとend_dateを条件にしてデータを絞り込んで描画のために加工しています。
以降はPlotlyと同じ記述方法でグラフを作成し、それをリターンしています。Plotlyで地図上にデータを描画する方法はこちらを参考にしています。
上記を実行すると以下のような画面が表示されます。(再生ボタンを押したり、スライドバーを動かすことでグラフの時系列を動かすことができます!)
file

応用

アウトプットとして、インプットと対応したグラフを返すこともできます。例えば上記の関数の返り値を以下のように変更します。

    return html.Div([
            dcc.Graph(
                figure = fig
            ),
            dcc.Dropdown(
                id='select-prefecture',
                options=prefecture_options,
                value="東京"
            ),
            dcc.Graph(
                id='show-prefecture-graph',
            )
        ])

インプットとしてselect-prefectureというid属性を持ったdcc.Dropdown、アウトプットとしてshow-prefecture-graphというid属性を持ったdcc.Graphを返り値に追加しました。
dcc.Dropdownで都道府県名を選択し、dcc.Graphに選択された都道府県の感染者数の推移を可視化してみたいと思います。
先ほどと同様の流れでコールバック処理を追加します。

@app.callback(
    Output('show-prefecture-graph', 'figure'),
    Input('select-prefecture', 'value'),
)
def update_output(value):
    df_selected_by_prefecture =df_sum.query('prefectureNameJ == @value')
    fig = go.Figure()
    fig.add_trace(
        go.Scatter(
            x=df_selected_by_prefecture['year_month_date'].values.tolist(),
            y=df_selected_by_prefecture['testedPositive_diff'].values.tolist(),
            mode="lines",
            yaxis="y2",
            name='1日あたりの感染者数'
        )
    )
    fig.add_trace(
        go.Bar(
            x=df_selected_by_prefecture['year_month_date'].values.tolist(),
            y=df_selected_by_prefecture['testedPositive'].values.tolist(),
            name="累計の感染者数"
        )
    )
    fig.update_layout(
        go.Layout(
            title={
                'text': '{}の感染者数の推移'.format(value),
                "x":0.5,
                "y": 0.9
            },
            yaxis1={
                "title": "累計の感染者数",
                "side": "left",
            },
            yaxis2={
                "title": "1日あたりの感染者数",
                "side": "right",
                "overlaying": "y"
            },
        )
    )
    return  fig

上記を実行すると先ほど描画した地図の下に以下のようなグラフが表示されます。
file
累計の感染者数を棒グラフ、1日の感染者数を折れ線グラフで表示しました。Plotlyに慣れていればこのような複合グラフが簡単に描画できます。

最後にcssを追加して見た目を整えます。classNameという名前でclass属性を指定することができます。また、dashはデフォルトでassetsフォルダの中に置いたcssを読みに行くので、そこにcssファイルを作成します。

file

まとめ

このように、Dashを使うと簡潔なコードでダッシュボードを作ることができます。Plotlyで描けるほとんどのグラフに対応しているので、他にも公式ドキュメントを参照しながら色々なグラフを載せたダッシュボードを作ってみてください。