streamlit とは

Python で Web アプリケーションを作成できるオープンソースのライブラリ。

インストール〜アプリ起動

ライブラリのインストール:

$ pip install streamlit

サンプルアプリの実装:

# sample.py
import streamlit as st

st.title('streamlit Tutorial')
st.header('This is a header')
st.subheader('This is a subheader')
st.text('Hello World!')

input_num = st.number_input('Input a number', value=0)
result = input_num ** 2
st.write('Result: ', result)

アプリの起動:

$ streamlit run sample.py

→ http://localhost:8501 にアクセス

スクリーンショット 2025-01-14 11 52 08

使い方

st.write の色々

Markdown 表記にも対応:

st.write('# This is h1')
st.write('## This is h2')
st.write('### This is h3')
st.write('**太字の例**')
st.write('~~取消線の例~~')

スクリーンショット 2025-01-14 11 52 16

リスト・辞書の表示:

st.write(['apple', 'orange', 'banana'])
st.write({'apple': 100, 'orange': 50, 'banana': 200})

スクリーンショット 2025-01-14 11 52 23

import pandas as pd

df = pd.DataFrame({
    'name': ['Tom', 'John'],
    'age': [19, 22],
    'birthPlace': ['U.K.', 'Australia']
})
st.write(df)

スクリーンショット 2025-01-14 11 52 29

入力フォーム

# 数値入力を受け取るボックスとデフォルト値を設定
n_input = st.number_input('Input a number', value=0)
st.write('Result: ', n_input**2)

# テキスト入力ボックス
text_input = st.text_input('Input', 'Input some text here.')
# テキストエリア
text_area = st.text_area('Text Area', 'Input some text here.')

ファイルのアップロード

import pandas as pd

uploaded_file = st.file_uploader("Choose a file")
if uploaded_file is not None:
    st.write(uploaded_file)
    # アップロードされたファイルを読み込んで操作
    data = pd.read_csv(uploaded_file)
    st.write(data)

スクリーンショット 2025-01-14 12 32 34

ボタン・チェックボックス

if st.button('My Button'):
    st.write('Hello World!')

if st.checkbox('My Checkbox'):
    st.write('Task 1 is done.')

opt_radio = st.radio(
    'My Radio Button', 
    ['A', 'B', 'C']
)

opt_select = st.selectbox(
    'My Select Box', 
    ['A', 'B', 'C']
)

opt_multiselect = st.multiselect(
    'My Multi Select Box',
    ['A', 'B', 'C', 'D'],
    default=['A', 'C'] # デフォルトの設定
)

# スライダー
value = st.slider('Select a value', 0, 100, 50) # min, max, default
if value < 30:
    # 値が30以下になったらテキストを表示
    st.write('value = {} < 30'.format(value))

# 両側スライダー、日付型
import datetime
d_min = datetime.date(1900, 1, 1)
d_max = datetime.date(2000, 12, 31)
d_lower = datetime.date(1930, 1, 1)
d_upper = datetime.date(1970, 12, 31)
values = st.slider('期間を指定してください', d_min, d_max, (d_lower, d_upper), format='YYYY-MM-DD (ddd)')
st.write('values: left = {}, right = {}'.format(values[0], values[1]))

スクリーンショット 2025-01-14 15 52 09

グラフの描画

matplotlib を利用してグラフを描画できる。

import matplotlib.pyplot as plt
import numpy as np

x = np.arange(10)
y1 = x**2
y2 = x*7
fig, ax = plt.subplots()
ax.plot(x, y1, label=r'$y=x^2$')
ax.plot(x, y2, label=r'$y=7x$')
plt.legend()
plt.grid()
st.pyplot(fig)

スクリーンショット 2025-01-14 15 16 54

地図

# 緯度経度のフィールド名は lat, lon でも可
points = [
    {'latitude': 35.689521, 'longitude': 139.691704},  # 東京都
    {'latitude': 34.686316, 'longitude': 135.519711}   # 大阪府
]
st.map(points)

スクリーンショット 2025-01-16 10 28 16

Latex 数式

st.latex(r'''
    \sum_{k=0}^{n-1} a r^k =
    a \left(\frac{1-r^{n}}{1-r}\right)
''')

スクリーンショット 2025-01-24 19 14 29

コンテナ

with st.container(height=100):
    st.write('Inside container')
    for i in range(10):
        st.write(i)

st.write('Outside container')

スクリーンショット 2025-01-14 23 06 10

展開

with st.expander('中身を見る'):
    st.write('expander の中身')

スクリーンショット 2025-01-14 23 06 51

カラム分割

col1, col2, col3, empty = st.columns([1,2,1,6])  # 比率を指定して分割
col1.write('カラム1に文字列を表示します')
col2.write(['カ', 'ラ', 'ム', '2'])
if col3.button('ボタン'):
    col3.write('ボタンがクリックされました')

