はじめに
こんにちは、エクスチュアの石原です。
データサイエンスや機械学習のプロジェクトでは、結果を視覚化し、共有することが重要です。また、共有の方法としてWebページのようにURLだけ渡すような簡単な共有方法があれば楽ですよね。
そんなWebアプリのお手軽構築を実現できるのが、本記事でご紹介するStreamlitの魅力です。Streamlitは、Pythonスクリプトを使ってシンプルかつ迅速にインタラクティブなウェブアプリケーションを作成するためのオープンソースフレームワークです。本記事では、Streamlitの基本的な使い方と具体的な例を紹介します。アプリ作成していく上で役立った機能もご紹介します。
Streamlitの概要
Streamlitは、データサイエンティストやエンジニアが迅速にウェブアプリケーションを作成するためのフレームワークです。Pythonを使ってコードを書くことで、データの可視化やインタラクティブなUIを簡単に構築できます。
特徴
- 簡単なセットアップ
- リアルタイムのインタラクティブなUI
- 強力なデータ可視化ツール
また、SnowflakeではStreamlit in Snowflakeとして提供されており、データやアプリケーションコードを外部システムに移動することなく、Snowflake のデータを処理して使用するアプリケーションを構築できます。
Streamlit in Snowflakeでも今回の内容は真似できることが多いと思いますので、そちらを使用される方も以下のサンプルコードを参考にしてみてください。
インストール方法
Streamlitを使うためには、Pythonが必要です。以下の手順でStreamlitをインストールします。
1 | pip install streamlit |
また、基本的なモジュールも併せてインストールしておきます。
1 | pip install pandas sympy matplotlib numpy |
基本的な使い方
Streamlitの基本的な使用方法を紹介します。まずは、シンプルなアプリケーションを作成してみましょう。
- 新しいPythonファイルを作成します(
app.py
) - 以下のコードをファイルに追加します。
1 2 3 4 | import streamlit as st st.title( 'Hello, Streamlit!' ) st.write( 'これは最初のStreamlitアプリケーションです。' ) |
- ターミナルで以下のコマンドを実行してアプリケーションを起動します。
1 | streamlit run app.py |

上記のコードを実行するとWEBアプリケーションが立ち上がります。
基本的な実行方法はこれだけです。しかも、ソースコードでリアルタイムで更新した内容が更新されるたびに反映されるので開発効率はかなり良いです。
具体的な例
次に、Streamlitを使って簡単なデータ可視化アプリケーションを作成します。
app.pyを書き換えて保存した後に、ページを更新してみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 | import pandas as pd import streamlit as st st.title( 'Hello, Streamlit!' ) df = pd.DataFrame({ '名前' : [ 'Alice' , 'Bob' , 'Charlie' ], 'スコア' : [ 85 , 90 , 78 ] }) st.write( "学生のスコア" ) st.dataframe(df) |

データフレームの値が表として表示されましたね。
以下のコードでは結果を図示することが出来ます。
1 2 3 4 5 6 7 8 9 10 11 12 13 | import matplotlib.pyplot as plt import numpy as np import pandas as pd import streamlit as st st.title( 'Hello, Streamlit!' ) # データの作成 data = np.random.randn( 100 , 3 ) df = pd.DataFrame(data, columns = [ 'A' , 'B' , 'C' ]) # チャートの作成 st.line_chart(df) |

注意
streamlitはユーザの入力がある度に、ファイル内の処理を再実行します。
例えば以下のようにボタンを押すとcount変数が増加するコードを作成します。
1 2 3 4 5 6 7 8 9 10 11 12 | import streamlit as st st.title( 'Hello, Streamlit!' ) st.write( '変数を設定' ) count = 0 if st.button( 'push me' ): st.write( 'ボタンを押しました' ) count + = 1 st.write( 'count = ' , count) |

ボタンを押すと以下のようになります。

