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ページ目を参照して欲しい。
AsaHP