Chapter 02

Streamlit

alivelimb
alivelimb
2022.05.26に更新

本章では YaEC の実装に入る前に Streamlit についての簡単な説明と、Streamlit を使う上で私がハマったポイントの紹介をしておきます。

Streamlit とは

Streamlit turns data scripts into shareable web apps in minutes. All in pure Python. No front‑end experience required.

公式の謳い文句をざっくり訳すと以下の通りです。

  • データスクリプトを数分で共有可能な Web アプリにできる
  • 全て Python で書けてフロントエンドの知識は必要ない

データスクリプトというのはおそらくstrpandas.DataFrameなどでしょうか。データ分析ではお馴染みのDataFrameを雑に渡すだけでテーブル表示をしてくれます。HTML/JS/CSS を書く必要はなく、事前に用意されたコンポーネントを組み合わせてページを作成します。公式のGalleryを見るだけでも、Streamlit の魅力が実感できるでしょう。

printを同じような書き方で Web 画面が出来るので、アプリエンジニアだけでなくデータサイエンティストなども簡単にデモアプリを作成できます。むしろ「機械学習やデータサイエンスのデモアプリを短時間で用意する」ことを主なユースケースとして想定しているようです。

基本的な機能

以下の手順で簡単に試すことができます。

  1. streamlit をインストール
pip install streamlit
  1. サンプルコードを用意する
# main.py
import streamlit as st


def main():
    st.write("Hello World!")


if __name__ == "__main__":
    main()
  1. 実行する
streamlit run main.py

デフォルトではhttp://localhost:8501で起動するため、ブラウザ等で簡単に作成した Web ページにアクセスすることができます。その他にも様々なコンポーネントが用意されています。公式ドキュメントやもみじあめさんの記事で紹介されていますので、適宜参照して下さい。

以降の節では ここで紹介していない様々なウィジェットや 、Streamlit を使う上で私がハマった「状態管理」「複数ページ対応」の 2 点について紹介していきます。

状態管理

Streamlit でセッション管理をするには、st.session_stateを使います。st.session_stateは Python の辞書型オブジェクトのように扱うことができ、ユーザアクションに応じて変化する状態を保持することが出来ます。

公式ドキュメントで挙げられている、カウンターの例を参考に説明していきます。カウンターの完成イメージは以下の通りです。

カウンター挙動

実装は以下の通りです。

import streamlit as st

st.title("Counter Example")
if "count" not in st.session_state: # (C)
    st.session_state.count = 0 # (A)

increment = st.button("Increment") # (B)
if increment:
    st.session_state.count += 1 # (A)

st.write("Count = ", st.session_state.count) # (A)

コード中(A)の部分でst.session_statecount変数の書込、読込を行います。

コード中(B)のincrementは,
ボタンが押下されたタイミングで True になります。

ではコード中(C)は何の意味があるのでしょうか?
(C)を除いて実行すると、以下のようにカウントが正しくされなくなります。

カウンター挙動

この原因を知るには Streamlit の描画のタイミングを理解する必要があります。

描画のタイミング

描画のタイミングについて公式ドキュメントには以下の記述があります。

Every time a user interacts with a widget, your script is re-executed and the output value of that widget is set to the new value during that run.

ざっくり訳すと「ユーザがウィジェット(ボタンなど)を操作するたびにスクリプトが再実行され、そのウィジェットの出力値が新しい値になる」です。つまり(C)を削除すると、ボタンが押されるたびにst.session_state.count = 0が実行されます。countが 0 になった後で 1 が加算されるため、何度ボタンを押しても 1 のままということになります。

そのためst.session_stateアプリが初期化されているかの状態を保持することがよくあります。YaEC でも使うお作法になるので、覚えておいてください。

コールバック処理

st.buttonなど、いくつかのウィジェット では「クリックされた時の処理を記述する」on_clickや「値が変化した時の処理を記述する」on_changeを引数として、コールバック関数を渡すことが可能です。カウンターをコールバック関数を用いて書くと以下のようになります。

import streamlit as st

st.title("Counter Example using Callbacks")
if "count" not in st.session_state:
    st.session_state.count = 0

def increment_counter():
    st.session_state.count += 1

st.button("Increment", on_click=increment_counter) # 戻り値は押下された時にTrueとなる

st.write("Count = ", st.session_state.count)

では戻り値で条件分岐するパターンと、コールバックを用いるパターンはどのようにかき分けたら良いのでしょうか。

