dash入門 データ可視化

【Plotly Dash】コールバックの基本をマスターする

2021年7月30日

前回は、ドロップダウン・メニューを使ってグラフを動的に表示する方法について紹介しました。

今回は、あらためてコールバック(Callback)の使い方の基本について、少し詳しく解説していきたいと思います。

以下の4つのコールバックについて、またまた株価の例を使って紹介します。

  • 通常のコールバック
  • 複数インプットのコールバック
  • 複数アウトプットのコールバック
  • コールバックの連鎖

最終的には以下のようなダッシュボードを作成します。

コールバックに関する公式HPの解説はこちらです。

では、今回もJupyter Dashを使っていきますので、まずは基本的なパッケージ等をインポートしておきます。

from jupyter_dash import JupyterDash
import dash_core_components as dcc
import dash_html_components as html
import plotly.graph_objects as go

そして、CSSを設定し、おまじないです。

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

では、順番に見ていきましょう。

通常のコールバック

ここでは、入力と出力が一つの基本的なコールバックについて見ていきます。

レイアウトの作成

最初にレイアウトを作成しましょう。

ここで、インプットとアウトプットに使うコンポーネントについては、それを特定するためにidが必要になります

ですので、インプットとなるdcc.Sliderには"stock_chart_slider"というidを振っておきます。

アウトプットするhtml.Divには"stock_chart"というidを振ります。

app.layout = html.Div([html.H1('Python Dash'),
                       html.H2('スライダーを使った散布図を作成する'),
                       # インプット用のコンポーネント
                       html.Div(dcc.Slider(id='stock_chart_slider', 
                                           min=1,
                                           max=12,
                                           value=1,
                                           marks={month: {'label': f'{month}月'}
                                                   for month in range(1, 13)}
                                           )),
                       # アウトプット用のコンポーネント
                       html.Div(id='stock_chart') 
                               ], style={'margin': '5%'})

では、以下のコードで実行してみましょう。

app.run_server()

すると以下のようなスライダーが表示されます。

ただ、この時点では何も起こりません。

ここにスライダーで選択された値をもとに、グラフを作成するコールバックを追加していきます。

コールバックの追加

では、インプットとアウトプットを処理するための、dash.dependencies.Inputとdash.dependencies.Outputをインポートします。

from dash.dependencies import Input, Output

グラフ描画用の関数を作成しておきましょう。

こちらはplotlyでただの散布図を作成する関数です。plotlyで散布図を作成する方法については「Python Plotly入門(③Scatter Plot)」をご参照ください。

def plot_data(fig, df, company_name_1, company_name_2, month):
  df_sub = df.query('month==@month')
  fig.add_trace(go.Scatter(x=df_sub[company_name_1],
                           y=df_sub[company_name_2],
                           mode='markers',
                           marker=dict(size=12,
                                       color='royalblue',
                                       opacity=0.7),
                           )
                )

ここからがコールバック部分です。

コールバックは@app.callbackから始めます。

@app.callback(
  Output(component_id='stock_chart', component_property='children'),
  Input(component_id='stock_chart_slider', component_property='value'),
)

@app.callbackの引数はOutputInputです。

Output、Inputともにcomponent_idcomponent_propertyを引数に取ります。

component_idはどのコンポーネントに結果を出力するか、もしくはどのコンポーネントをインプットとするか、を指定します。

component_propetyはcomponent_idで指定したコンポーネントのどのプロパティに結果を出力する、もしくはどのプロパティの値をインプットとするかを指定します。

@app.callbackの概要
  • 引数はOutputとInput。
  • Output、Inputはともにcomponent_idとcomponent_porpertyを指定する。
    • component_id
      出力もしくは入力に使うコンポーネントのIDを指定する。
    • component_property
      component_idで指定したコンポーネントの入力・出力に使うプロパティを指定する。

