Python PandasとJavascriptで高速にWeb開発 サンプル付き (3/5)
データ分析で用いられるPython Pandasと、Javascriptフレームワークを組み合わせて、シンプルなSPA(単一ページ)型のWeb開発を行う。この方法はWebアプリの行数を減らし、高速に開発できる。このソースは2019年に作成したものであり、現在の環境ではうまく動かない可能性がある。
グラフ表示ボタンと軸選択
ここからグラフ表示をするための準備作業を行う。単にグラフ表示するだけでなく、軸を選択してそれに合わせてグラフ表示を行う。グラフ表示ボタンと軸選択のHTMLは以下のようになる。
<div class="col-sm-3 border border-info rounded py-3"> <form id="graph-form"> <div style="margin:10px;"> <label for="graph-x">X軸:</label> <select name="graph-x" id="graph-x" class="custom-select custom-select-sm"> {% for col in cols: %} <option value="{{col}}">{{col}}</option> {% endfor %} </select> </div> <div style="margin:10px;"> <label for="graph-y">Y軸:</label> <select name="graph-y" id="graph-y" class="custom-select custom-select-sm"> {% for col in cols_graph_y: %} <option value="{{col[0]}}" {{col[1]}}>{{col[0]}}</option> {% endfor %} </select> </div> <button type="button" class="btn btn-primary" onclick="graph()" style="margin:10px;">グラフ表示</button> </form> </div>
これはBootstrapの列幅3の部分である。selectのclass部分でBootstrapの形式を指定して、select表示を整形している。このような指定方法だと、select部分は以下のように列幅全体を使う表示になる。このような表示にしたくない場合は、Bootstrapのform-inline機能などを使う必要がある。form-inline機能を使うとselect表示などが横並びになる。
optionとして選択する軸を列挙するが、長くなるのでFlaskの機能を使ってforループにしている。これはFlaskのJinja2という、HTMLを整形するテンプレートエンジンである。Pythonでindex.htmlを指定する際に、以下のような記述でcolsとcols_graph_yを指定する。
@app.route('/') def index(): cols = ['sepal_length', 'sepal_width', 'petal_length', 'petal_width'] cols_graph_y = [['sepal_length', ''], ['sepal_width', ''], ['petal_length', 'selected'], ['petal_width', '']] return render_template('index.html', cols=cols, cols_graph_y=cols_graph_y)
なおJinja2を使えばPythonからHTMLをそのまま書く事もできるが、全体の構造が崩れるのでやらない方がいい。
Javascriptでグラフデータの要求をサーバに送る。最初にXY軸が同じならエラーにしている。serialize関数によりHTMLのformに入力した内容をPOSTの形式に変更できる。
function graph() { let graphX = $('#graph-x').val(); let graphY = $('#graph-y').val(); if (graphX == graphY) { alert('XY軸が同じです'); return; } $.post('/get-graph',$('#graph-form').serialize()) .fail(function(ret) { alert('グラフ表示に失敗しました'); }) .done(function(ret) { … }); }
グラフ表示のPython前処理
C3.jsを使って散布図を表示する。書き方は以下のWebページに記載されている。
data指定部分だけを抜き出すと以下のようになる。種別名ごとにデータを分け、さらにそれをX軸とY軸に分け、データの先頭に種別名を入れる必要がある。
… data: { xs: { setosa: 'setosa_x', versicolor: 'versicolor_x', }, columns: [ ["setosa_x", 3.5, 3.0, 3.2, …], ["versicolor_x", 3.2, 3.2, 3.1, …], ["setosa", 0.2, 0.2, 0.2, …], ["versicolor", 1.4, 1.5, 1.5, …], ], type: 'scatter' }, …
普通にプログラムを組むと、そこそこ大変な処理である。これを簡単に処理するためPandasの機能を使う。まずJavascriptからのデータを受け取り、元データファイルからコメントとデータ部分を読み込む。
@app.route('/get-graph', methods=['POST']) def get_graph(): graph_x = request.form['graph-x'] graph_y = request.form['graph-y'] 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')
次に種別名を抽出する。種別名の抽出は、Pandasのunique関数を使うだけでできる。X軸側には"_x"という文字列を追加する必要があるので、その処理も行う。これらをまとめてデータフレームに入れておく。
names = df_data["name"].unique() names_x = names + "_x" df_names = pd.DataFrame({ 'x' : names_x }) df_names.index = names
df_namesデータフレームの内容は以下の通りである。xが列名、Iris-setosa等がインデックス、Iris-versicolor_x等がデータである。
Javascriptでデータを読ませるため、JSON形式に変換する。JSONはJavascriptのデータを文字列に変換し、ファイルなどで読み書きできるようにしたものである。Javascriptオブジェクトを主体にしたデータ形式だが、実際にはJavascriptのデータなら何でも扱える。記述方法はほとんどJavascriptのデータ記述方法と同じである。日本語などはエンコードする必要があるが、JSON変換時に自動処理されるのであまり気にする必要はない。
Pandasにはto_json関数というJSON形式を出力する機能がある。様々な出力オプションがあり、覚えるのに手間がかかるが使うのは簡単である。このあたりは完全に覚えるよりも、必要な時にWebで情報を検索すればよい。以下のWebページに完全な説明がある。
df_namesデータフレームにto_json関数を使うと、以下のような文字列になる。このx部分は、グラフ表示のxs部分そのものである。これをJavascriptに渡した後でそのまま使えばよい。
{"x":{"Iris-setosa":"Iris-setosa_x","Iris-versicolor":"Iris-versicolor_x","Iris-virginica":"Iris-virginica_x"}}
次に数値データを抽出する。数値データは種別名ごとに分割し、さらにX軸とY軸で抽出する必要がある。
これらもPandasの機能で簡単にできる。種別名ごとの分割は行選択の機能を、XY軸の抽出はloc関数を使えばよい。X軸がsepal_length、Y軸がpetal_lengthの場合は以下のようになる。最後に行列をT関数で転置しておく。
df_sel = df_data[df_data.name == 'Iris-setosa'] df_sel_xy = df_sel.loc[:, ['sepal_length', 'petal_length']].T
df_sel_xyデータフレームは以下のようになる。これはグラフ表示のcolumns部分の数値側である。sepal_length行がIris-setosa_xに、petal_length行がIris-setosaに相当する。
これをJSONに変化する。to_json関数のorientオプションを使い、データだけを抜き出す。
df_sel_xy.to_json(orient='values')
変換したJSONデータは以下のようになる。2次元の配列形式に変換される。
[[5.1,4.9,4.7,…],[1.4,1.4,1.3,…]]
最後に全部のデータを1つのJSONにまとめ、postの結果として返す。JSONは文字列データなので、細かい所は自作できる。種別名ごとにループで処理している。数値データのキーとして種別名(Iris-setosa等)を付けている。
Pythonにあるjson.dumps関数により、データフレームでない普通のPythonデータをJSONに変換できる。ここでは文字列だけを変換しているが、リストや辞書データでも変換可能である。
import json … @app.route('/get-graph', methods=['POST']) def get_graph(): … graph_json = '{"comment":' + df_comment.T.to_json() + ',"name":' + df_names.to_json() + ',"data":' head = '{' for name in df_names.index: df_sel = df_data[df_data.name == name] graph_json += head + json.dumps(name) + ':' + df_sel.loc[:, [graph_x, graph_y]].T.to_json(orient='values') head = ',' graph_json += '}}' return graph_json
グラフ表示のJavascript処理
上記のデータをJavascriptで受け取り、C3.jsによる散布図を作成する。まずHTMLによりグラフを書く場所を指定する。グラフの上にコメントも書くようにしておく。
<div id="graph-comment"></div> <div id="graph-plot"></div>
Pythonが作成したJSONデータを受け取る。jQueryの$.parseJSON関数により、JSONからJavascriptのデータ形式に変換できる。
グラフの数値データはまだ完全にできていなので、ここで整形する。種別名のループを作り、X軸用のデータとY軸用のデータを作成する。これをcolDataにまとめて入れる。unshift関数は配列先頭への挿入で、push関数は配列末尾への挿入である。
function graph() { let graphX = $('#graph-x').val(); let graphY = $('#graph-y').val(); … $.post('/get-graph',$('#graph-form').serialize()) … .done(function(ret) { let retParse = $.parseJSON(ret); let colData = []; Object.keys(retParse.name.x).forEach(function (key) { let colDataX = Array.from(retParse.data[key][0]); colDataX.unshift(key + '_x'); colData.push(colDataX); let colDataY = Array.from(retParse.data[key][1]); colDataY.unshift(key); colData.push(colDataY); }); … });
コメントを表示する。jQueryのhtml関数により、HTMLの内容を変更できる。
let commentStr = '元データ:' + retParse.comment[0].src; $('#graph-comment').html('<p>' + commentStr + '</p>');
最後にC3.jsの散布図グラフを表示する。bindtoでグラフの出力先を指定する。colorsで色の指定をしている。
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'}}, axis: { x: { label: graphX, tick: { fit: false}}, y: { label: graphY} }, tooltip: { grouped: false} });
表示されるグラフは以下のようになる。
これで第1段階のWebアプリは完成である。ソースについては1ページ目を参照して欲しい。