日々の学びと煩悩

Dashで機械学習ができるWebアプリを作る [Step4]

Dashを使って、機械学習をさせるWebアプリケーションを作ろうStep4 !

これまでの流れはこちら

流れ(おさらい)

Step1: タイトルとデータアップロードの枠だけ作り、cssでいい感じに表示させる

wimper-1996.hatenablog.com

Step2: 単純な線形モデルを作り、グラフとスコアを表示させる

wimper-1996.hatenablog.com

Step3: モデルを選択するドロップダウンを作って、選択したモデルに応じて出力結果が変わるような動的なページを作る

wimper-1996.hatenablog.com

今回はここ↓

Step4: ファイルをアップロードし、データフレームとして表示させる

Step5: アップロードしたデータを読み込んで学習させるようにする


Step4での完成イメージ

f:id:wimper_1996:20191105125628g:plain

アップロード部分にデータを投げたら、データフレームとして中身が見えるようになります!

わーい。

はい。

これを行うためのcallback関数は、がっつり公式ドキュメントを参考にさせていただいています。

まず、アップロードする入れ子を、layoutの中のアップロード部分の下に記述してください。

dash_table.Datatableというモジュールを使います!

# アップロードしたファイルをデータテーブルとして表示させる部分
        html.Div(
            children=[
                dash_table.DataTable(
                    id='output-data-upload',
                    column_selectable='multi',
                    fixed_rows={'headers': True, 'data': 0},
                    style_table={
                        'overflowX': 'scroll',
                        'overflowY': 'scroll',
                        'maxHeight': '250px'
                    },
                    style_header={
                        'fontWeight': 'bold',
                        'textAlign': 'center'}
                )
            ],
            style={
                'height': '300px'
            }),
        html.Br(),

今回はそのcallback関数の中身だけ、見てみましょう。

全コードは、Githubのapp4.pyに載せてあります。

# アップロードしたファイルをデータフレームとして読み込むための関数
def parse_contents(contents, filename):
    content_type, content_string = contents.split(',')

    decoded = base64.b64decode(content_string)
    try:
        if 'csv' in filename:
            # Assume that the user uploaded a CSV file
            df = pd.read_csv(
                io.StringIO(decoded.decode('utf-8')))
        elif 'xls' in filename:
            # Assume that the user uploaded an excel file
            df = pd.read_excel(io.BytesIO(decoded))
    except Exception as e:
        print(e)
        return html.Div([
            'There was an error processing this file.'
        ])

    data_ = df.to_dict('records')
    columns_ = [{'name': i, 'id': i} for i in df.columns]

    # データフレームの中身を送る
    return [data_, columns_]


@app.callback([Output('output-data-upload', 'data'),
               Output('output-data-upload', 'columns')],
              [Input('upload-data', 'contents')],
              [State('upload-data', 'filename')])
def update_output(list_of_contents, list_of_names):
    # ファイルがない時の自動コールバックを防ぐ
    if list_of_contents is None:
        raise dash.exceptions.PreventUpdate

    contents = [parse_contents(c, n) for c, n in zip(list_of_contents, list_of_names)]

    return [contents[0][0], contents[0][1]]

app.layoutを記述したあと、一旦、parse_contents関数を定義し、そのあとにその関数を使ってcallbackを記述しています。

簡単な解説

ファイルがアップロードされると、その中身はbase64エンコードされた文字列としてフロントエンドに保持されます。

中身を使うには、dashのコールバックにおいて、contentsプロパティで呼び出し、デコードして利用します。

It’s stored in the “front-end” as a base64 encoded string and transferred to your Python code via the dash callbacks via the contents property. Once its in your python callback, you can do whatever you want with it: save it to a file (which is one way for it to be reused in other callbacks: you just have to be careful about file naming), read it into a dataframe, etc.

引用元: https://community.plot.ly/t/data-from-file-in-dash-upload-component/4922/13

公式では、Outputとしてdivのchildrenそのものを返すようにしているのですが、

それだと後々、データテーブルの中のデータをcallbackのInputとして再び読み込む際に面倒になる*1ことが分かったので、

このようにdash_table.Datatableの中の要素2つ(componetn_property = dataと、component_property=columns)を返すようにしました。

そして、Inputとして同列に並んでいるStateは、

コールバックが自動で引き起こされないようにするための「ストッパー」の役割を果たします*2

ただ、今回、複数のOutputを設定したからか、Stateを定義してもエラーが出てしまった*3ので、

 if list_of_contents is not None:
     raise dash.exceptions.PreventUpdate

でエラー処理をしました。

Stateやら例外処理やらをやらないといけませんでしたが、これで無事データアップロード機能が完成したので、

お次、Step5でようやく、このデータ読み込みと機械学習を連携させて最終アプリの完成になります!

ここまで読んでくださりありがとうございました〜

*1:Inputする際に必要なcomponent_idも含めてreturnできなかった

*2:コールバックにより、アプリが起動した瞬間に自動でデータテーブルを表示させようとする

*3:内容:The callback ..output-data-upload.data...output-data-upload.columns.. is a multi-output. Expected the output type to be a list or tuple but got None.