ここでは、インプットのコンポーネントはidが"stock_chart_slider"のSliderコンポーネントで、そのコンポーネントのプロパティのうち、取ってきたいのは"value"になります。

アウトプットのコンポーネントはidが"stock_chart"のDivタグで、出力するプロパティは"children"になります。

続けて関数部分を書きます。今はわかりやすくするために分けていますが、@app.callbackと関数の定義は行間を開けずに書かないといけません。

def update_graph(month):
  if month is None or month == "":
    month = 1
  fig = go.Figure()
  plot_data(fig, df_return, 'スノーピーク', '楽天', month)
  fig.update_layout(title=dict(text=f'<b>2020年{month}月の株価リターン',
                               font_color='grey'),
                    showlegend=False,
                    plot_bgcolor='white',
                    width=1000,
                    height=500,
                    )
  fig.update_xaxes(layout_settings(title='スノーピーク'))
  fig.update_yaxes(layout_settings(title='楽天'))
  return dcc.Graph(figure=fig)

関数名は何でも良いので、ここではupdate_graphとしています。

Sliderコンポーネントの"value"がインプットであり、それは月を意味しているので、引数はmonthとしています。

あとは、plotlyを使ってグラフを作成します。

最後にdcc.Graph(figure=fig)としてグラフ・コンポーネントを返しています

そしてサーバーを起動します。

app.run_server()

すると、以下のように、スライダーで選択した月に合わせて散布図が更新される簡単なダッシュボードが作成されます。

複数インプットのコールバック

次は、複数のコンポーネントの値をインプットとする場合です。

こちらも通常のコールバックと大きな差はなく、Inputを複数設定することによって実現することが可能です。

例として、2つのドロップダウン・メニューから銘柄を選び、スライダーで月を設定するダッシュボードを作成してみましょう。

まず、ドロップダウン・メニューに表示するためのリストを作成します。

キーがlabelとvalueの辞書型変数のリストとして作成します。

drop_down_options = [{'label': 'スノーピーク', 'value': 'スノーピーク'},
                     {'label': '楽天', 'value': '楽天'},
                     {'label': 'ANA', 'value': 'ANA'},
                     {'label': 'ぐるなび', 'value': 'ぐるなび'},
                     {'label': 'アルペン', 'value': 'アルペン'},
                     {'label': '三越', 'value': '三越'},
                     {'label': 'トヨタ', 'value': 'トヨタ'}]

ドロップダウン・メニューの使い方については以下の投稿で詳しく記載していますので、参考にしてみていただければと思います。

『【Plotly Dash】ドロップダウン・メニューをマスターする』

次に、ダッシュボードのレイアウトを作成します。

app.layout = html.Div([html.H1('Python Dash'),
                       html.H2('スライダーを使った散布図を作成する'),
                       html.Div([
                          html.Div(dcc.Dropdown(id='stock_chart_dropdown_1',
                                              options=drop_down_options,
                                              multi=False,
                                              placeholder='銘柄1'
                                              ),
                                          style={'width': '15%', 'display': 'inline-block',
                                          'margin-right': 10}),
                          html.Div(dcc.Dropdown(id='stock_chart_dropdown_2',
                                                options=drop_down_options,
                                                multi=False,
                                                placeholder='銘柄2'),
                                  style={'width': '15%', 'display': 'inline-block',
                                          'margin-right': 10}),       
                       ]),
                       html.Div(dcc.Slider(id='stock_chart_slider',
                                              min=1,
                                              max=12,
                                              value=1,
                                              marks={month: {'label': f'{month}月'}
                                                      for month in range(1, 13)}
                                              ),
                                    style={'width': '50%', 'margin-top': 20}),
                       html.Div(id='stock_chart', style={'width': '50%'})
                               ], style={'margin': '5%'})

ハイライト部分がドロップダウン・メニューの部分です。

スライダーのときと同様に、インプットやアウトプットとなるコンポーネントにはidを設定する必要がありますので、ここではそれぞれstock_chart_dropdown_1、stock_chart_dropdown_2としています。

