前回は、ドロップダウン・メニューを使ってグラフを動的に表示する方法について紹介しました。
今回は、あらためてコールバック(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の引数はOutputとInputです。
Output、Inputともにcomponent_idとcomponent_propertyを引数に取ります。
component_idはどのコンポーネントに結果を出力するか、もしくはどのコンポーネントをインプットとするか、を指定します。
component_propetyは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で理解しにくいところはそれほどないと思いますので、是非マスターしていただければと思います。
もっと高度な使い方もありますので、その辺りも今後ご紹介していきたいと思います。
では!