Python PandasとJavascriptで高速にWeb開発 サンプル付き (5/5)
データ分析で用いられるPython Pandasと、Javascriptフレームワークを組み合わせて、シンプルなSPA(単一ページ)型のWeb開発を行う。この方法はWebアプリの行数を減らし、高速に開発できる。このソースは2019年に作成したものであり、現在の環境ではうまく動かない可能性がある。
学習結果の管理
このままでは学習結果を扱いにくいので、管理処理を追加する。学習結果の再表示・削除・名前変更ができるようにする。学習結果名ごとにディレクトリを分けているので、このディレクトリを操作すれば良い。
まずHTMLに以下の記述を追加する。ここは今まで学習結果名を入れるテキストボックスだった部分である。1つのformの中に様々なbuttonがあり、それぞれがJavascriptの関数を呼び出す形になっている。これにより1つのselectを使って複数のbutton処理ができる。
<div class="row"> <div class="col-sm-12 border border-info rounded py-2"> <form id="result-form"> <div class="form-inline"> <div class="form-group" style="margin:10px;"> <label for="result">学習結果:</label> <select name="result" id="result" class="custom-select custom-select-sm"> </select> </div> <button type="button" class="btn btn-primary" onclick="showResult()" style="margin:10px;">学習結果表示</button> <button type="button" class="btn btn-primary" onclick="deleteResult()" style="margin:10px;">学習結果削除</button> <button type="button" class="btn btn-primary" onclick="downloadResult()" style="margin:10px;">学習結果ダウンロード</button> </div> <div class="form-inline"> <div class="form-group" style="margin:10px;"> <label for="new-mod-result">新規・変更学習結果名:</label> <input type="text" name="new-mod-result" id="new-mod-result" class="form-control form-control-sm"> </div> <button type="button" class="btn btn-primary" onclick="modifyResult()" style="margin:10px;">学習結果名変更</button> </div> … </form> </div> </div>
ブラウザで見ると以下のようになる。学習結果の部分には、今まで作成した学習結果名がリスト表示される。
これだけボタンが多くなると、通常のWeb開発では作成に手間がかかる。Javascriptを使ったSPA(単一ページ)型なら比較的簡単である。
まず学習結果名のリスト表示機能を作成する。Pythonにおいて学習結果名の一覧を返す処理を作る。ディレクトリの一覧をソートして返す簡単な処理である。
@app.route('/get-results', methods=['POST']) def get_results(): return json.dumps(sorted(os.listdir(path='result')))
これをJavascript側で呼び出してリスト表示する処理を作る。関数にしておいて様々な場所から呼べるようにする。remove・append・val・textの関数は、jQueryの機能である。先頭に新規学習結果名の設定を入れておく。setResult関数でresultを指定した場合は、resultに相当する学習結果名を選択するようにしておく。
function setResults(result) { $.post('/get-results') .fail(function(ret) { alert('学習結果一覧の取得に失敗しました'); }) .done(function(ret) { let retParse = $.parseJSON(ret); $("#result option").remove() $("#result").append($("<option>").val("##new##").text("(新規)")); for (let i = 0; i < retParse.length; i++) { $("#result").append($("<option>").val(retParse[i]).text(retParse[i])); } if (result) { $("#result").val(result); } }); }
この処理を画面ロード時など、一覧を変更する必要のある適切な場所で呼べばよい。画面ロード時は$(function()…)の機能で実装できる。
次に学習結果表示の機能を作成する。これは過去に行った学習結果を再表示する機能である。Pythonにおいて過去の学習結果を返却する処理を作る。
@app.route('/show-result', methods=['POST']) def show_result(): result = request.form['result'] df_result_comment = pd.read_csv('result/%s/result.csv' % result, nrows=1, encoding='CP932') return '{"result":' + json.dumps(result) + ',"comment":' + df_result_comment.T.to_json() + '}'
この処理を学習結果表示のボタンが呼ばれた時に、Javascriptから呼び出す。画面に表示するwriteResult関数は既に作成済みなので、呼び出すだけでいい。
function showResult() { let result = $('#result').val(); if (result == '##new##') { return; } $.post('/show-result',$('#result-form').serialize()) .fail(function(ret) { alert('学習結果表示に失敗しました'); }) .done(function(ret) { let retParse = $.parseJSON(ret); writeResult(retParse); }); }
次に学習結果削除の機能を作成する。Pythonにおいて学習結果ディレクトリを削除する処理を作る。shutilの機能を使い、ディレクトリを階層的に削除する。これも簡単な処理である。
import shutil … @app.route('/delete-result', methods=['POST']) def delete_result(): result = request.form['result'] shutil.rmtree('result/%s' % result) return 'ok'
この処理を学習結果削除のボタンが呼ばれた時に、Javascriptから呼び出す。setResults関数によりリスト表示を変更している。
function deleteResult() { let result = $('#result').val(); if (result == '##new##') { return; } if(confirm(result + ' を削除してよろしいですか?')) { $.post('/delete-result',$('#result-form').serialize()) .fail(function(ret) { alert('削除に失敗しました'); }) .done(function(ret) { setResults(false); }); } }
学習結果ダウンロードは処理が特殊なので飛ばして、学習結果名変更の機能を作成する。Pythonにおいて学習結果ディレクトリ名を変更する処理を作る。安全のため一部の文字を変更する処理や、エラーチェック処理が入っている。
@app.route('/modify-result', methods=['POST']) def modify_result(): result = request.form['result'] new_mod_result = request.form['new-mod-result'] mod_result = new_mod_result.replace('/', '').replace('\\', '').replace(' ', '').replace('.', '') results = os.listdir(path='result') if (mod_result == ''): return '{"ret":"noName"}' if (results.count(mod_result) > 0): return '{"ret":"dupName"}' os.rename('result/%s' % result, 'result/%s' % mod_result) return '{"ret":"ok","result":' + json.dumps(mod_result) + '}'
この処理を学習結果名変更のボタンが呼ばれた時に、Javascriptから呼び出す。setResults関数によりリスト表示を変更している。
function modifyResult() { let result = $('#result').val(); if (result == '##new##') { return; } if(confirm(result + ' の名前を変更してよろしいですか?')) { $.post('/modify-result',$('#result-form').serialize()) .fail(function(ret) { alert('名前の変更に失敗しました'); }) .done(function(ret) { let retParse = $.parseJSON(ret); let retStr = retParse.ret; if (retStr == 'noName') { alert('新規・変更学習結果名がありません'); return; } if (retStr == 'dupName') { alert('新規・変更学習結果名が重複しています'); return; } setResults(retParse.result); }); } }
これで学習結果の再表示・削除・名前変更が一通りできるようになった。機械学習の処理もこれに合わせて一部修正が必要になるが、詳細については1ページ目にあるソースを見て欲しい。
学習結果のダウンロード
学習結果のダウンロードは、基本的には以前作成した元データのダウンロードと同じである。ただし1点だけ大きな違いがある。以前の元データダウンロードはJavascriptを介さずに作成したが、今回の学習結果ダウンロードはJavascriptを介して行う。これはformとボタン(submit・button)の構造が両者で異なるためである。
まずPythonにおいて学習結果をダウンロードする処理を作る。これは以前作成した元データのダウンロードとほぼ同じ処理である。
@app.route('/download-result', methods=['POST']) def download_result(): result = request.form['download-result-name'] return send_file('result/%s/result.csv' % result, as_attachment=True, attachment_filename='%s_result.csv' % result)
この処理を学習結果ダウンロードのボタンが呼ばれた時に、Javascriptから呼び出す。この処理は今までのpost関数などとは様子が異なる。何をやっているか分かるだろうか?
function downloadResult() { let result = $('#result').val(); if (result == '##new##') { return; } $('#download-result-name').val(result); $('#download-result').submit(); }
元データのダウンロードでは、1つのformにsubmitボタンを付けてダウンロードを行った。今回もこれと同じ方法でダウンロードする。そのためにdownload-result-nameという部分にresultの内容を設定し、download-resultというformのsubmitボタンを実行している。つまり一旦Javascriptを介しつつも、最終的にはJavascriptを介さずにダウンロードを行う。Javascriptによるダウンロードは他にも方法があるが複雑な手法が多い。今回は簡易化のため少し特殊な手法を選択している。
学習結果ダウンロードを実行するためのformをHTMLに別途記載する必要がある。styleに"display:none"を指定して見えなくしている。下側のiframeはダウンロード時の画面のちらつきを抑えるテクニックで、元データダウンロードで使ったものと同じである。
<form style="display:none" method="post" id="download-result" target="hidden-download" action="/download-result"> <input name="download-result-name" id="download-result-name"> <input type="submit"> </form> … <iframe style="display:none" name="hidden-download"></iframe>
グラフ表示の変更
グラフ表示機能を変更し、学習結果も表示できるようにする。
まずHTMLにおいて、グラフ表示ボタンのあるformに以下の記載を追加する。
<form id="graph-form"> <div style="margin:10px;"> <label for="graph-result">学習結果:</label> <select name="graph-result" id="graph-result" class="custom-select custom-select-sm"> </select> </div> … <button type="button" class="btn btn-primary" onclick="graph()" style="margin:10px;">グラフ表示</button> </form>
グラフ表示ボタンのformは以下のような画面表示になる。学習結果で元データを選ぶと元データがグラフ表示され、それ以外を選ぶと学習結果がグラフ表示される。
前述のsetResult関数を変更して、ここにも学習結果のリストが表示されるようにする。修正内容は省略する。
Pythonのget_graph関数を変更して、元データ指定でなければ学習結果を読むようにする。
@app.route('/get-graph', methods=['POST']) def get_graph(): graph_x = request.form['graph-x'] graph_y = request.form['graph-y'] result = request.form['graph-result'] if result == '##src##': df_comment = pd.read_csv('src/iris.csv', nrows=1, header=None, usecols=[0], encoding='CP932') df_comment.columns = ['src'] df_data = pd.read_csv('src/iris.csv', skiprows=1, encoding='CP932') else: df_comment = pd.read_csv('result/%s/result.csv' % result, nrows=1, encoding='CP932') df_data = pd.read_csv('result/%s/result.csv' % result, skiprows=2, encoding='CP932') …
最後にJavascriptによるグラフ表示処理を変更する。変更するのはコメントの部分と色指定の部分のみである。
function graph() { … $.post('/get-graph',$('#graph-form').serialize()) .fail(function(ret) { alert('グラフ表示に失敗しました'); }) .done(function(ret) { … let retParse = $.parseJSON(ret); … let graphResult = $('#graph-result').val(); let comment = retParse.comment[0]; let commentStr; if (graphResult == '##new##') { commentStr = '元データ:' + comment.src; } else { commentStr = '元データ:' + comment.src + '、モデル:' + comment.model + '、使用変数:' + comment.var; } $('#graph-comment').html('<p>' + commentStr + '</p>'); chart = c3.generate({ bindto: '#graph-plot', data: { xs: retParse.name.x, columns: colData, type: 'scatter', colors: {'Iris-setosa': '#ff0000ff', 'Iris-versicolor': '#00ff00ff', 'Iris-virginica': '#0000ffff', 'setosa-setosa': '#ff0000ff', 'setosa-versicolor': '#3f1f00ff', 'setosa-virginica': '#3f001fff', 'versicolor-setosa': '#1f3f00ff', 'versicolor-versicolor': '#00ff00ff', 'versicolor-virginica': '#003f1fff', 'virginica-setosa': '#1f003fff', 'virginica-versicolor': '#001f3fff', 'virginica-virginica': '#0000ffff'}}, … }
学習結果のグラフは以下のようになる。グレーの点が誤った学習をした箇所である。
これで第2段階のWebアプリは完成である。ソースについては1ページ目を参照して欲しい。