Dash（+Docker）で機械学習アプリを作ってみたpart3 ~実践編~

## はじめに
　PythonのWebアプリフレームワークDashを用いて簡単な機械学習アプリを作成したため、その学習記録として本記事を書きました（成果物は[こちら](https://dash-classification-app.herokuapp.com/)）。[前記事（part2）](https://qiita.com/NobuYoshi/items/9078d0689fef748486ac)ではDashアプリ作成での基本となるLayoutとCallbackについて紹介しました。本記事では応用例として、実際に私が作ったアプリの一部を取り上げて紹介したいと思います。下のように、テーブルデータに対してチェックボックスで選択した分析結果を表示させるアプリを実装します。
![スクリーンショット 2020-11-13 11.06.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/740683/5e84b7bd-de9a-e291-47c9-8a450cfb5c7a.png)
　動作環境については[こちら（part1）](https://qiita.com/NobuYoshi/items/58985860770e0c96ef27)をご覧ください。また本記事ではデータ加工にPandasを使用しておりますが、Pandasの知識がなくてもほぼ問題なく読めると思います。

## 準備
　実際に作成したアプリではcsvファイルなどをアップロード機能をつけましたが、今回は用意したデータを直接読み込むことにします。sample_data.csvの部分は、適当なサイズのお好きなテーブルデータで試してみてください（ペアプロットを実装するため数値変数は2つ以上あった方が良いです）。本記事ではKaggleのタイタニックコンペのデータ（train.csv）を使用します。

```
＜ディレクトリ構成＞
Dash_App/
　　├ sample_data.csv
　　├ app.py
　　├ Dockerfile
　　└ docker-compose.yml
```
Layout, Callback以外の部分は以下のようにしておきます。

```python:app.py
import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output, State
import pandas as pd
import plotly.express as px
import plotly.figure_factory as ff

# データの読み込み
data = pd.read_csv('src/sample_data.csv')

external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']
app = dash.Dash(__name__, external_stylesheets=external_stylesheets)
################################################################
                     　 Layout部分
################################################################
################################################################
                     　Callback部分
################################################################

if __name__ == '__main__':
    app.run_server(host='0.0.0.0', port=5050, debug=True)
```

## Layout部分の作成
　今回はチェックボックス（`dcc.RadioItems()`）とボタン（`html.Button()`）を使用してみようと思います。[公式サイト](https://dash.plotly.com/dash-core-components)にサンプルコードが載っているため、使いたいものを探してサンプルを真似て実装していくのが良いと思います。例えば`dcc.RadioItems()`のサンプルコードをみてみると、下のようになっています。
![スクリーンショット 2020-11-13 13.25.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/740683/73789ca9-a2ca-53b9-46fb-e3a935a1a8f5.png)
　見てみると引数`options`で選択肢を設定し、それぞれの選択肢に`label`（表示されるテキスト）と`value`を指定していることがわかります。さらに次の引数`value`で'MLT'を指定した結果’Montreal’にチェックがついていることから、引数`value`では初期値を設定することができ、UI操作によって選択された値に`value`が上書きされることが想像できます（読み解くよりも実際に動かしながら確認する方が断然早いですが...）。
　サンプルコードを真似ながら、Layout部分は以下のように作成してみました。`value`の初期値は設定せず、設定した選択肢の`value`はわかりやすく'AAA', 'BBB', 'CCC'としました。

```python:app.py
app.layout = html.Div(children=[

    html.H3('Step5: 数値変数の分析'),
    html.H5('分析方法を選択し、実行を押してください'),
    # チェックボックス部分
    dcc.RadioItems(
        id='num_analysis_selection',
        options=[
            {'label': '統計量一覧', 'value': 'AAA'},
            {'label': 'ペアプロット', 'value': 'BBB'},
            {'label': '相関行列', 'value': 'CCC'}
        ]
    ),
    # ボタン部分
    html.Button(id="num_analysis-button", n_clicks=0, children="実行", style={'background': '#DDDDDD'}),
    # 結果を表示させる部分
    html.Div(id='num_result')
])
```
　この状態でアプリを起動すると、下のように表示されます。当然ですが今のままではボタンを押しても何も起こらないため、次の節からCallbackで動きを付けていきたいと思います。
![スクリーンショット 2020-11-13 14.01.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/740683/3e502a05-7897-8a71-011b-4f9f0ccd879e.png)

## Callback部分
### 動作確認
　続いてCallback部分を書いていきます。データを使用する前に、まずは簡単な動作確認から行っていきます。app.pyのLayout部分の下に、以下のようにCallbackを書いてみます。

```python:app.py
@app.callback(
    Output(component_id='num_result', component_property='children'),
    [Input(component_id='num_analysis_selection', component_property='value')]
)
def num_analysis(input):
    return input
```
　この状態でアプリを動かしてみると、チェックをつけた選択肢の`value`の値がボタンの下に表示されていることが確認できます。[前記事（part2）]()の復習になりますが、動きとしては①Inputで指定した`dc.ReadItems()`の`value`の値が関数`num_analysis()`の引数（`input`）として渡され、②この関数の返り値（`input`）がOutputで指定した`html.Div(id='num_result')`の引数`children`に渡されていることになります。
![スクリーンショット 2020-11-13 14.13.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/740683/b9a6b14a-423a-91b1-1a00-d11900d3c9bb.png)
　動きが確認できたら、実際に読み込んだデータを使っていきたいと思います。今回は選択内容によって異なる処理をするため、入力値を使ってif文で条件分岐させていきます。

### Tableの描画
　まずは、「統計量一覧」が選択された場合の処理を書いていきます。具体的には、Pandasで`describe()`メソッドを行った結果の表を描画させたいと思います。Dashで表を描画させる方法は聞くつもあるようですが、ここでは最もシンプルな`html.Table`を使います。[公式チュートリアルのサンプルコード](https://dash.plotly.com/layout)を見てみると、
![スクリーンショット 2020-11-13 15.33.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/740683/5065e09f-20a1-1e89-bbb9-f97188a20fdb.png)
　やや複雑ですが、`generate_table()`と言う関数を作っているみたいです。今回は、この関数のreturnの部分を使って下のように実装してみました。

```python:app.py
@app.callback(
    Output(component_id='num_result', component_property='children'),
    [Input(component_id='num_analysis_selection', component_property='value')]
)
def num_analysis(input):
    if input == 'AAA':
        describe = data.describe()
        return html.Table([
            html.Thead(
                html.Tr([html.Th(col) for col in describe.columns])
            ),
            html.Tbody([
                html.Tr([
                    html.Td(describe.iloc[i][col]) for col in describe.columns
                ]) for i in range(len(describe))
            ])
        ])
```
　「統計量一覧」を選択すると、下のように表が描画できていると思います。
![スクリーンショット 2020-11-13 15.59.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/740683/fe01550b-8ec0-08f6-c880-12bc454b8f9e.png)

### Plotlyの図を描画する
　続けて、ペアプロットと相関行列（ヒートマップ）の描画を実装していきます。Dashの美味しいところは、なんと言ってもPlotlyのインタラクティブでカッコ良い図を使用できることなので、[Plotlyの公式サイト](https://plotly.com/python/)から探していきます。基本的な流れは同様のため、ペアプロットの方のみ見ていきます。
![スクリーンショット 2020-11-13 16.12.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/740683/ff57b561-4020-3938-ffab-3052a411d5a6.png)
　DashでPlotlyの図を描画する方法は、基本的にはPlotlyで`fig.show()`としているところを、`dcc.Graph(figure=fig)`に変えるだけです。それでは、ヒートマップ部分も合わせて実装していきます。

```python:app.py
@app.callback(
    Output(component_id='num_result', component_property='children'),
    [Input(component_id='num_analysis_selection', component_property='value')]
)
def num_analysis(input):
    # 統計量一覧の描画
    if input == 'AAA':
        describe = data.describe()
        return html.Table([
            html.Thead(
                html.Tr([html.Th(col) for col in describe.columns])
            ),
            html.Tbody([
                html.Tr([
                    html.Td(describe.iloc[i][col]) for col in describe.columns
                ]) for i in range(len(describe))
            ])
        ])
    # ペアプロットの描画
    elif input == 'BBB':
        fig = px.scatter_matrix(
            data, 
            dimensions=['Pclass', 'Age', 'Parch', 'Fare'], 
            color='Survived'
        )
        return dcc.Graph(figure=fig)

    # 相関係数（ヒートマップ）の描画
    elif input == 'CCC':
        corr = data[['Pclass', 'Age', 'Parch', 'Fare']].corr().round(4)
        fig = ff.create_annotated_heatmap(
            z=corr.values, 
            x=list(corr.columns),
            y=list(corr.index), 
            colorscale='Oranges',
            hoverinfo='none'
        )
        return dcc.Graph(figure=fig)
```
　これで、ペアプロットと相関行列も描画できるようになったかと思います。

### Stateを使ってボタンを機能させる
　最後にもう1ステップ。現時点では、チェックボックスで選択した瞬間に図の描画が開始され、下にある実行ボタンが機能していません。これを、選択したあと実行ボタンを押すことで結果が反映されるように修正していきます。これには、Callbackの中でStateという機能を使っていきます。一度if文以下をコメントアウトして、Callback部分を下のように修正します。

```python:app.py
@app.callback(
    Output(component_id='num_result', component_property='children'),
    [Input(component_id='num_analysis-button', component_property='n_clicks')],
    [State(component_id='num_analysis_selection', component_property='value')]
)
def num_analysis(n_clicks, input):
    return 'n_clicks:{}, input:{}'.format(n_clicks, input)
```
　Inputにボタンを指定し、元々InputだったチェックボックスをStateに書き換えました。こうすることにより、Stateによって指定された部分はアクションがあった時点では反映されず、Inputで指定された部分にアクションがあった際に同時に反映されるようになります。この際、関数に渡される引数がボタン由来（`n_clicks`）とチェックボックス由来（`input`）の2つになっていることに注意してください。ちなみに`n_clicks`にはボタンが押された回数が入ります。試しに上の状態でアプリを起動すると、実行ボタンを押すたびにn_clicksの値が増え、inputには'AAA'などが入っていることが確認できるかと思います。
![スクリーンショット 2020-11-13 18.25.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/740683/89968486-a215-0c3c-726c-8eb2d3093898.png)
　Stateの仕組みが理解できたところで、`num_analysis(n_clicks, input)`の関数の中身をif文以下に戻します。今回は`n_clicks`は特に使用しないので、これで完成になります。最後にもう一度完成したコードを載せておきます。

```python:app.py
import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output, State
import pandas as pd
import plotly.express as px
import plotly.figure_factory as ff

external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']

data = pd.read_csv('src/dash/titanic_train.csv')

app = dash.Dash(__name__, external_stylesheets=external_stylesheets)

app.layout = html.Div(children=[

    html.H3('Step5: 数値変数の分析'),
    html.H5('分析方法を選択し、実行を押してください'),
    # チェックボックス部分
    dcc.RadioItems(
        id='num_analysis_selection',
        options=[
            {'label': '統計量一覧', 'value': 'AAA'},
            {'label': 'ペアプロット', 'value': 'BBB'},
            {'label': '相関行列', 'value': 'CCC'}
        ]
    ),
    # ボタン部分
    html.Button(id="num_analysis-button", n_clicks=0, children="実行", style={'background': '#DDDDDD'}),
    # 結果を表示させる部分
    html.Div(id='num_result')
])
@app.callback(
    Output(component_id='num_result', component_property='children'),
    [Input(component_id='num_analysis-button', component_property='n_clicks')],
    [State(component_id='num_analysis_selection', component_property='value')]
)
def num_analysis(n_clicks, input):
    if input == 'AAA':
        describe = data.describe()
        return html.Table([
            html.Thead(
                html.Tr([html.Th(col) for col in describe.columns])
            ),
            html.Tbody([
                html.Tr([
                    html.Td(describe.iloc[i][col]) for col in describe.columns
                ]) for i in range(len(describe))
            ])
        ])
    # ペアプロットの描画
    elif input == 'BBB':
        fig = px.scatter_matrix(
            data, 
            dimensions=['Pclass', 'Age', 'Parch', 'Fare'], 
            color='Survived'
        )
        return dcc.Graph(figure=fig)
    
    # 相関係数（ヒートマップ）の描画
    elif input == 'CCC':
        corr = data[['Pclass', 'Age', 'Parch', 'Fare']].corr().round(4)
        fig = ff.create_annotated_heatmap(
            z=corr.values, 
            x=list(corr.columns),
            y=list(corr.index), 
            colorscale='Oranges',
            hoverinfo='none'
        )
        return dcc.Graph(figure=fig)
        

if __name__ == '__main__':
    app.run_server(host='0.0.0.0', port=5050, debug=True)
```

## おわりに
　実践編として、簡単なテーブルデータ分析アプリを実装しました。機械学習やデータ分析のアプリを作る場合、ほとんどが今回のように「PandasやScikit-learnでデータを加工する → DashやPlotlyに合うように値を渡す」という作業の繰り返しになりますので、色々と試してみてください。次の記事（ラスト）では、作成したアプリをDockerを使ってHerokuにデプロイする方法を紹介したいと思います。長くなってしまいましたが、ありがとうございました。