【Streamlit】JavaScriptが嫌いだからPythonだけでWebアプリをつくる

フロント（SPA）開発案件2つのプレイングマネージャーと開発リーダーやってますが、JavaScriptが死ぬほど嫌いです。
ブラウザ上で動作するスクリプトなので仕方ないし、async-awaitで大分便利になったけど、非同期処理がやっぱり好きじゃないです。 

JavaとかPythonとかそれなりの期間触った言語は大概「みんな違ってみんないい」みたいな感じになるんですが、JavaScriptだけそうならないので本当に嫌いなんだと思います。

 因みにCSSはもっと嫌いです。

機械学習モデルの構築をPythonで実装することは多いと思いますが、ちょっとしたデモアプリでも作るとなると、フロント側はどうしてもHTML、JavaScript、CSSで組まないといけないです。

Jupyter Notebookも選択肢に入るかもしれませんが、Webアプリと比べると表現の自由度は下がるし、コードセルが見えるのはなんか煩雑に見えます。

嫌いかどうかは置いておいて、フロント開発の煩わしさを抱えている人って結構いるんじゃないかなって思ってます。 ~~・・・いるよね？~~
7月の連休中に触ったStreamlitっていうライブラリが滅茶苦茶便利だったので、今回はこれを使ってPython **"だけ"** でWEBアプリを作ってみようと思います！