ここまではイメージ通りの挙動かと思いますが、再度ボタンを押してもcountの数値が変わりません。
これはコード全体がボタンを押す度に呼び出されているためです。
また、「ボタンを押しました」のメッセージが「変数を設定」と「count = 1」の間に表示されるのはボタンを押された時にst.button(‘push me)の値がTrueとFalseということだけを保持してそれ以外の処理は上から処理されたためです。実質的にボタンが押された後のコードは以下のコードと全く同じ挙動になります。
1 2 3 4 5 6 7 8 9 10 11 | import streamlit as st st.title( 'Hello, Streamlit!' ) st.write( '変数を設定' ) count = 0 st.write( 'ボタンを押しました' ) count + = 1 st.write( 'count = ' , count) |
個人的によく使う機能をご紹介
基礎的なコードの動かし方が分かったところでアプリケーション開発に役立つ機能をご紹介します。
- st.session_state
- st.write
- st.json
- st.data_editer
- st.columns
- st.empty
- st.container
- st.button(callback)
- st.spinner
- st.expander
- st.slidebar
- st.swith
- container_width
st.session_state
streamlitを使いこなす上で必ず覚えておく必要があるものがst.session_stateです。(公式ドキュメント)
streamlitでアプリケーションを開発する上で、最も大事といっても過言ではありません。先ほどのボタンを押すソースを改良してみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | import streamlit as st st.title( 'Hello, Streamlit!' ) if 'count' not in st.session_state: st.write( '変数を設定' ) st.session_state[ 'count' ] = 0 if st.button( 'push me' ): st.write( 'ボタンを押しました' ) st.session_state.count + = 1 # st.session_state['count'] += 1 でもOK st.write( 'count = ' , st.session_state.count) |
こちらのコードではボタンをクリックすると以下のように変更されます。ここで注目は「変数を設定」が表示されなくなっていることです。

さらにもう一度押すと

きちんと数値が増加しています。
st.session_stateは辞書型の値としてセッションを跨いでもkeyとvalueを保持することが出来ます。
if 'count' not in st.session_state:
では辞書内に既にcount
keyを持っているかを確認し、持っていない場合は変数を設定、持っている場合は= 0を処理しないという挙動が実現できます。(この条件分岐がないと毎回0にリセットされてしまいますからね)
基本的にはどんなvalueでも保持できます。
ボタンや入力フォームなどなどユーザの入力を受け付けるインタラクティブなアプリケーション開発では必須ですので覚えておきましょう。
st.write
サンプルのコードでも何度か出てきていますがこちらもどんな挙動をするか覚えておくとよいかと思います。(公式ドキュメント)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | import pandas as pd import streamlit as st st.title( 'Hello, Streamlit!' ) st.write( 'This is a simple example of Streamlit web app.' ) list_data = [ 'apple' , 'banana' , 'cherry' ] json_data = { 'name' : 'John' , 'age' : 22 } df_data = pd.DataFrame({ 'column1' : [ 1 , 2 , 3 ], 'column2' : [ 10 , 20 , 30 ] }) st.write( 'list' , list_data, 'json' , json_data, 'dataframe' , df_data) import sympy st.write(sympy.Rational( 3 , 2 )) import matplotlib.pyplot as plt import numpy as np x = np.linspace( 0 , 20 , 100 ) y = np.sin(x) fig = plt.figure() plt.plot(x, y) plt.xlabel( 'x' ) plt.ylabel( 'sin(x)' ) plt.title( 'Simple plot' ) plt.grid() st.write(fig) |
st.writeを使用することで、入力された値ごとに適切な出力が自動で選択されます。シンプルに記述したい場合はwriteを使用しておけば間違いないですね。
st.json
それでは、他の出力形式たとえばjsonやdataframeを使用する必要がないかというとそういう訳でもありません。下記のコードを実行するとwriteの実行時にエラーが発生してしまいます。これはwrite関数ではそれぞれの関数特有の値をセットできないためです。
jsonで言えばデフォルトの表示を折り畳む設定があります。テストではwriteを使い、UIを調整したい段階では使い分けるなどが良いと思います。
1 2 3 4 5 6 7 8 9 10 | import pandas as pd import streamlit as st st.title( 'Hello, Streamlit!' ) json_data = { 'name' : 'John' , 'age' : 22 } st.json(json_data,expanded = False ) st.write(json_data,expanded = False ) |
st.data_editer
st.data_editerはその名の通り値を編集可能なウィジットを作成することが出来ます。
下記のコードを実行し、上に表示されたエディターを編集すると2つ目のdfの値も変更されていきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 | import pandas as pd import streamlit as st st.title( 'Hello, Streamlit!' ) df_data = pd.DataFrame({ 'column1' : [ 1 , 2 , 3 ], 'column2' : [ 10 , 20 , 30 ] }) df_data = st.data_editor(df_data) st.dataframe(df_data) |

