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

dash入門

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

今回は、あらためてコールバック(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で理解しにくいところはそれほどないと思いますので、是非マスターしていただければと思います。

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

では!

mm0824

システム開発会社や金融機関で統計や金融工学を使ったモデリング・分析業務を長く担当してきました。

現在はコンサルティング会社のデータ・サイエンティストとして機械学習、自然言語処理技術を使ったモデル構築・データ分析を担当しています。

皆様の業務や勉強のお役に立てれば嬉しいです。

mm0824をフォローする
dash入門 データ可視化
mm0824をフォローする
楽しみながら理解する自然言語処理入門

コメント

タイトルとURLをコピーしました