東京都の新型コロナウイルス陽性者数をSlackに投稿するbotをつくる

東京都が、東京都における新型コロナウイルス（COVID-19）の陽性者数を日々公開しています。
このデータをSlackに自動投稿したいと考え、ソースコードを作成しました。

## できたもの
[東京都の新型コロナウイルス感染症対策サイト][1]のデータを取得・整理・プロットし、Slackに投稿します。
![c19bot.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/600317/d9e52694-873e-9e27-e5ec-02018ae2e915.png)

[1]:https://stopcovid19.metro.tokyo.lg.jp/

## 環境

+ Win10 bash on windows
+ Python 3.5.2

SlackbotのTokenをあらかじめ用意しておく必要がありますが、本記事ではTokenの発行方法については省略します。

## 投稿方法
### 概要

`python3 run.py`を実行することで、botがコンテンツをSlackへ投稿するように実装しました。

`run.py`の中身はこのようになっています。

``` python
import plot
import post

def main():
    df = plot.fetch_csv() # データ取得
    plot.plot_hist(df) # グラフ描画
    txt = plot.info_str(df) # 最新の陽性者数カウント
    post.post_message(txt) # 最新の陽性者数を投稿
    post.upload_figure("", "") # グラフを投稿
 
if __name__ == "__main__":
    main()
```

`run.py`と同じディレクトリに`post.py`と`plot.py`を用意しました。

### データ取得
`plot.fetch_csv()`の処理です。
[東京都の公開データ][2]を取得しています。

``` python
import requests
import pandas as pd

URL = "https://stopcovid19.metro.tokyo.lg.jp/"
def fetch_csv():
    r = requests.get(URL+"data/130001_tokyo_covid19_patients.csv")
    df = pd.read_csv(io.BytesIO(r.content),sep=",")
    return df
```

[2]:https://catalog.data.metro.tokyo.lg.jp/dataset/t000010d0000000068

### グラフ描画（pandas, matplotlib）

`plot.plot_hist(df)`の処理です。
取得したデータを用いてpandasでヒストグラムを描画します。

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

PNG = "hist"

def plot_hist(df):
    # 日付列のみをTimestampで抽出
    df = pd.to_datetime(df.loc[:, "公表_年月日"])
    # bins(トータルの日数)を計算
    num_bins = calc_num_days(df)
    # 上下に分割
    fig, axes = plt.subplots(nrows=2, ncols=1, sharex=True)
    df.hist(ax=axes[0], bins=num_bins)
    axes[0].set_title("日別", fontproperties=FP)
    df.hist(ax=axes[1], bins=num_bins, cumulative=True)
    plt.title("累積", fontproperties=FP)
    plt.suptitle("東京都 COVID-19 陽性者数(データ元 "+URL+")", fontproperties=FP)
    # x軸の刻み幅を1週間に設定
    plt.gca().get_xaxis().set_major_formatter(mdates.DateFormatter("%y/%m/%d"))
    plt.gca().get_xaxis().set_major_locator(mdates.DayLocator(interval=7))
    plt.xticks(rotation=90) # x軸ラベルを90度回転
    plt.savefig(PNG, bbox_inches="tight")
```

`calc_num_days(df)`で、ヒストグラムのbinsを計算しています。
取得データ（csv）で、ヘッダー行を除いて1行目が初めて陽性者数を確認した日の患者データ、末尾行が最新の患者データであるという前提で書いています。

```python
def calc_num_days(df):
    s = df.iloc[0] # 1行目の1列目
    e = df.iloc[-1] # 末尾行目の1列目
    delta_days = e - s
    return delta_days.days + 1
