StreamlitでPythonデータ分析ダッシュボードをサクッと作ってAWSにデプロイ

# はじめに
データを可視化して直観的に分析できるダッシュボードを作ってWebアプリとして公開したいです。
でもhtmlやcssやjsは面倒なのでいじりたくないです。
そんなとき、[Streamlit](https://www.streamlit.io/)なら面倒なことを一切抜きにして、Pythonスクリプト1つでデータ分析ダッシュボードを作成することができます。
この記事には、

- アプリ作成の過程（インストール、データ準備、ダッシュボード作成）
- AWS EC2インスタンスにデプロイしてみた手順

を残していきます。
こんな時期なので、題材としては[ヤフー・データソリューション](https://ds.yahoo.co.jp/)で公開されている東京23区内の人の往来が確認できるデータを眺めて、新型コロナ対策の影響を確認してみます。

# アプリ作成
## Streamlitのインストール
StreamlitはOSSです。
GitHub：https://github.com/streamlit/streamlit

インストールは以下でOK。

```shell
pip install streamlit
```

以下のコマンドを実行すると、ローカルでデモアプリを立ち上げてブラウザで確認することができます（ブラウザが勝手に立ち上がります）。

```shell
streamlit hello
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/559696/f39311d5-f0e6-e50b-e37a-8744fbbe538c.png)

使い方については、[公式のチュートリアルページ](https://docs.streamlit.io/index.html)がわかりやすいです。
[こちらの解説記事](https://dev.classmethod.jp/articles/streamlit-intro/)もどうぞ。

【追記】
こちらも参考になります。
- [[Python]簡単に可視化アプリケーションが作れる"Streamlit"をテスト書きした](https://qiita.com/prs-watch/items/ca5589a725c479e8c489)


## データ準備
こんな時期なので、新型コロナ対策が東京都内の人流にどんな影響を与えているのか見てみたくなりますよね。
それっぽいデータを探してみたところ、[ヤフーデータソリューション](https://ds.yahoo.co.jp/)で
「東京23区滞在人口推計値の日別遷移（全体・来訪者・住人）」データが
オープンデータとして公開されていました（2020年4月10日現在）。
今回はこれをいい感じに可視化してみることにします。

【出典：ヤフー・データソリューション（https://ds.yahoo.co.jp/report/, 2020/04/09）】
データは毎日更新されています。ここでは、2020年4月9日までのデータを用いています。

元データはエクセル形式で、日毎の東京23区の住人数・来訪者数・全体数が含まれます。
これが月毎に別々のシートに保存されているので、予めcsvに変換しておきます。  

これをそれぞれ読み込み、一つにまとめます。

```python
import numpy as np
import pandas as pd

data_02 = pd.read_csv('東京23区推移0409_2月.csv')
data_03 = pd.read_csv('東京23区推移0409_3月.csv')
data_04 = pd.read_csv('東京23区推移0409_4月.csv')

data_all = pd.concat([data_02, data_03.iloc[:, 2:], data_04.iloc[:, 2:]], axis=1)
```


```python
data_all.head()
```




<div>
<style scoped>
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>エリア</th>
      <th>対象分類</th>
      <th>2月1日</th>
      <th>2月2日</th>
      <th>2月3日</th>
      <th>2月4日</th>
      <th>2月5日</th>
      <th>2月6日</th>
      <th>2月7日</th>
      <th>2月8日</th>
      <th>...</th>
      <th>3月30日</th>
      <th>3月31日</th>
      <th>4月1日</th>
      <th>4月2日</th>
      <th>4月3日</th>
      <th>4月4日</th>
      <th>4月5日</th>
      <th>4月6日</th>
      <th>4月7日</th>
      <th>4月8日</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>東京23区全体</td>
      <td>全体</td>
      <td>10485000</td>
      <td>10164000</td>
      <td>11676000</td>
      <td>11687000</td>
      <td>11659000</td>
      <td>11690000</td>
      <td>11691000</td>
      <td>10471000</td>
      <td>...</td>
      <td>11393000</td>
      <td>11388000</td>
      <td>11288000</td>
      <td>11263000</td>
      <td>11256000</td>
      <td>10021000</td>
      <td>9737000</td>
      <td>11212000</td>
      <td>11104000</td>
      <td>10859000</td>
    </tr>
    <tr>
      <th>1</th>
      <td>NaN</td>
      <td>住人</td>
      <td>8921000</td>
      <td>8921000</td>
      <td>8921000</td>
      <td>8921000</td>
      <td>8921000</td>
      <td>8921000</td>
      <td>8921000</td>
      <td>8921000</td>
      <td>...</td>
      <td>8949000</td>
      <td>8949000</td>
      <td>8924000</td>
      <td>8924000</td>
      <td>8924000</td>
      <td>8924000</td>
      <td>8924000</td>
      <td>8924000</td>
      <td>8924000</td>
      <td>8924000</td>
    </tr>
    <tr>
      <th>2</th>
      <td>NaN</td>
      <td>来訪者</td>
      <td>1564000</td>
      <td>1243000</td>
      <td>2755000</td>
      <td>2766000</td>
      <td>2738000</td>
      <td>2769000</td>
      <td>2770000</td>
      <td>1550000</td>
      <td>...</td>
      <td>2444000</td>
      <td>2439000</td>
      <td>2364000</td>
      <td>2339000</td>
      <td>2332000</td>
      <td>1097000</td>
      <td>813000</td>
      <td>2288000</td>
      <td>2180000</td>
      <td>1935000</td>
    </tr>
    <tr>
      <th>3</th>
      <td>千代田区</td>
      <td>全体</td>
      <td>454900</td>
      <td>356900</td>
      <td>1028900</td>
      <td>1039900</td>
      <td>1031900</td>
      <td>1043900</td>
      <td>1041900</td>
      <td>453900</td>
      <td>...</td>
      <td>857000</td>
      <td>855000</td>
      <td>819500</td>
      <td>802500</td>
      <td>791500</td>
      <td>266500</td>
      <td>195500</td>
      <td>775500</td>
      <td>731500</td>
      <td>624500</td>
    </tr>
    <tr>
      <th>4</th>
      <td>NaN</td>
      <td>住人</td>
      <td>54900</td>
      <td>54900</td>
      <td>54900</td>
      <td>54900</td>
      <td>54900</td>
      <td>54900</td>
      <td>54900</td>
      <td>54900</td>
      <td>...</td>
      <td>56000</td>
      <td>56000</td>
      <td>55500</td>
      <td>55500</td>
      <td>55500</td>
      <td>55500</td>
      <td>55500</td>
      <td>55500</td>
      <td>55500</td>
      <td>55500</td>
    </tr>
  </tbody>
</table>
</div>

エリア、対象分類をMultiIndexとして整理し、出力します。

```python
data_all.fillna(method='ffill', inplace=True)
data_all.set_index(['エリア', '対象分類'], inplace=True)

data_all.to_csv('tokyo_0409.csv', index=True, header=True)
```

最終的なデータはこんな感じ。

```python
data_all.head(7)
```

<div>
<style scoped>
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th></th>
      <th>2月1日</th>
      <th>2月2日</th>
      <th>2月3日</th>
      <th>2月4日</th>
      <th>2月5日</th>
      <th>2月6日</th>
      <th>2月7日</th>
      <th>2月8日</th>
      <th>2月9日</th>
      <th>2月10日</th>
      <th>...</th>
      <th>3月30日</th>
      <th>3月31日</th>
      <th>4月1日</th>
      <th>4月2日</th>
      <th>4月3日</th>
      <th>4月4日</th>
      <th>4月5日</th>
      <th>4月6日</th>
      <th>4月7日</th>
      <th>4月8日</th>
    </tr>
    <tr>
      <th>エリア</th>
      <th>対象分類</th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th rowspan="3" valign="top">東京23区全体</th>
      <th>全体</th>
      <td>10485000</td>
      <td>10164000</td>
      <td>11676000</td>
      <td>11687000</td>
      <td>11659000</td>
      <td>11690000</td>
      <td>11691000</td>
      <td>10471000</td>
      <td>10149000</td>
      <td>11523000</td>
      <td>...</td>
      <td>11393000</td>
      <td>11388000</td>
      <td>11288000</td>
      <td>11263000</td>
      <td>11256000</td>
      <td>10021000</td>
      <td>9737000</td>
      <td>11212000</td>
      <td>11104000</td>
      <td>10859000</td>
    </tr>
    <tr>
      <th>住人</th>
      <td>8921000</td>
      <td>8921000</td>
      <td>8921000</td>
      <td>8921000</td>
      <td>8921000</td>
      <td>8921000</td>
      <td>8921000</td>
      <td>8921000</td>
      <td>8921000</td>
      <td>8921000</td>
      <td>...</td>
      <td>8949000</td>
      <td>8949000</td>
      <td>8924000</td>
      <td>8924000</td>
      <td>8924000</td>
      <td>8924000</td>
      <td>8924000</td>
      <td>8924000</td>
      <td>8924000</td>
      <td>8924000</td>
    </tr>
    <tr>
      <th>来訪者</th>
      <td>1564000</td>
      <td>1243000</td>
      <td>2755000</td>
      <td>2766000</td>
      <td>2738000</td>
      <td>2769000</td>
      <td>2770000</td>
      <td>1550000</td>
      <td>1228000</td>
      <td>2602000</td>
      <td>...</td>
      <td>2444000</td>
      <td>2439000</td>
      <td>2364000</td>
      <td>2339000</td>
      <td>2332000</td>
      <td>1097000</td>
      <td>813000</td>
      <td>2288000</td>
      <td>2180000</td>
      <td>1935000</td>
    </tr>
    <tr>
      <th rowspan="3" valign="top">千代田区</th>
      <th>全体</th>
      <td>454900</td>
      <td>356900</td>
      <td>1028900</td>
      <td>1039900</td>
      <td>1031900</td>
      <td>1043900</td>
      <td>1041900</td>
      <td>453900</td>
      <td>356900</td>
      <td>958900</td>
      <td>...</td>
      <td>857000</td>
      <td>855000</td>
      <td>819500</td>
      <td>802500</td>
      <td>791500</td>
      <td>266500</td>
      <td>195500</td>
      <td>775500</td>
      <td>731500</td>
      <td>624500</td>
    </tr>
    <tr>
      <th>住人</th>
      <td>54900</td>
      <td>54900</td>
      <td>54900</td>
      <td>54900</td>
      <td>54900</td>
      <td>54900</td>
      <td>54900</td>
      <td>54900</td>
      <td>54900</td>
      <td>54900</td>
      <td>...</td>
      <td>56000</td>
      <td>56000</td>
      <td>55500</td>
      <td>55500</td>
      <td>55500</td>
      <td>55500</td>
      <td>55500</td>
      <td>55500</td>
      <td>55500</td>
      <td>55500</td>
    </tr>
    <tr>
      <th>来訪者</th>
      <td>400000</td>
      <td>302000</td>
      <td>974000</td>
      <td>985000</td>
      <td>977000</td>
      <td>989000</td>
      <td>987000</td>
      <td>399000</td>
      <td>302000</td>
      <td>904000</td>
      <td>...</td>
      <td>801000</td>
      <td>799000</td>
      <td>764000</td>
      <td>747000</td>
      <td>736000</td>
      <td>211000</td>
      <td>140000</td>
      <td>720000</td>
      <td>676000</td>
      <td>569000</td>
    </tr>
    <tr>
      <th>中央区</th>
      <th>全体</th>
      <td>441000</td>
      <td>367000</td>
      <td>849000</td>
      <td>857000</td>
      <td>852000</td>
      <td>861000</td>
      <td>863000</td>
      <td>440000</td>
      <td>370000</td>
      <td>793000</td>
      <td>...</td>
      <td>733000</td>
      <td>728000</td>
      <td>701000</td>
      <td>691000</td>
      <td>684000</td>
      <td>307000</td>
      <td>256000</td>
      <td>675000</td>
      <td>641000</td>
      <td>563000</td>
    </tr>
  </tbody>
</table>
</div>

## ダッシュボード作成
ここから、実際にダッシュボードを作っていきます。
Streamlitでは、ダッシュボードを1つのPythonスクリプトのみで作ることができます。
今回は`streamlit_app.py`としてスクリプトを書いていきます。

データの中身をラインチャートで表示します。
セレクトボックスで対象地域を指定し、その地域の住人・来訪者・全体数の時系列推移を見ることができる仕様にします。

```python:streamlit_app.py
import numpy as np
import pandas as pd
import streamlit as st
import plotly.graph_objects as go

st.title('東京23区滞在人口推計値の日別遷移')
st.write('【出典：ヤフー・データソリューション】')

data_all = pd.read_csv('data/tokyo_0409.csv')
erea_list = data_all['エリア'].unique()

data_all.set_index(['エリア', '対象分類'], inplace=True)

# 値を縦持ちに変更
data_all = data_all.T
# 日付をdatetime型に変換
data_all.index = map(lambda x: '2020年'+x, data_all.index)
data_all.index = pd.to_datetime(data_all.index, format='%Y年%m月%d日')
data_all.index.name = '月日'

# 表示エリアをセレクトボックスで選択
selected_erea = st.sidebar.selectbox(
    '表示するエリアを選択：',
    erea_list
)

# グラフ表示
st.write(f'## 表示中：{selected_erea}')
data_plotly = data_all[(selected_erea)]
data_plot = [
    go.Scatter(x=data_plotly.index,
               y=data_plotly['住人'],
               mode='lines',
               name='住人'),
    go.Scatter(x=data_plotly.index,
               y=data_plotly['来訪者'],
               mode='lines',
               name='来訪者'),
    go.Scatter(x=data_plotly.index,
               y=data_plotly['全体'],
               mode='lines',
               name='全体')]
layout = go.Layout(
    xaxis={"title": "日付"},
    yaxis={"title": "人数"}
)
st.plotly_chart(go.Figure(data=data_plot, layout=layout))


```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/559696/8061a375-995b-6a52-8c08-5a3d5379be39.png)

`streamlit`モジュールの`st.write()`メソッドなどを使って、
画面に表示する文字列やテーブル、グラフを定義していきます。

また、対話的な処理として、`st.selectbox()`で対象地域の選択肢を表示し、
ユーザが選択した値を返り値として`selected_erea`に持つことで、
対応する地域の情報をグラフ描画しています。
`st.sidebar`で画面左側のサイドバーに要素を配置できて、ちょっと見た目がいい感じに。

グラフ表示には、`st.line_chart()`など簡便なメソッドが用意されていますが、
日付等をうまく扱えなさそうだったので、
今回は[Plotly](https://plotly.com/python/)で対話的なグラフを作成してそれを描画する`st.plotly_chart()`を使っています。

その他にStreamlitの特徴的な機能としては、
`st.map()`で地図上にデータをプロットできたり、
`st.progress()`で時間がかかる処理に対してプログレスバーを表示できたりします。
このあたりも使って作り込んでみたかったですが、今回は割愛。

htmlをまったく意識せずに、短いPythonスクリプトだけで画面を作っていけるのは便利ですね。

# AWSにデプロイ
想定されている使い方としては、手元のデータをサクッと確認したり結果をチームで共有したり、
っていうくらいかと思いますが、せっかくなのでAWS EC2インスタンスにデプロイしてテスト公開してみます。

手順：
1: AWS EC2無料枠のt2.microインスタンスを立てる
2: インスタンス内でアプリ実行

```shell
streamlit run streamlit_app.py
```

3: （必要に応じて）ドメイン取得
　今回は無料ドメインサービスの[freenom](https://www.freenom.com/ja/index.html)で適当なドメイン名`onedata.ga`を取得しました。
　（「.tk」、「.ml」、「.ga」、「.cf」、「.gq」ドメインが無料で取得できます）

4: streamlitのconfigファイル編集
`~/.streamlit`フォルダ内にある設定ファイル（`config.toml`）に、
アクセス用のアドレスとして取得したドメイン名を記入しておきます。

```toml
[browser]
gatherUsageStats = false
serverAddress = "onedata.ga"

[server]
port = 8501
```

5: 80番から8501番へポート転送
streamlitはデフォルトでは8501番ポートで通信を受け付けます。
ドメイン名でポート指定せずアクセスするためにデフォルトの80番ポートからアクセスできるようにしたいのですが、
そのためにはアプリ実行時にroot権限が必要になります。
そこでここでは、iptablesをいじって80番ポートへのアクセスを8501番ポートへ転送することで対応します。

```bash
sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 8501
```

6: 80番ポートの開放
EC2ダッシュボードのセキュリティグループ>インバウンドルールを編集し、80番ポートを開放しておきます。


作ったアプリがこちら↓
~~http\://onedata.ga~~
(20230326追記：EC2料金節約のため停止中です）


実装↓
https://github.com/tkmz-n/streamlit_app

# まとめ
Streamlitを使ってオープンデータを可視化する簡単なデモアプリを作成し、
AWSにデプロイして公開するまでをやってみました。

データを見ると、東京23区内の人の往来は3月末あたりから徐々に減りつつあることが確認できます。
新型コロナの一刻も早い収束を目指して、引き続きおうちに籠もりましょう。