はじめに
こんにちは!
私は インメモリカラムナーデータ処理技術、特に Apache Arrow や Polars に魅了され、個人的にアプリ開発に挑戦しています。また、最近は AI にも興味を持っています。
この度、「データ分析はエンジニアやアナリストだけのもの」——そんな前提を変えたくて、
POLCA-Aisight(ポルカ-アイサイト)というアプリを作ってみました。
狙いはシンプルです...
誰もが、普段使うPCで、自然言語でデータ分析できる
今回は そのPoC(概念実証)として、クラウドのAI APIを一切使わず、ローカルLLMだけで
この考えを実装してみました。この記事では、その設計と試行錯誤の記録をまとめます。
ℹ️ POLCA-Aisight は Polars Cool Application - AI + Insight から命名しました。
アプリの機能概要
POLCA-Aisightは、CSV・Parquet・JSON・Excel ファイルを取り込み、「顧客名列の表記のゆれを統一して」「年齢が35歳以下の女性を抽出して降順にソートして」のような自然言語の指示だけでデータのクレンジングと分析ができるデスクトップアプリです。
代表的な画面イメージ プロンプト画面です。
主な機能は以下の通りです。
- データインポート(CSV / Parquet / JSON / Excel)
- テーブル一覧(名前・行数・列数・メモリサイズの表示、リネーム・削除)
- データビュー(ページング対応のグリッド表示)
- プロンプト実行(自然言語 → Polarsコード自動生成 → 実行)
- レシピ(実行履歴の記録とUndo)
- 表記ゆれ変換辞書
- 定型アプリ生成(よく使う処理をパラメータ化して再利用)
- データエクスポート(CSV / Parquet)
- 操作ログ
ローカルLLMで自動生成されたコード(バックグラウンドの処理)は、全て可視化して、「何をしたのか分からないブラックボックス」にならないようにしています。
ローカルLLMを使う理由
分析対象のデータには、顧客情報や売上といった機微なものも少なくありません。データをクラウドのAI APIにそのまま投げるのは避けたい、という声は多いはずです。POLCA-AisightがローカルLLMにこだわったのは、主に次の3つの理由からです。
- データが外に出ない:実データは一切外部に送信しない設計にできる
- 試行錯誤のコストを気にしなくていい:API従量課金がないため、後述のSelf-Correction(自己修復)のように何度もリトライする設計と相性がいい
- オフラインで完結する:ネットワーク環境に依存しない
実装アーキテクチャ
ポイントは、「Text-to-Polars」で、頭脳(コード生成)とデータ処理エンジン(計算)を明確に分離したことです。LLMは自然言語の指示をPolarsコードに変換し、その結果を解釈して人間向けの説明文を作る「頭脳」役に徹します。実際の計算は一切LLMにやらせず、全てPolarsエンジンに丸投げします。
この分離のおかげで、LLMに渡すのは「テーブル名・列名・型・行数概算・サンプル値数件」といったスキーマ情報だけで済みます。1億行のビッグデータであってもLLMに実データを読ませる必要がないので、プライバシー面でも処理速度の面でも無理がありません。
採用した技術
| レイヤー | 技術 |
|---|---|
| デスクトップシェル | Tauri2 (Rust) |
| フロントエンド | React + TypeScript、AG-Grid、Zustand、react-i18next |
| バックエンドAPI | FastAPI (Python) |
| データ処理エンジン | Polars (LazyFrame) |
| ローカルLLM | Qwen2.5-Coder (via Ollama) |
| ローカルDB | SQLite(レシピ・表記ゆれ辞書・操作ログ) |
| 対応データ形式 | CSV、Parquet、JSON、Excel |
| 国際化 | 英語 / 日本語 |
LLMモデルの選択
コード生成に強いローカルLLMをいくつか比較検討しました。
| モデル | パラメータ数 | 特徴 |
|---|---|---|
| Qwen2.5-Coder | 0.5B〜32B | コード生成・分析能力が最トップクラス。数式やデータ集計の指示に非常に強い。Ollama対応が手厚い。 |
| CodeLlama | 7B〜34B | Meta製の老舗だが、近年のコード生成ベンチマークでは見劣りする |
| StarCoder2 | 3B〜15B | 多言語対応だが、日本語の自然言語指示への追従はやや弱い |
| DeepSeek-Coder(-V2) | 1.3B〜236B(MoE) | 性能は高いが、大型モデルはローカル実行のハードルが高い |
最終的にQwen2.5-Coderを採用しました。決め手は、コード生成特化モデル(LLMにPolarsやSQLのコードを書かせ、ローカルのPython環境で実行して結果を得る手法)として精度が高いことに加えて、0.5B〜32Bまで幅広いサイズ展開がある点です。これにより「速さ」と「精度」を両立させる 後述の 2段構成が実現できました。
LLMの性能 ── 2段構成モデル
ローカルLLMの悩みどころは、精度を求めるほど大きなモデルが必要になり、速度が犠牲になることです。下表は、モデル毎の生成速度で、プロンプトの応答時間は 表に示すような差が出ました。1
| モデル | 生成速度 | 典型的なプロンプトの応答時間 |
|---|---|---|
| Qwen2.5-Coder 7B | 約25 トークン/秒 | 約3〜4秒 |
| Qwen2.5-Coder 32B | 約5.7 トークン/秒 | 約13〜18秒 |
そこでPOLCA-Aisightでは、普段は高速な7Bで まず 試行し、実行エラーや意味的な誤りを検知した場合のみ高精度な32Bにエスカレーションする 2段構成 を採用しました。一度エスカレーションしたら7Bには戻さず、そのプロンプト内では32Bで固定します。体感速度と精度のバランスを取る、実用的な落としどころです。
参考:ベンチマーク
Qwen2.5-Coder 32B と7B を複数プロンプトで比較するベンチマーク結果を下記に示します。実測環境は上記と同じです。
########################################
Qwen2.5-Coder 7B
########################################
(ウォームアップ中...)
--- prompt1 ---
モデルロード: 2 ms
プロンプト処理: 49 ms (1161 tokens)
生成: 2834 ms (71 tokens, 25.1 tokens/秒)
df_result = df.filter(pl.col("gender") == "female").filter(pl.col("age") <= 35).sort(by="age", descending=True)
年齢が35歳以下の女性を抽出し、年齢で降順にソートします。
--- prompt2 ---
モデルロード: 1 ms
プロンプト処理: 449 ms (1163 tokens)
生成: 2910 ms (73 tokens, 25.1 tokens/秒)
df_result = df.filter(pl.col("amount") >= 5000).sort(by="amount", descending=True)
金額が5000円未満の注文を除外し、残りの注文を金額の高い順に並べます。
--- prompt3 ---
モデルロード: 3 ms
プロンプト処理: 449 ms (1146 tokens)
生成: 3354 ms (84 tokens, 25.0 tokens/秒)
df_filtered = df.filter((pl.col("price") >= 1000) & (pl.col("price") < 3000)).group_by("category").agg(pl.len())
価格が1000円以上3000円未満の商品をカテゴリごとに件数集計します。
########################################
Qwen2.5-Coder 32B
########################################
(ウォームアップ中...)
--- prompt1 ---
モデルロード: 13 ms
プロンプト処理: 184 ms (1161 tokens)
生成: 13251 ms (76 tokens, 5.7 tokens/秒)
df_result = df.filter((pl.col("age") <= 35) & (pl.col("gender") == "female")).sort("age", descending=True)
年齢が35歳以下かつ女性の顧客を抽出し、年齢で降順にソートします。
--- prompt2 ---
モデルロード: 3 ms
プロンプト処理: 2114 ms (1163 tokens)
生成: 12737 ms (73 tokens, 5.7 tokens/秒)
df_result = df.filter(pl.col("amount") >= 5000).sort("amount", descending=True)
金額が5000円未満の注文を除外し、残りの注文を金額の高い順に並べ替えます。
--- prompt3 ---
モデルロード: 1 ms
プロンプト処理: 2057 ms (1146 tokens)
生成: 15388 ms (88 tokens, 5.7 tokens/秒)
df_result = df.filter((pl.col("price") >= 1000) & (pl.col("price") < 3000)).group_by("category").agg(pl.len().alias("count"))
価格が1000円以上3000円未満の商品をカテゴリごとに件数を集計します。
LLMの精度 ── RAG でアプリの特性を教える
小型モデルでは、指示を出すだけでは 期待通りのコードを生成してくれない ことがあります。POLCA-AisightではRAG(検索拡張生成)の仕組みを使い、プロンプトを送る直前に 以下の情報を自動的に組み立てて 注入しています。
- 対象テーブルのスキーマ情報(列名・データタイプ・サンプル値)
- 表記ゆれ変換辞書(登録されていれば)
- クレンジング/分析タスクごとのプロンプトテンプレート
- 直近の実行履歴(文脈維持)
これにより、モデルのファインチューニングをせずに、アプリ特有の制約や語彙をその場で教えることができます。テンプレートには「やってよいこと/やってはいけないこと」を明記し、コード生成範囲を絞り込むことで、小型モデルでも実用的な精度を引き出しています。
参考:モデルのカスタマイズ
モデルをカスタマイズする Modelfile を下記に示します。
# =====================================================================
# Modelfile - Text-to-Polars コード生成用カスタムモデル
# ベースモデル: Qwen2.5-Coder
# 用途: ビッグデータ(最大1億行)のプロファイリング/クレンジング/分析 を行う
# Polars(LazyFrame) コードの自動生成
# =====================================================================
FROM qwen2.5-coder:32b
# --- 生成パラメータ ---
# コード生成タスクのため、決定論的な出力に寄せる(創造性より正確性・再現性を優先)
PARAMETER temperature 0.1
PARAMETER top_p 0.9
PARAMETER top_k 40
PARAMETER num_ctx 16384
PARAMETER repeat_penalty 1.1
# 出力の暴走防止(1回の応答で生成しうる最大トークン数)
PARAMETER num_predict 2048
# --- システムプロンプト ---
SYSTEM """
あなたは Polars(Python版, LazyFrame API)専用のコード生成アシスタントです。
あなたの役割は「頭脳」であり、実際の計算は全て Polars エンジンが行います。
以下のルールを厳格に守ってください。
## 前提条件
- あなたには生データそのものは渡されません。渡されるのはテーブルのスキーマ情報
(テーブル名、列名、列の型、行数概算、サンプル値数件)と、表記ゆれ変換辞書、
過去の実行レシピ(文脈)のみです。
- 生成したコードは Polars エンジン側でサンドボックス実行され、結果(成功時は
実行結果の要約統計、失敗時はエラーメッセージ)がフィードバックされます。
## コード生成ルール
1. 必ず `polars` を `pl` としてインポート済みである前提でコードを書く(import文は不要)。
2. 必ず LazyFrame API(`.lazy()` / `pl.scan_*` 系)を使用し、`.collect()` は
明示的に指示された場合、または最終結果を確定する必要がある場合のみ呼び出す。
中間ステップでは `.collect()` を呼ばない。
:
以下省略
:
:
クレンジング用テンプレート(6種類のひとつ)を下記に示します。
# テンプレート: 欠損値処理(Missing Values)
## 適用条件
「欠損」「NULL」「NaN」「空欄」「未入力」等のキーワードが含まれる場合。
## コード生成の範囲
**やってよいこと**
- `.fill_null()` による固定値・統計量(平均・中央値・最頻値)・前後値(forward/backward fill)補完
- `.drop_nulls()` による欠損行の除外(対象列を明示すること)
- 欠損率の集計(`.null_count()`)を先に提示するコードの生成
**やってはいけないこと**
- 補完方法をユーザー指示なしに一方的に決めない(未指定の場合は複数案を
result-explanation に提示し、デフォルトとして最も保守的な方法=行除外を選ばない
で「補完なし・欠損可視化のみ」のコードを返す)
- 数値以外の列に平均・中央値補完を適用しない
## プロンプト注入テンプレート
```
### タスク: 欠損値処理
対象列: {target_columns}(未指定なら欠損を含む全列を対象に欠損率を集計するコードを生成)
補完方針: {fill_strategy}(例: 平均値, 中央値, 固定値X, 前方補完, 除外 など。未指定なら
まず欠損状況の集計コードのみを生成し、補完方法の提案を result-explanation に記載する)
```
分析用テンプレート(5種類のひとつ)を下記の示します。
# テンプレート: 集計・グループ集計(Aggregation / GroupBy)
## 適用条件
「集計」「合計」「平均」「グループごと」「〜別に」等のキーワードが含まれる場合。
## コード生成の範囲
**やってよいこと**
- `.group_by()` + `.agg()` によるグループ集計(sum, mean, count, min, max, median等)
- 集計後の並び替え(`.sort()`)、上位N件抽出(`.head()`)
- 集計軸・集計値がスキーマ上存在することを確認した上でのコード生成
**やってはいけないこと**
- 明示されていない集計関数を勝手に追加しない(合計のみ要求時に平均も追加しない)
- グループ数が極端に多くなりうる列(高カーディナリティ)をキーにする場合、
result-explanation でその旨(表示件数が多くなる可能性)に触れる
## プロンプト注入テンプレート
```
### タスク: 集計
グループ化する列: {group_by_columns}
集計対象列と関数: {agg_specs}(例: amount: sum, quantity: mean)
並び替え: {sort_option}(未指定なら集計値の降順)
```
課題と対策
PoCのため、いくつか未解決・未実装などの課題があります。
-
モデルカスタマイズの限界:クレンジング・分析用のプロンプトテンプレートを用意し、それに沿った語彙・出力フォーマットにさせることで、確かに 生成コードの安定性は上がります。しかし、テスト段階で、7Bモデルは「35歳以下」は正しく<=35と生成できる一方、「10歳以下」では<10(未満相当)に間違えるケースがありました。このコードは構文的には正常なので、サンドボックス実行はエラーなく完了してしまいます。このように、小型モデル(7B)は、テンプレートで明示した境界値表現ですら確率的にしか守れないケースがあり、テンプレートだけでは 完全な精度保証 できませんでした。
そのため、ユーザープロンプト中の「数値+境界値キーワード」と、生成コード中でその数値に隣接する比較演算子を突き合わせ、矛盾があれば検証エラーとして報告するアルゴリズムを追加しました。既存のSelf-Correctionのリトライループにそのまま乗り、2段構成モデルの仕組みにより32Bへのエスカレーションされる仕組みです。これは、あくまで ヒューリスティック(Heuristic)であり、完全な保証はありません。 - PC要件が厳しい:後述の通り、精度重視の32Bモデルはメモリ・GPU要件が高く、誰の手元のPCでも快適に動くわけではありません。
- 未実装の機能がある:PoCの段階のため、分析結果のグラフ化機能や、複数テーブルを跨いだジョイン機能はまだ実装できていません。今後の展開課題です。
- 品質不安定:まだ、アプリの品質は不安定です。
PoCの成果
- ひと通り 動作するアプリが完成した。
- ローカルLLMの使い方を 試行錯誤しながら 理解できた。(たぶん)
- 実用に耐えうる手応えは掴めた。
- LLM推論の速度はハードウェア依存です。
本稿のベンチマーク値は他の環境ではそのまま当てはまりません。 - NVIDIA GPU搭載機(Windows/Linux)で、VRAMが32Bモデル(約19〜23GB)を収められれば、高速になる可能性が高い(NVIDIA GPUの方が メモリ帯域が広いため)。
VRAMが不足する場合はCPUオフロードが発生し大幅に遅くなります。 - GPU非搭載機(内蔵GPUのみ)で32Bモデルの実用は困難です(推論で数分かかる可能性あり)。7Bモデルであれば実用速度が出る可能性はあります。
- メモリ要件は、32Bモデルは 最低でも24GB以上のメモリを推奨します。 7Bモデルは 16GBあれば動作します。
- LLM推論の速度はハードウェア依存です。
お試し版の提供
PoC版 POLCA-Aisightは ここ からダウンロードできます。macOS 2 のみ提供しています。
お試しいただく場合の、最低限のPC要件の目安です。
| 項目 | 最低要件(7B運用) | 推奨(32Bも運用) |
|---|---|---|
| メモリ | 16GB以上 | 32GB以上 |
| GPU/メモリ帯域 | Apple Siliconユニファイドメモリ 16GB以上 | Apple Siliconユニファイドメモリ 32GB以上 |
| ストレージ空き容量 | 約10GB | 約25GB以上 |
| OS | macOS (Apple Silicon) | 同左 |
| その他 | Ollama導入済み | 同左 |
まずは7Bモデルだけでも十分に体験できます。より精度を求める場合は、32Bモデルとの
2段構成をお試しください。
まとめ
「誰もが、普段使うPCで、自然言語でデータ分析する」というゴールに向けて、ローカルLLMだけでどこまで実用的なものが作れるか——それがこのPoCの狙いでした。
Text-to-Polarsで 頭脳とエンジンを分離し、2段構成モデルとRAGで速度と精度のバランスを取ることで、実用に耐えうる手応えは掴めたと感じています。一方で、分析結果のグラフ化や複数テーブルのジョインといった機能面の拡張、そして小型モデル特有の精度のブレへの対処は、まだ道半ばです。
引き続き PoC を進めるつもりです。
最後まで お読み頂き ありがとうございました。