Lean Baseball

No Engineering, No Baseball.

実践Dash(詳細解説Version) - 手を抜きつつもちゃんとWebアプリを作るLow-code

PyCon JP 2024発表の詳細版です.

Python製のLow-codeであるDashについて話をさせてもらいました(自分のレポート).

トーク本編では話さなかったor話せなかった件を中心に紹介します.

なお, 本エントリーのサンプルコードは以下のGitHub Repositoryにて公開しています.

github.com

本エントリーの目的

「とりあえずDashでHello worldをやりながら構成を理解, 作りたいものを作れるぐらいの知識を提供」

すること目的としています.

FlaskでPythonのアプリを作ったことあるかつ, pandasやplotlyを使えるといい感じにお楽しみ頂けるかと思います.

TL;DR

ひとまずデータの可視化をするアプリをPoC的にエイッと作るなら役立ちます&案外大きいアプリケーションも作れます.

認証認可などちょっと厄介(痒いところに手が届かない)欠点はありますが使えると思います(個人的な感想).

Dashとは何か

Dashはデータの可視化(ビジュアライゼーション)に全振りした, Low-code(≒いい感じに楽に作れる)Web アプリのフレームワークです.

いい感じのLow-codeです

敢えて特徴を3つに絞るならば,

  • Low-codeなWeb Frameworkで, データの可視化に全振りしている.
  • Python版(注: 他の言語版*1もある)はFlaskがベース. Flaskで使えるものはだいたい使える.
  • OSS版(このエントリーで紹介しているもの)とEnterprise版有償*2)が存在.

といった所でしょうか.

なおPythonのLibraryなので導入はこれで行けます.

pip install dash pandas # どのみちpandas使うので最初から入れると幸せ(なはず)

ちなみにEnterprise版は使ったこと無いです, あしからず🙏

Hello world

とある野球データ(Sean LahmanのMLBデータ, 打撃版)のCSVを可視化するサンプル(元のコード).

from dash import Dash, html, dcc, callback, Output, Input
import plotly.express as px
import pandas as pd

df = pd.read_csv('Batting.csv')

# applicationの base object
app = Dash()
server = app.server

# HTMLタグおよび組み込みのコンポーネントでページを作る
app.layout = [
    html.H1(children='Dash App for Baseball', style={'textAlign':'center'}),
    dcc.Dropdown(df.playerID.unique(), 'ohtansh01', id='dropdown-selection'),  # default value is Ohtani,Shohei('ohtansh01')
    dcc.Graph(id='graph-content'),
    html.P('Sample data is "Lahman Baseball Database"', style={'textAlign':'center'})
]

# Drop downの選択でcallback発火 -> グラフを更新
@callback(
    Output('graph-content', 'figure'),
    Input('dropdown-selection', 'value')
)
def update_graph(value):
    dff = df[df.playerID==value]
    return px.bar(dff, x='yearID', y='HR')

if __name__ == '__main__':
    app.run(debug=True)

こちらをapp.pyというファイル名で保存,

python app.py

を実行するとこのようなページが表示されます.

最もシンプルなサンプル⚾️

コードブロック毎にざっくり紹介します.

基本的な構成

from dash import Dash, html, dcc, callback, Output, Input
import plotly.express as px
import pandas as pd

df = pd.read_csv('Batting.csv')

# applicationの base object
app = Dash()
server = app.server

pandasのDataframe(df)をGlobal領域にて宣言・読み込み.

app = Dash() はFlaskで言う所のapp = Flask(__name__) みたいなもので, Application本体の基本的なobject.

server = app.server の部分はgunicornなどのWebサーバーでホストする際のエントリーポイントになる部分となります(参考).

コンポーネント(html, dcc)

# HTMLタグおよび組み込みのコンポーネントでページを作る
app.layout = [
    html.H1(children='Dash App for Baseball', style={'textAlign':'center'}),
    dcc.Dropdown(df.playerID.unique(), 'ohtansh01', id='dropdown-selection'),  # default value is Ohtani,Shohei('ohtansh01')
    dcc.Graph(id='graph-content'),
    html.P('Sample data is "Lahman Baseball Database"', style={'textAlign':'center'})
]

上記はコメントの通り, ページのレイアウトを複数のコンポーネントで構成しています.

H1とかPとかはもうお察しだと思います(それぞれHTMLのタグと同等).

dcc.Dropdown など, dccの部分は「Dash Core Components」という基本的な組み込みコンポーネントです.

dash.plotly.com

入力(この例だとDropdown), 出力(この例だとGraphの描画)といったアプリのベースとなる部分は基本的*3にこのコンポーネントから部品を選んで構築することになります.

コールバック

アプリケーションで動的な動きをする部分, このサンプルだと「プルダウンを選択したら値とグラフが変わります」的な実装はすべてcallbackで実装します.

