Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

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

はじめに

 PythonのWebアプリフレームワークDashを用いて簡単な機械学習アプリを作成したため、その学習記録として本記事を書きました(成果物はこちら)。前記事(part2)ではDashアプリ作成での基本となるLayoutとCallbackについて紹介しました。本記事では応用例として、実際に私が作ったアプリの一部を取り上げて紹介したいと思います。下のように、テーブルデータに対してチェックボックスで選択した分析結果を表示させるアプリを実装します。
スクリーンショット 2020-11-13 11.06.15.png
 動作環境についてはこちら(part1)をご覧ください。また本記事ではデータ加工にPandasを使用しておりますが、Pandasの知識がなくてもほぼ問題なく読めると思います。

準備

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

<ディレクトリ構成>
Dash_App/
  ├ sample_data.csv
  ├ app.py
  ├ Dockerfile
  └ docker-compose.yml

Layout, Callback以外の部分は以下のようにしておきます。

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())を使用してみようと思います。公式サイトにサンプルコードが載っているため、使いたいものを探してサンプルを真似て実装していくのが良いと思います。例えばdcc.RadioItems()のサンプルコードをみてみると、下のようになっています。
スクリーンショット 2020-11-13 13.25.35.png
 見てみると引数optionsで選択肢を設定し、それぞれの選択肢にlabel(表示されるテキスト)とvalueを指定していることがわかります。さらに次の引数valueで'MLT'を指定した結果’Montreal’にチェックがついていることから、引数valueでは初期値を設定することができ、UI操作によって選択された値にvalueが上書きされることが想像できます(読み解くよりも実際に動かしながら確認する方が断然早いですが...)。
 サンプルコードを真似ながら、Layout部分は以下のように作成してみました。valueの初期値は設定せず、設定した選択肢のvalueはわかりやすく'AAA', 'BBB', 'CCC'としました。

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

Callback部分

動作確認

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

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
 動きが確認できたら、実際に読み込んだデータを使っていきたいと思います。今回は選択内容によって異なる処理をするため、入力値を使ってif文で条件分岐させていきます。

Tableの描画

 まずは、「統計量一覧」が選択された場合の処理を書いていきます。具体的には、Pandasでdescribe()メソッドを行った結果の表を描画させたいと思います。Dashで表を描画させる方法は聞くつもあるようですが、ここでは最もシンプルなhtml.Tableを使います。公式チュートリアルのサンプルコードを見てみると、
スクリーンショット 2020-11-13 15.33.06.png
 やや複雑ですが、generate_table()と言う関数を作っているみたいです。今回は、この関数のreturnの部分を使って下のように実装してみました。

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

Plotlyの図を描画する

 続けて、ペアプロットと相関行列(ヒートマップ)の描画を実装していきます。Dashの美味しいところは、なんと言ってもPlotlyのインタラクティブでカッコ良い図を使用できることなので、Plotlyの公式サイトから探していきます。基本的な流れは同様のため、ペアプロットの方のみ見ていきます。
スクリーンショット 2020-11-13 16.12.27.png
 DashでPlotlyの図を描画する方法は、基本的にはPlotlyでfig.show()としているところを、dcc.Graph(figure=fig)に変えるだけです。それでは、ヒートマップ部分も合わせて実装していきます。

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部分を下のように修正します。

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
 Stateの仕組みが理解できたところで、num_analysis(n_clicks, input)の関数の中身をif文以下に戻します。今回はn_clicksは特に使用しないので、これで完成になります。最後にもう一度完成したコードを載せておきます。

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にデプロイする方法を紹介したいと思います。長くなってしまいましたが、ありがとうございました。

NobuYoshi
機械学習・データサイエンスを勉強しています。よろしくお願いいたします。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away