Google Colab で Plotly Dash する

python のデータ可視化のフレームワークに Dash(https://dash.plotly.com/) があります。

これが Google Colab でも利用可能に、、と思ったのですが、すでに解説されている良記事を見つけてしまいました。
https://qiita.com/OgawaHideyuki/items/725f4ffd93ffb0d30b6c

ということで、この記事は参考にさせていただいて自分の手を動かしました、という記録です。
お題は安直に各国のコロナ感染者数の可視化です。地図と時系列のグラフを表示してみます。

**2020-12-27 追記:** 地図表示を追加しました。

### 使用する環境やリソースなど

Google Colaboratory を使用します。

##### 各種リソース

* **Dash**  https://dash.plotly.com/
    * グラフライブラリである Plotly と Flask を使用して、インンタラクティブなデータ可視化画面を簡単に作れる Python フレームワークです。
* **国・地域別感染者データ**　https://github.com/CSSEGISandData/COVID-19
    * Johns Hopkins 大学の国・地域別感染者データを使用します。 
* **Mapbox**　https://www.mapbox.com/
    * 地図上の散布図表示で使用する地図サービスです。

##### 今回のノートブック

マップの散布図表示は Mapbox の token と Google drive を使用するため、ノートブックは時系列とマップで 2つに分けています。

* **時系列グラフ表示**
    * https://colab.research.google.com/drive/1fUP4818fSsFFFlUHlLGNoTxq8uoL2VAu?usp=sharing
* **マップ散布図表示**
    * https://colab.research.google.com/drive/1MTECSFu3lrl7w0_NHZXYp8ENPYHu4dEg?usp=sharing


### 時系列のグラフ表示

#### 実行準備とデータ

まず Google Colab/Jupyter ノートブックから Dash を使用するためのパッケージをインストールします。

```
! pip install jupyter_dash
! pip install --upgrade plotly
```

Dash と関連するパッケージを import します。

```Python
import dash 
from jupyter_dash import JupyterDash 
import dash_core_components as dcc 
import dash_html_components as html 
import plotly.express as px
from dash.dependencies import Input, Output
```

コロナの感染者データを GitHub から取得します。
データに関しては下記のページを参考にさせていただいています。
https://dodotechno.com/covd-19-visualization/

```
! wget https://github.com/CSSEGISandData/COVID-19/raw/master/csse_covid_19_data/csse_covid_19_time_series/time_series_covid19_confirmed_global.csv
```

データフレームにダウンロードした csv を読み込みます。

```Python
import pandas as pd
df = pd.read_csv("time_series_covid19_confirmed_global.csv")
```

region ごとの集計、緯度経度の削除、国ごとの列に転置し、date 列の追加を行います。

```Python
df = df.groupby(['Country/Region'], as_index=False).sum()
df.drop(["Lat","Long"], axis=1,inplace=True)
df = df.T
df.columns =  df.iloc[0]    
df = df[1:]
df.reset_index(inplace=True)
df.rename(columns={'index': 'date'},inplace=True)
df
```

結果、得られる表は以下のようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323666/8a3e4d4b-2db3-77f6-571a-b4f65fb23b52.png)

#### ノートブックに表示してみる

まず日本の感染者数をグラフ化してみます。
横軸を日付、縦軸が感染者数としており、第一波(4月下旬)、第二波(8月上旬)、第三波(11月)らしき増加が見えます。

```Python
px.line(df, x="date", y="Japan")
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323666/f88ac7a6-c9a0-3220-d677-ce2815329e07.png)


つづいて任意の国をドロップダウンで選択可能にしてみます。
ノートブック上で選択できますので、試してみてください。ランタイムが停止している場合は、メニューの「ランタイム」→「すべてのセルを実行」を行ってみてください。
https://colab.research.google.com/drive/1fUP4818fSsFFFlUHlLGNoTxq8uoL2VAu#scrollTo=Kr-FsvLIpCoN&line=1&uniqifier=1

```Python
app = JupyterDash(__name__)

app.layout = html.Div([
  dcc.Dropdown(id="my_dropdown",
    options=[{"value": country, "label": country} for country in df.columns.unique()],
    value=["Japan"],
    multi=True
    ),
  dcc.Graph(id="my_graph")
])

@app.callback(Output("my_graph", "figure"), Input("my_dropdown", "value"))
def update_graph(selected_country):
  return px.line(df, x="date", y=selected_country)

