RAKSUL TechBlog

RAKSULグループのエンジニアが技術トピックを発信するブログです

Streamlitアプリを効率化するst.fragment 入門

この記事は ノバセル Advent Calendar  16日目です。

こんにちは。 ノバセルにてデータサイエンティストをしています、石川ナディーム(@nadeemishikawa)です。 Streamlitでアプリを開発する際、ページ全体が再レンダリングされてしまい困ってしまう場面に遭遇したことはありませんか?大規模なStreamlitアプリを開発していると、フィルターや操作が一箇所で起こるたびにページ全体が再実行され、不要なレンダリングが発生しがちです。 そこで登場したのがアプリ全体ではなく、部分的な再レンダリングを可能とする、st.fragmentです。本記事では、この機能の概要、使い方、そして簡単なサンプルコードを解説します。

Streamlitとは

Streamlitは、Pythonを用いて簡単にWebアプリケーションを作成できるオープンソースのフレームワークです。データサイエンスや機械学習の分野でも分析結果やデータの可視化をするために広く利用されています。

主な特徴としては、フロントエンドの知識があまり無くても、数行のコードでボタンやスライダーといった基本的なUIコンポーネントを簡単に実装できるので導入コストがとても低いものになります。

また、Streamlitは、コミュニティ運営にも力を入れていて、多くのユーザーが独自作成のコンポーネントを公開していたり、公式コミュニティのフォーラム(日本語カテゴリあり)が活発です。日本語コミュニティとしては、SnowVillage Slackの#streamlitチャンネルも要チェックです。

st.fragment とは

Streamlitでは、ユーザーがウィジェットを操作すると、全コードが再実行されて、フロントエンドが更新されるという仕組みになっています。そのため、更新が不要な部分に関しても、しばしば実行されてしまい、開発において煩わしさを感じることがあります。

そこで、Streamlitではバージョン1.37.0から全コードを実行するのではなく、部分的に実行することを可能とするst.fragment を提供しています。 (現在Streamlit in Snowflakeでサポート されているStreamlitライブラリのバージョンの制約により、まだこの機能はStreamlit in Snowflake上では利用できません。)

主なユースケースについては公式のドキュメント内で以下の記述があります。

  • Your app has multiple visualizations and each one takes time to load, but you have a filter input that only updates one of them.
  • You have a dynamic form that doesn't need to update the rest of your app (until the form is complete).
  • You want to automatically update a single component or group of components to stream data.

複数のコンポーネントに別々のフィルター操作を適用する場合や、フォーム入力途中で不要な再実行を避けたい場合など、全体ではなく一部のみ更新したいシナリオで st.fragment は有効です。

st.fragment の基本的な使い方

Streamlitでは、デコレータst.fragmentを関数に適用することで、簡単に利用できます。デコレータを適用することで、ユーザーが適用された関数内のウィジェットを操作してもスクリプト全体ではなく、その関数部分のみが実行され、アプリの他の部分はそのまま維持されます。

以下では、簡単なst.fragment を使ったコードを実装しています。デコレータを用いることで、数行のみで部分的な実行を実装しています。

import streamlit as st

# 初期化
if "app_runs" not in st.session_state:
    st.session_state.app_runs = 0
    st.session_state.fragment_runs = 0

# Fragment部分の定義
@st.fragment
def counter_fragment():
    st.button("部分更新")
    st.session_state.fragment_runs += 1
    st.write(f"部分更新: {st.session_state.fragment_runs} ")

# 全体実行のカウンター
st.session_state.app_runs += 1

# Fragmentの呼び出し
counter_fragment()

# 全体更新のボタンとカウンター
st.button("全体更新")
st.write(f"全体更新回数: {st.session_state.app_runs} ")

デコレータを適用した関数の自動実行

st.fragment には、関数を定期実行する機能もあります。こちらもアプリ内で定期実行したい関数にデコレータと変数として実行間隔(s)を適用するだけで利用することができます。こちらも全体が実行されるわけではなく、適用された関数のみが更新される形式です。ステータスのモニタリングや、株価データ等更新頻度の多いデータをリアルタイムに可視化するのに非常に役に立ちそうですね。

以下に簡単なサンプルコードを掲載します。このコードは、単純に1秒ごとにランダムな値が表示されるコードとなっています。

import streamlit as st
import random
from datetime import datetime

@st.fragment(run_every=1)  # 1秒ごとに自動更新
def update_data():
    current_time = datetime.now().strftime("%H:%M:%S")
    random_value = random.randint(0, 100) 
    st.metric("ランダムな値", random_value)
    st.text(f"最終更新時刻: {current_time}")
    
update_data()

FragmentとCache機能の比較

全体のコードが走ってしまい、パフォーマンスが落ちてしまうことに対しての解決策としては、streamlitが用意しているCache機能もあります。このCache機能とFragmentの違いについても述べようと思います。

Cache機能の概要は、実行に時間がかかるような関数、例えばデータフレームや学習済みモデルのロードなどの結果を保存し、同じ入力で再度呼び出された際に保存された結果を関数を再実行する代わりに返すことです。これにより、無駄な読み込みが無くなり、アプリケーションのパフォーマンスを大幅に向上させることが期待できます。

Cache機能はデータフレームや学習済みモデルのロードなどの重い処理、更新頻度が高くない用途に適していますが、動的で柔軟な操作性には限界がありそうでした。その一方で、Fragmentはリアルタイム性と効率性を兼ね備えており、アプリ全体を再実行することなく、ユーザーの操作に応じて即座に必要な部分だけを更新できます。そのため、動的でインタラクティブなアプリを構築する際には、Fragmentを活用するのが最適な選択と考えられます。全体を実行するのではなく部分的に実行するFragmentか、全体を実行する際に再実行されるのを防ぐCache機能か、場面に応じて最適なものを吟味して選択することが大切です。 加えて実はStreamlitにおいて、現在このFragmentとCache機能を同じ関数に対して併用することはできません。なおさら用途によってどちらを採用するかを考える必要がありそうです。

まとめ

st.fragment は、Streamlitアプリの一部だけを再レンダリングすることで、不要な全体更新を避け、パフォーマンスを向上させる新しいツールです。本記事ではその概要、使い方、サンプルコード、そして Cache 機能との違いを解説しました。 st.fragment を活用することで、インタラクションの多い大規模なStreamlitアプリでも効率的な更新が可能になります。アプリの規模や要件に応じて、適切にst.fragmentを使ってみてください。

参考資料まとめ

docs.streamlit.io

docs.streamlit.io

docs.streamlit.io