Streamlitのコンテナを使って動的にページを表示する

今回もStreamlitの記事です。まだ続く予定です。

今回はStreamlitのコンテナやプレースホルダーの紹介です。Streamlitでは基本的にコードで実行された順番に上から下へと要素が順次描写されていきます。
しかし、場合によっては描写済みのところに遡って要素を追加したり、一度描写した要素を再度書き直したりしたいという場面もあります。

一応、何かしらウィジェットを操作すれば画面全体が全部再描写されるので、前回の記事で紹介したst.session_stateをフル活用すれば大抵のことはできてしまうのですが、この記事で紹介するコンテナを有効に使えばより手軽で柔軟に実装できます。

空要素をプレースホルダーとして挿入するst.empty()

最初の方法は、単一の要素を格納できるプレースホルダーを挿入するst.empty()です。
ドキュメントはこちら

これは場所を一つ予約しておいて、後からその中身を書き換えることができます。これの次に複数要素を格納できるコンテナってのを紹介しますが、それと違って単一要素しか入れられないので何か書き込むと値が上書きされて書き変わっていきます。

通常、Streamlitで何か書き換えたかったらページ全体が再描写されるので、単一要素を書き換えられるのは良いですね。使い方としてはwith文で使う方法と、変数に格納しておく方法があります。ドキュメントで、時間によるカウントアップの例が取り上げられているのでやって見ましょう。普通にfor文でst.write()したら毎行追記されてしまいますが、プレースホルダーを用意しているのでその中身が順次書き換えられていく動きが実装できます。

with文で使う場合はこうなります。

import streamlit as st
import time


with st.empty():
    for seconds in range(60):
        st.write(f"{seconds}秒経過 ")
        time.sleep(1)
    st.write("一分経過")

変数に格納する場合はこうです。

import streamlit as st
import time


placeholder = st.empty()
for seconds in range(60):
    placeholder.write(f"{seconds}秒経過 ")
    time.sleep(1)
placeholder.write("1分経過")

withだと宣言した直後にそこを書き換えるような使い方しかできませんが、変数を用意せずにすみコードがシンプルになります。

一方で変数に格納しておくと、上記のサンプルコードでは直後に書き換えているのであまりメリット感じませんが実際は、何か別の処理を色々やってから改めて確保していた場所を書き換えるといったことができます。

複数の要素を格納できるコンテナを作るst.container()

次に紹介するのが、st.container()です。ドキュメントはこちら

これもempty()同様にほぼプレースホルダーなのですが、違うのは複数の要素を格納できることです。その代わり、上書き修正ができません。

後からこのコンテナの中に要素を追加していくことができます。コンテナの中では追加されたものが上から順番に表示されます。

超シンプルな例ですが、以下のようなのをやって見ましょう。

import streamlit as st


st.write("1行目")
container = st.container()
st.write("2行目")
container.write("3行目")
container.write("4行目")

全部st.writeで実装したら、1行目から4行目までが順番に表示されるところですが、コンテナを1行目と2行目の間に置いているので、これを実行すると3行目と4行目は1行目と2行目の間に出力されます。

Streamlitの変数がリセットされないようにsession単位で保存する

今回もStreamlitのお話です。前回、各種ウィジェットの使い方を紹介しましたが、ウィジェットを何かしら操作するとコードが一通り再実行されます。
ウィジェットの値自体は保存されているのですが、それ以外のPythonコード中の変数は全てリセットされてしまいます。

例えば、次のように、スライダーで値を選択してボタンを押したらそれが合計されていく、みたいなことをしようとした(しかしできてない)ソースを書いて見ます。

import streamlit as st


sum_ = 0
value = st.slider(label="スライダー", min_value=0, max_value=10)
if st.button("合計"):
    sum_ += value
    st.write(sum_)

これ、スライダーで値を選んで、「合計」ボタンを押したらどんどん値が足されてその結果が表示されそうなコードに見えませんか?

しかし実際はそのような挙動にならず、スライダーの値を変更したり、ボタンをクリックしたりすると一番最初のsum_=0の部分も再実行されるので値は積み重なっていかず、逐一リセットされます。

また、スライダー動かしただけ、の場合、st.write() の部分で書き出した数字まで消えます。(ボタン押したときだけ数字が表示される。)

このような場合に使うのが、st.session_stateです。
参考: Session State – Streamlit Docs

これはほとんど辞書のように使えます。
一番最初はst.session_stateが空っぽなのでif文で判定を入れて初期化し、それ以降は辞書の値を読み書きするのと同じイメージで実装していきます。先ほどのコードは次のようになります。