app.run_server(mode="inline")
```

ドロップダウンから、日本とカナダを選択して表示します。カナダも増加傾向のようですね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323666/0961eb6e-3ba7-219c-0f0f-99c5a40600eb.png)

続いてアメリカを追加してみると、本当に日本の比ではないですね。。やはりグラフで見るとインパクトがあります。
ワクチンが有効打になって欲しいです(人ごとではありませんが)。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323666/21d729f4-277d-fcad-3d42-e4f1f41b1a71.png)


### 地図上に表示

同じデータを使用して地図上で散布図を表示してみます。

#### 実行準備とデータ

Mapbox という地図サービスを利用します。利用にはアクセストークンが必要になります。アカウントがない場合は、下記から sign up してアスセストークンを取得してください。
https://account.mapbox.com/
最低限、ID, パスワード, メールアドレスがあれば利用できます。

今回は Google drive にトークンを格納します。ご自身で実行される場合は、文字列としてコードに埋め込んで実行することも可能です。

ここでは例として、Mapbox のトークンの内容を貼り付けたテキストファイル `mapbox-token.txt` を Google drive のマイドライブ直下にアップロードします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323666/2e0e8eb6-e92e-6b9f-3d8c-4287015fe8c2.png)

Google drive をマウントして、Mapbox のトークンを読み込みます。
マウント時の OAuth のトークンは実行時に表示される URL のページに飛ぶと表示されるので、コピー&ペーストなどで入力します。

```Python
from google.colab import drive
drive.mount('/content/drive')

f = open('/content/drive/My Drive/mapbox-token.txt', 'r')
MAPBOX_TOKEN = f.read()
f.close()
```

Jupyter dash の import は時系列のグラフと同様です。

```
! pip install jupyter_dash
! pip install --upgrade plotly
```

```Python
import dash 
from jupyter_dash import JupyterDash 
import dash_core_components as dcc 
import dash_html_components as html 
import plotly.express as px
from dash.dependencies import Input, Output
```

コロナの感染者データも同様に取得します。

```
! wget https://github.com/CSSEGISandData/COVID-19/raw/master/csse_covid_19_data/csse_covid_19_time_series/time_series_covid19_confirmed_global.csv
```

データフレームにダウンロードした csv を読み込みます。

```Python
df_map = pd.read_csv("./time_series_covid19_confirmed_global.csv")
df_map
```

今回は地域ごとの集計のみ取り、緯度経度を残して転置は行わない形にします。

```Python
df_map = df_map.groupby(['Country/Region'], as_index=False).sum()
df_map
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323666/696be391-5d21-f08a-f6f5-b75b9c188c01.png)

準備ができましたので、地図で表示します。

#### 地図上で表示

日付と色指定の基準を選択可能にして表示します。
こちらを参考にしています。
https://qiita.com/banquet_kuma/items/e02ba60661cf91af37de

* color_opt, date_opt はデータフレームの列名から、緯度経度、緯度経度+国名を除いています。
* `MAPBOX_TOKEN` は先ほど Google drive にアップしたファイルから読み込んだ値です。お手元で実行する場合は、文字列でそのまま貼り付けても OK です。
    * ノートブックのコードで、実行される方の Google アカウントのドライブにアクセスできるはずですが、自分のアカウント以外では試せていないので、うまくいかなかったらご指摘ください。
* 見た目も色々と変更できます。オプションについて後述します。

```Python
app = JupyterDash(__name__)

color_opt = [dict(label=x, value=x) for x in df_map.columns]
del color_opt[2]
del color_opt[1]
date_opt = color_opt.copy()
del date_opt[0]

app.layout = html.Div(
    [
        html.Div(
            [
                html.P(["date:", dcc.Dropdown(id='date', options=date_opt)]),
                html.P(["color:", dcc.Dropdown(id='color', options=color_opt)]),
            ],
            style={"width": "20%", "float": "left"}
        ),
        dcc.Graph(id="graph", style={"width": "80%", "display": "inline-block"}),
    ]
)

@app.callback(Output("graph", "figure"), [Input("date", "value"), Input("color", "value")])
def update_graph(date, color):
  px.set_mapbox_access_token(MAPBOX_TOKEN)
  if not color:
    color = date
  return  px.scatter_mapbox(df_map,
                        lat="Lat",
                        lon="Long",
                        color=color,
                        size=date,
                        size_max=20,
                        zoom=0,
                        center={'lat': 35, 'lon': 135},
                        title="各国のコロナ感染者数",
                        color_continuous_scale=px.colors.diverging.BrBG,
                        hover_name=date)

app.run_server(mode="inline")
```

実行するとこんな感じです。表示がキレイです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323666/c29e0666-9e17-bc92-fbc0-dd4c4e439535.png)

日付を選択すると、その時点の累積の感染者を表示します。
2020-12-01 はこんな感じです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323666/37bedbb2-4929-99f7-1c55-c0023df8426e.png)

表示は `plotly.express.scatter_mapbox` という関数で行なっています。
`plotly.express.scatter_mapbox` のパラメーターの日本語の説明が見当たらなかったので参考に簡単な説明を載せておきます(よい情報源をご存知の方、コメントなどで教えてください)。

https://plotly.github.io/plotly.py-docs/generated/plotly.express.scatter_mapbox.html