```
グラフのタイトルに日本語フォントを使うため、`plt.title("累積", fontproperties=FP)`のように`fontproperties`を指定しています。

```python
FONT_TTF = "/home/[ユーザ名]/.local/lib/python3.5/site-packages/matplotlib/mpl-data/fonts/ttf/ipaexg.ttf"
FP = FontProperties(fname=FONT_TTF)
```
環境によってはわざわざ`fontproperties`を指定せずとも日本語フォントが使用できると思います。

ちなみに、元々はヒストグラムでなく棒グラフで描画したいと考えていましたが、うまくいきませんでした。
うまくいかなかった点は、横軸を時間軸でなくラベルとして扱ってしまい、陽性者数が0の日がうまく描画できなかったことです。
陽性者数が0の日のラベルを作ればよいかと考えたのですが、それよりもヒストグラムとして描画するほうが楽かなと考え、ヒストグラムで描画しました。
よりスマートな方法があれば教えていただけると嬉しいです。

### 最新の陽性者数カウント
`plot.info_str(df)`での処理です。
データからSlackへ投稿したい文面を作成しています。

```python

def info_str(df):
    # 日付列のみを抽出
    df = df.loc[:, "公表_年月日"]
    today = df.iloc[-1] # 末尾行目の1列目
    # index: 日付, colums: 人数
    df = df.value_counts().sort_index()
    num_y = int(df.iloc[-2]) # yesterday
    num_t = int(df.iloc[-1]) # today
    txt = today+"時点の陽性者:"+str(num_t)+"人\n" \
           +"(昨日比:"+str(num_t - num_y)+"人, 計"+str(df.sum())+"人)\n" \
           + URL
    return txt
```

### 最新の陽性者数を投稿（Slackbot）
`post.post_message(txt)`の処理です。
![c19bot_u.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/600317/702ece0d-318f-90da-660d-af519e83f2cb.png)

↑こちらをSlackに投稿します。

PythonのslackbotモジュールとSlack APIを使用します。

```python

import json
import requests

SLACK_API = "https://slack.com/api/"
TITLE = "COVID-19 東京都の陽性者数"

def post_message(txt, title=TITLE):
    API_METHOD = "chat.postMessage"
    response = requests.post(SLACK_API + API_METHOD, data={
        "token": SLACK_TOKEN,
        "channel": SLACK_CHANNEL,
        "username": USER_NAME,
        "as_user": True,
        "attachments": json.dumps([
            {
                "fallback": title,
                "color":  "#00FFFF",
                "title": title,
                "text": txt,
            }
        ])
    }).json()
```
`SLACK_TOKEN`にはSlackbotのtokenを記述します。
`SLACK_CHANNEL`にはチャンネル名（`"#hogehuga"`）を記述します。
`USER_NAME`にはbotのユーザ名を記述します。

### グラフ投稿
`post.upload_figure("", "")`の処理です。
![c19bot_b.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/600317/3341b8ef-27e5-6bb6-2568-dc631e745559.png)

↑こちらの画像をSlackに投稿します。

```python
PNG = "hist"
PNG_FILE = PNG+".png"

def upload_figure(txt, title=TITLE):
    files = {"file": open(PNG_FILE, "rb")}
    param = {
        "token": SLACK_TOKEN,
        "channels": CHANNEL_ID,
        "filename": PNG_FILE,
        "initial_comment": title+"\n"+txt,
        "title": title
        }
    requests.post(url=SLACK_API+"files.upload", params=param, files=files)
```

`CHANNEL_ID`は、チャンネルのURL末尾の文字列と同じものです。チャンネル名ではありません。

## 毎日自動投稿するには（crontab）

常時起動している端末（サーバー）であれば、crontabを利用することでコマンドの定期実行が可能です。
例えば毎日15時05分に自動投稿したい場合、`crontab -e`でエディタを開き、以下を記述します。

```
05 15 * * * cd [ソースコードがあるディレクトリ]; python3 run.py >run.log
```


## まとめ

東京都における新型コロナウイルスの陽性者数を取得し、Slackに投稿するbotを作成しました。

今後の課題ですが、[東京都のデータ][2]が更新されたタイミングで自動通知できればなおよいと考えています。
もしよい方法を思いついたらトライしてみたいと思います。