import streamlit as st


# session_state に値がない場合初期化する。
if "sum_" not in st.session_state:
    st.session_state.sum_ = 0

value = st.slider(label="スライダー", min_value=0, max_value=10)
if st.button("合計"):
    # session_stateのkeyに対応した値を更新する
    st.session_state.sum_ += value

# 表示
st.write(st.session_state.sum_)

スライダーを動かした時に消えないようにst.writeはif文の外に出しました。これで、合計ボタンを押すたびにスライダーで指定している値分だけ数字が足され、合計が表示されます。

これは単純に変数の値を保存しておくだけでなく、DBやWebなどの外部リソースからのデータ取得を伴う処理の効率化、要するにキャッシュにも使えます。

df = {DBからデータを取ってくる処理}
みたいなのをそのまま実装してしまうと、ウィジェットを何か動かすたびにSQLを発行してDBに負担をかけてしまいます。ここで、一度データを取得したらst.session_state.df などに値を保存しておき、値があったら再取得しないような実装にすると、DBなどの外部リソースへの負荷を減らし、レスポンス速度も改善できます。

ちなみに、値を消したかったら、対象のkeyに対してdelをやれば良いです。
del st.session_state.{key}

また、GUI上でも、右上の3点リーダーにClear cache というメニューがあるのでこれで消せます。

Streamlitの入力ウィジェット

今回もStreamlitの話です。

Streamlitではいろんな操作をインタラクティブに行えるようにさまざまなウィジェットが用意されています。ドキュメントのウィジェットのページはこちら。
参考: Input widgets – Streamlit Docs

テキストエリアやチェックボックス、ラジオボタンにドロップダウン、日時のセレクターなど主要なものは一通り揃っていると思います。

一個一個説明するよりも、ざっと使用例をお見せしたほうがいいと思うのでサンプルコードを最初に紹介します。基本的に、 st.{ウィジェット名}(ラベルなどの引数) で使って、戻り値を変数で受け取ってのちの処理を実装します。今回のサンプルコードでは全部書き出すようにしました。

import streamlit as st

# ボタン。ラベルを指定する。
if st.button("ボタン"):
    st.write("ボタンが押されました!")
else:
    st.write("ボタンがまだ押されていません。")

# チェックボックス。ラベルを指定する。
check = st.checkbox("チェックボックス")
if check:
    st.write("チェックされました")
else:
    st.write("チェックされていません")

# トグル。ラベルを指定する。
toggle = st.toggle("トグル")
if toggle:
    st.write("ONです")
else:
    st.write("OFFです")

# ラジオボタン。ラベルと選択肢を渡す。
color = st.radio(
    "色を選んでください",
    ("赤", "青", "緑")
)
st.write(f"選択された色: {color}")

# 1個の値を選択するドロップリスト。
fruits = st.selectbox(
    "フルーツを選んでください",
    ("りんご", "オレンジ", "バナナ")
)

st.write(f"選択されたフルーツ: {fruits}")
# 複数選択可能なドロップリスト。ラベルと選択肢を指定する。
colors = st.multiselect(
    "色を選んでください",
    ["赤", "青", "緑", "黄", "黒", "白"]
)
# 選択した値は配列で返される。
st.write(f"選択された色: {colors}")

# スライダー。ラベルと最小値, 最大値, 初期値, ステップ を指定する。(省略可)
# 下の例は0才から100才まで5才刻みで選択でき、初期値が20才
age = st.slider('年齢を選択してください', 0, 100, 20, 5)
st.write(f"選択された年齢: {age}")

# 日付入力。
date = st.date_input("日付")
st.write(f"選択された日付: {date}")

# 時間入力。
time = st.time_input("時刻")
st.write(f"選択された時刻: {time}")

# テキストボックス(1行)
text_single = st.text_input("1行のテキストを入力できます。")
st.write(f"入力されたテキスト: {text_single}")

# テキストボックス(複数行)
text_multi= st.text_area("複数行のテキストを入力できます。")
# 出力はマークダウンなので、改行したい場合は行末に半角スペース2個必要です。
st.write(f"入力されたテキスト: {text_multi}")

これを実行すると次のような画面になります。

コードからイメージした通りのものになっているのではないでしょうか。

画面内のウィジェットのうちどれか一つを操作すると、画面全体が再描写されます。他のウィジェットの現時点の値はリセットされないので安心です。(ただ、それ以外の変数の値などはリセットされます。)

先週のグラフの可視化の記事と組み合わせると、ウィジェットで描写するデータを絞り込んで対象のデータ分のグラフを書く、といった使い方ができますね。