## Streamlitとは
Streamlit社が開発したWebアプリケーションフレームワークです。
メジャーなグラフライブラリ（あと、OpenCVも）をサポートしており、[HP](https://www.streamlit.io/)での触れ込みの通り、Pythonだけでデータ可視化アプリや、AIのデモアプリを簡単に構築できます。
コード上にHTMLを埋め込むようなこともありません。

> Streamlit’s open-source app framework is the easiest way for data scientists and machine learning engineers to create beautiful, performant apps in only a few hours!  All in pure Python. All for free.

[リファレンス](https://docs.streamlit.io/en/stable/index.html)が充実していて、使い方を読み始めてからこれから紹介するデモアプリを作るまで（構想込みで）1時間強しかかかりませんでした。

今回は[東京都 新型コロナウイルス感染症対策サイト](https://github.com/tokyo-metropolitan-gov/covid19)のデータを使って日毎の感染者数を可視化します。

## 導入
Streamlitをインストールします。
今回はグラフライブラリにmatplotlibも使うので一緒にインストールしておきます。
```pip install streamlit matplotlib
```
インストールが完了したら、以下のコマンドでデモページが見れるか確認します。
```
streamlit hello
```

[http://localhost:8501/](http://localhost:8501/)でデモページが立ち上がります。     
   
![streamlit_hello.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/355661/0c7310e9-006f-94de-6587-432fd89cd4ca.png)


## テーブルデータの可視化
[日毎の感染者数データ](https://github.com/tokyo-metropolitan-gov/covid19/blob/development/data/daily_positive_detail.json)を取得してデータフレームを表示します。
読み込んだデータをDataFrameに変換して、```st.write```に突っ込むだけでテーブル表示できます。
データ読込など、都度実行したくない処理は```@st.cache```をつけておくとキャッシュされ、2回目以降の実行では引数や外部変数に変更がなければキャッシュを利用します。


```python

import streamlit as st
import pandas as pd
import requests

@st.cache
def get_covid_df(url):
    response_json = requests.get(url).json()
    df = pd.DataFrame(response_json['data'])
    return df

url = 'https://raw.githubusercontent.com/tokyo-metropolitan-gov/covid19/development/data/daily_positive_detail.json'
df_covid = get_covid_df(url)

"""
# 東京都のCOVID-19感染者数
東京都 新型コロナウイルス感染症対策サイトの[Github](https://github.com/tokyo-metropolitan-gov/covid19)からデータを取得
"""

st.write(df_covid)
```

ここまでできたら以下のコマンドでアプリを起動します。

```
streamlit run app.py
```

導入のときと同じく、[http://localhost:8501/](http://localhost:8501/)でアプリが立ち上がります。

<img width="960" alt="01_table.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/355661/7f56a6bb-002c-18f7-cb5b-97f164d03666.png">


昇順、降順でソートなんかもできちゃいます。

<img width="960" alt="02_sorted.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/355661/e60bf802-a118-bd4e-3ca9-a1e0b8dccfe6.png">


## グラフを書いてみる

matplotlibを使ってplotしたグラフを表示します。
基本的にはテーブルデータの時と同じでmatplotlibでグラフを描画した後、figureオブジェクトを```st.write```に突っ込むだけです。

```python

import matplotlib.pyplot as plt
import matplotlib.dates as mdates

"""
# 日毎の感染者数
"""


x = [
    datetime.datetime.strptime(diagnosed_date, '%Y-%m-%d')
    for diagnosed_date in df_covid['diagnosed_date'].values
]
y_count = df_covid['count'].values

fig = plt.figure()
ax = fig.add_subplot(111)
ax.plot(x, y_count)

xfmt = mdates.DateFormatter('%m/%d')
xloc = mdates.DayLocator(interval=20)

ax.xaxis.set_major_locator(xloc)
ax.xaxis.set_major_formatter(xfmt)
st.write(fig)
```

ソースを更新するとコードを再実行するか聞かれるのでRerunを押します。

<img width="960" alt="03_source_change.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/355661/fbd1ad00-8525-b473-4a22-23cac36c57d5.png">


先程、取得したデータのうち、感染者数をplotしたグラフが表示できました。

<img width="960" alt="04_matplotlib.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/355661/486b3714-8c9e-0bb9-55b6-b3bd4da0a923.png">


## WEBUIを使ってインタラクティブなアプリにする
ここまでだと、Jupyterで可視化してるのとあまり差がないのでWEBアプリケーションらしいインタラクションを実装します。

データ取得処理の後に日付の範囲指定をするコンポーネントの表示処理を追加して、データフレームを絞り込みます。
bodyの場合は```st.コンポーネント名```、サイドバーにの場合、```st.sidebar.コンポーネント名```でUIコンポーネントを追加します。

```python

url = 'https://raw.githubusercontent.com/tokyo-metropolitan-gov/covid19/development/data/daily_positive_detail.json'
df_covid = get_covid_df(url)

diagnosed_date_list = df_covid['diagnosed_date'].values
str_maxdate = diagnosed_date_list[len(diagnosed_date_list)-1]
mindate = datetime.datetime.strptime(diagnosed_date_list[0], '%Y-%m-%d')
maxdate = datetime.datetime.strptime(str_maxdate, '%Y-%m-%d')

selected_date = st.sidebar.date_input(
    "表示したい期間を入力してください",
    [mindate, maxdate],
    min_value=mindate,
    max_value=maxdate
)

str_startdate = selected_date[0].strftime('%Y-%m-%d')
str_enddate = selected_date[1].strftime(
    '%Y-%m-%d') if len(selected_date) == 2 else str_maxdate

"""
# 東京都のCOVID-19感染者数
東京都 新型コロナウイルス感染症対策サイトの[Github](https://github.com/tokyo-metropolitan-gov/covid19)からデータを取得
"""

df_selected = df_covid.query(
    f'"{str_startdate}" <= diagnosed_date <= "{str_enddate}"')
st.write(df_selected)
```

Rerunするとサイドバーに日付が書かれたコンポーネントを表示しています。

<img width="960" alt="05_sidebar.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/355661/e16e5d43-a2b4-cab2-df75-aa4ba1e43db1.png">


開始日と終了日を指定することでデータフレームとグラフの内容を絞り込むことができます。

<img width="960" alt="06_date_range.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/355661/abe0072a-f221-3284-c326-045517fd0813.png">


最後に週毎の平均感染者数を表示するかどうかのオプションも追加したいと思います。

```python

"""
# 日毎の感染者数
"""


x = [
    datetime.datetime.strptime(diagnosed_date, '%Y-%m-%d')
    for diagnosed_date in df_selected['diagnosed_date'].values
]
y_count = df_selected['count'].values

fig = plt.figure()
ax = fig.add_subplot(111)
ax.plot(x, y_count)

wac_shown = st.sidebar.checkbox('週毎の平均感染者数も表示する')
if wac_shown:
    y_weekly_average_count = df_selected['weekly_average_count'].values
    ax.plot(x, y_weekly_average_count)

xfmt = mdates.DateFormatter('%m/%d')
xloc = mdates.DayLocator(interval=20)

ax.xaxis.set_major_locator(xloc)
ax.xaxis.set_major_formatter(xfmt)
st.write(fig)
```
Rerunすると、チェックボックスがサイドバーに追加されています。

<img width="960" alt="07_checkbox.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/355661/27b35aab-fe99-1b9c-d08e-572407327630.png">


チェックをつけることで週毎の平均感染者数を追加で描画します。

<img width="960" alt="08_add_average.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/355661/e50cd3ea-abe6-7f73-39a5-1b6c391df135.png">


## 所感
- 本当にPythonだけでWebアプリを作れた
    今回のデモアプリだと70行で実装できてしまいました。
    学習コストも低く、フロント側のコードを一切書かなくていいのは控えめに言って最高でした。   


- 凝ったデザインのアプリは作れない
    フロント側は意識しないのがコンセプトなのでUIの自由度はあまりないです。
    レイアウトもbody + sidebarのシンプルなものしかなく、「ぼくのかんがえたさいきょうのUI」を実装したいなら素直にフロント側のコードを書いた方がいいです。
    ちゃんとしたプロダクトを作るためのものというよりはデモアプリ用のフレームワークという印象です。   

今回はデータの可視化だけでしたが、これを使ってAIのデモアプリとかも作ってみたいなと思いました。