plot_data関数は特にいじる必要がなかったので修正していません。

続いて、コールバック部分です。

アウトプットはそのままですが、インプットにドロップダウン・メニューののInputを2つ追加します

@app.callback(
  Output(component_id='stock_chart', component_property='children'),
  Input(component_id='stock_chart_slider', component_property='value'),
  Input(component_id='stock_chart_dropdown_1', component_property='value'),
  Input(component_id='stock_chart_dropdown_2', component_property='value'),
)
def update_graph(month, company_name_1, company_name_2):
  if (company_name_1 == "" or company_name_1 is None) or \
     (company_name_2 == "" or company_name_2 is None):
    return None
  fig = go.Figure()
  plot_data(fig, df_return, company_name_1, company_name_2, month)
  fig.update_layout(title=dict(text=f'<b>2020年{month}月の株価リターン',
                               font_color='grey'),
                    showlegend=False,
                    plot_bgcolor='white',
                    width=1000,
                    height=500,
                    )
  fig.update_xaxes(layout_settings(title=company_name_1))
  fig.update_yaxes(layout_settings(title=company_name_2))
  return dcc.Graph(figure=fig)
app.run_server()

Inputを2つ追加し、component_idにstock_chart_dropdown_1、stock_chart_dropdown_1、component_propertyにvalueを設定しています。

そして、関数update_graphの引数に@app.callbackに追加した2つのInputに対応するcompany_name_1とcompany_name_2というドロップダウン・メニューで選択された値を受ける変数を追加しています。

これで完成です。

このプログラムを実行すると以下のようなダッシュボードが出来上がります。

プログラムは正しいのに、エラーが出るという場合は、以下のコードから実行しているか確認してみてください。

app = JupyterDash(__name__, external_stylesheets=external_stylesheets)
server = app.server

複数アウトプットのコールバック

続いて、複数のコンポーネントに出力する場合です。

こちらも単一のアウトプットのときと大きく変わりません。

@app.callbackのところでOutputを複数設定し関数の戻り値を複数設定するだけです。

では、順番に作成していきましょう。

まずはレイアウトです。

app.layout = html.Div([html.H1('Python Dash'),
                       html.H2('スライダーを使った散布図と線グラフを作成する'),
                       html.Div([
                          html.Div(dcc.Dropdown(id='stock_chart_dropdown_1',
                                              options=drop_down_options,
                                              multi=False,
                                              placeholder='銘柄1'
                                              ),
                                          style={'width': '15%', 'display': 'inline-block',
                                          'margin-right': 10}),
                          html.Div(dcc.Dropdown(id='stock_chart_dropdown_2',
                                                options=drop_down_options,
                                                multi=False,
                                                placeholder='銘柄2'),
                                  style={'width': '15%', 'display': 'inline-block',
                                          'margin-right': 10}),       
                       ]),
                       html.Div(dcc.Slider(id='stock_chart_slider',
                                              min=1,
                                              max=12,
                                              value=1,
                                              marks={month: {'label': f'{month}月'}
                                                      for month in range(1, 13)}
                                              ),
                                    style={'width': '80%', 'margin-top': 20}),
                       html.Div([
                           html.Div(id='stock_scatter_plot', style={'width': '45%', 'display': 'inline-block'}),
                           html.Div(id='stock_line_chart', style={'width': '55%', 'display': 'inline-block'})
                               ], style={'margin': '5%', 'width': '90%'})
                    ]
                      )

変わったところを説明しますと、ハイライトしている以下の部分です。

html.Div([html.Div(id='stock_scatter_plot', style={'width': '45%', 'display': 'inline-block'}),
          html.Div(id='stock_line_chart', style={'width': '55%', 'display': 'inline-block'})
          ],
          style={'margin': '5%', 'width': '90%'})

Divブロックに対して、リストで2つのDivブロックを渡しています。

