Dashで作る機械学習アプリ

# はじめに

[default of credit card clients Data Set](https://archive.ics.uci.edu/ml/datasets/default+of+credit+card+clients)データを使って、基礎分析や簡単な分類モデルの構築などを行ってきた。せっかくなので動かせるものを作ろうと思い、Dashを用いたウェブアプリの作成を行った。機械学習アプリと名乗るにはおこがましい出来だが、どうぞご容赦を・・・。

# アプリのイメージ

上記データには債権者がデフォルトするかしないかを示すフラグ "default payment next month" という項目があるので、デフォルトの分類モデルを作る。アプリのイメージは以下。
1. ロジスティック回帰、決定木、サポートベクターマシンの3種類からモデルを選択
2. 説明変数（量的変数、質的変数）を複数選択
3. 上記2項目を設定後、決定ボタンを押すことで学習開始
4. 学習結果を表示

とりあえず動くものができればいいので、機械学習の精度とデザインは一旦置いておく。

# コールバック関数の引数State

今回のアプリを作成するにあたり、[Dashでウェブアプリ作成①](https://qiita.com/honda28/items/ecebf82ba2d40bbec866)、[Dashでウェブアプリ作成②](https://qiita.com/honda28/items/d9420629404f843d7ed8)、[Dashで世界遺産を世界地図にプロット](https://qiita.com/honda28/items/d9be242bece0caa8eedb)あたりで学習したこと以外で、dash.dependenciesライブラリの__State__が便利だったので書いておく。
もともとコールバック関数の引数としては Input（と Output ）しか知らなかったため、上記1および2にて項目を選択する度にコールバック関数が呼び出されてしまっていた。これはイメージしたものと違うなと感じ調査したところ、State なるものを見つけた。Input と使い方はほぼ同様だが、State はコールバック関数を呼び出すトリガーにはならないらしい。上記1と2で選択した情報は State としてコールバック関数に渡して、3の決定ボタンをトリガーに State の情報をもとに学習するようにすれば、イメージ通りのものが作れるはず。

上記を踏まえたコードは以下に示す。（①CSV形式に直したデータセット、②標準化およびダミー変数化などの前処理後データが必要。過去記事ありきのコードとなってしまっているため、このままでは実行できない。。。）

```python:app.py
# Run this app with `python app.py` and
# visit http://127.0.0.1:8050/ in your web browser.

import dash
import dash_html_components as html
import dash_core_components as dcc
from dash.dependencies import Input, Output, State
import plotly.express as px
import pandas as pd

# For models
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.metrics import roc_curve, roc_auc_score
from sklearn.linear_model import LogisticRegression
from sklearn.tree import DecisionTreeClassifier
from sklearn.svm import SVC
# from sklearn.preprocessing import StandardScaler

df = pd.read_csv(
    "c:/***/data/default_of_credit_card_clients.csv", header=1, index_col=0)
col = df.columns.values
quantity_col = col[[0, 4, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22]]
quality_col = col[[1, 2, 3, 5, 6, 7, 8, 9, 10]]

df_p = pd.read_csv(
    "c:/***/preprocessed_data.csv", header=0, index_col=0)
col_p = df_p.columns.values
# quantity_col_p = df_p.iloc[:, :14]
quality_col_p = df_p.iloc[:, 14:-1]

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

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

app.layout = html.Div([
    html.H4(children='default of credit card clients Data Set'),

    html.Div([
        html.Label('Training model'),
        dcc.Dropdown(id='model',
                     options=[
                         {'label': 'Logistic Regression',
                          'value': 'Logistic Regression'},
                         {'label': 'Decision Tree', 'value': 'Decision Tree'},
                         {'label': 'SVM', 'value': 'SVM'}
                     ],
                     value='Logistic Regression'
                     ),

        html.Label(children='Quantity column'),
        dcc.Dropdown(id='quantity-col',
                     options=[{'label': i, 'value': i} for i in quantity_col],
                     value=[i for i in quantity_col],
                     multi=True
                     ),

        html.Label(children='Quality column'),
        dcc.Dropdown(id='quality-col',
                     options=[{'label': i, 'value': i} for i in quality_col],
                     value=[i for i in quality_col],
                     multi=True
                     ),

        html.Button('Enter', id='enter-button', n_clicks=0)
    ]),

    html.H5('Result'),
    html.Div(id='result')
])


def make_dummycol(column):  # ダミー変数を含む列名のリストを作成
    dummycol = []
    for c in column:
        dummycol += [c + "_{}".format(i) for i in range(-1, 9)]
    l = [cp for cp in quality_col_p if cp in dummycol]
    return l


def train(model, quantity_col, quality_col):  # 選択モデルとデータを使用して学習、AUCスコアを返す
    col_list = list(quantity_col) + make_dummycol(quality_col)
    X = df_p[col_list]
    Y = df["default payment next month"]
    X_train, X_test, Y_train, Y_test = train_test_split(
        X, Y, test_size=0.2, random_state=0)

    if model == 'Logistic Regression':
        lr = LogisticRegression(max_iter=2000)
        lr.fit(X_train, Y_train)
        THRESHOLD = 0.30
        Y_pred = np.where(lr.predict_proba(X_test)[:, 1] > THRESHOLD, 1, 0)
    elif model == 'Decision Tree':
        dt = DecisionTreeClassifier(max_depth=4)
        dt = dt.fit(X_train, Y_train)
        Y_pred = dt.predict(X_test)
    elif model == 'SVM':
        svm = SVC(kernel="rbf", gamma="scale")
        svm.fit(X_train, Y_train)
        Y_pred = svm.predict(X_test)

    return html.Div([model, ": AUC Score:", round(roc_auc_score(Y_test, Y_pred), 3)])


@app.callback(
    Output('result', 'children'),
    Input('enter-button', 'n_clicks'),
    State('model', 'value'),
    State('quantity-col', 'value'),
    State('quality-col', 'value'))
def show_result(enter, model, quantity_col, quality_col):
    return train(model, quantity_col, quality_col)
    # print("Coefficients:", lr.coef_[0])
    # print("Intercept:", lr.intercept_[0])


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

実行結果は以下のようになる。（以下GIFでは為井していないが、SVMは他2つと比べ実行時間が長い。）
![app.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/769545/98924dbe-1c33-a71f-b97e-5fb2decdb6d7.gif)

# おわりに

各モデルごとのパラメータ修正やその他結果の表示など修正すべき点は多々あるが、一旦ここまで。動くものを作るのはおもしろい。