Blow Up by Black Swan

pythonのwebアプリケーションフレームワークDashで、リアルタイムで更新されるグラフを作る方法

今回、PythonのWEBアプリケーションフレームワークDashで、リアルタイムでグラフが更新されていく、動的なアプリケーションを作ってみましたので、その内容をまとめています。Dashのユーザガイドについては、先日、その内容をまとめた記事をあげていますので、参考にして頂ければと思います。

1. 今回作成したアプリケーションの概要

今回作成したのは、以前plotlyの記事でも作成したのと同じ、コインで表が出る確率が1/2となる、大数の法則を表すグラフです。但し、今回はリアルタイムにアップデートさせていくので、事前に10回の試行を行い、そこに1秒ごとに行われる新たな試行の結果を追加し、グラフに反映させていきます。

コイン投げをして、表が出るか裏が出るかを記録する(これを試行と言います)、という数学の授業で必ずでてくる部分は、pythonのrandomモジュールを利用して再現しています。この1回1回の試行の結果はそれぞれ独立し、予測することはできなくとも、十分に大きな数の試行を行えば、ある事象の起こる確率が一定の確率に収束することを大数の法則と言います。

大数の法則の例としては、コインの裏表以外にも、サイコロの出目の確率も有名だと思います。リアルタイムアップデートのイメージは、Macbookのアクティビティモニタで表示されるグラフのようなもので、リアルタイムでデータを取得し、随時グラフに反映していくというものです。

2. Dashでリアルタイムにアップデートされていくグラフの実装概要

リアルタイムにアップデートを行っていくアプリケーションで利用される技術に「PUSH通知」、または「server PUSH」と言われるものがあります。HTTPで使われるクライアントーサーバモデルでは、クライアントからのリクエストを起点としなければならないという問題点がありますが、これを克服し、クライアントからのリクエストがなくともサーバからデータを送れるようにすることを目指して開発されたようです。

スマートフォンのアプリで知らぬ間にアプリアイコンの上に通知数がでている経験があると思いますが、ああいったところでPUSH通知技術が利用されています。サーバPUSHでは下記サイトにあるように様々な方法があるようです。

その中でDashでは、おそらくはポーリング方式やAjax方式のような方法ではないかと思います。

実際にHTTPリクエストとレスポンスの動きを見て見ると、設定した間隔ごとにサーバにPOSTリクエストを送っていることがわかります。

チャートアプリの実行イメージ

Dashでは、dash-core-components(以下dccと表記)の中にあるIntervalというメソッドがこのPUSH通知用に設けられているようで、このメソッドのinterval引数に設定した値ごとに、自動でHTTPリクエスト(POST)がクライアントからサーバに送られ、それレスポンスという形でデータがクライアントに返されます。

dashトリガー機能の実行イメージ
dcc.Intervalがinterval引数で設定した感覚で自動で発動
-> コールバック関数の呼び出し
-> wrapper関数で求められた値がOutput部に渡される
-> ブラウザで表示される

つまり、dcc.Intervalを利用することで、上記の図で示されるコールバック発火の流れが設定した間隔ごとに繰り返されるということです。Intervalメソッドの機能と特徴は以下のようになります。

  • dcc.Interval…自動で自動でコンポーネントをアップデートするために利用されるメソッド
    • intervalメソッド…アップデートの間隔を設定する引数(ミリ秒単位)
    • n_intervalsintervalの発動回数を格納する引数

3. 完成コード

実装したコードは以下になります。

import random
import dash
import dash_core_components as dcc
import dash_html_components as html
import plotly.graph_objs as go
from dash.dependencies import Input, Output

coin_list = ["表","裏"]
results_list = []
db_probability = []

#事前に行われる11回の試行
for i in range(1, 11):
    results_list.append(random.choice(coin_list))
    db_probability.append(results_list.count("表")/len(results_list))

markdown_text = '''
#### <参考サイト>
* [Dash by plotly(公式サイト)](https://plot.ly/products/dash/)
* [Blow Up By Black Swan](https://blowup-bbs.com/python-dash-framework-plotly1/)
'''

app = dash.Dash(__name__)

app.layout = html.Div(
    children=[
        html.H1("Python Web Application Framework: Dash"),
        html.Div(id="the number"),
        dcc.Graph(id="graph"),
        dcc.Markdown(markdown_text),
        dcc.Interval(
            id="interval-component",
            interval=1*1000,
            n_intervals=0
        )
    ]
)

@app.callback(Output("the number", "children"),
               [Input("interval-component", "n_intervals"),
                Input("interval-component", "interval")])
def display_num(n_intervals, intervals):
    style = {'padding': '5px', 'fontSize': '16px'}
    return html.Div('"n_intervals"={}回, "intervals"={}ミリ秒'.format(n_intervals, intervals),
                     style=style)

@app.callback(Output("graph", "figure"),
              [Input("interval-component", "n_intervals")])
def making_figure(n):
    # making figure object
    c_results_list = results_list
    c_db_probability = db_probability
    c_results_list.append(random.choice(coin_list))
    c_db_probability.append((c_results_list.count("表"))/len(c_results_list))
    id_map = map(lambda x: x, range(1, len(c_results_list)+1))
    trace = go.Scatter(x=list(id_map),
                       y=c_db_probability,
                       mode="markers+lines",  # グラフの表示の仕方
                       marker={'symbol': 0, 'size': 5, 'color': 'darkblue',
                               'line': {'color': 'blue', 'width': 2}}
                       )
    ex_layout = {'title': 'コインで表が出る確率が1/2に収束していく図(大数の法則)',
                 'width': 1300,
                 'height': 600,
                 'yaxis': {'range': [0, 1],
                           'dtick':0.1}
                 }
    figure = go.Figure(data=[trace],layout=ex_layout)
    return figure

if __name__=="__main__":
    app.run_server(debug=True)


コールバック関数との繋がりは以下になります(InputからOutputへの流れ)。

(1)dcc.Interval  ->  @callback関数  ->  html.Div(アップデートに関する情報を記載)
(2)dcc.Interval  ->  @callback関数  ->  dcc.Graph(大数の法則を表すグラフ)

全体の流れは以下になります。

事前に10回の試行を実施
-> (1)Dashアプリケーション起動
-> (2)dcc.Intervalが1秒ごとに発火
-> (3)2つのコールバックが起動
-> (4)コールバックから帰ってきた戻り値をブラウザで表示
-> (5)以後、(2)~(4)を繰り返し

完成すると、ブラウザで下図のような画面を表示することができます。画像は静止画になっていますが、実際はコールバックの発火によって、どんどん更新されていきます。

dashアプリの実行イメージ2

このコードを書くときに注意した点と新たに発見したことは以下になります。

  • 注意点: グローバル変数(results_listdb_probability)は、callback関数内で直接処理しない
    • これは、ユーザガイドで触れられているように、他のユーザセッションに影響を与えないため。そのため、callback関数内で新たな変数を設定している(c_results_listc_db_probability)
  • 発見: callback内の変数は新しい値が都度格納されていき、発火されるたびにcallback関数内の変数がリセットされるわけではない
    • callback関数が発火するたびに、関数内の変数(c_results_listc_db_probability)に新しい値が「追加」され、グローバル変数(results_listdb_probability)に格納されている10個の要素に戻されない

4. まとめ

以上がDashでリアルタイムにグラフをアップデートするコードです。チャートなど、様々な場面でも応用が効くと思います。私も今作成中のアプリで使って見るつもりです。公式ドキュメントの下記ページを主に参考にしています。

参考になりましたら、幸いです。読んで頂き、ありがとうございます。