それぞれidを"stock_scatter_plot"と"stock_line_chart"としており、散布図を出力する場所と線グラフを出力する場所を表しています

styleを指定することで幅を微調整しています。

続いて、グラフの描画部分です。

line_plot_dataという関数を作成し、グラフを作成します。

こちらは単に、2つの時系列データを表示するだけです。

def line_plot_data(fig, df, company_name_1, company_name_2, month):
  df_sub = df.query('month==@month')
  fig.add_trace(go.Scatter(x=df_sub.index,
                           y=df_sub[company_name_1],
                           mode='lines+markers',
                           marker=dict(size=8,
                                       color='royalblue',
                                       opacity=0.7),
                           name=company_name_1,
                           hovertemplate='%{x}<br>%{y:.2%}'
                           )
                )
  fig.add_trace(go.Scatter(x=df_sub.index,
                           y=df_sub[company_name_2],
                           mode='lines+markers',
                           marker=dict(size=8,
                                       color='lightblue',
                                       opacity=0.7),
                           name=company_name_2,
                           hovertemplate='%{x}<br>%{y:.2%}'
                           )
                )

最後に、コールバック部分を書いていきます。

@app.callback(
  Output(component_id='stock_scatter_plot', component_property='children'),
  Output(component_id='stock_line_chart', component_property='children'),
  Input(component_id='stock_chart_slider', component_property='value'),
  Input(component_id='stock_chart_dropdown_1', component_property='value'),
  Input(component_id='stock_chart_dropdown_2', component_property='value'),
)
def update_graph(month, company_name_1, company_name_2):
  if (company_name_1 == "" or company_name_1 is None) or \
     (company_name_2 == "" or company_name_2 is None):
    return None, None
  
  fig_1 = go.Figure()
  plot_data(fig_1, df_return, company_name_1, company_name_2, month)
  
  fig_1.update_layout(title=dict(text=f'<b>2020年{month}月の株価リターン散布図',
                               font_color='grey'),
                      showlegend=False,
                      plot_bgcolor='white',
                      )
  fig_1.update_xaxes(layout_settings(title=company_name_1))
  fig_1.update_yaxes(layout_settings(title=company_name_2))
  
  fig_2 = go.Figure()
  line_plot_data(fig_2, df_return, company_name_1, company_name_2, month)
  fig_2.update_layout(title=dict(text=f'<b>2020年{month}月の株価リターン時系列推移',
                               font_color='grey'),
                      showlegend=True,
                      legend=dict(orientation='h',
                                  x=0,
                                  y=1.05),
                      plot_bgcolor='white',
                      hovermode='x'
                      )
  fig_2.update_xaxes(layout_settings(title=""))
  fig_2.update_xaxes(tickformat='%b-%d',)

  fig_2.update_yaxes(layout_settings(title="リターン"))
  fig_2.update_yaxes(ticklen=0)

  return dcc.Graph(figure=fig_1), dcc.Graph(figure=fig_2)
app.run_server()

@app.callbackで、Outputを2つ指定しています。

1つ目(fig_1)が散布図で2つ目(fig_2)が線グラフです。

そして、Outputを2つ指定しているので、関数update_graphの戻り値を2つ返す必要があります。

ここでは、dcc.Graph(figure=fig_1)とdcc.Graph(figure=fig_2)を返しています。

ちょっと関数が長くなってしまっていますが、ほとんどグラフのレイアウト設定のためのコードなので、無視していただいても大丈夫です。

これにより、以下のようなダッシュボードが出来上がります。

コールバックの連鎖

最後にコールバックの連鎖です。

コールバックの連鎖とは、あるコンポーネントの変化を受けてコールバックにより別のコンポーネントを設定しますが、それを受けてさらに別のコンポーネントも設定を変更するものです。

ここでは、銘柄1を選択すると、その銘柄は2銘柄目のドロップダウン・メニューで選択できないようにします

