streamlit入門 データ可視化

Streamlit入門 - 状態の保持とコールバックの使い方

2022年7月11日

今までStreamlitの基本的な使い方ウィジェットの使い方について説明してきました。

しかしながら、これまでの使い方だとインプットボックスなどのウィジェットの値が変わると、すべての処理が1から再度実行され、ウィジェットの状態を記憶することができません

そこで今回は状態を保持する方法について見ていきたいと思います。

また、その際に使えると便利なコールバックという機能についても説明します。

公式ドキュメントはこちらです。

『Session State』

『Add Statefulnes to apps』

では、早速見ていきましょう。

状態を保持する: st.session_state

Streamlitは非常にシンプルで、ボタンが押されたり、セレクトボックスやテキストインプットが変化すると、一通りのプログラムが実行されました

例えば、以下のようにボタンを押すと1を足していくプログラムを作成したとします。

import streamlit as st

value = 0
st.subheader(f'初期値は{value}です。')
btn = st.button('+1する')
if btn:
    value += 1
    st.write(f'{value}になりました。')

しかしながら、何度「+1をする」ボタンを教えても値は1のままで増えません。

これはボタンを押すたびに毎回すべての処理が実行されるからです。

毎回valueがゼロに初期化され、その状態でvalueが+1されています。

では、押すたびに1, 2, 3と増えていくようにするにしたい場合どうしましょう?というのが今回ご紹介する内容になります。

これを実現したい場合は、今の状態を覚えている必要があります。

それを記憶する方法が"session_state"です。

st.session_stateで値を保持したり、保持された値を取りだしたりすることができます。

st.session_state自体は単なる辞書型の変数で、何もセットしなければ空の辞書になります。

では、session_stateに値を入れてみましょう。

以下のようなコードを実行します。2通りの設定方法があるので、それぞれkey_1に"value_1"、key_2に"value_2"を設定しています。

st.session_state['key_1'] = 'value_1'
st.session_state.key_2 = 'value_2'
st.write(st.session_state)

すると、以下のような辞書型変数になっているのがわかります。

以上のようにst.session_state自体は単なる辞書型変数です。

ただ、この変数はsessionが終わるまで、つまりページがリロードされるまでクリアされません

この機能を使って、ボタンを押すと1ずつ増やしていく機能を作ってみましょう。

以下のように、st.session_stateのキーに"increment"が設定されていなければ、{'increment': 0}というように辞書を初期化します。

btn = st.button('+1する')
if 'increment' not in st.session_state: # 初期化
    st.session_state['increment'] = 0 
    st.write(f"初期値は{st.session_state['increment']}です。")
    
if btn:
    st.session_state['increment'] += 1 # 値を増やす
    st.write(f"{st.session_state['increment']}になりました。")

そして、ボタンが押されたら値をincrementに対応する値を1ずつ増やします。

初期化されずに値が保持されていますね!

では、この機能を使って少し遊んでみましょう。

以下のようなコードを作成します。

from PIL import Image
import time

btn = st.button('スライムを増やす')
image = Image.open('スライム.jpg')
image_area = st.empty()
cols = image_area.columns(8)
text_area = st.empty()

if 'num_of_slime' not in st.session_state:
    st.session_state['num_of_slime'] = 1
    cols[0].image(image, width=70)
    text_area.write('スライムが現れた。')
    
if btn:
    st.session_state['num_of_slime'] += 1
    text_area.write('スライムは仲間を呼んだ。')
    for col in range(st.session_state['num_of_slime']):
        cols[col].image(image, width=70)
    
    if st.session_state['num_of_slime'] == 8:
        with st.spinner('ん...'):
            time.sleep(5)
            image = Image.open('キングスライム.jpg')
            text_area.subheader('なんとキングスライムが現れた!')
            image_area.image(image)

すると、以下のようにスライムが増え、8匹集まると…

ちょっと遊んでみただけです...

以上がst.session_stateの使い方です。

では、続いて知っておくと便利なコールバックについて見ていきたいと思います。

コールバックの使い方

コールバック関数とはボタンがクリックされたとき、セレクトボックスやインプットボックスなどの値が変わったときに呼び出される関数のことを言います

このコールバックのポイントは、クリックされたとき、値が変わったときに真っ先に指定した関数が呼び出される点です。

基本的に、Streamlitはクリックや値が変わったときには上から順番にすべての処理が実行されますが、コールバックで指定すると、何よりも初めに指定したコールバックが呼び出されます。

コールバックを使わない場合

例えばコールバックを使わない例を見てみましょう。

上に株価を表示し、エクスパンダ―にデータを表示しています。

そして、「さらに見る」ボタンをクリックすると、5件追加で表示されます

if 'num_of_data' not in st.session_state.keys():
    st.session_state['num_of_data'] = 5

st.header('2021年の株価推移')
st.line_chart(df)
with st.expander('データを見る'):
    st.table(df.head(st.session_state['num_of_data']))
    btn = st.button('さらに見る')
    if btn:
        st.session_state['num_of_data'] += 5