# Drop downの選択でcallback発火 -> グラフを更新
@callback(
    Output('graph-content', 'figure'),
    Input('dropdown-selection', 'value')
)
def update_graph(value):
    dff = df[df.playerID==value]
    return px.bar(dff, x='yearID', y='HR')
  • 関数に@callback デコレータを付けて実装. 引数としてInput/Outputのオブジェクト(書き換え先のIDおよび種別・引数など)をもたせる
  • callbackのInput/Outputに合わせて関数を実装

これでいい感じにcallbackイベントが実装可能です.

Dashの実装で最も時間がかかるのはこのcallbackかもしれません(個人的な感想).

ちなみにcallback関数のテストはこんな感じでいけます(公式のサンプルから引用).

# 別途dash[testing]のインストールが必要
from contextvars import copy_context
from dash._callback_context import context_value
from dash._utils import AttributeDict

# Import the names of callback functions you want to test
from app import display, update

def test_update_callback():
    output = update(1, 0)
    assert output == 'button 1: 1 & button 2: 0'

def test_display_callback():
    def run_callback():
        context_value.set(AttributeDict(**{"triggered_inputs": [{"prop_id": "btn-1-ctx-example.n_clicks"}]}))
        return display(1, 0, 0)

    ctx = copy_context()
    output = ctx.run(run_callback)
    assert output == f'You last clicked button with ID btn-1-ctx-example'

個人的にはcallbackそのもののテストは結構大変なので, データ取得やグラフ描画を関数として切り出してそれに対するテストにしちゃったほうが良いかなと思います*4.

認証認可

Basic認証で良ければ, 上記のコードにちょこっと追加することで実現可能です(サンプルコード).

# クラスをimport
from dash_auth import BasicAuth

# 使い方

app = Dash()
server = app.server

# Basic認証を組み込む
auth = BasicAuth(app, {'basic_auth_user':'basic_auth_password'})

これでid: basic_auth_user password: basic_auth_passwordでbasic認証がかかります.

なお, PyCon JP 2024のトークでは認証認可について以下の通り紹介しました.

自作もできるけど無理は禁物

大切なポイント*5として,

公式の案内ではOSS版はBasic認証のみ, Enterprise版で他の形式*6をサポートとなっている.

ことです.

最初の解説で, 「DashはFlaskベース」であると解説したとおり, 無理をすればFlaskの知見がある方がいい感じにやれば自分で組むこともできそうですが, (個人的には)セキュリティやライセンスなどのリスクも大いにありそう(特に前者)なので, よほどの上級者じゃない限りはやらないことを強くオススメします(特に業務目的ならなおのこと*7).

Basic認証が嫌かつ, Enterpriseを使わない場合であれば,

  • Auth0やFirebase Authenticationなどのクラウドサービスの認証局を準備し, その支配下で認証認可を入れる.
  • IP制限などのネットワーク層で参照できる範囲を限定する

といった, 「アプリケーションの外側で解決する」アプローチ*8を強く推奨します.

マルチページ

ここまでの説明はすべてSPA(Single Page Application)の解説でした.

DashはSPAだけでなく, MPA(Multi Page Application), すなわち複数のURL(動的含む)を持ったApplicationの構築が可能です.

dash.plotly.com

例えばここまで紹介してきた⚾️Applicationを「このあと投手成績のページも作らなきゃ」と思い立ち, とりあえず,

  • / : Topページ
  • /batting : 打者成績のページ

の様に再構築したくなったとします(サンプルはこれ).

となった場合は以下の構成で実現可能です.

app.py

ベースとなるApplication構成.

import dash
from dash import Dash, html, dcc

app = Dash(__name__, use_pages=True)
server = app.server

app.layout = html.Div([
    html.H1('Multi Page'),
    html.Div([
        html.Div(
            dcc.Link(f"{page['name']} - {page['path']}", href=page["relative_path"])
        ) for page in dash.page_registry.values()
    ]),
    dash.page_container
])

if __name__ == '__main__':
    app.run(debug=True)

以下のコンポーネントがヘッダー(タイトルと各ページのリンク)

    html.H1('Multi Page'),
    html.Div([
        html.Div(
            dcc.Link(f"{page['name']} - {page['path']}", href=page["relative_path"])
        ) for page in dash.page_registry.values()
    ]),

各ページの内容はdash.page_containerに書き込まれる.

pages

pages/top.pyにtopページ

from dash import html, register_page

register_page(__name__, path='/')

# HTMLタグおよび組み込みのコンポーネントでページを作る
layout = [
    html.H1(children='Dash App for Baseball(Top)', style={'textAlign':'center'}),
    html.P('こちらはダミーのページ', style={'textAlign':'center'})
]

page固有のコンテンツはlayoutに引き渡し. ちなみに関数で書いてもOK.

register_page(__name__, path='/')の部分がURLの登録.

同様に打者成績のページ(pages/batting.py)も以下の通り実装.

from dash import html, dcc, callback, register_page, Output, Input
import plotly.express as px
import pandas as pd

