テーブルデータをLLMに渡して分析させる方法を試してみた

## はじめに
近年、LLM（Large Language Model）の活用がさまざまな分野で進んでいます。その中で、テーブルデータ（例えば、pandasのDataFrameなど）をLLMに渡し、データ解析や要約を行いたいという場面も出てくると思います。

しかし、LLMは構造化データを直接的に理解することができません。具体的には、LLMは自然言語やテキスト形式を処理するように設計されており、以下の理由からテーブルデータを適切な形式に変換する必要があります。


### なぜテーブルデータの変換が必要なのか？
1. **LLMはテキスト処理を前提に設計されている**  
   - LLMは文章や単純なテキスト構造をもとにトレーニングされており、`DataFrame`や`CSV`などの形式をそのままでは解釈できません。
   - そのため、テーブルデータをLLMが処理可能な形式（例えば、JSONやMarkdownなど）に変換する必要があります。

2. **データの構造を保つため**  
   - テーブルデータには行・列という構造があります。この構造を損なわずにLLMに渡すことで、データの意味や関係性を正確に理解させることができます。
   - 適切なフォーマットを選ぶことで、LLMが「どの列が何を表しているか」を認識しやすくなります。

3. **応答の精度向上のため**  
   - 適切な形式でデータを渡すと、LLMはより高い精度で応答を生成できます。逆に、フォーマットが不適切だと情報が欠落したり、誤解される可能性があります。

### なぜこの検証を行うのか？
テーブルデータをテキストで表現するにあたり、データ構造が複雑であるとLLMが行列の関係を正しく理解できず解釈を誤ってしまうケースがあるのではないでしょうか。変換するフォーマットが違うことで解釈の精度差が出てくることも考えられます。
そういった疑問を感じたため検証を行いました。