コールバックを使うケース

st.text_inputのような入力を受け付ける「ウィジェットの値」を変更したい場合にコールバックを使う必要があります。以下のようにボタンを押すと入力ウィジェットの値が変わる例を考えてみます。

text_inputの値をボタンで変更する

実装は以下のようになります。

import streamlit as st

st.text_input("Message", key="text_input") # (A)


def change_value():
    st.session_state["text_input"] = "Hello, World" # (B)


st.button("Click", on_click=change_value) # (C)

コード中の(A)についてですが、keyを渡されたウィジェットは、自動的にst.session_stateにそのkeyで追加されます。そして(B)のようにウィジェットの値(value)を変更することが出来ます。最後に(C)のようにコールバック関数を渡すことで、入力を受け付けるウィジェットの値を変更することが可能です。

「コールバックを使わずにifで書くとどうなるの?」と思った方もいるかもしれませんが、例外(StreamlitAPIException)が発生します。

# ※ボタン押下時に例外が発生する
import streamlit as st

st.text_input("Message", key="text_input")


if st.button("Click"):
    st.session_state["text_input"] = "Hello, World"

StreamlitAPIException: st.session_state.text_input cannot be modified after the widget with key text_input is instantiated.

また、keyを使えば以下のように初期値を定めることも可能です。ただし、st.session_stateとウィジェットのvalueの両方を指定すると warning が発生するので注意して下さい。

import streamlit as st

key = "text_input"

def change_value():
    st.session_state[key] = "World"

if key not in st.session_state:
    st.session_state[key] = "Hello"

st.text_input("Message", key=key)
st.button("Click", on_click=change_value)

st.session_stateやコールバックについては公式ドキュメントに記載があるので適宜参照して下さい。

複数ページ対応

次に複数ページ対応について説明します。Streamlit に「複数ページ対応」や「ページルーティング」といった機能は(執筆時点では)ありません。一方でユーザアクションに応じて実行する関数を切り替えることで「複数ページ」あるように見せることが可能です。

以下のようにボタンを押すとページが切り替わる例をみてみます。

複数ページ対応

この実装は以下のようになります。

import streamlit as st

def page1():
    st.title("Page1")

def page2():
    st.title("Page2")


pages = dict(
    page1="ページ1",
    page2="ページ2",
)

page_id = st.sidebar.selectbox( # st.sidebar.*でサイドバーに表示する
    "ページ名",
    ["page1", "page2"],
    format_func=lambda page_id: pages[page_id], # 描画する項目を日本語に変換
)

if page_id == "page1":
    page1()

if page_id == "page2":
    page2()

selectboxの値に応じて、実行する関数を切り替えることで複数ページのように見せています。

ページ間でデータをやり取りする

実際の Web アプリではページがただ切り替わるだけではなく、ユーザが入力した値が別ページで表示されることもあります。以下のような例で見てみましょう。

複数ページ対応(データのやり取り)

これを Streamlit で行うならば、既に紹介したst.session_stateを用います。page1 と page2 の関数を以下のように修正すれば OK です。

def page1():
    st.title("What's your name?")
    st.text_input("Name", key="name")


def page2():
    name = st.session_state["name"]
    st.title(f"Hello, {name}")

既に紹介したst.text_inputkeyを指定することで自動的にst.session_stateに値を入れるパターンですね。

遷移するボタンを作る

データのやり取りは出来ましたが、実際の Web アプリはこうではないと思います。以下のように名前を入力し、確定ボタンを押したら遷移する方がしっくり来ないでしょうか?

複数ページ対応(遷移ボタン)

これは page_id をセッション管理することで実現可能です。まずselectboxkeyを持たせてセッションで保持します。

page_id = st.sidebar.selectbox(
    "ページ名",
    ["page1", "page2"],
    format_func=lambda page_id: pages[page_id],
    key="page-select",
)

次に page1 のボタン押下時に、selectboxの値を page2 に変更することでページが遷移したように見せます。なお、これは入力を受け付けるウィジェットの値を変更することになります。そのためon_clickでコールバック関数を使う必要があるので注意して下さい。

def page1():
    st.title("What's your name?")

    def change_page():
        st.session_state["page-select"] = "page2"

    with st.form(key="name-form"):
        st.text_input("Name", key="name")
        st.form_submit_button(label="Submit", on_click=change_page)