0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【PoC開発記】誰もが、普段使うPCで、自然言語で データ分析 ── POLCA-Aisight

0
Last updated at Posted at 2026-09-02

はじめに

こんにちは!
私は インメモリカラムナーデータ処理技術、特に 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つの理由からです。

  1. データが外に出ない:実データは一切外部に送信しない設計にできる
  2. 試行錯誤のコストを気にしなくていい:API従量課金がないため、後述のSelf-Correction(自己修復)のように何度もリトライする設計と相性がいい
  3. オフラインで完結する:ネットワーク環境に依存しない

実装アーキテクチャ

ポイントは、「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(検索拡張生成)の仕組みを使い、プロンプトを送る直前に 以下の情報を自動的に組み立てて 注入しています。

  1. 対象テーブルのスキーマ情報(列名・データタイプ・サンプル値)
  2. 表記ゆれ変換辞書(登録されていれば)
  3. クレンジング/分析タスクごとのプロンプトテンプレート
  4. 直近の実行履歴(文脈維持)

これにより、モデルのファインチューニングをせずに、アプリ特有の制約や語彙をその場で教えることができます。テンプレートには「やってよいこと/やってはいけないこと」を明記し、コード生成範囲を絞り込むことで、小型モデルでも実用的な精度を引き出しています。

参考:モデルのカスタマイズ

モデルをカスタマイズする 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のため、いくつか未解決・未実装などの課題があります。

  1. モデルカスタマイズの限界:クレンジング・分析用のプロンプトテンプレートを用意し、それに沿った語彙・出力フォーマットにさせることで、確かに 生成コードの安定性は上がります。しかし、テスト段階で、7Bモデルは「35歳以下」は正しく<=35と生成できる一方、「10歳以下」では<10(未満相当)に間違えるケースがありました。このコードは構文的には正常なので、サンドボックス実行はエラーなく完了してしまいます。このように、小型モデル(7B)は、テンプレートで明示した境界値表現ですら確率的にしか守れないケースがあり、テンプレートだけでは 完全な精度保証 できませんでした。
    そのため、ユーザープロンプト中の「数値+境界値キーワード」と、生成コード中でその数値に隣接する比較演算子を突き合わせ、矛盾があれば検証エラーとして報告するアルゴリズムを追加しました。既存のSelf-Correctionのリトライループにそのまま乗り、2段構成モデルの仕組みにより32Bへのエスカレーションされる仕組みです。これは、あくまで ヒューリスティック(Heuristic)であり、完全な保証はありません。
  2. PC要件が厳しい:後述の通り、精度重視の32Bモデルはメモリ・GPU要件が高く、誰の手元のPCでも快適に動くわけではありません。
  3. 未実装の機能がある:PoCの段階のため、分析結果のグラフ化機能や、複数テーブルを跨いだジョイン機能はまだ実装できていません。今後の展開課題です。
  4. 品質不安定:まだ、アプリの品質は不安定です。

PoCの成果

  1. ひと通り 動作するアプリが完成した。
  2. ローカルLLMの使い方を 試行錯誤しながら 理解できた。(たぶん)
  3. 実用に耐えうる手応えは掴めた。
    • LLM推論の速度はハードウェア依存です。
      本稿のベンチマーク値は他の環境ではそのまま当てはまりません。
    • NVIDIA GPU搭載機(Windows/Linux)で、VRAMが32Bモデル(約19〜23GB)を収められれば、高速になる可能性が高い(NVIDIA GPUの方が メモリ帯域が広いため)。
      VRAMが不足する場合はCPUオフロードが発生し大幅に遅くなります。
    • GPU非搭載機(内蔵GPUのみ)で32Bモデルの実用は困難です(推論で数分かかる可能性あり)。7Bモデルであれば実用速度が出る可能性はあります。
    • メモリ要件は、32Bモデルは 最低でも24GB以上のメモリを推奨します。 7Bモデルは 16GBあれば動作します。

お試し版の提供

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 を進めるつもりです。

最後まで お読み頂き ありがとうございました。

  1. 実測環境は MacBook Pro(M1 Pro 32GB SSD)、macOS(Tahoe 26.6)です。 ↩

  2. Apple Silicon Mac 用です。 ↩

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?