## 目次
1. [結果のサマリー](#結果のサマリー)
2. [LLMのセットアップ](#llmのセットアップ)
3. [使用データのセットアップ](#使用データのセットアップ)
4. [プロンプトのセットアップ](#プロンプトのセットアップ)
5. [各データ形式の検証](#各データ形式の検証)
   - [1. `dict`形式](#1-dict形式)
   - [2. `json`形式](#2-json形式)
   - [3. `pipe`形式](#3-pipe形式)
   - [4. `list`形式](#4-list形式)
   - [5. `markdown`形式](#5-markdown形式)
   - [6. `yaml`形式](#6-yaml形式)
   - [7. `csv`形式](#7-csv形式)
   - [8. `tsv`形式](#8-tsv形式)
   - [9. `latex`形式](#9-latex形式)
6. [まとめ](#まとめ)
7. [おわりに](#おわりに)


## 結果のサマリー
:::note info
結論、どの形式でもちゃんと回答できました
:::
ただしXMLとHTML形式ではテキスト量が多く、LLMのコンテキストサイズを超えてしまってエラーを発したため未検証となります。



| フォーマット   | 結果   |
|---------------|--------|
| `dict`        | ○   |
| `json`        | ○    | 
| `pipe`        | ○    | 
| `list`        | ○    | 
| `markdown`    | ○    | 
| `yaml`        | ○   | 
| `csv`         | ○    |
| `tsv`         | ○   |
| `latex`       | ○   |
| `xml`         | - | 
| `html`        | -|

検証を行ったものに関しては、どのフォーマットでもうまく解釈できていることが確認できました。システムの中にLLMを組み込むことを考えたときに、どの形式でも対応ができそうなのは嬉しい結果です。
しかし、今回はシンプルなデータセットを用いたため正しく解釈ができていた可能性があることは否めません。もう少し複雑なデータセットや複数回答をさせるケースについてはさらに検証が必要だと思います。

## LLMのセットアップ

○環境
- **Google Colab** 
- **LLM**: elyza/Llama-3-ELYZA-JP-8B-GGUF

```python
from llama_cpp import Llama

llm = Llama.from_pretrained(
	repo_id="elyza/Llama-3-ELYZA-JP-8B-GGUF",
	filename="Llama-3-ELYZA-JP-8B-q4_k_m.gguf",
)
```

## 使用データのセットアップ
今回は第3回WBSCプレミア12スーパーラウンド、ベネズエラ戦の日本のスターティングメンバーのデータを使用しました。
```python
import pandas as pd

# データの作成
data = {
    "打順": [1, 2, 3, 4, 5, 6, 7, 8, 9], 
    "守備位置": ["Left Field", "Second Base", "Center Field", "Right Field", 
                 "Third Base", "First Base", "Shortstop", "DH", "Catcher"],
    "選手": ["桑原将志", "小園海斗", "辰己涼介", "森下翔太", 
               "栗原陵矢", "牧秀悟", "源田壮亮", "佐野恵太", "坂倉将吾"],
    "所属球団": ["DeNA", "広島", "楽天", "阪神", 
             "ソフトバンク", "DeNA", "西武", "DeNA", "広島"]
}

# データフレームの作成
df = pd.DataFrame(data)
```

| index | 打順 | 守備位置       | 選手       | 所属球団   |
|-------|------|----------------|------------|------------|
| 0     | 1    | Left Field     | 桑原将志   | DeNA       |
| 1     | 2    | Second Base    | 小園海斗   | 広島       |
| 2     | 3    | Center Field   | 辰己涼介   | 楽天       |
| 3     | 4    | Right Field    | 森下翔太   | 阪神       |
| 4     | 5    | Third Base     | 栗原陵矢   | ソフトバンク|
| 5     | 6    | First Base     | 牧秀悟     | DeNA       |
| 6     | 7    | Shortstop      | 源田壮亮   | 西武       |
| 7     | 8    | DH             | 佐野恵太   | DeNA       |
| 8     | 9    | Catcher        | 坂倉将吾   | 広島       |

## プロンプトのセットアップ

```python
from langchain_core.prompts import PromptTemplate

prompt_template = PromptTemplate.from_template("""
    以下の質問に答えてください。
    質問: プレミア12の日本vsベネズエラ戦で、{team} の {player} 選手の打順を教えてください。int型の数値のみ回答するようにしてください。
    
    参考情報: {table}
""")
```
補足ですが、元々langchainのLCEL記法で書くつもりをしていました。しかし手元の環境の都合で量子化モデルを用いることにしてtransformersからllama-cppに変更した経緯があり、llama_cpp ライブラリはプロンプトとして文字列（str 型）を期待しているのであえてPromptTempleteを用いる必要はありませんでした。
今回のケースだとテンプレートを用いずそのままプロンプトを直書きでも問題ないです。

## 各データ形式の検証

### 1. dict形式

```python
df_dict = df.to_dict(orient="records")
df_dict
```

変換結果
```
[{'打順': 1, '守備位置': 'Left Field', '選手': '桑原将志', '所属球団': 'DeNA'},
 {'打順': 2, '守備位置': 'Second Base', '選手': '小園海斗', '所属球団': '広島'},
 {'打順': 3, '守備位置': 'Center Field', '選手': '辰己涼介', '所属球団': '楽天'},
 {'打順': 4, '守備位置': 'Right Field', '選手': '森下翔太', '所属球団': '阪神'},
 {'打順': 5, '守備位置': 'Third Base', '選手': '栗原陵矢', '所属球団': 'ソフトバンク'},
 {'打順': 6, '守備位置': 'First Base', '選手': '牧秀悟', '所属球団': 'DeNA'},
 {'打順': 7, '守備位置': 'Shortstop', '選手': '源田壮亮', '所属球団': '西武'},
 {'打順': 8, '守備位置': 'DH', '選手': '佐野恵太', '所属球団': 'DeNA'},
 {'打順': 9, '守備位置': 'Catcher', '選手': '坂倉将吾', '所属球団': '広島'}]
```

実行コード
```python
# 入力データの設定
input_data = {
    "team": "阪神",
    "player": "森下翔太",
    "table": df_dict
}

# プロンプトの生成
prompt = prompt_template.format(**input_data)

# プロンプトを文字列に変換
prompt_str = str(prompt)

# モデルの実行
response = llm(prompt_str)

# 回答部分の抽出
answer = response['choices'][0]['text'].strip()

print(answer)
```

```
答え: 4
```

### 2. json形式

```python
df_json = df.to_json(orient="records", force_ascii=False)
df_json
```

変換結果
```json
[{"打順":1,"守備位置":"Left Field","選手":"桑原将志","所属球団":"DeNA"},{"打順":2,"守備位置":"Second Base","選手":"小園海斗","所属球団":"広島"},{"打順":3,"守備位置":"Center Field","選手":"辰己涼介","所属球団":"楽天"},{"打順":4,"守備位置":"Right Field","選手":"森下翔太","所属球団":"阪神"},{"打順":5,"守備位置":"Third Base","選手":"栗原陵矢","所属球団":"ソフトバンク"},{"打順":6,"守備位置":"First Base","選手":"牧秀悟","所属球団":"DeNA"},{"打順":7,"守備位置":"Shortstop","選手":"源田壮亮","所属球団":"西武"},{"打順":8,"守備位置":"DH","選手":"佐野恵太","所属球団":"DeNA"},{"打順":9,"守備位置":"Catcher","選手":"坂倉将吾","所属球団":"広島"}]
```

実行コード
```python
# 入力データの設定
input_data = {
    "team": "阪神",
    "player": "森下翔太",
    "table": df_json
}

# プロンプトの生成
prompt = prompt_template.format(**input_data)

# プロンプトを文字列に変換
prompt_str = str(prompt)

# モデルの実行
response = llm(prompt_str)

# 回答部分の抽出
answer = response['choices'][0]['text'].strip()

print(answer)
```

```
答え: 4
```

### 3. pipe形式

```python
df_pipe = df.to_csv(index=False, sep="|", mode='a')
df_pipe
```

変換結果
```
打順|守備位置|選手|所属球団
1|Left Field|桑原将志|DeNA
2|Second Base|小園海斗|広島
3|Center Field|辰己涼介|楽天
4|Right Field|森下翔太|阪神
5|Third Base|栗原陵矢|ソフトバンク
6|First Base|牧秀悟|DeNA
7|Shortstop|源田壮亮|西武
8|DH|佐野恵太|DeNA
9|Catcher|坂倉将吾|広島
```

実行コード
```python
# 入力データの設定
input_data = {
    "team": "阪神",
    "player": "森下翔太",
    "table": df_pipe
}

# プロンプトの生成
prompt = prompt_template.format(**input_data)

# プロンプトを文字列に変換
prompt_str = str(prompt)

# モデルの実行
response = llm(prompt_str)

# 回答部分の抽出
answer = response['choices'][0]['text'].strip()

print(answer)
```

```
答え: 4
```

### 4. list形式

```python
df_list = df.values.tolist()
df_list
```

変換結果
```
[[1, 'Left Field', '桑原将志', 'DeNA'],
 [2, 'Second Base', '小園海斗', '広島'],
 [3, 'Center Field', '辰己涼介', '楽天'],
 [4, 'Right Field', '森下翔太', '阪神'],
 [5, 'Third Base', '栗原陵矢', 'ソフトバンク'],
 [6, 'First Base', '牧秀悟', 'DeNA'],
 [7, 'Shortstop', '源田壮亮', '西武'],
 [8, 'DH', '佐野恵太', 'DeNA'],
 [9, 'Catcher', '坂倉将吾', '広島']]
```

実行コード
```python
# 入力データの設定
input_data = {
    "team": "阪神",
    "player": "森下翔太",
    "table": df_list
}

# プロンプトの生成
prompt = prompt_template.format(**input_data)

# プロンプトを文字列に変換
prompt_str = str(prompt)

# モデルの実行
response = llm(prompt_str)

# 回答部分の抽出
answer = response['choices'][0]['text'].strip()

print(answer)
```

```
答え: 4
```

### 5. markdown形式

```python
df_markdown = df.to_markdown(index=False)
df_markdown
```

変換結果
```markdown
|   打順 | 守備位置     | 選手     | 所属球団     |
|-------:|:-------------|:---------|:-------------|
|      1 | Left Field   | 桑原将志 | DeNA         |
|      2 | Second Base  | 小園海斗 | 広島         |
|      3 | Center Field | 辰己涼介 | 楽天         |
|      4 | Right Field  | 森下翔太 | 阪神         |
|      5 | Third Base   | 栗原陵矢 | ソフトバンク |
|      6 | First Base   | 牧秀悟   | DeNA         |
|      7 | Shortstop    | 源田壮亮 | 西武         |
|      8 | DH           | 佐野恵太 | DeNA         |
|      9 | Catcher      | 坂倉将吾 | 広島         |
```

実行コード
```python
# 入力データの設定
input_data = {
    "team": "阪神",
    "player": "森下翔太",
    "table": df_markdown
}

# プロンプトの生成
prompt = prompt_template.format(**input_data)

# プロンプトを文字列に変換
prompt_str = str(prompt)

# モデルの実行
response = llm(prompt_str)

# 回答部分の抽出
answer = response['choices'][0]['text'].strip()

print(answer)
```

```
答え: 4
```

### 6. yaml形式

```python
import yaml

# YAMLに変換
df_yaml = yaml.dump(df.to_dict(orient="records"), allow_unicode=True)
df_yaml
```

変換結果
```
- 守備位置: Left Field
  所属球団: DeNA
  打順: 1
  選手: 桑原将志
- 守備位置: Second Base
  所属球団: 広島
  打順: 2
  選手: 小園海斗
- 守備位置: Center Field
  所属球団: 楽天
  打順: 3
  選手: 辰己涼介
- 守備位置: Right Field
  所属球団: 阪神
  打順: 4
  選手: 森下翔太
- 守備位置: Third Base
  所属球団: ソフトバンク
  打順: 5
  選手: 栗原陵矢
- 守備位置: First Base
  所属球団: DeNA
  打順: 6
  選手: 牧秀悟
- 守備位置: Shortstop
  所属球団: 西武
  打順: 7
  選手: 源田壮亮
- 守備位置: DH
  所属球団: DeNA
  打順: 8
  選手: 佐野恵太
- 守備位置: Catcher
  所属球団: 広島
  打順: 9
  選手: 坂倉将吾

```

実行コード
```python
# 入力データの設定
input_data = {
    "team": "阪神",
    "player": "森下翔太",
    "table": df_yaml
}

# プロンプトの生成
prompt = prompt_template.format(**input_data)

# プロンプトを文字列に変換
prompt_str = str(prompt)

# モデルの実行
response = llm(prompt_str)

# 回答部分の抽出
answer = response['choices'][0]['text'].strip()

print(answer)
```

```
答え: 4
```

### 7. csv形式

```python
df_csv = df.to_csv(index=False)
df_csv
```

変換結果
```
打順,守備位置,選手,所属球団
1,Left Field,桑原将志,DeNA
2,Second Base,小園海斗,広島
3,Center Field,辰己涼介,楽天
4,Right Field,森下翔太,阪神
5,Third Base,栗原陵矢,ソフトバンク
6,First Base,牧秀悟,DeNA
7,Shortstop,源田壮亮,西武
8,DH,佐野恵太,DeNA
9,Catcher,坂倉将吾,広島

```

実行コード
```python
# 入力データの設定
input_data = {
    "team": "阪神",
    "player": "森下翔太",
    "table": df_csv
}

# プロンプトの生成
prompt = prompt_template.format(**input_data)

# プロンプトを文字列に変換
prompt_str = str(prompt)

# モデルの実行
response = llm(prompt_str)

# 回答部分の抽出
answer = response['choices'][0]['text'].strip()

print(answer)
```

```
答え: 4
```

### 8. tsv形式

```python
df_tsv = df.to_csv(index=False, sep='\t')
df_tsv
```

変換結果
```
打順	守備位置	選手	所属球団
1	Left Field	桑原将志	DeNA
2	Second Base	小園海斗	広島
3	Center Field	辰己涼介	楽天
4	Right Field	森下翔太	阪神
5	Third Base	栗原陵矢	ソフトバンク
6	First Base	牧秀悟	DeNA
7	Shortstop	源田壮亮	西武
8	DH	佐野恵太	DeNA
9	Catcher	坂倉将吾	広島

```

実行コード
```python
# 入力データの設定
input_data = {
    "team": "阪神",
    "player": "森下翔太",
    "table": df_tsv
}

# プロンプトの生成
prompt = prompt_template.format(**input_data)

# プロンプトを文字列に変換
prompt_str = str(prompt)

# モデルの実行
response = llm(prompt_str)

# 回答部分の抽出
answer = response['choices'][0]['text'].strip()

print(answer)
```

```
答え: 4
```

### 9. latex形式

```python
df_latex = df.to_latex(index=False)
df_latex
```

変換結果
```
\begin{tabular}{rlll}
\toprule
打順 & 守備位置 & 選手 & 所属球団 \\
\midrule
1 & Left Field & 桑原将志 & DeNA \\
2 & Second Base & 小園海斗 & 広島 \\
3 & Center Field & 辰己涼介 & 楽天 \\
4 & Right Field & 森下翔太 & 阪神 \\
5 & Third Base & 栗原陵矢 & ソフトバンク \\
6 & First Base & 牧秀悟 & DeNA \\
7 & Shortstop & 源田壮亮 & 西武 \\
8 & DH & 佐野恵太 & DeNA \\
9 & Catcher & 坂倉将吾 & 広島 \\
\bottomrule
\end{tabular}

```

実行コード
```python
# 入力データの設定
input_data = {
    "team": "阪神",
    "player": "森下翔太",
    "table": df_latex
}

# プロンプトの生成
prompt = prompt_template.format(**input_data)

# プロンプトを文字列に変換
prompt_str = str(prompt)

# モデルの実行
response = llm(prompt_str)

# 回答部分の抽出
answer = response['choices'][0]['text'].strip()

print(answer)
```

```
答え: 4
```

## まとめ

今回の検証では、pandas DataFrameをさまざまな形式に変換し、それをLLMに入力して適切な応答が得られるかをテストしました。その結果、基本的にはどの形式でもうまく回答できることがわかりました。




### 今後の課題と次のステップ
今回の検証を踏まえ、今後検討すべき課題は以下があると思います。
- **LLMのコンテキストサイズを超えるデータへの対応**:  
  テーブルを分割するか、別の連携方法をとる
- **使用モデルによる精度の差異**:  
  企業モデルや他のOSSモデルで試す
- **より複雑なデータセットを用いた検証**:  
  もっとレコードの多いデータで試してみる
  
これらの要件を変えることで、`dict`や`csv`などLLMが理解するのにより適している形式が新しく見えてくるかもしれません。


## おわりに

この記事では、テーブルデータをLLMに入力するための形式変換とその検証結果を共有しました。この結果が、皆さんのプロジェクトやデータ活用に役立つヒントとなれば幸いです。
最後までお読みいただきありがとうございました！この記事が役に立った場合は、ぜひ「いいね」やコメントをお願いします！