スクリーンショット 2025-01-14 22 22 23

水平区切り線

st.write('AAAAA')
st.divider()
st.write('BBBBB')

スクリーンショット 2025-01-24 19 14 36

サイドバー

st.sidebar.write('サイドバー')
side_slider = st.sidebar.slider('width', 0, 100, 30)

スクリーンショット 2025-01-14 22 50 58

タブ

tab_a, tab_b, tab_c = st.tabs(['A', 'B', 'C'])

with tab_a:
    st.write('This tab is A')

with tab_b:
    st.write('This tab is B')

with tab_c:
    st.write('This tab is C')

スクリーンショット 2025-01-14 22 54 31

処理が長引く場合の wait 表示

import time

if st.button('Slow process'):
    with st.spinner('処理中です。しばらくお待ち下さい'):
        st.write('Processing...')
        time.sleep(3)
        st.write('Done!')

スクリーンショット 2025-01-14 23 01 25

空のウィジット

  • 空っぽのウィジットを生成して場所を確保しておき、ボタン押下などのユーザ行動をトリガーにその位置に表示させる
  • 作ってあったウィジットを画面から消す

といったことができる。

例えば以下の例では、

  • 初期状態:数値を入力するフォームと「submit」ボタン
  • ボタンを押下したとき:フォームの数値のみが画面に表示され、フォームとボタンは消失
widget_out = st.empty()
widget_in = st.empty()
btn = st.empty()
num = widget_in.number_input('Input a number', value=0)
if btn.button('submit'):
    widget_in.empty()
    widget_out.write(num)
    btn.empty()

TIPS

ページ全体の設定

streamlit.set_page_config を使えば、

  • ブラウザタブに表示されるページタイトル
  • ファビコン
  • 画面の表示領域の幅
  • サイドバーの開閉の初期状態

などを設定できる。

import streamlit as st

st.set_page_config(
    page_title='My Sample Page',
    page_icon='📕',
    layout='wide',
    initial_sidebar_state='expanded',
    menu_items={
        'Get Help': 'https://hkawabata.github.io/technical-note/note/Language/Python/Library/streamlit',
        'About': 'https://streamlit.io/',
        'Report a bug': 'http://example.com'
    }
)

データの保持

streamlit はボタンを押したときなどのタイミングで全てのデータがリセットされるため、通常の変数の値は保持されない。
→ 例えば「ボタンを押すたびにカウンタを1大きくする」といったことができない。

これを解決するには session_state の機能を使う:

import streamlit as st

# 変数の定義(ボタンが押された回数)
count = 0
if 'count' not in st.session_state:
    st.session_state.count = 0

# ボタンを表示し、クリックされた回数を表示する
if st.button('クリックしてください'):
    count += 1
    st.session_state.count += 1

st.write(f'クリック回数: {count}(通常の変数)')
st.write(f'クリック回数: {st.session_state.count}(session_state)')

スクリーンショット 2025-01-14 15 25 10

キャッシュ

import streamlit as st
import time
import datetime

@st.cache_resource
def get_result_of_heavy_process(arg):
    # 時間がかかる処理
    time.sleep(1)
    return arg * 10

def try_once(arg):
    t = datetime.datetime.now()
    r = get_result_of_heavy_process(arg)
    print(f'result = {r}, ', datetime.datetime.now() - t)

try_once(4)
try_once(4)
try_once(7)
result = 40,  0:00:01.006119
result = 40,  0:00:00.000730  # キャッシュが効いてすぐに結果が返る
result = 70,  0:00:01.007979  # 初めての引数で関数を呼び出したので3秒かかる

次に ttl(キャッシュの有効時間)を設定してみる:

@st.cache_resource(ttl='5s')   # 5秒でキャッシュが切れる設定
def get_result_of_heavy_process(arg):
    # 時間がかかる処理
    time.sleep(1)
    return arg * 10

def try_once(arg):
    t = datetime.datetime.now()
    r = get_result_of_heavy_process(arg)
    print(f'result = {r}, ', datetime.datetime.now() - t)

try_once(4)
try_once(4)
time.sleep(6)
try_once(4)
time.sleep(3)
try_once(4)
time.sleep(3)
try_once(4)
result = 40,  0:00:01.009000
result = 40,  0:00:00.002509  # キャッシュが効いてすぐに結果が返る
result = 40,  0:00:01.007050  # 有効期限切れで再処理
result = 40,  0:00:00.000667  # まだ有効期限内なのですぐ結果が返る
result = 40,  0:00:01.006122  # 有効期限切れで再処理

→ キャッシュされた値への再アクセスがあっても、キャッシュの残り時間はリセットされない模様

他にも st.cache_resource(max_entries=n) とすると、キャッシュする件数を n 件に制限できる(これを超えると古いものから削除)