この処理には表示する件数を記憶しないといけないため、st.session_stateを使っています。

では、結果を見てみましょう。

わかりにくくて恐縮ですが、1回目に「さらに見る」ボタンを押しても表示件数が増えていません

これはSteamlitはボタンを押された際に上から順番にすべての処理をすることが原因です。

もう一度プログラムを見ると、ボタンを押したあとにnum_of_dataを+5件と更新しており、その前にst.table()でテーブルが表示されています。

if 'num_of_data' not in st.session_state.keys():
    st.session_state['num_of_data'] = 5

st.header('2021年の株価推移')
st.line_chart(df)
with st.expander('データを見る'):
    st.table(df.head(st.session_state['num_of_data']))
    btn = st.button('さらに見る')
    if btn:
        st.session_state['num_of_data'] += 5

つまり、1回目にボタンを押した段階では、num_of_dataが5件のままになっています。

これを回避するには、ボタンをテーブルの上に持ってきたり(ボタンが押されたときの処理はボタンより上に掛けないため)、プログラムの初めにsession_stateを更新する必要があります。

テーブルとボタンの位置を変えたくない場合も多いと思います。

ここで便利になるのがコールバックです。

コールバックは必ず最初に呼び出されるので配置の順番を意識する必要がありません。

では、コールバックを指定していきましょう。

コールバックの設定

コールバックの指定方法ですが、各インプットウィジェットに引数として設定することができます。

ボタンの場合はon_click、インプットボックスやセレクトボックス(ドロップダウン)の場合はon_changeを設定します。

その前にコールバック関数を書いておき、on_clickの引数として作成したコールバック関数名を指定します。

if 'num_of_data' not in st.session_state:
    st.session_state['num_of_data'] = 5

# コールバック関数
def update_num_of_data():
    st.session_state['num_of_data'] += 5

st.header('2021年の株価推移')
st.line_chart(df)
with st.expander('データを見る'):
    st.table(df.head(st.session_state['num_of_data']))
    # on_clickでコールバック関数を指定
    btn = st.button('さらに見る', on_click=update_num_of_data)

すると以下のように、ボタンを押した時点でコールバック関数が実行されるため、一回目のクリックで正しく行数を増やすことができます。

コールバックに引数を渡す

コールバック関数には引数を渡すことができます。

その際はargs、kwargsを使います。

これは通常のPythonの関数と同じで、argsはタプル、kwargsは辞書型で渡します。

argsを使う

まず、argsで渡す場合を見てみましょう。

先ほどの例では、「さらに増やす」ボタンを押すと表示するデータを5件増やしていましたが、ここでは増やす数を数値のインプットボックスで決められるようにします。

処理の流れとしては、「さらに増やす」ボタンを押す ⇒ インプットボックスから増やす行数を取ってくる ⇒ 表示する行数を増やす、という形になります。

インプットボックスの値の取得方法ですが、インプットボックスにkeyという引数を設定することで値を取得することができます

if 'num_of_data' not in st.session_state:
    st.session_state['num_of_data'] = 5

# コールバック関数
def update_num_of_data(num_of_add):
    st.session_state['num_of_data'] += num_of_add

st.header('2021年の株価推移')
st.line_chart(df)
with st.expander('データを見る'):
    st.table(df.head(st.session_state['num_of_data']))
    # keyでnum_of_addという名前をつける
    st.number_input(label='増やす件数', min_value=1, step=1, key='num_of_add')
    # コールバック関数を指定し、引数でインプットぼっくの値を渡す
    st.button('さらに見る', on_click=update_num_of_data, args=(st.session_state['num_of_add'], ))

ポイントを1つず見てみましょう。

5、6行目でコールバック関数を作成していますが、その引数をnum_of_addとしています。

def update_num_of_data(num_of_add):
    st.session_state['num_of_data'] += num_of_add

次に、13行目のインプットボックスでkeyプロパティに'num_of_add'という名前を設定することで、このインプットボックスのキーが'num_of_add'になります。

st.number_input(label='増やす件数', min_value=1, step=1, key='num_of_add')

keyをnum_of_addとしたインプットボックスの値を使うには、st.session_state['num_of_add']とします。

そして、それをargsの引数としてタプルで渡し、on_clickにupdate_num_of_dataを指定することでupdate_num_of_dataという関数を呼び出します。

st.button('さらに見る', on_click=update_num_of_data, args=(st.session_state['num_of_add'], ))

kwargsを使う

kwargsを使うときも同じで、引数を辞書型で渡します。

st.button('さらに見る', on_click=update_num_of_data, 
           kwargs={'num_of_add': st.session_state['num_of_add']})

まとめ

以上が、状態の保持とコールバック関数の使い方でした。

今までで重要な機能はほとんどご紹介したのかなと思います。

足りなければ是非ご連絡ください☆

次回は具体的なダッシュボードを作っていき、その次の回でデプロイするところまでご紹介したいと思います。

では!!

-streamlit入門, データ可視化
-, ,