Streamlitの実装・運用を楽にするTips集(BigQuery、GA4連携、可視化等)
はじめに
こんにちは、atama plus でデータエンジニアをしている kumewata です。
先日リリースした新機能のダッシュボードを作ることになり、初めて Streamlit で実装をしました。既存コードを読んだり、自分で実装する中で実装・運用を楽にする Tips がたまったので紹介します。
弊社では Streamlit を利用したアプリケーションを簡単にデプロイできる基盤があり、今回はそれを利用しています。詳しくはこちらの記事をご覧ください。
Streamlit とは
Streamlit はデータアプリケーションを簡単に構築できるフレームワークです。Python の実装でデータの可視化やインタラクティブな UI を作れ、とても便利です。
弊社でも BigQuery と連携したダッシュボードやプロトタイプ開発などで利用しています。
Tips 紹介
コンセプト理解
Streamlit の概要を確認
Streamlit の利用自体は既存アプリやサンプルコードのコピペから簡単に始められますが、公式ドキュメントの App model summary を読んでおくと実装時の見通しがよくなります。
アプリが上から順次実行される Python スクリプトであり、レイアウトはその影響を受けて配置されること。状態が変わる度にページが再描画されること。などを頭に入れておけば、ひとまず大丈夫です 🙆♂️
アプリモデルの概要
個々の部分について少し理解できたので、ループを閉じて、それらがどのように連携して動作するかを確認しましょう。
- Streamlit アプリは上から下へ実行される Python スクリプトです。
- ユーザーがアプリを指すブラウザ タブを開くたびに、スクリプトが実行され、新しいセッションが開始されます。
- スクリプトが実行されると、Streamlit はその出力をブラウザにライブで描画します。
- ユーザーがウィジェットを操作するたびに、スクリプトが再実行され、Streamlit はブラウザでその出力を再描画します。
- そのウィジェットの出力値は、再実行中の新しい値と一致します。
- スクリプトは Streamlit キャッシュを使用して、コストのかかる関数の再計算を回避するため、更新が非常に高速に行われます。
- セッション状態を使用すると、単純なウィジェット以上のものが必要な場合に、再実行間で保持される情報を保存できます。
- Streamlit アプリには複数のページを含めることができ、それらは pages フォルダー内の個別の.py ファイルで定義されます。
(Google 翻訳のコピペ)
データ連携
BigQuery 連携時のキャッシュ利用
BigQuery に対してクエリ実行する際は、st.cache_data
アノテーションとセットで使うと利用 slot の節約ができます。特にダッシュボードでは長期間に対するクエリ実行をすることが多いと思うので、下記のような Utility 関数を用意しておくと便利です。
import streamlit as st
import pandas as pd
### Utility ###
@st.cache_data
def exec_bq_query(query: str) -> pd.DataFrame:
with st.spinner("loading..."):
df = pd.read_gbq(
query=query,
project_id="(利用するGoogleプロジェクトID)",
dialect="standard",
use_bqstorage_api=True,
)
return df
参考:https://docs.streamlit.io/get-started/fundamentals/advanced-concepts#caching
複数ページ化とパラメータ/セッションの引き継ぎ
pages ディレクトリ配下に Python ファイルを配置すると、Streamlit がサブページとして認識してくれます。
今回のダッシュボードでは、main ページにクエリパラメータ付きのリンクを置いて詳細ページに飛ばすようにしました。
遷移先でst.query_params
でクエリパラメータを読み取りst.session_state
にセットし、その値を使って BigQuery にクエリ実行しています。
以下、コードの例です。
import streamlit as st
### Functions ###
def get_query_params():
params = st.query_params.to_dict()
session_uuid = params.get("session_uuid", "")
return {"session_uuid": session_uuid}
def update_query_params_if_needed():
# session_uuidは機能で利用するIDのこと
query_session_uuid = get_query_params().get("session_uuid")
session_uuid = st.session_state.session_uuid
if session_uuid != query_session_uuid:
st.query_params.session_uuid = session_uuid
### Main ###
# クエリパラメータの取得とフィールドへの入力
session_uuid = get_query_params()
session_uuid_str = st.text_input(
label="session uuid",
value=session_uuid,
key="session_uuid",
on_change=update_query_params_if_needed,
)
if "session_uuid" not in st.session_state:
st.session_state["session_uuid"] = None
# 以降、データ取得と表示
- 参考:https://docs.streamlit.io/get-started/fundamentals/additional-features#pages
- 参考:https://docs.streamlit.io/get-started/fundamentals/advanced-concepts#session-state
可視化
グラフ用処理の共通化
可視化で利用するグラフはある程度決まってくるので、グラフ表示のための関数を用意しておくと便利です。
import numpy as np
import streamlit as st
import pandas as pd
import matplotlib.pyplot as plt
from typing import Sequence
# 円グラフプロット
def plot_pie_chart(data: pd.Series, labels: Sequence[str]):
fig, ax = plt.subplots()
ax.pie(data, labels=labels, autopct="%1.1f%%")
ax.axis("equal")
st.pyplot(fig)
# 棒グラフプロット
def plot_bar_chart_unsorted(data: pd.DataFrame, x: str, y: str):
# HACK: bar_chartを使うと本部名でx軸がソートされてしまうので連番を振る
# https://github.com/streamlit/streamlit/issues/7111
data[x] = [f"{i+1:02} {elem}" for i, elem in enumerate(data[x])]
st.bar_chart(data=data, x=x, y=y)
# ヒストグラムのビン数を計算
def calculate_bins_using_freedman_diaconis(data):
"""
Freedman–Diaconis rule に基づいてビン数を計算
"""
q25, q75 = np.percentile(data, [25, 75])
iqr = q75 - q25
bin_width = 2 * iqr / np.cbrt(len(data))
if bin_width > 0:
bins = int(np.ceil((data.max() - data.min()) / bin_width))
else:
bins = 10 # データが少ない場合のデフォルトビン数
return bins
テーブルの表示
Streamlit でテーブルを表示するには、いくつかの選択肢があります。
- 標準の関数 st.dataframe を利用する
- streamlit-aggrid などのテーブルライブラリを利用する
- (番外編)dataframe.to_markdown を利用する
- セル内でLaTeX 表示をする際、
st.markdown
の機能を使うために採用しました。本来テーブルに欲しい機能が使えなくなるので最終手段です。
- セル内でLaTeX 表示をする際、
今回は streamlit-aggrid を利用したのですが、何も設定しなくてもソートやフィルタ、列の並べ替えが使えて便利でした。
streamlit-aggrid の利用例
運用
Google Analytics の連携
弊社ではダッシュボードの利用状況を把握するため、Google Analytics へ連携するようにしました。
Google Analytics を使う方法以外にも、Streamlit へのアクセスを確認する方法はいくつかあります。しかし、前述の通り弊社では Streamlit アプリが簡単にデプロイできる状況であり、アクセスログを BigQuery の分析基盤に集約することで全体のダッシュボード管理をしやすくする狙いがありました。
ところで、Google Analytics は Streamlit から公式にサポートされていません。そのため今回はワークアラウンド的な手段を採用しました。
個人的には streamlit-analytics からフォークされたstreamlit-analytics2で GA4 対応が進み、この対応を捨てられることを願ってます。
実装としては次のようなスクリプトを用意し、デプロイ環境で Streamlit サーバーを立ち上げる前に実行するようにしています。
from bs4 import BeautifulSoup
import pathlib
import shutil
import streamlit as st
GA_ID = "google_analytics"
GA_SCRIPT = """
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-##########"></script>
<script id='google_analytics'>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-##########');
</script>
"""
# streamlitパッケージ内のindex.htmlを書き換えることで各ダッシュボードにGA_SCRIPT仕込むための関数
# GA_SCRIPTを編集する場合は、streamlitパッケージの再インストールをした上でこの関数を実行する。そうでなければ、編集内容が反映されない
def inject_ga():
index_path = pathlib.Path(st.__file__).parent / "static" / "index.html"
soup = BeautifulSoup(index_path.read_text(), features="html.parser")
if not soup.find(id=GA_ID): # GA_SCRIPTが複数仕込まれないようにする
bck_index = index_path.with_suffix(".bck")
if bck_index.exists():
shutil.copy(bck_index, index_path)
else:
shutil.copy(index_path, bck_index)
html = str(soup)
new_html = html.replace("<head>", "<head>\n" + GA_SCRIPT)
index_path.write_text(new_html)
inject_ga()
参考:https://discuss.streamlit.io/t/how-to-add-google-analytics-or-js-code-in-a-streamlit-app/1610/38
※ components API も試してみましたが、筆者の環境ではうまくいきませんでした。
さいごに
今回は Streamlit の実装・運用を楽にする Tips をいくつか紹介しました。
弊社ではダッシュボード以外にも Chatbot アプリやプロトタイプ作成などさまざまな場面で Streamlit を活用しています。
Discussion