Streamlit Elements を使ってドラッグ・サイズ変更可能なダッシュボードを作ってみた
この記事は、Retty Advent Calendar 2023、7日目の記事です🎄🎁 (1日遅れての投稿です > <)
サマリ
-
streamlit-elementsを使うと、ドラッグ・サイズ変換可能なダッシュボードを作ることができる
-
Streamlitだと、データの集計対象の絞り込み・表示/非表示の出し分けのコントロールも行いやすい
-
実装した挙動はこんな感じ↓実際の挙動
※本記事の細かい実装内容はhttps://github.com/Igecchi/bq_streamlit/をご参照ください -
↓試せるようにしました! ※ダークモードの場合、一部表示崩れがありますmm
📊こちらからアクセス📊
Streamlitとは?
Streamlitは、Pythonで実装されたオープンソースのWebアプリケーションのフレームワークです。
このフレームワークを使うことで、フロントエンドの経験がなくともPythonのみで、機械学習やデータの可視化を行ったWebアプリを作成することができます。
直近では、Snowflake上でStreamlitアプリをデプロイできるようになり、小耳にはさむ機会も増えています。
なおStreamlitの基本的な使い方に関しては、他の記事を探してみてください!
[参考になりそうな記事]
・30 Days of Streamlit
・Streamlit入門+応用 ~ データ分析Webアプリを爆速で開発する
・Streamlit documentation(公式ドキュメント)
・Streamlit | Snowflake Documentation
データの準備
まずは、Streamlitアプリでチャート作成するために必要なデータの準備から進めていきます。
サンプルデータの用意
今回は、BigQueryの公開データであるcovid19_open_dataを絞り込んで利用しました。
都度BQにクエリ発行したくなかったため、日本のデータに絞り込みcsvとしてデータを取り込みました。(抽出したデータはこちらからDLできます。)
ちなみにデータ取得に用いたクエリは下記です↓
select
date
, location_key
, country_name
, subregion1_name
, subregion2_name
, population
, population_male
, population_female
, cumulative_confirmed
, cumulative_deceased
, cumulative_recovered
from `bigquery-public-data.covid19_open_data.covid19_open_data`
where country_name = 'Japan'
必要なパッケージのインストール
streamlitおよび、今回利用するstreamlit-elementsのパッケージをインストールします。
自分の環境では、下記のバージョンを利用しました。(※Pythonのバージョンは3.10.10)
pip install streamlit==1.27
pip install streamlit-elements==0.1
アプリケーションの作成
フォルダ構成
開発するコードを記載するstreamlit_app.py
と、読み込むデータを格納しているdata/test_covid19_analysis_japan.csv
から構成されています。
※ファイル名は任意のものを設定可能です。
streamlit_app.py
data
└─ test_covid19_analysis_japan.csv
コードの実装
実装するコードの全体像は下記となります。
コードの全体像
## ----- Import Library(必要なライブラリのインポート) ----- ##
import pandas as pd
import streamlit as st
from datetime import datetime
from streamlit_elements import elements, dashboard, mui, nivo
from streamlit_elements import dashboard
## ----- Load Data & Process(データの読み込みと加工)----- ##
# Read csv file(csvの読み込み)
df = pd.read_csv('data/test_covid19_analysis_japan.csv', index_col=0)
df_dataset = df.reset_index(drop=True) # Reset index(indexの振り直し)
# Filter dataset by Japan and extract prefecture name list
# (データを日本に絞り込み、都道府県リストを抽出する)
df_dataset = df_dataset[(df_dataset['country_name']=='Japan')]
df_dataset = df_dataset.rename({'subregion1_name': 'prefecture_name'}, axis='columns')
prefecture_name_list = df_dataset['prefecture_name'].unique()
column_list = df_dataset.columns.to_list()
## Fill NaN with 0(null値を0埋めする)
df_dataset = df_dataset.fillna(0)
df_dataset_all = df_dataset.copy()
## ----- Sidebar(サイドバーの設定) ----- ##
## --- Input box(データ絞り込み用の入力欄の作成) --- ##
## Input box of prefecture_name(複数都道府県で絞り込みを行うための入力Boxの作成)
prefecture_name = st.sidebar.multiselect(
'Prefecutre Name'
, prefecture_name_list
, default=['Tokyo']
)
## Input box of Start day(集計期間の起点となる日付)
start_date = pd.to_datetime(st.sidebar.date_input('Start date', datetime(2020, 3, 1)))
## Input box of End day(集計期間の終点となる日付)
end_date = pd.to_datetime(st.sidebar.date_input('End date', datetime(2021, 12, 31)))
st.sidebar.write('------------------')
## --- Check box(グラフ・表の表示/非表示を出し分けるためのチェックボックス作成) --- ##
st.sidebar.write('Graph Check Box')
is_graph_active_confirmed = st.sidebar.checkbox('Show Confirmed Graph', value=True)
is_graph_active_deceased = st.sidebar.checkbox('Show Deceased Graph', value=True)
# is_graph_active_recovered = st.sidebar.checkbox('Show Recovered Graph', value=True)
# is_graph_active_pupulation = st.sidebar.checkbox('Show Pupulation Graph', value=True)
is_graph_active_male_pupulation = st.sidebar.checkbox('Show Male Pupulation Graph', value=True)
is_graph_active_female_pupulation = st.sidebar.checkbox('Show Female Pupulation Graph', value=True)
st.sidebar.write('------------------')
st.sidebar.write('Table Column Check Box')
is_table_active = st.sidebar.checkbox('Show Table', value=True)
column_list = st.sidebar.multiselect(
'Show Table'
, column_list
, default=['country_name', 'prefecture_name', 'population', 'population_male', 'population_female', 'cumulative_confirmed', 'cumulative_deceased', 'cumulative_recovered']
)
## ----- Dataset processing(データセットの加工・絞り込み) ----- ##
## -------- Dataset processing(データセットの加工・絞り込み) -------- ##
df_dataset['date'] = pd.to_datetime(df_dataset['date'])
# Filter dataset by first day of month(月初日のみ抽出)
df_dataset = df_dataset[(df_dataset['date'].dt.day == 1)]
## Filter dataset by selected prefecture_name&term(サイドバーで選択された都道府県・集計期間に絞り込み)
df_dataset = df_dataset[(df_dataset['prefecture_name'].isin(prefecture_name)) & (df_dataset['date'] >= start_date) & (df_dataset['date'] <= end_date)]
# Change date type to str(日付型だと想定通りの可視化ができなかったため、文字列型に変換)
df_dataset['date'] = df_dataset['date'].astype(str)
## duplicate dataset 1.for graph, 2.for table
df_dataset_graph = df_dataset.copy()
df_dataset_table = df_dataset.copy()
### -------- Graph Visualization setting(グラフの可視化を行うための設定) -------- ###
tmp = df_dataset_graph.groupby(['date']).sum()[['cumulative_confirmed']]
tmp['date'] = tmp.index
tmp = tmp.rename(columns={'date': 'x', 'cumulative_confirmed': 'y'})[['x', 'y']].to_json(orient='records')
tmp_data = [
{
"id": prefecture_name,
"data": eval(tmp)
}
]
## If you want to show detail data, you can use this code.
# st.write(tmp_data)
def create_data(y_data):
tmp = df_dataset_graph.groupby(['date']).sum()[[y_data]]
tmp['date'] = tmp.index
tmp = tmp.rename(columns={'date': 'x', y_data: 'y'})[['x', 'y']].to_json(orient='records')
return [
{
"id": "+".join(prefecture_name),
"data": eval(tmp)
}
]
def create_chart(KEYNAME, CARD_TITLE, INPUT_DATA):
with mui.Card(key=KEYNAME, sx={"display": "flex", "flexDirection": "column"}):
mui.CardHeader(title=CARD_TITLE, className="draggable")
with mui.CardContent(sx={"flex": 1, "minHeight": 0}):
nivo.Line(
data=INPUT_DATA,
margin={ 'top': 10, 'right': 80, 'bottom': 90, 'left': 80 },
xScale={
'type': 'point',
'min': 'auto',
'max': 'auto',
'stacked': False,
'reverse': False
},
yScale={
'type': 'linear',
'min': 'auto',
'max': 'auto',
'stacked': True,
'reverse': False
},
yFormat=" >-,.2~d",
axisTop=None,
axisRight=None,
axisBottom={
'tickSize': 1,
'tickPadding': 1,
'tickRotation': -70,
'legend': '日付',
'legendOffset': 80,
'legendPosition': 'middle'
},
axisLeft={
'tickSize': 3,
'tickPadding': 3,
'tickRotation': 0,
'legend': 'count',
'legendOffset': -75,
'legendPosition': 'middle'
},
enableGridX=False,
enableGridY=False,
enablePoints=False,
pointSize=2,
pointColor={ 'theme': 'background' },
pointBorderWidth=1,
pointBorderColor={ 'from': 'serieColor' },
pointLabelYOffset=-7,
useMesh=True,
# -- 凡例の設定 -- #
legends=[
{
'anchor': 'top-left',
'direction': 'column',
'justify': False,
'translateX': 15,
'translateY': 0,
'itemsSpacing': 0,
'itemDirection': 'left-to-right',
'itemWidth': 80,
'itemHeight': 10,
'itemOpacity': 0.75,
'symbolSize': 7,
'symbolShape': 'circle',
'symbolBorderColor': 'rgba(0, 0, 0, .5)',
'effects': [
{
'on': 'hover',
'style': {
'itemBackground': 'rgba(0, 0, 0, .03)',
'itemOpacity': 1
}
}
]
}
]
)
### ----- Graph Visualization(グラフの可視化) ----- ###
with elements("dashboard"):
# default layout setting(デフォルトレイアウトの設定)
layout = [
# Parameters: element_identifier, x_pos, y_pos, width, height, [item properties...]
dashboard.Item("confirmed_chart", 0, 0, 6, 3.5),
dashboard.Item("deceased_chart", 6, 0, 6, 3.5),
dashboard.Item("male_pupulation_chart", 0, 0, 6, 3.5),
dashboard.Item("female_pupulation_chart", 6, 0, 6, 3.5),
]
# IF check box is ON, show graph.(サイドバーでチェックボックスがONになっている場合のみグラフを表示)
with dashboard.Grid(layout, draggableHandle=".draggable"):
if is_graph_active_confirmed:
dataset_graph_confirmed = create_data(y_data='cumulative_confirmed')
create_chart(KEYNAME="confirmed_chart", CARD_TITLE="Confirmed Chart", INPUT_DATA=dataset_graph_confirmed)
if is_graph_active_deceased:
dataset_graph_deceased = create_data(y_data='cumulative_deceased')
create_chart(KEYNAME="deceased_chart", CARD_TITLE="Deceased Chart", INPUT_DATA=dataset_graph_deceased)
if is_graph_active_male_pupulation:
dataset_graph_population_male = create_data(y_data='population_male')
create_chart(KEYNAME="male_pupulation_chart", CARD_TITLE="Male Pupulation Chart", INPUT_DATA=dataset_graph_population_male)
if is_graph_active_female_pupulation:
dataset_graph_population_female = create_data(y_data='population_female')
create_chart(KEYNAME="female_pupulation_chart", CARD_TITLE="Female Pupulation Chart", INPUT_DATA=dataset_graph_population_female)
### ----- Table Visualization(表の可視化) ----- ###
## Check box what index to show(Table)
if is_table_active:
## Show result as a table with scroll bar.
st.dataframe(df_dataset_table[df_dataset_table['prefecture_name'].isin(prefecture_name)].groupby(['date']).sum()[column_list].T)
## ----- SQL Editor(SQLエディタの作成) ----- ##
conn = sqlite3.connect('data.db')
conn.commit()
df_dataset_table.to_sql('my_table', conn, if_exists='replace', index=False)
sql_editor_md = """
## SQL Editor
You can use SQL Editor.
Sample Query:
```sql
select * from my_table;
``` ``` ← 実装時は「```」のみとしてくださいmm
### ↓Enter your SQL query below
"""
st.markdown(sql_editor_md)
query = st.text_input('※Table Name is `my_table`')
if query:
results = pd.read_sql_query(query, conn)
st.write(results)
コードの解説
コードとUIの対応について画像を交えて説明します。
サイドバー(データの集計対象を絞り込む)
今回の実装では、サイドバーで選択した項目でデータのフィルタリングを行う方針としています。
後のコードにて、選択された値でフィルターをかけています。
サイドバー(都道府県と期間の指定)
## ----- Sidebar(サイドバーの設定) ----- ##
## --- Input box(データ絞り込み用の入力欄の作成) --- ##
## Input box of prefecture_name(複数都道府県で絞り込みを行うための入力Boxの作成)
prefecture_name = st.sidebar.multiselect(
'Prefecutre Name'
, prefecture_name_list
, default=['Tokyo']
)
## Input box of Start day(集計期間の起点となる日付)
start_date = pd.to_datetime(st.sidebar.date_input('Start date', datetime(2020, 3, 1)))
## Input box of End day(集計期間の終点となる日付)
end_date = pd.to_datetime(st.sidebar.date_input('End date', datetime(2021, 12, 31)))
st.sidebar.write('------------------')
サイドバー(グラフ・表の表示/非表示を出し分ける)
続いてグラフ・表の表示/非表示を出し分けるために、サイドバーにチェックボックスを作成します。こちらも後のコードにて、チェックボックスを入力されて得られたtrue/falseの値を元に、表示/非表示を出し分けるよう実装しています。
グラフの表示/非表示の出し分け
グラフの表示/非表示の出し分け
## --- Check box(グラフ・表の表示/非表示を出し分けるためのチェックボックス作成) --- ##
st.sidebar.write('Graph Check Box')
is_graph_active_confirmed = st.sidebar.checkbox('Show Confirmed Graph', value=True)
is_graph_active_deceased = st.sidebar.checkbox('Show Deceased Graph', value=True)
# is_graph_active_recovered = st.sidebar.checkbox('Show Recovered Graph', value=True)
# is_graph_active_pupulation = st.sidebar.checkbox('Show Pupulation Graph', value=True)
is_graph_active_male_pupulation = st.sidebar.checkbox('Show Male Pupulation Graph', value=True)
is_graph_active_female_pupulation = st.sidebar.checkbox('Show Female Pupulation Graph', value=True)
表の表示/非表示の出し分け
st.sidebar.write('------------------')
st.sidebar.write('Table Column Check Box')
is_table_active = st.sidebar.checkbox('Show Table', value=True)
column_list = st.sidebar.multiselect(
'Show Table'
, column_list
, default=['country_name', 'prefecture_name', 'population', 'population_male', 'population_female', 'cumulative_confirmed', 'cumulative_deceased', 'cumulative_recovered']
)
データセットの加工
pandasを用いて、データセットの加工・絞り込みを行います。
prefecture_name
, start_date
, end_date
は、前述のサイドバーで入力された変数が入ります。
## -------- Dataset processing(データセットの加工・絞り込み) -------- ##
df_dataset['date'] = pd.to_datetime(df_dataset['date'])
# Filter dataset by first day of month(月初日のみ抽出)
df_dataset = df_dataset[(df_dataset['date'].dt.day == 1)]
## Filter dataset by selected prefecture_name&term(サイドバーで選択された都道府県・集計期間に絞り込み)
df_dataset = df_dataset[(df_dataset['prefecture_name'].isin(prefecture_name)) & (df_dataset['date'] >= start_date) & (df_dataset['date'] <= end_date)]
# Change date type to str(日付型だと想定通りの可視化ができなかったため、文字列型に変換)
df_dataset['date'] = df_dataset['date'].astype(str)
## duplicate dataset 1.for graph, 2.for table
df_dataset_graph = df_dataset.copy()
df_dataset_table = df_dataset.copy()
グラフの可視化を行うための設定
今回はドラッグ・サイズ変更可能なグラフを作成したかったため、streamlit-elementsを利用しています。
現状のstreamlit-elementsでは、Reactのチャートライブラリであるnivoが採用されています。
※現状のstreamlit-elementsでは、streamlit標準のチャートをドラッグ・サイズ変更可能なコンポーネントに変更することはできないようでした。
したがって、nivoで必要とされているデータフォーマットへの修正とチャートの設定をここで行います。(設定については、Line chart | nivoをご覧ください。)
コード(長いため折りたたんでいます)
### -------- Graph Visualization setting(グラフの可視化を行うための設定) -------- ###
tmp = df_dataset_graph.groupby(['date']).sum()[['cumulative_confirmed']]
tmp['date'] = tmp.index
tmp = tmp.rename(columns={'date': 'x', 'cumulative_confirmed': 'y'})[['x', 'y']].to_json(orient='records')
tmp_data = [
{
"id": prefecture_name,
"data": eval(tmp)
}
]
## If you want to show detail data, you can use this code.
# st.write(tmp_data)
def create_data(y_data):
tmp = df_dataset_graph.groupby(['date']).sum()[[y_data]]
tmp['date'] = tmp.index
tmp = tmp.rename(columns={'date': 'x', y_data: 'y'})[['x', 'y']].to_json(orient='records')
return [
{
"id": "+".join(prefecture_name),
"data": eval(tmp)
}
]
def create_chart(KEYNAME, CARD_TITLE, INPUT_DATA):
with mui.Card(key=KEYNAME, sx={"display": "flex", "flexDirection": "column"}):
mui.CardHeader(title=CARD_TITLE, className="draggable")
with mui.CardContent(sx={"flex": 1, "minHeight": 0}):
nivo.Line(
data=INPUT_DATA,
margin={ 'top': 10, 'right': 80, 'bottom': 90, 'left': 80 },
xScale={
'type': 'point',
'min': 'auto',
'max': 'auto',
'stacked': False,
'reverse': False
},
yScale={
'type': 'linear',
'min': 'auto',
'max': 'auto',
'stacked': True,
'reverse': False
},
yFormat=" >-,.2~d",
axisTop=None,
axisRight=None,
axisBottom={
'tickSize': 1,
'tickPadding': 1,
'tickRotation': -70,
'legend': '日付',
'legendOffset': 80,
'legendPosition': 'middle'
},
axisLeft={
'tickSize': 3,
'tickPadding': 3,
'tickRotation': 0,
'legend': 'count',
'legendOffset': -75,
'legendPosition': 'middle'
},
enableGridX=False,
enableGridY=False,
enablePoints=False,
pointSize=2,
pointColor={ 'theme': 'background' },
pointBorderWidth=1,
pointBorderColor={ 'from': 'serieColor' },
pointLabelYOffset=-7,
useMesh=True,
# -- 凡例の設定 -- #
legends=[
{
'anchor': 'top-left',
'direction': 'column',
'justify': False,
'translateX': 15,
'translateY': 0,
'itemsSpacing': 0,
'itemDirection': 'left-to-right',
'itemWidth': 80,
'itemHeight': 10,
'itemOpacity': 0.75,
'symbolSize': 7,
'symbolShape': 'circle',
'symbolBorderColor': 'rgba(0, 0, 0, .5)',
'effects': [
{
'on': 'hover',
'style': {
'itemBackground': 'rgba(0, 0, 0, .03)',
'itemOpacity': 1
}
}
]
}
]
)
表の可視化
サイドバーで設定した内容をもとに表の表示/非表示・表示する行の絞り込みを行います。
### ----- Table Visualization(表の可視化) ----- ###
## Check box what index to show(Table)
if is_table_active:
## Show result as a table with scroll bar.
st.dataframe(df_dataset_table[df_dataset_table['prefecture_name'].isin(prefecture_name)].groupby(['date']).sum()[column_list].T)
SQLエディタの作成
StreamlitではBigQueryやSnowflakeのようなSQLエディタを、GUI上に設定することもできるようです。
SQLエディタ
## ----- SQL Editor(SQLエディタの作成) ----- ##
conn = sqlite3.connect('data.db')
conn.commit()
df_dataset_table.to_sql('my_table', conn, if_exists='replace', index=False)
sql_editor_md = """
## SQL Editor
You can use SQL Editor.
Sample Query:
```sql
select * from my_table;
``` ``` ← 実装時は「```」のみとしてくださいmm
### ↓Enter your SQL query below
"""
st.markdown(sql_editor_md)
query = st.text_input('※Table Name is `my_table`')
if query:
results = pd.read_sql_query(query, conn)
st.write(results)
所感
Streamlit(+nivo)を用いることで、BIツールのように利用者がグラフや表のドラッグ、サイズ変更、表示/非表示の切り替えを行えるダッシュボードを作成できました。
今回の実装だと、データ可視化の部分はstreamlitではなくReactのチャートライブラリであるnivoに依存しているため、とっつきにくさや保守・運用観点での課題がありそうと感じました。
他のもっといい手段をご存知の方いれば、ぜひぜひコメント、DMいただけると嬉しいです!
Discussion