PythonのウェブフレームワークDashで京都の新型コロナアプリを作成した

PythonのウェブフレームワークDashを使って、京都の新型コロナアプリを作成しました。[Dash](https://dash.plotly.com/)はFlask、React、Plotlyを使って作られたフレームワークで、ダッシュボードのようなアプリケーションを短時間で作成できます。またコールバックを使ってアプリケーションをインタラクティブに動作させられます。私のようにデータをこねるのは好きだけど、ReactもVueもいまいちよくわからないという人には、容易にダッシュボードを作れるフレームワークです。     
     
作成したアプリケーションは以下のURLにあります。    
      
https://chomoku.herokuapp.com/kyoto-covid

PC向け
[![Image from Gyazo](https://i.gyazo.com/b0b21bb9f30a7bb6ec9f1f8b63398b30.gif)](https://gyazo.com/b0b21bb9f30a7bb6ec9f1f8b63398b30)

スマホ向け
[![Image from Gyazo](https://i.gyazo.com/28f8fef98d77c2b83dcfe4f005f4b07c.gif)](https://gyazo.com/28f8fef98d77c2b83dcfe4f005f4b07c)

コードは[github](https://github.com/mazarimono/chomoku/blob/master/app1/kyoto_covid.py)を参照ください。

開発環境は以下の通りです。    


Windows10 Pro 
Python3.7.5
dash 1.9.1 
pandas 1.0.0

## アプリの機能

アプリは京都府のサイトから取得したデータ（取得方法などは後述）をCSVファイルとして読み込み運用しています。CSVファイルは[pandas](https://pandas.pydata.org/)を使って読み込み、処理を行います。アプリは3つの機能を持ちます。    
     
まず、全体の数値を表示する部分です。   
     
![data.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/85433/f2b55918-456c-8900-8e59-28028779fcba.png)

ここに表示されるデータは先に読み込んだCSVファイルを集計して表示するようにしています。    

次にグラフと表によるデータ表示部分です。    
     
![graphandtable.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/85433/a768d7f7-5100-842d-f992-35853caa4e9f.png)

1つ目のパイグラフはラジオボタンで感染者の現在の状況と男女比を切り替えて表示できます。2つ目のツリーグラフは感染者の年齢と性別の比率を表します。3つ目の棒グラフは新規感染者数と累計感染者数の推移を示します。4つ目の表は年齢別の感染者の状況、地域別の感染者の状況を示します。

最後に全体のテーブルデータの表示です。      
     
![tabledata.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/85433/669d4033-a5f0-28c5-a4f5-a3ead212133a.png)

テーブルはdash_tableというライブラリを用いて作成しています。これを使うと、下のようにテーブルをフィルタするような機能も簡単に作れます。また、EXPORTボタンを押すとフィルタしたデータをCSVをしてダウンロードすることも可能です。もちろんフィルタなどしていないそのままのデータもダウンロードできます！     
     
![tabledata1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/85433/362b4676-1cb4-7164-1436-df5292dc0ccb.png)

このライブラリの良いところはこのような機能が短いコードで作成できるところです。下のコードはここで使っているテーブルのコードです。このテーブルはフィルタリングのほかにソートもでき、その他スタイル設定を行っていたりするのですが、この程度の行数で作成できます。    

```python

table = dash_table.DataTable(
                # テーブル作成部分
                            columns=[{"id": i, "name": i} for i in kyoto_data.columns],
                            data=kyoto_data.to_dict("records"),
                # スタイル作成部分
                            style_cell={"textAlign": "center"},
                            style_as_list_view=True,
                            style_data_conditional=[
                                {
                                    "if": {"row_index": "odd"},
                                    "backgroundColor": "rgb(248, 248, 248)",
                                }
                            ],
               # フィルタリング機能
                            filter_action="native",
               # ソート機能
                            sort_action="native",
                            sort_mode="multi",
                            fixed_rows={"headers": True},
               # ファイルダウンロード機能
                            export_format="csv",
                            virtualization=True,
                        ),
```

## データの取得と前処理

データは[京都府のサイト](https://www.pref.kyoto.jp/kentai/news/novelcoronavirus.html#F)のテーブルから取得しています。テーブルデータはpandasの[read_html関数](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.read_html.html)を用います。実際の取り方は下のような感じですが、実際にはサイトのテーブル数が変化して最後の数値の部分を変更したりする必要があったります。    

     
```python

df = pd.read_html("https://www.pref.kyoto.jp/kentai/news/novelcoronavirus.html#F")[1]
```

またテーブルのデータ数が増えてきて、現在のところ1-50件目と51-100件目のデータが異なるサイトにあります。そのため、それらのページのテーブルを別々に読み込み、最終的にpandasの[concat関数](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.concat.html?highlight=concat)を使ってデータフレームをがっちゃんこします。    
    
```python 
df = pd.read_html("https://www.pref.kyoto.jp/kentai/news/novelcoronavirus.html#F")[1]
df1 = pd.read_html("https://www.pref.kyoto.jp/kentai/corona/hassei51-100.html")[0]
df2 = pd.read_html("http://www.pref.kyoto.jp/kentai/corona/hassei1-50.html")[0]
dff = pd.concat([df,df1,df2])
```

データを読み込んだ後は、令和を西暦にしたり退院の日などをその列に割り当てたりの処理を行います。実際にアプリケーションに用いるデータは、アプリケーションの一番下の部分の表の左上の「EXPORT」ボタンを押すことで、CSVファイルでダウンロードできます。     
     
[![Image from Gyazo](https://i.gyazo.com/57d65ad73f1ac385630f247ab2adac85.gif)](https://gyazo.com/57d65ad73f1ac385630f247ab2adac85)

データ取得は以上です。データが土日は更新されなかったり、発表と異なる場合がありますが、まぁ細かいことは気にしない。また、現在はCSVファイルを利用していますが、運用が安定してくればAPI化もしくは、外部で作られているものに変更しようかとは思っています。

## アプリケーションの作成

アプリケーションの作成はDashのコンポーネントを組み合わせて作成します。本アプリケーションで用いるの、dashをpip install したときに同時にインストールされるdash_html_components、dash_core_components、dash_tableを用います。    
     
[dash_html_components](https://dash.plotly.com/dash-html-components)はhtml要素を、[dash_core_components](https://dash.plotly.com/dash-core-components)はツールやグラフを、[dash_table](https://dash.plotly.com/datatable)はデータテーブルを作るコンポーネントです。    
     
Dashのコンポーネントは宣言的に使えます。例えば一番上のタイトルの部分のコードは次のようになります。   
    
![title.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/85433/d2c71373-bcff-7c83-91eb-15e75c090b79.png)
 
      
```python
html.Div(
            [
                html.H3(
                    "京都府の新型コロナ感染者推移",
                    style={"display": "inline-block", "marginRight": 40},
                ),
                html.P(
                    f"最終更新日 {update_date.date()}", style={"display": "inline-block"}, className="update_date",
                ), 
                html.A("データ出所: 京都府ウェブページ", href="https://www.pref.kyoto.jp/kentai/news/novelcoronavirus.html#F", style={"display": "block"}),
            ],
            style={"backgroundColor": "aqua", "borderRadius": 20, "padding": "2%"},
        ),
```

最初の円グラフは現在の感染者さんの状況と、感染者の性別をラヂオボタンで切り替えて表示できるようにしています。

![pie.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/85433/dc266583-b9a2-caf9-efa9-95951285ba6a.png)

このようなにアプリケーションをインタラテクィブに動かす部分では、Dashのコールバック機能を使っています。次に、この円グラフをラヂオボタンで切り替えて表示できるDashアプリケーションを作成できる、コードを示します。単体のアプリケーションとしてテスト的に動かせるよう、データはplotly.expressのサンプルを使って、円グラフと散布図を切り替えるアプリケーションのコードを作成します（コピペで動作しますw）。    
    
```python
import plotly.express as px
import dash 
import dash_html_components as html 
import dash_core_components as dcc 

from dash.dependencies import Input, Output 

# gapminderデータの読み込み
gapminder = px.data.gapminder()
gapminder2007 = gapminder[gapminder.year==2007].sort_values("gdpPercap")[-30:]

# 2種類のグラフの作成
gapminder_bar = px.bar(
    gapminder2007,
    x="country",
    y="gdpPercap"
)

gapminder_scatter = px.scatter(
    gapminder,
    x="gdpPercap",
    y="lifeExp",
    size="pop",
    size_max=70,
    animation_frame="year",
    log_x=True,
    range_y=[20, 90],
    color="continent"
)

# ➌ Dashインスタンスの作成
app = dash.Dash(__name__)

# ➍ レイアウトの作成
app.layout = html.Div([
    dcc.RadioItems(
        id="gap_radio",
        options=[{"label": i, "value": i} for i in ["円グラフ", "散布図"]],
        value="円グラフ"
    ),
    dcc.Graph(id="gap_graph")
])

# ➎ コールバックの作成
@app.callback(Output("gap_graph", "figure"), [Input("gap_radio", "value")])
def update_graph(selected_value):
    if selected_value == "円グラフ":
        return gapminder_pie
    return gapminder_scatter 

if __name__ == "__main__":
    app.run_server(debug=True)

```

上のコードではまずplotly.expressのgapminderデータを読み込みます（➊）。次に切り替えで使う2種類のグラフを、[plotly.express](https://plotly.com/python/plotly-express/)を用いて作成します（➋）。plotly.expressはデータフレームを用いて、列名を宣言的に渡すだけで直感的にグラフが描ける優れものです。一つ目のグラフは2007年の一人当たりGDPの棒グラフ、2つ目のグラフは全部のデータの散布図です。    
     
次にDashのインスタンスを作成します（➌）。次にレイアウトを作成します（➍）。レイアウトはコンポーネントを組み合わせて作成します。ここではラヂオボタンでグラフを2種類切り替えるレイアウトを作成します。最後にコールバックです（➎）。コールバックでは、ラヂオボタンで選択された値に合わせて、グラフを返します。     
      
コードを実行すると、下のようにまずは棒グラフが描画されます。    
     
![bar.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/85433/26d11402-5e73-ac83-fa60-6d26ef5720fc.png)

次に散布図を選択すると、再生ボタンとスライダのついた散布図が描画されます。再生ボタンを押すと年が自動的に更新され、散布図で表示されるデータが切り替えられます。これはscatter関数の引数animation_frameに"year"を渡しているため行われます。

[![Image from Gyazo](https://i.gyazo.com/e02b2b37f1a7823fc485a1b242a62572.gif)](https://gyazo.com/e02b2b37f1a7823fc485a1b242a62572)

このような感じで、アプリケーションの各グラフの切り替えを作成しています。    
     
## モバイル対応

モバイル対応は[MDNのレスポンシブデザイン](https://developer.mozilla.org/ja/docs/Learn/CSS/CSS_layout/Responsive_Design)を読んで作成しました。これを読んでいるとメタタグを使うこと、そしてメディアクエリを用いて画面サイズに合わせたCSSを設定することで、モバイル対応ができることが分かりました。    
      
Dashでメタタグを渡すには、Dashクラスの引数meta_tagsを用います。実際には下のように渡しています。    
     
```python
app = dash.Dash(__name__, meta_tags=[
        {"name": "viewport", "content": "width=device-width, initial-scale=1.0"}
    ]
```

そうして、[CSSはassetsディレクトリに入れています](https://github.com/mazarimono/chomoku/blob/master/assets/style.css)。DashはCSSやJavaScriptをassetsディレクトリに入れておくと、勝手に読み込んでくれるようになっています。     
      
## まとめ

以上のような感じで、京都の新型コロナアプリを作成しました。もう少し詳しくDashを知りたいと思われた方には、私がだいぶん前に書いたDashの記事がフィットするかもしれません。     
      
https://qiita.com/OgawaHideyuki/items/6df65fbbc688f52eb82c

でも公式ドキュメントが一番良いと思います。   
     
https://dash.plotly.com/

あと、はんなりPythonの会でハンズオンを継続的に行っているので、分からないところがあれば聞いてもらえば、ほかのつわものが答えてくれると思います。     
     
https://hannari-python.connpass.com/event/173990/