register_page(__name__, path='/batting')
df = pd.read_csv('Batting.csv')

# HTMLタグおよび組み込みのコンポーネントでページを作る
layout = [
    html.H1(children='Dash App for Baseball', style={'textAlign':'center'}),
    dcc.Dropdown(df.playerID.unique(), 'ohtansh01', id='dropdown-selection'),  # default value is Ohtani,Shohei('ohtansh01')
    dcc.Graph(id='graph-content'),
    html.P('Sample data is "Lahman Baseball Database"', style={'textAlign':'center'})
]

# Drop downの選択でcallback発火 -> グラフを更新
@callback(
    Output('graph-content', 'figure'),
    Input('dropdown-selection', 'value')
)
def update_graph(value):
    dff = df[df.playerID==value]
    return px.bar(dff, x='yearID', y='HR')

これでMPAとして動くようになります.

世の中に公開する

FlaskやDjango, Fast APIといった他のFramework同様(というよりWebアプリ全般のテンプレとして), Container化してしまえばよいです.

コンテナ化の準備をしたうえでやりましょう.

具体的には,

  • gunicornなどのWebサーバーで動かす準備をする(公式手順). ちなみにapp.runはあくまでもDebug用のサーバーなのでproduction利用はやめましょう.
  • Dockerfileを書く. これはFlaskやDjangoと大差ないと思います*9.
  • お好きな環境にデプロイする. 私の発表ではCloud Run(Google Cloud)を使いましたが, 別にk8sでもAmazon ECSでも何でも良いです*10.

といった(PythonでWebを使う人なら)お馴染みの方法でDeployが可能です.

Streamlitとの比較

Python製のLow-codeといえば, 最近流行ってると言えそうなStreamlitもあります*11.

streamlit.io

私もチャットアプリを作るなどの場面でStreamlitを多用しています.

shinyorke.hatenablog.com

Dash同様Streamlitも便利なLow-codeですが, どちらも特性が異なるので両方使う(長所に合わせて使い分ける)のがベストだと考えます*12.

長所・短所を理解して使い分けよう.

ざっくり書くと,

  • かっこよく, いい感じのレイアウトでデータ可視化のアプリを作るならDash
  • 入出力が多い, チャットなどのインタラクティブなアプリならStreamlit

この使い分けがベストだと私は考えます.

結び

これで,

「とりあえずDashでHello worldをやりながら構成を理解, 作りたいものを作れるぐらいの知識を提供」

は出来た気がします.

コメントやご質問等あれば, コメントなり何なり頂けると嬉しいです.

最後に自分の考えをちょこっと述べますが,

結局自分で実装しないほうがいいような気がします.

(Dashが果たす目的から察するに)TableauやLooker Studioなどのサービスで実装せずに済むならそれに越したことは無い*13です, 実務上の意思決定という意味では特に.

DashもStreamlitも組んで運用するといざ大変(PoCみたいに使い捨てできればいいのですが)なので, これでガチでアプリ作って運用する人はそれなりに覚悟を以てやると良いでしょう.

お後がよろしいようで&最後までお読み頂きありがとうございました.

Appendix

ちょっと前の書籍ですがこちらが参考になるかと.

*1:Julia, R, F# の3つ(公式サイトより)

*2:サブスク的なプランは無いっぽく, 問い合わせたうえで決まるらしいです.

*3:「基本的に」と書いたのはBootstrapのコンポーネント(Dash Bootstrap Components, 略してdbc)など他のサードパーティのコンポーネントを使うケースもあるからです.

*4:Dashと関係ない話ですが, ここまで頑張るならWebアプリとしてのEnd to End(E2E)テストを組むほうがよほど賢い解決策なように思えます, 急がば回れみたいな発想にはなりますが.

*5:この認証認可周りはPyCon JP 2024でも複数の質問がありました.というよりこの件しか質問がありませんでした.

*6:Enterprise版によると色々サポートしてるみたいです, 私は触ったことありませんが.

*7:業務目的ならそれなりのコストをかけて(変にケチって情報流出や漏洩などのリスクを抱えることなく), 外部サービスで認証認可入れるでいいと思います. 下手なハックをすると大変です.

*8:つまりサービス全体, システム全体で俯瞰して考えましょうって事です.

*9:つまり同じようなテクニックで解決します(のでサンプルコードは省略).

*10:ここは本当に好みで良いです, 無理にベンダーロックイン(クラウド縛り)する必要は無いです.

*11:私が使い始めたのは2020年, あのAIサービスの開発がデビュー戦でしたが. 4年近く経った今, ここまで流行るとは思いませんでした...超便利な道具として重宝してるので当然か.

*12:言葉にすると普通の事を言ってますが, 「比較」だの何だの言う以前に両方使って強みを知っとけというのが持論だったりします.

*13:私が野球のアプリをDashで組んだのは, Looker Studioなどではどうしても表現できない可視化があったからです. 言うたら「しょうがなく」使っています.