さらに、列の値をさまざまな表示に変更することもできます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | import pandas as pd import streamlit as st st.title( 'Hello, Streamlit!' ) df = pd.DataFrame( [ { "Sales quantity" : "29,82,55,77,43,43,79,49,36,38,35,36,40,17,49,17,41,77,82,31,25,65,75,89,26" , "compared to previous year" : "0.55921745316719,0.9714691340818024,0.6734865039264561,0.33394622351153125,0.5028502211475531,0.7617577465475921,0.77286463740065,0.033682679655736014,0.6322795679701955,0.8760484895547053,0.0013691360753895765,0.9821578838118802,0.140954801174501,0.7071256599980984,0.9264423841909245,0.3185370631226976,0.9918933376848775,0.8174032338567488,0.09635411006621841,0.49069727888767223,0.6338474730494819,0.2341927475220612,0.30872646565279116,0.3049823957257939,0.5744113520919163" , "is_inventory" : True }, { "Sales quantity" : "48,40,29,65,51,76,84,13,54,42,32,82,87,48,58,12,32,23,35,59,81,14,32,23,55" , "compared to previous year" : "0.01379496881603992,0.9304456267138574,0.5649452520585204,0.743217519503495,0.5244815927017313,0.3769253733048501,0.7889156501012278,0.14654922470745202,0.4618327333075182,0.9563426325645549,0.56168432041344,0.9475055660537064,0.8438223825838812,0.688508906214851,0.06135382730127392,0.22714965316639468,0.5825974535494179,0.9755924493416012,0.7534887059682834,0.986447984406746,0.6995666254050656,0.6427208462549219,0.472087864664831,0.343398064256479,0.0748765861611943" , "is_inventory" : False }, { "Sales quantity" : "10,45,59,40,58,71,84,35,28,14,48,54,44,59,73,76,76,68,11,27,32,79,76,88,81" , "compared to previous year" : "0.7586255927339749,0.45828170652710165,0.38158110883616747,0.915196136270585,0.1959385244077817,0.19832388673897738,0.9760755463739904,0.9002133695121806,0.6216429618172397,0.436692159739706,0.7764328119584399,0.9594371929225926,0.7342877731415655,0.7393656256414073,0.6818084670253696,0.5816767140656959,0.06212698311195286,0.04144959966830952,0.526301682845678,0.5046874024090726,0.3831375575326377,0.32103853715766195,0.22205068697471575,0.982498257713865,0.010069610690874198" , "is_inventory" : True }, ] ) edited_df = st.data_editor( df, column_config = { "Sales quantity" : st.column_config.BarChartColumn( "Sales (last 25 days)" , help = "The sales volume in the last 25 days" , y_min = 0 , y_max = 100 ), "compared to previous year" : st.column_config.AreaChartColumn( "compared to previous year" , width = "medium" , y_min = 0 , y_max = 1 ,), "is_inventory" : st.column_config.CheckboxColumn( "Is it in stock?" , default = False , ), }, disabled = [ "Sales quantity" , "compared" ], hide_index = True , use_container_width = True ) |

st.columns
ボタンを作成する際に👍👎のようにフィードバックボタンを付けたいとき、以下の様に実装したとすると
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | import pandas as pd import streamlit as st st.title( 'Hello, Streamlit!' ) feedback = None if st.button( '👍' ): feedback = 'positive' if st.button( '👎' ): feedback = 'negative' if feedback: st.write(f 'You selected {feedback} feedback.' ) |

このように縦にボタンが並んでしまいます。
この解決としてst.columnsを使用することが出来ます。
ここで作成されたcol1,col2はstreamlit内でウィジットを設定できる空白になっています。stで呼び出している部分を空のウィジットに置き換えるとその位置にボタンが表示されたりといったことが実現できます。
columnsは空白のウィジットを比率のリストもしくは、個数で横並びに作成することが出来ます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | import pandas as pd import streamlit as st st.title( 'Hello, Streamlit!' ) feedback = None col1, col2, _ = st.columns([ 1 , 1 , 8 ]) # 比率を指定 # col1, col2 = st.columns(2) # 個数のみ指定 if col1.button( '👍' ): feedback = 'positive' if col2.button( '👎' ): feedback = 'negative' if feedback: st.write(f 'You selected {feedback} feedback.' ) |

st.empty
ウィジットを操作する上で重要な関数はemptyです。
ここで呼び出したウィジットは空の物になっていて、大きさの値なども持っていません。
どうやって使用するのかというと
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | import pandas as pd import streamlit as st st.title( 'Hello, Streamlit!' ) input = st.empty() output = st.empty() btn = st.empty() message = input .text_input( 'Enter a message:' ) # 入力フォームを作成(st.text_input) if btn.button( 'Submit' ): input .empty() output.text(message) btn.empty() |

文字を入力してみます。

