【Python】 StreamlitとAltairでダッシュボードを作成する方法

Altair

Pythonベースでインタラクティブなダッシュボードを作成するためのフレームワークの1つにStreamlitがあります。現在最も人気があるフレームワークはDashのようですが、下のグラフのようにGithub上では2020年以降Streamlitの人気が急激に伸びていることが分かります。

StreamlitはDashのように見た目の細かいカスタマイズができないという欠点がありますが、ある程度デフォルトの設定を受け入れることができれば非常に簡単にダッシュボードを作成することができます。今回はStreamlitと可視化ライブラリのAltairを使ってvega_datasetsのcarsデータセットを可視化するダッシュボードを作る過程を解説します。

本記事で紹介しているコードはこちらのGitHub Repositoryに公開しています。

ダッシュボードの完成イメージ

今回使用するcarsデータセットは以下のように自動車の燃費(Miles_per_Gallon)や馬力(Horsepower)、製造年月・地域(Origin)等が格納されたデータセットです。

このデータを使って以下のような、サイドバーで指定した期間や地域に絞って、選択した項目のグラフが表示されるようなダッシュボードを作成します。左上にサイドバーで指定したデータ数を表示した棒グラフ、右上に選択した2つの定量データの散布図を配置します。左下・右下にはそれぞれ選択した定量データの月毎の平均水準の推移を折れ線グラフで配置します。

例えば、直近5年間の米国と日本の自動車の燃費と排出量を比較したいときに、サイドバーで項目を指定するだけで簡単に比較できるようなダッシュボードを作成することを目指します。

モジュールとデータセットのインストール

StreamlitとAltairをインストールします。また可視化するデータを取得するためにvega_datasetsも併せてインストールします。

conda install -c conda-forge streamlit altair vega_datasets

必要なモジュールをインポートします。また今回使用するcarsデータセットを読み込みます。

import altair as alt
import streamlit as st
from vega_datasets import data

# carsデータセットの読み込み
df = data.cars()

サイドバーで設定する項目をリスト化します。また製造年月は年単位で選択できるように西暦を抽出した列を作成し、西暦の最小値と最大値を格納する変数を設定します。

# 定量データ項目のリスト
item_list = [
    col for col in df.columns if df[col].dtype in ['float64', 'int64']]

# 製造地域のリスト
origin_list = list(df['Origin'].unique())

# 西暦列の作成
df['YYYY'] = df['Year'].apply(lambda x: x.year)
min_year = df['YYYY'].min().item()
max_year = df['YYYY'].max().item()

サイドバーのコンポーネントの作成

ダッシュボードのコンポーネントを作成していく前に、スクリーン全体を使ってグラフが表示されるように設定します。 以下のように layout=”wide”と設定しないとグラフが中心によって両サイドに不必要な空白が生まれます。

st.set_page_config(layout="wide")

次にStreamlitの基本的な使い方ですが、Streamlitではst.メソッド名と記載するだけで、ダッシュボードのコンポーネントを簡単に追加することができます。サイドバーに必要なコンポーネントは以下の通りです。

  • Dashboard of Cars Datasetとタイトルを表示
  • Settingsと太字&斜体で表示
  • 製造年の範囲を指定するスライダー
  • 製造地域を複数選択するためのドロップダウンリスト
  • 1つ目の定量データ項目を選択するためのドロップダウンリスト
  • 2つ目の定量データ項目を選択するためのドロップダウンリスト
  • 各コンポーネントに適度な間隔をあける

これらをStreamlitで表現すると次のコードのようになります。

# サイドバー
st.title("Dashboard of Cars Dataset")
st.markdown('###')
st.markdown("### *Settings*")
start_year, end_year = st.slider(
    "Period",
    min_value=min_year, max_value=max_year,
    value=(min_year, max_year))

st.markdown('###')
origins = st.multiselect('Origins', origin_list,
                          default=origin_list)
st.markdown('###')
item1 = st.selectbox('Item 1', item_list, index=0)
item2 = st.selectbox('Item 2', item_list, index=3)