では、実際にやってみましょう。

ドロップダウン・メニューの表示項目を設定する関数を作成しておきます。

引数selectedで渡された銘柄については、disabledをTrueに設定します。

def set_drop_down_options(selected=None):
  drop_down_options = [
                      {'label': 'スノーピーク', 'value': 'スノーピーク'},
                      {'label': '楽天', 'value': '楽天'},
                      {'label': 'ANA', 'value': 'ANA'},
                      {'label': 'ぐるなび', 'value': 'ぐるなび'},
                      {'label': 'アルペン', 'value': 'アルペン'},
                      {'label': '三越', 'value': '三越'},
                      {'label': 'トヨタ', 'value': 'トヨタ'},
                      ]
  if selected is not None:
    for i, drop_down_option in enumerate(drop_down_options):
      if drop_down_option['label'] == selected:
        drop_down_options[i]['disabled'] = True

  return drop_down_options

あとは、銘柄1のドロップダウンをインプット、銘柄2のドロップダウンをアウトプットとするようなコールバックを作成します。

@app.callback(
  Output(component_id='stock_chart_dropdown_2', component_property='options'),
  Input(component_id='stock_chart_dropdown_1', component_property='value'),
)
def update_dropdown(company_name_1):
  return set_drop_down_options(company_name_1)

ここで、インプットは銘柄1のドロップダウンの選択された銘柄ですが、アウトプットは銘柄2のドロップダウンに設定する銘柄名のリストですので、component_propertyはリストを設定するプロパティである"options"を指定します

すると、以下のように銘柄1で選択した銘柄は銘柄2のドロップダウンでは選択できなくなります。

もちろん、disableにするのではなく、リストから削除する形にするのもありです。

また、ここでは銘柄1を選択した場合の銘柄2の挙動だけを制御しましたが、銘柄2を選択した場合の銘柄1の挙動を制御することも必要ですので、実際にダッシュボードを作成する際にはこの辺りも考慮していただければと思います。

状態を保持するコールバック

これまでのコールバックでは、入力するコンポーネントが変化すると必ずコールバックが発生していました。

この場合、例えば細かく設定をして最後に表示、ということができず、設定を変更するたびにグラフが変化します

色々設定を変えてもグラフは変化させず、特定のコンポーネントだけが変化した場合に他のコンポーネントも参照しながらグラフを作成するには、Stateを使います

ここでは、ちょっと無理がある設定ですが、ドロップダウンで銘柄を変えたときはグラフを変更せず、スライダーで月を変化させた場合だけ、銘柄とスライダーの値を取ってきてグラフを変化させるようにしたいと思います。

まず、dash.dependenciesからStateをインポートします。

from dash.dependencies import State

あとは、@app.callbackでドロップダウンのインプットの2つをInputではなくStateに変更します

@app.callback(
  Output(component_id='stock_scatter_plot', component_property='children'),
  Output(component_id='stock_line_chart', component_property='children'),
  Input(component_id='stock_chart_slider', component_property='value'),
  State(component_id='stock_chart_dropdown_1', component_property='value'),
  State(component_id='stock_chart_dropdown_2', component_property='value'),
)

これにより、ドロップダウンで銘柄が変更されてもコールバックは発生しません。

Inputとなっているスライダーの値が変更された場合のみ、コールバックが発生し、その際にStateとなっているドロップダウンの値も参照することができます。

実行結果は以下のようになります。

あまり例が良くないので、こういったダッシュボードはダメですが、一応機能の紹介になります。

皆さまは適切にStateを使っていただければと思います。

まとめ

今回はコールバックの基礎的な使い方をご紹介しました。

コールバックがわかればDashで理解しにくいところはそれほどないと思いますので、是非マスターしていただければと思います。

もっと高度な使い方もありますので、その辺りも今後ご紹介していきたいと思います。

では!

-dash入門, データ可視化
-,