すると、入力フォームとボタンがなくなり、入力した結果だけが出力されました。
あらかじめ作成した空のウィジットをそれぞれ値が入力フォームとボタンに置き換え、ボタンが押された際にはアウトプット用のウィジットへ書き込みとそれぞれのウィジットを空のウィジットへ置き換えています。
このようにボタンが押された場合にボタンを削除するなどの動作をすることで、特定の場合のみ一時的に表示されるボタンや、インプットの結果が入力フォームよりも上で表示されるアプリケーションなどを作成できます。空のウィジットを変数にしておいて後で別のウィジットを呼び出すのはいろいろ使えそうな機能です。
実行した後に押せるボタンが表示されるのが気になったりする方や、UIにこだわる場合、覚えておきましょう。
st.container
containerもウィジットの位置を管理するのに役立ちます。
1 2 3 4 5 6 7 8 9 10 11 12 | import streamlit as st st.title( 'Hello, Streamlit!' ) x = st.slider( 'Slide me' , 0 , 10 ) with st.container(height = 100 ): st.write( "This is inside the container" ) for i in range (x): st.write(i) st.write( "This is outside the container" ) |

実行すると枠が表示されているのが確認できました。これの内部がwith st.containerの内部になっています。スライダーを動かすと数値が増えていきますが増えていくのは範囲内に収まっています。これはheightオプションで高さを固定しているので、スクロールするコンテナを作成できています。

columnsと合わせてこんなこともできます。
1 2 3 4 5 6 7 8 9 10 11 12 | import streamlit as st st.title( 'Hello, Streamlit!' ) x = st.slider( 'Slide me' , 0 , 10 ) rows = [st.columns(x) for i in range (x)] for col in rows: for con in col: tile = con.container(height = 120 ) tile.title( ":balloon:" ) |

st.button(callback)
ボタンを作成するときにこんなコードを書くと挙動がおかしいと感じるときがあるかもしれません。
実際に動かしてみると違和感を覚えるでしょうか?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | import streamlit as st st.title( 'Hello, Streamlit!' ) if 'name' not in st.session_state: st.session_state[ 'name' ] = '' st.write( 'What is your name?' ) st.write( 'My name is' , st.session_state.name) name = st.text_input( 'Name' ) if st.button( 'submit' ): st.session_state.name = name |

ボタンをクリックしても反応しません。しかし、もう一度クリックすると内容が反映されます。これはstreamlitが読み込むたびに上から処理を行っているためです。うまくemptyなどを使用すればこれも回避できますがこのような場合はon_click引数を設定するのが簡単かもしれないです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | import streamlit as st def on_submit(name): st.toast( 'Submitted!' ) st.session_state.name = name st.title( 'Hello, Streamlit!' ) if 'name' not in st.session_state: st.session_state[ 'name' ] = '' st.write( 'What is your name?' ) st.write( 'My name is' , st.session_state.name) name = st.text_input( 'Name' ) st.button( 'submit' , on_click = on_submit, args = [name]) |
入力してからボタンを押すと

すぐに反映されました。これはボタンを押して再実行が入る際にon_clickに設定した関数を最初に呼びだしてから描画が開始されます。そのため、すぐに反映された結果が確認できたという訳です。
ちなみに画面の右上に一定期間だけ表示されたメニューはトーストメニューというらしいです🍞。
st.spinner
長い処理がある場合にユーザへの一時的なメッセージ表示に使用します。
ブロック内の処理がすべて完了するまで表示され続けます。このようなメッセージがあるだけでもユーザ体験の向上が期待できます。何を待っているのかわからないのは辛いですからね。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | import time import streamlit as st def very_long_process(): import time time.sleep( 5 ) return 'done' st.title( 'Hello, Streamlit!' ) if st.button( 'Click me' ): with st.spinner( 'Wait for it...' ): result = very_long_process() st.write( 'Done!' ) time.sleep( 1 ) st.toast( 'Finished!' ) time.sleep( 1 ) st.write( 'Result:' , result) |


st.expander
こちらはコンテナと同じような使い方ができる折り畳みメニューです。
1 2 3 4 5 6 7 8 9 10 11 12 | import streamlit as st st.title( 'Hello, Streamlit!' ) x = st.slider( 'Slide me' , 0 , 10 ) with st.expander( "See more details" ): st.write( "This is inside the expander" ) for i in range (x): st.write(i) st.write( "This is outside the expander" ) |

クリックすると