各メソッドは次のような役目を持ちます。

  • st.title:タイトルを表示
  • st.markdown:markdown記法に従った文字列を表示
  • st.markdown(‘###’):見出し3のサイズのスペースを作成
  • st.slider:min_valueとmax_valueで指定できる期間の最小値と最大値を設定。valueで初期値を設定(ここでは全期間がデフォルト)
  • st.multiselect:複数項目選択可能なドロップダウンを表示。第1引数はタイトル、第2引数は選択肢のリスト、第3引数は初期値をそれぞれ設定
  • st.selectbox:ドロップダウンを表示。引数はst.multiselectと同様

スライダーとドロップダウンリストで選択した結果を後段で使用するので、それぞれ変数start_year, end_year, origins, item1, item2に格納します。

次にcarsデータセットから必要な製造年・地域のデータを抽出します。この操作でサイドバーで指定した内容を後段で作成するグラフにインタラクティブに反映します。

df_rng = df[(df['YYYY'] >= start_year) & (df['YYYY'] <= end_year)]
source = df_rng[df_rng['Origin'].isin(origins)]

今までのコードをapp.pyに保存して、terminal上でstreamlit run app.pyと実行すると次の画面がブラウザ上に表示されます。

サイドバーのレイアウト

前節で作成したコンポーネントをサイドバーとして表示するためには、stとメソッド名の間にsidebarを追加します。

# サイドバー
st.sidebar.title("Dashboard of Cars Dataset")
st.sidebar.markdown('###')
st.sidebar.markdown("### *Settings*")
start_year, end_year = st.sidebar.slider(
    "Period",
    min_value=min_year, max_value=max_year,
    value=(min_year, max_year))

st.sidebar.markdown('###')
origins = st.sidebar.multiselect('Origins', origin_list,
                                 default=origin_list)
st.sidebar.markdown('###')
item1 = st.sidebar.selectbox('Item 1', item_list, index=0)
item2 = st.sidebar.selectbox('Item 2', item_list, index=3)

コードを変更すると先ほど立ち上げた画面の右上にRerunボタンが表示されるので、そのボタンを押すと画面が更新されます。

簡単にサイドバーを作成することができました。

コンテンツのグラフの作成

ダッシュボードの右側のコンテンツ部分に表示するグラフをAltairで作成します。

# コンテンツ
base = alt.Chart(source).properties(height=300)

bar = base.mark_bar().encode(
    x=alt.X('count(Origin):Q', title='Number of Records'),
    y=alt.Y('Origin:N', title='Origin'),
    color=alt.Color('Origin:N', legend=None)
)

point = base.mark_circle(size=50).encode(
    x=alt.X(item1 + ':Q', title=item1),
    y=alt.Y(item2 + ':Q', title=item2),
    color=alt.Color('Origin:N', title='',
                    legend=alt.Legend(orient='bottom-left'))
)

line1 = base.mark_line(size=5).encode(
    x=alt.X('yearmonth(Year):T', title='Date'),
    y=alt.Y('mean(' + item1 + '):Q', title=item1),
    color=alt.Color('Origin:N', title='',
                    legend=alt.Legend(orient='bottom-left'))
)

line2 = base.mark_line(size=5).encode(
    x=alt.X('yearmonth(Year):T', title='Date'),
    y=alt.Y('mean(' + item2 + '):Q', title=item2),
    color=alt.Color('Origin:N', title='',
                    legend=alt.Legend(orient='bottom-left'))
)

コンテンツに表示するグラフをそれぞれbar, point, line1, line2に格納したので、次節でレイアウトを設定していきます。Altairの詳しい使い方についてはこちらの記事にまとめました。

コンテンツのレイアウト

Steamlitでは画面全体を任意の数だけ縦に分割するためにカラム数を設定できます。今回はコンテンツ上部の左側に棒グラフ、右側に散布図、下部には折れ線グラフを2つ横に並べるので、以下のようにカラム数を2に設定し、左側をleft_column、右側をright_columnsとします。

# レイアウト (コンテンツ)
left_column, right_column = st.columns(2)

left_columnに左側に表示するコンポーネントを割り当て、right_columnに右側に表示するコンポーネントを割り当てます。コンテンツ部分に表示するコンポーネントは以下の通りです。

  • 左上の棒グラフのタイトルと棒グラフ本体
  • 右上の散布図のタイトルと散布図本体
  • 左下の折れ線グラフのタイトルと折れ線グラフ本体
  • 右下の折れ線グラフのタイトルと折れ線グラフ本体

これらをStreamlitで表現すると以下のコードになります。

left_column.markdown(
    '**Number of Records (' + str(start_year) + '-' + str(end_year) + ')**')
left_column.altair_chart(bar, use_container_width=True)

right_column.markdown(
    '**Scatter Plot of _' + item1 + '_ and _' + item2 + '_**')
right_column.altair_chart(point, use_container_width=True)

left_column.markdown('**_' + item1 + '_ (Monthly Average)**')
left_column.altair_chart(line1, use_container_width=True)

right_column.markdown('**_' + item2 + '_ (Monthly Average)**')
right_column.altair_chart(line2, use_container_width=True)

コード全体を改めて表示します。

import altair as alt
import streamlit as st
from vega_datasets import data

# carsデータセットの読み込み
df = data.cars()

# 定量データ項目のリスト
item_list = [
    col for col in df.columns if df[col].dtype in ['float64', 'int64']]

# 製造地域のリスト
origin_list = list(df['Origin'].unique())

# 西暦列の作成
df['YYYY'] = df['Year'].apply(lambda x: x.year)
min_year = df['YYYY'].min().item()
max_year = df['YYYY'].max().item()

st.set_page_config(layout="wide")

# サイドバー
st.sidebar.title("Dashboard of Cars Dataset")
st.sidebar.markdown('###')
st.sidebar.markdown("### *Settings*")
start_year, end_year = st.sidebar.slider(
    "Period",
    min_value=min_year, max_value=max_year,
    value=(min_year, max_year))

st.sidebar.markdown('###')
origins = st.sidebar.multiselect('Origins', origin_list,
                                 default=origin_list)
st.sidebar.markdown('###')
item1 = st.sidebar.selectbox('Item 1', item_list, index=0)
item2 = st.sidebar.selectbox('Item 2', item_list, index=3)

df_rng = df[(df['YYYY'] >= start_year) & (df['YYYY'] <= end_year)]
source = df_rng[df_rng['Origin'].isin(origins)]

# コンテンツ
base = alt.Chart(source).properties(height=300)

bar = base.mark_bar().encode(
    x=alt.X('count(Origin):Q', title='Number of Records'),
    y=alt.Y('Origin:N', title='Origin'),
    color=alt.Color('Origin:N', legend=None)
)

point = base.mark_circle(size=50).encode(
    x=alt.X(item1 + ':Q', title=item1),
    y=alt.Y(item2 + ':Q', title=item2),
    color=alt.Color('Origin:N', title='',
                    legend=alt.Legend(orient='bottom-left'))
)

line1 = base.mark_line(size=5).encode(
    x=alt.X('yearmonth(Year):T', title='Date'),
    y=alt.Y('mean(' + item1 + '):Q', title=item1),
    color=alt.Color('Origin:N', title='',
                    legend=alt.Legend(orient='bottom-left'))
)

line2 = base.mark_line(size=5).encode(
    x=alt.X('yearmonth(Year):T', title='Date'),
    y=alt.Y('mean(' + item2 + '):Q', title=item2),
    color=alt.Color('Origin:N', title='',
                    legend=alt.Legend(orient='bottom-left'))
)

# レイアウト (コンテンツ)
left_column, right_column = st.columns(2)

left_column.markdown(
    '**Number of Records (' + str(start_year) + '-' + str(end_year) + ')**')
left_column.altair_chart(bar, use_container_width=True)

right_column.markdown(
    '**Scatter Plot of _' + item1 + '_ and _' + item2 + '_**')
right_column.altair_chart(point, use_container_width=True)

left_column.markdown('**_' + item1 + '_ (Monthly Average)**')
left_column.altair_chart(line1, use_container_width=True)

right_column.markdown('**_' + item2 + '_ (Monthly Average)**')
right_column.altair_chart(line2, use_container_width=True)

ブラウザ上でRerunボタンを押し、sliderで期間を1977年から1982年、Originsを米国と日本、item 1をMiles_per_Gallon(燃費)、item 2をDisplacement(排出量)を指定すると以下のようにダッシュボードが変化します。

散布図や折れ線グラフを見ると、1977年-1982年の期間で青色の日本の自動車データはオレンジ色の米国の自動車データと比べて、燃費が良く排出量が少ないことが良く分かります。

これでインタラクティブなダッシュボードが完成しました。Streamlitは見た目の細かなカスタマイズはできないものの、可視化ライブラリのAltairと組み合わせることでの最低限のコーディングで綺麗な見た目のダッシュボードが非常に簡単に作成できることが分かりました。

StreamlitとPlotlyでダッシュボードを作成する方法はこちらです。

また別の記事ではDashとPlotlyでダッシュボードを作成する手順を3回に分けて書いています。Dashは細かいカスタマイズが可能なので、手間を惜しまず理想の形に作りこみたい人にはDashもオススメです。

コメント

タイトルとURLをコピーしました