Streamlitとは
StreamlitはPython向けのWebアプリケーションフレームワークです。
Streamlitを使うとフロントエンドの知識ゼロでもデータを可視化するためのWebアプリケーションを簡単に作れます。以下のような機能が特徴。
- 変数を地の文に書くだけで、GUIに出力される(マジックコマンド)
-
st.radio
やst.text_input
のようなWidget APIを呼び出すだけで、GUIコンポーネントが生成される
Streamlitの最新バージョン
Streamlitの最新バージョンはChangelogで確認できます。
2024/8/29時点で最新バージョンは1.38.0です。
以下に記載した変更点は、1.38.0でHighlights、Notable ChangesおよびOther Changesとしてアナウンスされたものの抜粋です。
- Pandas以外のDataFrame形式に対応
-
st.json
の展開状態の設定 -
st.code
における行折り返し表示 - Kubernetesで用いられる形式のシークレットに対応
- pydantic 1.x.x系への対応の一部打ち切り
-
experimental_allow_widgets
の削除 - WebSocketの再接続でRerunされなくなった
今回の目玉アップデートはやはり最初のPandas以外のDataFrame形式に対応したことですね。
Pandas以外のDataFrame形式に対応
StreamlitはこれまではPandas DataFrameを主に扱ってきたのですが、これがなんと色々なDataFrameに対応しました。
Dask、Modin、Numpy、Polars、PyArrow、Snowpark、Xarray…などなどに対応したそうです。
PythonデータベースAPI(PEP 249)もしくはPython dataframe interchange protocolに対応していればOKだそうです。
例えばPolarsはPandasよりも高速に動作するシーンが多いので嬉しいですね!
DataFrameをテーブル形式で表示するst.dataframe
だけではなく、グラフで表示するst.bar_chart
やst.line_chart
など、データを編集するst.data_editor
も対応しました。
import streamlit as st
import pandas as pd
import polars as pl
import ibis
import duckdb
# PandasでCSVを読み込む関数
def read_csv_pandas(file_path):
return pd.read_csv(file_path)
# PolarsでCSVを読み込む関数
def read_csv_polars(file_path):
return pl.read_csv(file_path)
# IbisでSQLiteに接続してDataFrameとして返す関数
def read_csv_ibis(db_path, table_name):
conn = ibis.sqlite.connect(db_path)
return conn.table(table_name)
# DuckDBに接続してConnectionを返す関数
def connect_duckdb(db_path, table_name):
conn = duckdb.connect(db_path)
query = f"SELECT * FROM {table_name}"
conn.execute(query)
return conn
# PandasのDataFrameを表示
st.subheader('Pandas')
df_pandas = read_csv_pandas('dummy.csv')
st.dataframe(df_pandas)
# PolarsのDataFrameを表示
st.subheader('Polars')
df_polars = read_csv_polars('dummy.csv')
st.dataframe(df_polars)
# IbisのDataFrameを表示
st.subheader('Ibis')
df_ibis = read_csv_ibis('db.sqlite', 'test')
st.dataframe(df_ibis)
# DuckDBのConnectionを表示
st.subheader('DuckDB by PEP 249')
conn_duckdb = connect_duckdb('db.duckdb', 'test')
st.dataframe(conn_duckdb)
# それぞれのオブジェクトを使ってチャートを表示
st.subheader('Chart from Pandas')
st.bar_chart(df_pandas, x='ID', y=['Val1', 'Val2'])
st.subheader('Chart from Polars')
st.bar_chart(df_polars, x='ID', y=['Val1', 'Val2'])
st.subheader('Chart from Ibis')
st.bar_chart(df_ibis, x='ID', y=['Val1', 'Val2'])
st.subheader('Chart from DuckDB by PEP 249')
# これは上手く表示されない:上記のst.dataframe(conn_duckdb)でcursorが動いてしまっているため
# cursorが動いていないときであれば動作する
st.bar_chart(conn_duckdb, x='ID', y=['Val1', 'Val2'])
# それぞれのオブジェクトを使ってdata_editorを表示
st.subheader('Data Editor from Pandas')
st.data_editor(df_pandas)
st.subheader('Data Editor from Polars')
st.data_editor(df_polars)
# これはエラーになる
# StreamlitAPIException: The data type (Table) or format is not supported by the data editor. Please convert your data into a Pandas Dataframe or another supported data format.
# st.subheader('Data Editor from Ibis')
# st.data_editor(df_ibis)
# これは上手く表示されない:上記のst.dataframe(conn_duckdb)でcursorが動いてしまっているため
# cursorが動いていないときであれば動作する
st.subheader('Data Editor from DuckDB by PEP 249')
st.data_editor(conn_duckdb, key='duckdb1')
ただし、1.38.0時点ではいくつか明記されていない制限があるようです。
PythonデータベースAPI(PEP 249)を表示するときの制限
1回表示すると内部のcursorが動いてしまうようで、2回目の表示はできませんでした。
なお、1回目であればst.data_editor
も使えますが、編集したデータのデータベースへの書き戻しはされないのでご注意ください。
st.data_editor
の制限
対応しているDataFrame形式と対応していないDataFrame形式がありそうです。
具体的にはIbisはダメでした。
また、PythonデータベースAPI(PEP 249)をソースとしてst.data_editor
を使うときだけ、オプション引数のkey
を指定することを要求されました。
st.json
の展開状態の設定
st.json
の展開状態を設定することができるようになりました。
オプション引数のexpanded
がTrue
だとすべて展開、False
だとすべて非展開、数値だとそのレベルまで展開します。
import streamlit as st
# 選択肢を表示
val = st.selectbox("expandedに指定する値", ["True", "False", "1", "2", "3"])
if val == "True":
expanded = True
elif val == "False":
expanded = False
else:
expanded = int(val)
st.json(
{
"foo": "bar",
"stuff": [
"stuff 1",
"stuff 2",
"stuff 3",
],
"level1": {"level2": {"level3": {"a": "b"}}},
},
expanded=expanded,
)
st.code
における行折り返し表示
st.code
が折り返し表示できるようになりました。
オプション引数のwrap_lines
で設定できます。
import streamlit as st
wrap_lines = st.checkbox("wrap_lines", value=False)
long_text = "This is very very long text. " * 10
code = f'''
def hello():
print("{long_text}")
'''
st.code(code, language="python", wrap_lines=wrap_lines)
Kubernetesで用いられる形式のシークレットに対応
Snowpark Container Servicesとの連携を想定して、Kubernetesで用いられる形式のシークレット、具体的にはディレクトリにひとつひとつのシークレットを配置する方法に対応しました。
例えば以下のようにファイルを配置することで各ファイルの中身をシークレットとして読み出すことができます。
.streamlit/
└ .secrets/
├ user1/
│ ├ user
│ └ password
├ user2/
│ ├ user
│ └ password
└ somevalue
└ somesecret
デフォルトでは.streamlit/secrets.toml
のみを読み込もうとするため、.streamlit/config.toml
の構成オプションにシークレットを配置するディレクトリを指定してあげる必要があります。
なお、構成オプションの反映はRerun
ではなくStreamlitの再起動が必要なので注意してください。
また、Streamlitの実装の都合上、1.38.0時点では指定するディレクトリ名の末尾が「.toml」だとPermission denied
で怒られます。うーむ、あるある。
[secrets]
files = [ ".streamlit/secrets.toml", ".streamlit/.secrets" ]
Python側で実際にシークレットを読み出すときは以下のようになります。
指定したディレクトリからの相対パスがキーに変換されます。
また、ディレクトリ内にファイルが一つだけの場合はファイル名に相当するキーが省略されます。
import streamlit as st
# 従来の.streamlit/secrets.tomlに記載した場合
st.write("OpenAI_key: " + st.secrets["OpenAI_key"])
st.write("[database] user: " + st.secrets["database"]["user"])
st.write("[database] password: " + st.secrets["database"]["password"])
# 以降はKubernetesで用いられる形式のシークレットで記載した場合
# .streamlit/.secrets/user1にuserファイルとpasswordファイルを配置
st.write("[user1] user: " + st.secrets["user1"]["user"])
st.write("[user1] password: " + st.secrets["user1"]["password"])
# .streamlit/.secrets/user2にuserファイルとpasswordファイルを配置
st.write("[user2] user: " + st.secrets["user2"]["user"])
st.write("[user2] password: " + st.secrets["user2"]["password"])
# .streamlit/.secrets/somevalueにファイルを一つだけ配置
st.write("somevalue: " + st.secrets["somevalue"])
# 全体像を表示
st.write(st.secrets)
pydantic 1.x.x系への対応の一部打ち切り
pydanticはPythonのバリデーションライブラリです。
2023/6/30に2.0.0がリリースされて、それ以降もある程度は1.x.x系と2.x.x系が並行で開発されているようです。
Streamlitと1.x.x系を組み合わせるとカスタムバリデーションという機能が使えなくなるので、これを修正するためにStreamlitが動的にパッチを適用していたのようなのですが、2.x.x系がリリースされてだいぶ経つからそろそろいいだろう…ということでそのパッチが削除されました。
ちなみに、pydanticはJSONで入出力するときのスキーマ定義に使われることがしばしばあるようで、例えばopenai-python
の依存関係にも含まれています。
なお、どうしても1.x.x系でカスタムバリデーションを使いたいときはユーザ側で対応できるとのことです。
Pydanticの1.x.x系を試してみたコード、2.x.x系を試してみたコードは以下の通りです。
# pydantic 1.x.x系の実装
import streamlit as st
from pydantic import BaseModel, Field, validator
class User(BaseModel):
name: str = Field(..., min_length=4, max_length=16)
# ここがカスタムバリデーション
@validator("name")
@classmethod
def validate_alphanumeric(cls, v: str) -> str:
if not v.isalnum():
raise ValueError("must be alphanumeric")
return v
example_names = ["John", "John Doe", "John@Doe"]
for i, name in enumerate(example_names):
try:
st.subheader(f"example {i+1}:")
user = User(name=name)
st.json(user.json(indent=2))
except ValueError as err:
st.write("ValueError occurred:")
st.json(err.json(indent=4))
# pydantic 2.x.x系の実装
import streamlit as st
from pydantic import BaseModel, Field, field_validator
class User(BaseModel):
name: str = Field(..., min_length=4, max_length=16)
# ここがカスタムバリデーション
@field_validator("name")
@classmethod
def validate_alphanumeric(cls, v: str) -> str:
if not v.isalnum():
raise ValueError("must be alphanumeric")
return v
example_names = ["John", "John Doe", "John@Doe"]
for i, name in enumerate(example_names):
try:
st.subheader(f"example {i+1}:")
user = User(name=name)
st.json(user.model_dump_json(indent=2))
except ValueError as err:
st.write("ValueError occurred:")
st.json(err.json(indent=4))
Pydanticの1.x.x系を試してみたコードをStreamlit==1.38.0
かつpydantic<2
の環境で実行すると以下のようなエラーが発生します。
ConfigError: duplicate validator function "main.User.validate_alphanumeric"; if this is intended, set
allow_reuse=True
experimental_allow_widgets
の削除
これはst.cache_data
に指定する実験的なパラメータで、キャッシュした関数内でst.slider
のような入力要素を作成すると、その入力要素が返した値ごとにキャッシュを保持してくれる、というものでした。
import streamlit as st
import pandas as pd
import numpy as np
@st.cache_data(experimental_allow_widgets=True)
def get_data():
# この選択結果ごとにキャッシュが保持される
num_cols = st.slider("列数", min_value=1, max_value=10, value=1)
data = pd.DataFrame(
np.random.randn(20, num_cols), columns=("col %d" % i for i in range(num_cols))
)
return data
data = get_data()
st.line_chart(data)
便利なシーンはありそうなのですが、メモリ消費量がとても気軽に増えてしまうこと、他の機能を実装するときにしばしば影響があること、st.fragment
による部分更新という違うアプローチで高速化できるようになったことなどの複合的な理由から廃止になったようです。
1.38.0時点では以下のようなエラーが表示されて、入力要素の状態に関わらずキャッシュされます。
import streamlit as st
import pandas as pd
import numpy as np
@st.cache_data(experimental_allow_widgets=True)
def get_data():
# この選択結果ごとにキャッシュが保持される…はずだった
# 1.38.0からは最初に呼び出したとき、すなわちvalue=1のときのdataがキャッシュされる
num_cols = st.slider("列数", min_value=1, max_value=10, value=1)
data = pd.DataFrame(
np.random.randn(20, num_cols), columns=("col %d" % i for i in range(num_cols))
)
return data
data = get_data()
st.line_chart(data)
WebSocketの再接続でRerunされなくなった
書いてあるそのままです。
Streamlitは裏側ではサーバプロセス(Python)とブラウザの間でWebSocket接続して通信しています。ただ、その裏側のネットワークがどうこうしたからRerunしてほしい、ということはあまりないので、いいアップデートだと思います。
例えば、以下のようなプログラムを用意してローカルでStreamlitを動かしているときにPCをスリープにしてみると違いが分かります。
1.37.0だとPCがスリープするたびに表示されるグラフが変わるのですが、1.38.0ではPCがスリープしても表示されるグラフは変わりません。
import streamlit as st
import pandas as pd
import numpy as np
def get_data():
data = pd.DataFrame(
np.random.randn(8, 3), columns=['a', 'b', 'c']
)
return data
data = get_data()
st.line_chart(data)