再度クリックすると元に戻ります。
st.sidebar
st.writeのように記載している部分にsidebarを追加します。するとアプリケーションの左側にサイドバーが表示されます。
サイドバーに表示されるウィジットは通常のアプリと同じような記法ができます。
インタラクティブ性の向上が期待できますね。
1 2 3 4 5 6 7 8 9 10 11 12 13 | import streamlit as st st.title( 'Hello, Streamlit!' ) x = st.sidebar.slider( 'Slide height' , 1 , 10 ) y = st.sidebar.slider( 'Slide width' , 1 , 10 ) rows = [st.columns(y) for i in range (x)] for col in rows: for con in col: tile = con.container(height = 120 ) tile.title( ":balloon:" ) |

また、with記法を用いることもできます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | import streamlit as st st.title( 'Hello, Streamlit!' ) with st.sidebar: x = st.slider( 'Slide height' , 1 , 10 ) y = st.slider( 'Slide width' , 1 , 10 ) rows = [st.columns(y) for i in range (x)] for col in rows: for con in col: tile = con.container(height = 120 ) tile.title( ":balloon:" ) |
st.tabs
切り替え式のタブを作成できます。
疑似的な別ページとしても使えますね。その場合は変数の扱いには注意が必要です。
use_column_widthは自身の使用できる最大サイズまで横幅を広げてくれます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | import streamlit as st def on_submit(name): st.toast( 'Submitted!' ) st.session_state.name = name st.title( 'Hello, Streamlit!' ) tab1, tab2, tab3 = st.tabs([ "Name" , "balloon:balloon:" , "dog" ]) with tab1: if 'name' not in st.session_state: st.session_state[ 'name' ] = '' st.write( 'What is your name?' ) st.write( 'My name is' , st.session_state.name) name = st.text_input( 'Name' ) st.button( 'submit' , on_click = on_submit, args = [name]) with tab2: x = st.slider( 'Slide me' , 0 , 10 ) rows = [st.columns(x) for i in range (x)] for col in rows: for con in col: tile = con.container(height = 120 ) tile.title( ":balloon:" ) with tab3: rows = [st.columns(x) for i in range (x)] for col in rows: for con in col: tile = con.container(height = 120 ) tile.write(name) |

サイドバーにもタブ付けることができますよ。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | import streamlit as st def on_submit(name): st.toast( 'Submitted!' ) st.session_state.name = name st.title( 'Hello, Streamlit!' ) tab1, tab2, tab3 = st.tabs([ "Name" , "balloon:balloon:" , "dog" ]) side_tab1, side_tab2 = st.sidebar.tabs([ "height" , "width" ]) with side_tab1: y = st.slider( 'Slide me' , 1 , 10 , key = 'height' ) x = side_tab2.slider( 'Slide me' , 1 , 10 , key = 'width' ) with tab1: if 'name' not in st.session_state: st.session_state[ 'name' ] = '' st.write( 'What is your name?' ) st.write( 'My name is' , st.session_state.name) name = st.text_input( 'Name' ) st.button( 'submit' , on_click = on_submit, args = [name]) with tab2: rows = [st.columns(x) for i in range (y)] for col in rows: for con in col: tile = con.container(height = 120 ) tile.title( ":balloon:" ) with tab3: rows = [st.columns(x) for i in range (y)] for col in rows: for con in col: tile = con.container(height = 120 ) tile.write(name) |
この時、sliderに与えているキーはそれぞれのウィジットが持つ内部のキーが重複してしまっているエラーを回避するために必要なものです。ボタンなどをforで回すと良く出てくるので、enumrateでkeyに変数値を渡すなどして回避しましょう。

ここで指定したウィジットのキーはst.session_stateに保存されてます。
複雑なアプリケーションを開発すると、何度かkeyの重複に悩まされるかもしれません。処理の中で何度もボタン関数が呼び出されたり、繰り返し処理がある時は確認してみましょう。
1 | st.write(st.session_state) |

まとめ
Streamlitを使うことで、Pythonスクリプトから簡単にインタラクティブなウェブアプリケーションを作成できます。データサイエンスや機械学習のプロジェクトで結果を共有する際に非常に便利です。是非、Streamlitを試してみてください。
ただし、今回までの内容ではローカル環境上でのみ確認できる状態になっており、URLで共有というわけにはいきません。(同一ネットワーク上なら可能ですが)次回の作成の記事ではCloud Runを使用したStreamlitの構築についてご説明したいと思います。次回の記事もぜひご覧ください。
エクスチュアはマーケティングテクノロジーを実践的に利用することで企業のマーケティング活動を支援しています。
ツールの活用にお困りの方はお気軽にお問い合わせください。