はじめに
ブラウザ上でブロックを組み立てるだけでPythonのデータ分析が実行できるWebアプリケーションModuloxs をリリースしました!
この記事では、Moduloxsを支える技術的な側面に焦点を当てて紹介してみようと思います!
Moduloxs って?
Moduloxsはブラウザ上で完結する、データ分析特化のブロックプログラミングアプリです
【Scratch×データ分析】プログラミング初心者でも触れるModuloxs入門
ブラウザ上で完結するため、プログラミング初学者やデータ分析を学習したいと思っている方でも開発環境を気にすることなく、気軽にプログラミングすることができます
アーキテクチャ
- フロントエンド: Next.js, React, TypeScript, Material-UI
- ブロックエディタ: Blockly
- Python実行環境: Pyodide
- グラフ表示: Plotly
ブロックエディタ周りにはScatachが使用しているBlocklyを採用し、コーディングから実行までをブラウザで完結させるために、Pythonの実行環境にはPyodideを採用しています。
変わった点としてはグラフ表示などの可視化には、Pythonでよく使われるmatplotlibやseabornではなくPlotylyを採用しています
BlocklyによるPythonコード生成の工夫
Moduloxsの心臓部ともいえる「ブロックからPythonコードへの変換」は、Blocklyのカスタムブロック機能を使って実装しています。
コード生成のテンプレート化
Blocklyのコードジェネレータはとっても強力なんですが、import
文の管理みたいに、複数のブロックにまたがるコードの依存関係を解決するのが、素直にやるとちょっと大変だったりします。
そこで、Moduloxsではちょっとした独自ルールを設けて、コード生成をテンプレート化してみました。
Pythonスクリプトのテンプレートをsrc/lib/blockly/workspace/blocks/template
に用意して、その中に次のような”仕切り”を入れています。
import pandas as pd
from sklearn.model_selection import train_test_split
# --- BLOCKLY TEMPLATE ---
df = pd.read_csv('./data.csv')
# ... (ブロックによって生成されるコード)
コードを生成するときに、この# --- BLOCKLY TEMPLATE ---
という文字列でスクリプトを真っ二つに分割します。
-
前半部分:
import
文みたいに、スクリプト全体で必要になるコードをまとめて書いておきます。これで、複数のブロックで同じライブラリが必要になっても、ここで一元管理できます - 後半部分: 実際にブロックが変換されるコードのテンプレート部分です
export function stripImports(
script: string,
generator: PythonGenerator,
): string {
const parts = script.split(SEPARATOR);
const header = parts[0];
const body = parts.length > 1 ? parts[1] : '';
const importRegex = /(?:import|from) .*(?:\n|$)/g;
const imports = header.match(importRegex) || [];
for (const imp of imports) {
const cleanImport = imp.trim();
if (cleanImport) {
(generator as any).definitions_[cleanImport] = cleanImport;
}
}
return body.trim();
}
(generator as any).definitions_[cleanImport] = cleanImport;
の部分がBlocklyのジェネレーターで必ず先頭に置く要素として追加する箇所です。(この書き方については公式のリファレンスで触れられている箇所がなかったため、AIに聞きながら実装しました🫠)
この仕組みのおかげで、各ブロックのコード生成ロジックは「自分のことだけ」を考えればよくなり、import
文が重複しちゃう…みたいな心配をしなくて済むようになりました!
Pyodideによるブラウザ内Python実行
フロントエンドでPythonコードを実行するために、Pyodideに頑張ってもらっています。
Pyodideは、Pythonの実行環境そのものをWebAssemblyにコンパイルしたもの、ブラウザの上でpandas
, scikit-learn
といった主要な科学計算ライブラリはビルドインされているため直接動かすことができます。
また、ビルドインされていないパッケージについても、micropip
を通して追加でインストールすることができます
await pyodideRef.current.loadPackage('micropip');
const micropip = pyodideRef.current.pyimport('micropip');
await micropip.install('plotly');
Pythonコードの実行
Blocklyで生成したPythonコードは、pyodide.runPythonAsync()
という関数に渡すだけで、あっさり実行できちゃいます。
// Pyodideのインスタンスを召喚
const pyodide = await loadPyodide();
// 必要なライブラリをロード
await pyodide.loadPackage(['pandas', 'scikit-learn']);
// Blocklyが生成したPythonコード
const pythonCode = `
import pandas as pd
print("Hello from Pyodide!")
`;
// Pythonコードを実行!
const result = await pyodide.runPythonAsync(pythonCode);
PyodideとPlotly.jsでグラフを描画する 📈
Moduloxsの面白い機能の一つに、分析結果をグラフで可視化する機能があります。これもPyodideと、グラフ描画ライブラリのPlotlyを連携させることで実現しています。
なぜPlotlyかというと、Python (plotly
) と JavaScript (plotly.js
) の両方にライブラリが存在していて、JSON形式でグラフのデータをやり取りできるので、Moduloxsとの相性が抜群でした
具体的な処理の流れはこんな感じです。
-
【Python側】Plotlyでグラフオブジェクトを作り、JSONに変換する
pandas
で処理したデータを元に、plotly.express
などでグラフのオブジェクト(Figure
)を作ります。そして、そのオブジェクトを.to_json()
メソッドでJSON文字列に変換します。# Python側 (Pyodideで実行) import pandas as pd import plotly.express as px import json # サンプルのDataFrameを作成 df = px.data.iris() # グラフオブジェクトを作成 fig = px.scatter(df, x="sepal_width", y="sepal_length", color="species") # グラフをJSON形式に変換して、標準出力で流す print(fig.to_json())
-
【JavaScript側】JSONを受け取ってPlotly.jsで描画する
JavaScript側ではPyodideの標準出力からplotlyのjson形式に当てはまるものをキャッチしてグラフに変換しますpyodideRef.setStdout({ batched: (message: string) => { const parsed = PlotlyFigureSchema.safeParse(message); if (parsed.success) { log.addGraph({ figure: parsed.data }); } else { log.addLog({ message }); } }, });
これによって、グラフの作成、表示までをPythonで行い、JavaScript側では表示にのみ専念することができるため、Blocklyでのブロック作成に制限を掛けることなくデータの可視化ができます!
まとめ
この記事では、BlocklyやPyodide,Plotlyというライブラリを活用して、ブラウザ完結型のPythonデータ分析アプリ「Moduloxs」を開発した際の、技術的な工夫や裏側について紹介しました。
「環境構築」という最初の壁を少しでも低くして、一人でも多くの人がデータ分析の世界に飛び込むきっかけになれば嬉しいです!
実際に触ってみて、フィードバックをいただけると開発の励みになります!🚀