Streamlitで札幌のオープンデータを可視化する
ゆるWeb勉強会@札幌 Advent Calendar 2020 の22日目の記事です。
Streamlit のお勉強をします。題材は、アドベントカレンダー的に札幌のオープンデータとします。DATA-SMART CITY SAPPOROで公開されているデータを可視化してみました。
Streamlitについて
Streamlitは、PythonだけでWebアプリケーションを構築することが出来るフレームワークです。
- データ分析した結果を、
- フロントエンドの知識無しに、
- スライダー、セレクトボックス等リッチなウィジェットも使えて、
- 素早くグラフ等のかたちで可視化できます
データエンジニア、機械学習エンジニア等が、収集・生成したデータをさくっと可視化してデプロイしてみたい、というときに良いフレームワークだと思いました。
DATA-SMART CITY SAPPORO について
札幌市にまつわるオープンデータが公開されています。
ダッシュボートとして既にいくつか可視化されていますが、テキストデータ(CSV, JSON等)のみの公開も多いです。今回はそれを対象にします。
本記事にはDATA-SMART CITY SAPPOROのデータが含まれたスクリーンショットがあります。今回使用したデータのライセンスは クリエイティブ・コモンズ 表示 4.0 国際 になっています。
環境
- Python 3.9.0
- Streamlit 0.73.0
インストールは簡単です。numpy
やpandas
等も依存で付いてきます。
$ pip install streamlit
ただし、Windows環境の場合、2020/12/19現在の最新であるnumpy==1.19.4
では問題が発生します(参照)。1つバージョンを落とします。
$ pip install numpy==1.19.3
成果物
リポジトリ
さわってみる
Streamlit公式でデプロイ先を用意してくれています。本記事で作ったものはこちらからどうぞ。
手元で実行
streamlitの実行時にWeb上のPythonファイルへのURLを示せば、それを動かすことができます。
$ streamlit run https://raw.githubusercontent.com/shimat/streamlit_sapporo_data/main/population.py
$ streamlit run https://raw.githubusercontent.com/shimat/streamlit_sapporo_data/main/covid.py
$ streamlit run https://raw.githubusercontent.com/shimat/streamlit_sapporo_data/main/chikaho.py
$ streamlit run https://raw.githubusercontent.com/shimat/streamlit_sapporo_data/main/hospital.py
私のものに限らず、ネットに上がっているStreamlitスクリプトのURLを指定すれば手元で動かせます。
その1. 人口
データ
ページを開くと、データエクスプローラ
があって、表形式でデータを見ることができます。Streamlitでこの表をグラフにすることに取り組んでみましょう。
住民基本台帳人口 令和元年(2019年)のWeb上の表形式データ (出典: DATA-SMART CITY SAPPORO)
Graph
のところを押せば、以下のようにもうWeb上でグラフにできちゃったりするのですが、必死に見ないふりをします。
住民基本台帳人口 令和元年(2019年)中央区の人口グラフ (出典: DATA-SMART CITY SAPPORO)
JSONを取得しDataFrameに流す
右上のほうにある データAPI というボタンを押すと、データが取得できるURLを教えてくれます。
このURLからGETして、pandas.DataFrame
に流します。
# population.py
import requests
import pandas as pd
response = requests.get("https://ckan.pf-sapporo.jp/api/action/datastore_search?resource_id=dcb6abdc-a73a-400d-abf0-82dffa5b5d40&limit=12", verify=False)
df = pd.json_normalize(response.json(), record_path=["result", "records"])
df
df
と書いただけですが、これを拾ってStreamlitが表形式で表示してくれます (streamlit.write(df)
も可)。さっそくStreamlitを実行すると、自動的にブラウザが開くはずです。
$ streamlit run population.py
住民基本台帳人口 令和元年(2019年)のStreamlitによる表形式データ (出典: DATA-SMART CITY SAPPORO)
Streamlitを起動後にPythonコードを変更し保存すると、ブラウザの右上に Rerun
Always rerun
というボタン表示が出ます。Always rerun
を押しておけば、以後はコードを保存するだけですぐにブラウザも更新されるので捗ります。
数値が文字列になっているので修正する
これでさっそくグラフ化へ、といきたいのですが、ところで取得したJSONデータの数値はstr
になっています。
{ ... "records": [{"手稲区の女": "75206", "西区の女": "115863", ... }
DataFrameに入れるとobject
になりました。
print(df["手稲区の女"].dtype) # object
このままグラフを作ると意図した結果になりません。intやfloatにするスマートな解決策を探したのですが見つからず、以下のStack Overflowを参考にしました。https://stackoverflow.com/questions/45068797/how-to-convert-string-int-json-into-real-int-with-json-loads/45069099
import json
class MyDecoder(json.JSONDecoder):
def decode(self, s):
result = super().decode(s)
return self._decode(result)
def _decode(self, o):
if isinstance(o, str):
try:
if '.' in o:
return float(o)
return int(o)
except ValueError:
return o
elif isinstance(o, dict):
return {k: self._decode(v) for k, v in o.items()}
elif isinstance(o, list):
return [self._decode(v) for v in o]
else:
return o
以後はこのMyDecoder
を使うことにします。
折れ線グラフにする
streamlit.line_chart
にDataFrameを渡すだけです。
列が多すぎるので、少し絞ってからグラフ化してみます。
import json
import pandas as pd
import requests
import streamlit as st
class MyDecoder(json.JSONDecoder):
... # (省略)
response = requests.get("https://ckan.pf-sapporo.jp/api/action/datastore_search?resource_id=5678d107-d9a4-4f81-8f57-092aac11db5e&limit=100", verify=False)
response_json = MyDecoder().decode(response.text)
df = pd.json_normalize(response_json, record_path=["result", "records"])
view = df.set_index("月")[["手稲区の人口", "清田区の人口"]]
st.line_chart(view)
実につまらないグラフが出来上がりました。
住民基本台帳人口 令和元年(2019年)の手稲区と清田区の人口のグラフ (出典: DATA-SMART CITY SAPPORO)
軸の調整など細かい設定は、不可能ではなさそうですが簡単にはできないようです。このへんにStreamlitを使うべきか否かのユースケースの判断材料が垣間見えます。さっそく微妙な例を出しましたが、気を取り直し次行きましょう。
列を選択できるようにする
次に行く前に便利なTipsのメモです。multiselect
を使うと、列をGUIからインタラクティブに選択できるようになります。
response = requests.get("https://ckan.pf-sapporo.jp/api/action/datastore_search?resource_id=dcb6abdc-a73a-400d-abf0-82dffa5b5d40&limit=12", verify=False)
response_json = MyDecoder().decode(response.text)
df = pd.json_normalize(response_json, record_path=["result", "records"])
df.set_index("月", inplace=True)
df.drop(columns=["_id", "年"], inplace=True)
selected_targets = st.multiselect('select targets', sorted(df.columns))
view = df[selected_targets]
st.line_chart(view)
北区と南区の世帯数を選択したときのグラフ (出典: DATA-SMART CITY SAPPORO)
その2. 新型コロナウイルス陽性患者数
これのニュースを聞かない日は無いと思うホットな話題です。日付と陽性患者数が1対1でシンプルなテーブルです。(先にこっちをやればよかった)
さくっと例示して終わりにします。
import json
import streamlit as st
import pandas as pd
import requests
class MyDecoder(json.JSONDecoder):
... # (省略)
@st.cache
def load_data():
response = requests.get("https://ckan.pf-sapporo.jp/api/action/datastore_search?resource_id=b83606f6-3aa2-4e0c-8a1a-509dd36be2ae&limit=300", verify=False)
response_json = MyDecoder().decode(response.text)
df = pd.json_normalize(response_json, record_path=["result", "records"])
df["日付"] = df["日付"].map(lambda x: re.search(r'\d{4}-\d{2}-\d{2}', x).group())
return df
df = load_data().copy() \
.set_index("日付") \
.drop(columns="_id")
st.write(df)
st.line_chart(df)
下のラベルがやばいですが、これも line_chart
を使う以上はどうにもならなそうです。グラフの形はニュースで見慣れた感じで、11月に急増したのが見て取れます。
札幌市内の新型コロナウイルス(COVID-19)陽性患者数のグラフ (出典: DATA-SMART CITY SAPPORO)
今回の実装にて @st.cache
というのを使いましたが、これによりDataFrameの値をキャッシュしてくれます。再実行のたびにデータソースのWebAPIにリクエストされることを防げます。基本的には積極的に使うべきでしょう。仕様は以下を参照してください。キャッシュする際は、取得したDataFrameを破壊しないようにするのが重要で、破壊の疑いがあるときは警告が出ます。
その3. チカホ人流データ
1アレイでのグラフ
札幌駅前通地下歩行空間(チ・カ・ホ)人流データ 2020年11月
札幌市民の誇りである地下通路の通過人数です。このデータは、表を見るとわかるようにTidy Dataになっています。日時とアレイの2変数によって結果(人流)が定まります。このデータはDATA-SMART CITY SAPPOROのWeb上では単純にグラフ化できないようでした。ようやくStreamlit、というかpandasの面目躍如になりそうです(私のpandas力が低いのでだいぶ厳しいですが)。
例として、アレイJ1
に絞ってグラフ化してみます。
import json
import streamlit as st
import pandas as pd
import requests
class MyDecoder(json.JSONDecoder):
... # (省略)
@st.cache
def load_data():
response = requests.get("https://ckan.pf-sapporo.jp/api/action/datastore_search?resource_id=5678d107-d9a4-4f81-8f57-092aac11db5e&limit=100", verify=False)
response_json = MyDecoder().decode(response.text)
df = pd.json_normalize(response_json, record_path=["result", "records"])
return df
df = load_data().copy()
df_pt = df[df['アレイ'].isin(['J1'])].set_index('日時')[['大通り→札幌', '札幌→大通り']]
st.write(df_pt)
st.line_chart(df_pt)
札幌駅前通地下歩行空間(チ・カ・ホ)人流データとグラフ (出典: DATA-SMART CITY SAPPORO)
ピボットテーブル
以下を参考にすると、ピボットテーブルとして可視化できるみたいです。
多分こんな感じでいけると思いますが、あまり動作芳しくなく、いったん保留です・・・。一応足跡を残しておきます。
$ pip install pivottablejs
import json
import pandas as pd
import requests
import streamlit as st
import streamlit.components.v1 as components
from pivottablejs import pivot_ui
df = load_data().copy() # load_data()は既出の通り。省略。
df.drop(columns=["_id", "補正"], inplace=True)
df_pt = df.set_index(['日時', 'アレイ']).unstack(['アレイ'])
st.write(df_pt)
r = pivot_ui(df)
with open(r.src, encoding="utf-8") as t:
components.html(t.read(), width=800, height=600, scrolling=True)
その4. 医療機関
病院などの医療機関の情報が、緯度経度付きでリストになっています。Streamlitの地図へのプロット機能を試しましょう。
これもぶっちゃけWeb上でできますが、気にせず進めます。
医療機関一覧の地図プロット結果 (出典: DATA-SMART CITY SAPPORO)
ScatterplotLayer
どんなレイヤーが使えるかは、こちらを参照してください。はじめに ScatterplotLayer を使います。
pydeck
を追加で使います。
$ pip install pydeck
レイヤー定義の get_position
と get_fill_color
にて、DataFrameの特定の列を割り当てます。区コード(1101=中央区, 1102=北区, ...)ごとに別々の色を割り当てて、新しい列に入れておきます。
import json
import streamlit as st
import pandas as pd
import pydeck as pdk
import requests
class MyDecoder(json.JSONDecoder):
... # (省略)
@st.cache
def load_data():
response = requests.get("https://ckan.pf-sapporo.jp/api/action/datastore_search?resource_id=f2599ba4-0340-40e1-9735-5516541649f6&limit=3000", verify=False)
response_json = MyDecoder().decode(response.text)
df = pd.json_normalize(response_json, record_path=["result", "records"])
return df
df = load_data().copy() \
.drop(columns=["名称_カナ", "方書", "備考", "市町村名", "電話番号", "都道府県名"])
WARD_COLORS = {
1101: [255, 32, 32, 160],
1102: [64, 128, 64, 160],
1103: [32, 128, 255, 160],
1104: [0, 255, 0, 160],
1105: [0, 0, 255, 160],
1106: [255, 0, 255, 160],
1107: [128, 0, 255, 160],
1108: [255, 0, 128, 160],
1109: [255, 128, 0, 160],
1110: [139, 69, 19, 160],
}
df["ward_color"] = df["区コード"].apply(lambda x: WARD_COLORS[x])
st.pydeck_chart(pdk.Deck(
map_style='mapbox://styles/mapbox/streets-v11',
initial_view_state=pdk.ViewState(
latitude=43.05,
longitude=141.35,
zoom=10.5,
pitch=50,
),
layers=[
pdk.Layer(
'ScatterplotLayer',
data=df,
get_position='[経度, 緯度]',
get_fill_color="ward_color",
get_radius=100,
),
],
))
医療機関の区ごとの地図プロット結果 (出典: DATA-SMART CITY SAPPORO)
HeatmapLayer
続いて HeatmapLayer を使う例です。1つ目は医療機関の位置によるヒートマップ、2つ目が病床数によるヒートマップです。get_weight
を自分で指定するかどうかで変わります。
df
はScatterplotLayerの例と同じですので省略しています。
st.pydeck_chart(pdk.Deck(
map_style='mapbox://styles/mapbox/light-v10',
initial_view_state=pdk.ViewState(
latitude=43.05,
longitude=141.35,
zoom=10.5,
pitch=50,
),
layers=[
pdk.Layer(
'HeatmapLayer',
data=df,
get_position='[経度, 緯度]',
radius=200,
elevation_scale=4,
elevation_range=[0, 1000]
)
],
))
st.pydeck_chart(pdk.Deck(
map_style='mapbox://styles/mapbox/light-v10',
initial_view_state=pdk.ViewState(
latitude=43.05,
longitude=141.35,
zoom=10.5,
pitch=50,
),
layers=[
pdk.Layer(
'HeatmapLayer',
data=df,
get_position='[経度, 緯度]',
get_weight="病床数",
opacity=0.8,
cell_size_pixels=15,
elevation_scale=4,
elevation_range=[0, 1000]
)
],
))
医療機関の位置によるヒートマップです。下がズームしたところで、順当に大通・札幌駅近辺が赤いですね。
医療機関の位置によるヒートマップ地図 (出典: DATA-SMART CITY SAPPORO)
医療機関の位置によるヒートマップ地図[中心部にズーム] (出典: DATA-SMART CITY SAPPORO)
続いて病床数です。下がズームしたところです。札幌医大付属病院は病床数2位ですが、近辺にはNTT東日本札幌病院・中村記念病院という大病院がひしめいており、遠目では合わさって最も赤くなります。
医療機関の病床数によるヒートマップ地図 (出典: DATA-SMART CITY SAPPORO)
医療機関の病床数によるヒートマップ地図[札医大・市立札幌病院・北大病院を俯瞰] (出典: DATA-SMART CITY SAPPORO)
デプロイ
一般公開して問題ないデータであれば、 https://share.streamlit.io/ にデプロイすることができます。
申し込んで数日でメールが来ると思います。GitHubリポジトリ、デプロイする単一.pyファイルを指定するだけで、30秒くらいで完了します。
まとめ・感想
- Streamlitで、手元のデータ(主に
pd.DataFrame
)をほんの10行前後で可視化してWebアプリケーションにできます。JSONのデシリアライズが長くなってしまいましたが、実装の短さは特徴的で、データサイエンティスト等が使うのに適している感触があります。- 凝ったことをしようとするとかなり早期につまづく印象で、フロントエンド (主にJavaScriptの各種可視化ライブラリや、そのPythonラッパ) の知識から逃れられはしません。凝らない前提と割り切ったり、デプロイのしやすさを良いと感じるなら、有用だと思いました。
-
streamlit run <URL>
でWeb上のスクリプトを動かせることを考えると、単一ファイルで全て実装するのがStreamlit流かと思われます。もしコードが長くなって厳しくなってきたとすればStreamlitを卒業する潮時かもしれません。実装が短くて済む範囲において使い倒すのが醍醐味に思えます。
- 何か面白い見た目のグラフが作れるだろうと思っていたのですが、素人故かそこまでパンチのきいた例が作れませんでした。他のデータでも試したいですね。
Discussion