* **data_frame** (DataFrame or array-like or dict) – 列名でデータを渡すための DataFrame です。
* **lat** (str or int or Series or array-like) – 地図上の緯度です。
* **lon** (str or int or Series or array-like) – 地図上の経度です。
* **color** (str or int or Series or array-like) – 地図上のマーカーに色を割り当てるための指定です。
* **text** (str or int or Series or array-like) – figure 中の表示のためのテキストラベルです。
* **hover_name** (str or int or Series or array-like) – マウスのホバー時のツールチップ表示名です。
* **hover_data** (list of str or int, or Series or array-like, or dict) – マウスのホバー時のツールチップデータです。2つ要素を指定でき、1番目の要素は表示の True(デフォルト)/False またはデータ表示フォーマットを指定します。2番目の要素は表示する値です。
* **custom_data** (list of str or int, or Series or array-like) – ウィジットや Dash コールバックで使用する追加のデータです。ユーザーには表示されませんが figure のイベント(投げ縄ツール、註:たぶん範囲選択)などで使用します。
* **size** (str or int or Series or array-like) – 地図上のマークのサイズです。
* **animation_frame** (str or int or Series or array-like) – アニメーションフレーム(註：アニメーションフレームがよくわかっていません。。)にマークを割り当てをするために使用します。
* **animation_group** (str or int or Series or array-like) – アニメーションフレーム全体で不変のオブジェクトを提供し、同一の animation_group は各フレームで同じオブジェクトを記述するように扱います。
* **category_orders** 軸、凡例、ファセットのカテゴリ値の順序を指定します(デフォルトは {} )。python 3.6 以前では順序が保証されません。
* **labels** (dict with str keys and str values (default {})) – デフォルトでは列名は、軸のタイトル、凡例のエントリ、およびホバーの図で使用されます。これを上書きする列名に対応する dict です。
* **color_discrete_sequence** (list of str) – 色の循環指定です。CSS として有効な色指定を行います。 
    * `plotly.express.colors` 特に `plotly.express.colors.qualitative` は便利なカラーシーケンスがあります。
* **color_discrete_map** (dict with str keys and str values (default {})) – 有効な CSS 色の指定列です。
* **color_continuous_scale** (list of str) – カラーで指定された列が連続値の数値データである場合のカラースケールを指定します。
    * 便利なカラースケール  `plotly.express.colors` 特に `plotly.express.colors.sequential, plotly.express.colors.diverging and plotly.express.colors.cyclical` で使用できます。
* **range_color**  (list of two numbers) – 指定すると連続するカラースケールの指定を上書きします。
* **color_continuous_midpoint** (number (default None)) – 連続カラースケールの中間の境界点を指定します。 `plotly.express.colors.diverging` を使用する場合に推奨されます。
* **opacity** (float) – マーカーの透明度を指定します。
* **size_max** (int (default 20)) – マーカーの最大サイズを指定します。
* **zoom** (int (default 8)) – マップのズームサイズを 0〜20 で指定します。
* **center** (dict) – マップの中央を緯度経度(lat, lon をキーとする dict)で指定します。
* **mapbox_style** (str (default 'basic', needs Mapbox API token)) – マップのスタイルを指定します。スタイルによっては、Mapbox API token が必要です。 
    * `'open-street-map', 'white-bg', 'carto-positron', 'carto-darkmatter', 'stamen- terrain', 'stamen-toner', 'stamen-watercolor'` は token が不要です。 
    * `'basic', 'streets', 'outdoors', 'light', 'dark', 'satellite', 'satellite- streets'` は token が必要です。
* **title** (str) – タイトルを指定します。
* **template** (str or dict or plotly.graph_objects.layout.Template instance) – テンプレート名または定義を指定します。
* **width** (int (default None)) – 幅を pixels で指定します。
* **height** (int (default None)) – 高さを pixels で指定します。

多い。。

#### 残課題

課題点をいくつか挙げておきます。

* 日付はスライバーバー選択などが嬉しい。
* 値の桁数が違うせいか、ほとんど見えない国があるので調整が必要か(対数で出すとか?)。
* ツールチップが表示されていない。原因不明。

### おわりに

ちょっとした可視化なら簡単に実装でき、Google Colab でインターネット公開も容易です。
csv の加工が一番手間かも。

地図上の可視化なども追加してみたいと思います。(→2020-12-27 地図表示を追加しました、残課題あり)
まだ色々可視化できそうなので、そのうちやるかもしれません。

実行してみてわかりましたが、Google Colab のランタイム停止時にグラフの表示が残らないので、公開が必要な場合は Heroku かノートブックなら他の方法(matplotlib, plotly など)も併用した方が良いかもしれません。


### 関連・参考 URL

##### Dash 関連
* Dash　https://dash.plotly.com/
* Jupyter上でDashを使えるjupyter_dash　　https://qiita.com/OgawaHideyuki/items/725f4ffd93ffb0d30b6c

##### データ関連
* COVID-19 Data Repository by the Center for Systems Science and Engineering (CSSE) at Johns Hopkins University　　https://github.com/CSSEGISandData/COVID-19
* コロナ感染者数をPython+Matplotlibで可視化してみました。(4/4現在)　
https://dodotechno.com/covd-19-visualization/

##### Mapbox 表示関連

* Mapbox　https://www.mapbox.com/
* OBDから取得した車両データをDashとPlotly Expressで可視化する その2　https://qiita.com/banquet_kuma/items/e02ba60661cf91af37de
* plotly.express.scatter_mapbox　https://plotly.github.io/plotly.py-docs/generated/plotly.express.scatter_mapbox.html