DashはPythonでデータアプリケーションを作成するためのフレームワークです。フロントエンドに関する知識はほぼ不要で、Pythonのみでアプリケーションを作成することができるので、特にPython以外の言語に馴染の薄いデータサイエンティストが、サクッとプロトタイプを作成するのに重宝します。類似のフレームワークにstreamlitがありますが、Dashの方がコード量は多くなるもののカスタマイズ性に優れ、より見栄えの良いアプリケーションを作成することができます。
今回はDashでアプリケーションを作成してみて学んだ細かい使いどころを備忘録としてまとめました。コールバックなどの基本的な使い方は他に優れた記事がたくさんにあるのでそちらをご覧ください。
レイアウト関連
マルチページ
Dashはマルチページに対応しています。作り方はとても簡単で以下の設定を行うだけです。
- appの宣言時に
use_pages=True
と設定する - 各ページに対応するスクリプトを
pages/
に配置する-
dash.register_page(__name__)
でページとして登録する -
layout
に各ページのレイアウトを設定する
-
# app.py
app = Dash(__name__, use_pages=True) # use_pages=Trueと設定
# pages/page1.py
dash.register_page(__name__) # ページとして登録
layout = html.H1("Hello") # 変数layoutにレイアウトを設定
CSS
各コンポーネントのスタイルはstyle
属性を通して個別にカスタマイズできますが、全体的なスタイルを整えたいときはCSSを利用するのが便利です。Dashではassets/
にcssファイルを配置すると自動的に読み込まれ、スタイルに反映されます。
Dash Bootstrap Components
Dash Bootstrap Components (dbc) はBootstrapフレームワークを利用して、優れたデザインのコンポーネントを提供してくれる3rd-party製ツールです。dbcを利用するだけで、簡単に見栄えの良いアプリケーションに仕立て上げることができます。
テーマ
dbcでは様々なデザインのスタイルシートが提供されており、これを読み込ませることで全体のテーマを自分好みにカスタマイズできます。利用できるテーマの一覧は上記リンクのAvailable themesから確認できます。
app = Dash(__name__, external_stylesheets=[dbc.themes.FLATLY]) # スタイルシートを読み込む
レイアウト
dbcを利用することで、各コンポーネントを美しく整列することができます。dbc.Container
の下にdbc.Row
、さらにその下にdbc.Col
という階層で記述します。dbc.Container
の引数としてfluid=True
を与えることで、不要な余白を除去して表示します。
layout = dbc.Container(
[
dbc.Row(
[
dbc.Col(html.Div("Hello"), width=2),
dbc.Col(html.Div("World"), width=10),
],
),
],
fluid=True,
)
コンポーネント
その他、dbcではさまざまなコンポーネントが用意されており、これらを組み合わせることでよりリッチなレイアウトを作成できます。アクションアイテムはDash組み込みのコンポーネント(Dash Core Component: dbc)と同様にコールバック利用も可能です。
例えば、以下のようなコンポーネントが用意されています。
グラフの状態の保持
デフォルトの振る舞いでは、ユーザーがグラフの表示範囲を変更したのち、適当なタイミングでコールバックが呼ばれると、表示範囲がデフォルトの状態にリセットされてしまいます。これは、場合によってはユーザー体験を損ねる原因になります(例えば、地図上である範囲にフォーカスしてインタラクティブな操作を行いたい場合など)。
figureのlayout
属性でuirevision=True
と設定すればコールバックが呼ばれても状態を保持することができます(詳細は上記リンク参照)。
コールバック関連
コールバックへの複数のインプット
コールバックに複数のInput
を与えることも可能です。これにより、Input
に紐づけられた複数のコンポーネントのいずれかが操作されたタイミングで発火し、Output
に紐づけられたコンポーネントの状態が変化します。
@callback(
Output(component_id="figure", component_property="figure"),
Input(component_id="dropdown", component_property="value"),
Input(component_id="checklist", component_property="value"),
State(component_id="state", component_property="data"),
)
def update_figure(files: List[str], checklist: List[str], state: str):
....
コールバック間のデータのやり取り
Dashでは、複数のコールバック間でデータのやり取りをするための機能としてdcc.Store
が提供されています。例えば、アップロードしたデータを重い処理にかけたのち、その結果を複数の図で表示させたい場合、コールバックごとに行うのではなく、dcc.Store
を介してデータを共有することで効率的に実行することができます。
app.layout = html.Div([
dcc.Graph(id='graph'),
dcc.Dropdown(id='dropdown'),
dcc.Store(id='intermediate-value')
])
@callback(
Output('intermediate-value', 'data'),
Input('dropdown', 'value')
)
def convert_data(value):
df = processing(value)
return df.to_json(date_format='iso', orient='split')
@callback(
Output('graph', 'figure'),
Input('intermediate-value', 'data')
)
def update_graph(data):
df = pd.read_json(data, orient='split')
figure = create_figure(df)
return figure
ただし、dcc.Store
はデータをユーザーのブラウザセッション上に保存するため、重いデータを保存するのには向いていません(ネットワークコストが高くつく)。また、データをやり取りするためにjson形式にフォーマットする必要があり、やや手間がかかります。
そのため、場合によってはサーバーのファイルシステムなどに処理結果を保存し、dcc.Store
には処理が完了したことを知らせるシグナルとしての役割を任せる、などの使い方の方が望ましいかもしれません(詳細は上記のリンクを参照ください)。
その他
ファイルのアップロード
Dash組み込みのアップローダーとしてはdcc.uploader
が提供されていますが、ファイルサイズに制限がある、ファイルの種類にかかわらずbase64形式の文字列でエンコーディングされるためパースが必要、などの点でややクセがあります。そこで、代替として3rd-party製のdash-uploaderがオススメです。dash-uploaderの方が作りがシンプルで、かつ
- プログレスバーの表示機能
- 指定したディレクトリ(
/tmp/
など)へのファイルの保存機能
などの機能をデフォルトでサポートしており使い勝手がよいです。
import dash_uploader as du
app = dash.Dash(__name__)
du.configure_upload(app, "/tmp/uploads") # ファイルをアップロードするディレクトリを設定
app.layout = html.Div([
du.Upload(), # コンポーネントを設定
])
dash-uploaderは依存関係によってはマルチページだとうまく動作しない問題があるようです。対処法は下のリンクを参照ください。
認証
Dashではdash-authという簡易的な認証の仕組みもサポートされています。
VALID_USERNAME_PASSWORD_PAIRS = {'hello': 'world'} # ユーザー名とパスワードの組
app = Dash(__name__)
auth = dash_auth.BasicAuth(app, VALID_USERNAME_PASSWORD_PAIRS) # 認証を設定
デプロイ
デプロイ時には、gunicornをインストールしエントリーポイントとしてgunicorn app:server
を実行します。
# app.py
app = Dash(__name__)
server = app.server # これが必要
app.layout = html.H1('Hello World')
if __name__ == '__main__':
app.run(debug=True)
# Procfile
web: gunicorn app:server