atama plus techblog
📊

Streamlitの実装・運用を楽にするTips集(BigQuery、GA4連携、可視化等)

2024/12/04に公開

はじめに

こんにちは、atama plus でデータエンジニアをしている kumewata です。

先日リリースした新機能のダッシュボードを作ることになり、初めて Streamlit で実装をしました。既存コードを読んだり、自分で実装する中で実装・運用を楽にする Tips がたまったので紹介します。

https://prtimes.jp/main/html/rd/p/000000102.000037602.html

弊社では Streamlit を利用したアプリケーションを簡単にデプロイできる基盤があり、今回はそれを利用しています。詳しくはこちらの記事をご覧ください。

Streamlit とは

Streamlit はデータアプリケーションを簡単に構築できるフレームワークです。Python の実装でデータの可視化やインタラクティブな UI を作れ、とても便利です。
弊社でも BigQuery と連携したダッシュボードやプロトタイプ開発などで利用しています。

Tips 紹介

コンセプト理解

Streamlit の概要を確認

Streamlit の利用自体は既存アプリやサンプルコードのコピペから簡単に始められますが、公式ドキュメントの App model summary を読んでおくと実装時の見通しがよくなります。

アプリが上から順次実行される Python スクリプトであり、レイアウトはその影響を受けて配置されること。状態が変わる度にページが再描画されること。などを頭に入れておけば、ひとまず大丈夫です 🙆‍♂️

アプリモデルの概要

個々の部分について少し理解できたので、ループを閉じて、それらがどのように連携して動作するかを確認しましょう。

  1. Streamlit アプリは上から下へ実行される Python スクリプトです。
  2. ユーザーがアプリを指すブラウザ タブを開くたびに、スクリプトが実行され、新しいセッションが開始されます。
  3. スクリプトが実行されると、Streamlit はその出力をブラウザにライブで描画します。
  4. ユーザーがウィジェットを操作するたびに、スクリプトが再実行され、Streamlit はブラウザでその出力を再描画します。
    • そのウィジェットの出力値は、再実行中の新しい値と一致します。
  5. スクリプトは Streamlit キャッシュを使用して、コストのかかる関数の再計算を回避するため、更新が非常に高速に行われます。
  6. セッション状態を使用すると、単純なウィジェット以上のものが必要な場合に、再実行間で保持される情報を保存できます。
  7. 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

# 以降、データ取得と表示

可視化

グラフ用処理の共通化

可視化で利用するグラフはある程度決まってくるので、グラフ表示のための関数を用意しておくと便利です。

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の機能を使うために採用しました。本来テーブルに欲しい機能が使えなくなるので最終手段です。

今回は 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 を活用しています。

atama plus techblog
atama plus techblog

Discussion