はじめに
前回のチュートリアルで簡単なデータの可視化手法を学びました。
しかし、前回までの内容ならば他のライブラリでも可能です。そこで今回は、図に対して操作を行なえる対話形式のアプリを作成することを目指します。
なお今回はPythonのデコレータについての知識が必要になるので、以下を参照すると理解しやすいでしょう。
対話形式の図を作成する
入力した値を反映させる
まずは以下のコードと結果をご覧ください。
import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output
app = dash.Dash()
app.layout = html.Div([
dcc.Input(
id='my-id',
value='initial value',
type='text'
),
html.Div(id='my-div')
])
@app.callback(
Output(component_id='my-div', component_property='children'),
[Input(component_id='my-id', component_property='value')]
)
def update_output_div(input_value):
return 'You have entered "{}"'.format(input_value)
if __name__ == '__main__':
app.run_server()
上の図を見てわかる通り、Input
ボックスの中に入力した値が、ボックスの直下にあるDivタグ内の値に反映されています。
この対話形式の描画を可能にしたのはapp.callback
デコレータで追加した部分です。
@app.callback(
Output(component_id='my-div', component_property='children'),
[Input(component_id='my-id', component_property='value')]
)
def update_output_div(input_value):
return 'You have entered "{}"'.format(input_value)
Dashでは入力と出力は、単に特定のコンポーネントのプロパティにすぎません。
上の例では、入力はmy-id
のIDをもつコンポーネントのvalue
プロパティであり、出力はmy-div
のIDをもつコンポーネントのchildren
プロパティになります。
デコレートされた関数は、入力プロパティが変更されるたびに自動的に呼び出されます。
Dashでは、入力プロパティに新しく入力された値を入力引数として関数に提供し、関数の返り値を出力コンポーネントの出力プロパティに更新します。
なおmy-div
では初期設定でchildren
に何も設定していませんが、これはDashが起動すると、出力コンポーネントの初期値を入力コンポーネントから呼び出すため、設定していたとしても上書きされるからです。
スライダーを使って図を操作する
import dash
import dash_core_components as dcc
import dash_html_components as html
import pandas as pd
import plotly.graph_objs as go
df = pd.read_csv(
'https://raw.githubusercontent.com/plotly/'
'datasets/master/gapminderDataFiveYear.csv')
このデータフレームは以下のようになります。
アプリの外観を決定します。
app = dash.Dash()
app.layout = html.Div([
# 最初グラフには何も設定しない。(上書きされるため)
dcc.Graph(id='graph-with-slider'),
dcc.Slider(
# 入力コンポーネントとしてIDを設定
id='year-slider',
# スライダーの最小値・最大値を設定
min=df['year'].min(),
max=df['year'].max(),
# 今回の入力値(初期値)
value=df['year'].min(),
step=None,
# スライダーのラベルを設定
marks={str(year): str(year) for year in df['year'].unique()}
)
])
次に入力値と出力値を設定し、年ごとのデータを出力します。
@app.callback(
dash.dependencies.Output('graph-with-slider', 'figure'),
[dash.dependencies.Input('year-slider', 'value')])
def update_figure(selected_year):
# 入力値には`value`、つまり年のデータが送られる。
# 年ごとのデータを抽出する。
filtered_df = df[df.year == selected_year]
# figureを格納する空リストを作成
traces = []
# 大陸別にグループ分けを行う
for i in filtered_df.continent.unique():
df_by_continent = filtered_df[filtered_df['continent'] == i]
traces.append(go.Scatter(
x=df_by_continent['gdpPercap'],
y=df_by_continent['lifeExp'],
text=df_by_continent['country'],
mode='markers',
opacity=0.7,
marker={
'size': 15,
'line': {'width': 0.5, 'color': 'white'}
},
name=i
))
return {
'data': traces,
'layout': go.Layout(
xaxis={'type': 'log', 'title': 'GDP Per Capita'},
yaxis={'title': 'Life Expectancy', 'range': [20, 90]},
margin={'l': 40, 'b': 40, 't': 10, 'r': 10},
legend={'x': 0, 'y': 1},
hovermode='closest'
)
}
if __name__ == '__main__':
app.run_server()
今回はSlider
のvalue
プロパティが入力値として使用されており、出力値はGraph
のfigure
プロパティです。
データをロードする作業df = pd.read_csv(...)
はコストのかかる作業ですが、以上のようにコードを実行すると、データのロードは最初に行われるだけであり、ユーザーが対話式に操作する際はすでにデータはメモリ内に格納されています。
注意点は、callback
がスコープ外の変数に手を加えないようにしておくことです。これを守らなかった場合、ユーザーがデータそのものに手を加えることが可能になってしまいます。
複数の入力値をとる
今回は、Dropdown
から2つ、RadioItems
から2つ、Slider
から1つの合計5つの入力値を受けてグラフを操作することを目指します。
import dash
import dash_core_components as dcc
import dash_html_components as html
import pandas as pd
import plotly.graph_objs as go
app = dash.Dash()
df = pd.read_csv(
'https://gist.githubusercontent.com/chriddyp/'
'cb5392c35661370d95f300086accea51/raw/'
'8e0768211f6b747c0db42a9ce9a0937dafcbd8b2/'
'indicators.csv')
available_indicators = df['Indicator Name'].unique()
このデータフレームは以下のようになります。
次にアプリの外観を決定します。
app.layout = html.Div([
html.Div([
html.Div([
dcc.Dropdown(
id='xaxis-column',
# available_indicator変数を選択肢として設定
options=[{'label': i, 'value': i} for i in available_indicators],
# 初期値
value='Fertility rate, total (births per woman)'
),
dcc.RadioItems(
id='xaxis-type',
options=[{'label': i, 'value': i} for i in ['Linear', 'Log']],
# 初期値、ここでは1つだけ選択する
value='Linear',
labelStyle={'display': 'inline-block'}
)
],
# 上の2つの要素をひとまとめにstyleを設定する
style={'width': '48%', 'display': 'inline-block'}),
html.Div([
dcc.Dropdown(
id='yaxis-column',
options=[{'label': i, 'value': i} for i in available_indicators],
# 初期値
value='Life expectancy at birth, total (years)'
),
dcc.RadioItems(
id='yaxis-type',
options=[{'label': i, 'value': i} for i in ['Linear', 'Log']],
# 初期値
value='Linear',
labelStyle={'display': 'inline-block'}
)
# 上の2つの要素をひとまとめにstyleを設定する
],style={'width': '48%', 'float': 'right', 'display': 'inline-block'})
]),
# ここに出力する図を挿入する
dcc.Graph(id='indicator-graphic'),
dcc.Slider(
id='year--slider',
min=df['Year'].min(),
max=df['Year'].max(),
# 初期値
value=df['Year'].max(),
step=None,
marks={str(year): str(year) for year in df['Year'].unique()}
)
])
次に複数の入力値と出力値を設定し、図に反映させましょう。
@app.callback(
dash.dependencies.Output('indicator-graphic', 'figure'),
[dash.dependencies.Input('xaxis-column', 'value'),
dash.dependencies.Input('yaxis-column', 'value'),
dash.dependencies.Input('xaxis-type', 'value'),
dash.dependencies.Input('yaxis-type', 'value'),
dash.dependencies.Input('year--slider', 'value')])
def update_graph(xaxis_column_name, yaxis_column_name,
xaxis_type, yaxis_type,
year_value):
dff = df[df['Year'] == year_value]
return {
'data': [go.Scatter(
x=dff[dff['Indicator Name'] == xaxis_column_name]['Value'],
y=dff[dff['Indicator Name'] == yaxis_column_name]['Value'],
text=dff[dff['Indicator Name'] == yaxis_column_name]['Country Name'],
mode='markers',
marker={
'size': 15,
'opacity': 0.5,
'line': {'width': 0.5, 'color': 'white'}
}
)],
'layout': go.Layout(
xaxis={
'title': xaxis_column_name,
'type': 'linear' if xaxis_type == 'Linear' else 'log'
},
yaxis={
'title': yaxis_column_name,
'type': 'linear' if yaxis_type == 'Linear' else 'log'
},
margin={'l': 40, 'b': 40, 't': 10, 'r': 0},
hovermode='closest'
)
}
if __name__ == '__main__':
app.run_server()
複数の出力値をとる
Dashのcallback
関数では出力は1つしか取れません。そこで複数の出力値を設定する場合は、複数の関数を定義します。
import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output
次にアプリの外観を決定します。
app = dash.Dash()
app.layout = html.Div([
dcc.RadioItems(
id="dropdown-a",
options=[{"label": i, "value": i} for i in ["Canada", "USA", Mexico]],
value="Canada",
),
html.Div(id="output-a"),
dcc.RadioItems(
id="dropdown-b",
options=[{"label": i, "value": i} for i in ["MTL", "NYC", "SF"]],
value="MTL"
),
html.Div(id="output-b")
])
次に複数の出力値を設定するために複数の関数を定義する。
# 1つ目の出力
@app.callback(
Output("output-a", "children"),
[Input("dropdown-b", "value")]
)
def callback_a(dropdown_value):
return "You have selected {}".format(dropdown_value)
# 2つ目の出力
@app.callback(
Output("output-b", "children"),
[Input("dropdown-b", "value")]
)
def callback_b(dropdown_input):
return "You have selected {}".format(dropdown_input)
if __name__ == "__main__":
app.run_server(debug=True)
callback関数からの出力値は、他のcallback関数への入力値として扱うこともできます。
これは1つの入力値が、他の入力値へ影響を与えるような動的なUIを作成する場合に使われています。
import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output
次にアプリの外観を決定します。
app = dash.Dash()
all_options = {
"America": ["New York City", "San Francisco", "Cincinnati"],
"Canada": ["Montreal", "Toronto", "Ottawa"]
}
app.layout = html.Div([
dcc.RadioItems(
id="countries-dropdown",
options=[{"label":i, "value":i} for i in all_options.key()],
value="America"
),
html.Hr(),
dcc.RadioItems(id="cities-dropdown"),
html.Hr(),
Html.Div(id="display-selected-values)
])
次に入力値と出力値を設定する。
@app.callback(
Output("cities-dropdown", "value"),
[Input("countries-dropdown", "value")]
)
def set_cities_options(selected_country):
return [{"label":i, "value":i} for i in all_options[selected_country]]
@app.callback(
Output("cities-dropdown", "value"),
[Input("cities-dropdown", "options")]
)
def set_cities_value(selected_options):
return available_options[0]["value"]
@app.callback(
Output("display-selected-values", "children"),
[Input("countries-dropdown", "value"),
Input("cities-dropdown", "value")]
)
def set_display_children(selected_country, selected_city):
return "{} is a city in {}".format(selected_city, selected_country)
if __name__ == "__main__":
app.run_server(debug=True)
最初のコールバックは、最初のRadioItemsコンポーネントで選択された値に基づいて、2番目のRadioItemsコンポーネントで使用可能なオプションを更新します。
2番目のコールバックは、optionsプロパティが変更されたときに初期値を設定します
options配列の最初の値に設定します。
最後のコールバックには、各コンポーネントの選択された値が表示されます。各国のRadioItemsコンポーネントの値を変更すると、最終コールバックを呼び出す前にcitiesコンポーネントの値が更新されるまでDashが待機します。これにより、コールバックが "USA"や "Montreal"のような一貫性のない状態で呼び出されるのを防ぎます。