生成AI予測モデル
生成AI(LLM)を活用した競馬予測の新しいアプローチを学びます。
このチュートリアルで学ぶこと
- • LLMを使った競馬データの分析と予測
- • プロンプトエンジニアリングによる予測精度の向上
- • 機械学習モデルとLLMの組み合わせ
- • 自然言語による予測理由の生成
なぜ生成AIを使うのか?
従来の機械学習モデル(LightGBM等)は数値データの処理に優れていますが、 生成AIには以下の強みがあります:
テキスト情報の活用
コメント、レース展開予想、調教師のコメントなど 非構造化テキストを直接分析できる
文脈理解
「休み明け」「距離延長」「初コース」などの 複合的な状況を自然に理解できる
説明可能性
予測の根拠を自然言語で説明できるため、 人間が判断を検証しやすい
柔軟性
新しい条件や例外的なケースにも プロンプトの調整で対応できる
Step 1: 環境セットアップ
OpenAI API または Anthropic Claude API を使用します。
pip install openai anthropic pandas requests # 環境変数の設定 # OPENAI_API_KEY=sk-... # ANTHROPIC_API_KEY=sk-ant-... # JRDB_API_KEY=your_jrdb_api_key # JRDB_AUTH_KEY=your_jrdb_auth_key
import os
import json
import pandas as pd
import requests
from openai import OpenAI
# from anthropic import Anthropic
# APIクライアントの初期化
openai_client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
# anthropic_client = Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
API_KEY = os.getenv("JRDB_API_KEY")
AUTH_KEY = os.getenv("JRDB_AUTH_KEY")Step 2: LLM用データの準備
LLMに渡すデータを人間が読みやすい形式に変換します。
def fetch(endpoint, racedate):
"""今週データを取得して DataFrame にする"""
url = f"https://dev.api.bigtime.world/v1/{endpoint}?api_key={API_KEY}"
headers = {
"Content-Type": "application/json",
"x-user-key": AUTH_KEY
}
response = requests.post(url, json={"racedate": racedate}, headers=headers)
data = response.json()
# 成功時は result.status == 200。エラー時はトップレベルに error/message が返る
if data.get("error") or data["result"]["status"] != 200:
raise Exception(data.get("message", "Unknown error"))
return pd.DataFrame(data["result"]["data"])
def fetch_race_data(racedate, racekey):
"""出馬表・レース条件・直前情報をまとめて1レース分取り出す"""
ky = fetch("tky", racedate) # 指数・騎手・血統など
ra = fetch("tra", racedate) # レース名・距離・トラック種別
pd_ = fetch("tpd", racedate) # 馬体重・当日オッズ
df = ky.merge(
ra[['racekey', 'race_name', 'distance', 'track_type',
'racecourse_cd', 'turf_condition_cd']],
on='racekey', how='left'
).merge(
pd_[['racekey', 'horse_number', 'horse_weight',
'weight_cycling', 'win_odds']],
on=['racekey', 'horse_number'], how='left'
)
return df[df['racekey'] == racekey]
# コード → 表示名(コード表ページを参照)
SEX = {'1': '牡', '2': '牝', '3': 'セ'}
FOOTTYPE = {'1': '逃げ', '2': '先行', '3': '差し', '4': '追込',
'5': '好位差し', '6': '自在'}
TRACK = {'1': '芝', '2': 'ダート', '3': '障害'}
CONDITION = {'1': '良', '2': '稍重', '3': '重', '4': '不良'}
RACECOURSE = {'01': '札幌', '02': '函館', '03': '福島', '04': '新潟', '05': '東京',
'06': '中山', '07': '中京', '08': '京都', '09': '阪神', '10': '小倉'}
def format_horse_info(row):
"""馬情報をテキスト形式に変換"""
info = f"""
【{row['horse_number']}番 {row['horse_name']}】
・性齢: {SEX.get(str(row['sex_cd']), '?')}{row['horse_age']}歳
・騎手: {row['jockey_name']}
・調教師: {row['trainer_name']}
・馬体重: {row['horse_weight']}kg({row['weight_cycling']}kg)
・オッズ: {row['win_odds']}倍(基準{row['criteria_win_popularity']}番人気)
・IDM: {row['adjust_idm']}
・前走: {row['lastrace_racekey1']}
・脚質: {FOOTTYPE.get(str(row['foottype_cd']), '?')}
"""
return info.strip()
def format_race_info(df):
"""レース情報をテキスト形式に変換"""
# レース基本情報(1行目から取得)
race = df.iloc[0]
race_info = f"""
【レース情報】
・レース名: {race.get('race_name', 'Unknown')}
・競馬場: {RACECOURSE.get(str(race.get('racecourse_cd')), 'Unknown')}
・距離: {race.get('distance', 'Unknown')}m {TRACK.get(str(race.get('track_type')), '')}
・馬場状態: {CONDITION.get(str(race.get('turf_condition_cd')), 'Unknown')}
・頭数: {len(df)}頭
"""
horses_info = "\n【出走馬一覧】\n"
for _, row in df.iterrows():
horses_info += format_horse_info(row) + "\n"
return race_info + horses_infoStep 3: LLMによる基本予測
シンプルなプロンプトでLLMに予測させます。
def predict_with_llm(race_text, model="gpt-4o"):
"""LLMでレース予測を行う"""
system_prompt = """あなたは競馬予想の専門家です。
与えられたレース情報を分析し、上位3頭を予測してください。
予測の際は以下の観点を考慮してください:
- 各馬の能力指数(IDM)
- 距離適性と脚質
- 馬体重の増減
- 騎手・調教師の成績
- 前走からの上積み
回答は以下のJSON形式で返してください:
{
"predictions": [
{"rank": 1, "horse_number": 1, "horse_name": "馬名", "reason": "選出理由"},
{"rank": 2, "horse_number": 2, "horse_name": "馬名", "reason": "選出理由"},
{"rank": 3, "horse_number": 3, "horse_name": "馬名", "reason": "選出理由"}
],
"race_analysis": "レース展開の予想",
"risk_factors": ["リスク要因1", "リスク要因2"]
}"""
response = openai_client.chat.completions.create(
model=model,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": race_text}
],
response_format={"type": "json_object"},
temperature=0.3 # 安定した出力のため低めに設定
)
result = json.loads(response.choices[0].message.content)
return result
# 実行例
df = fetch_race_data("20250105", "2025010506010101")
race_text = format_race_info(df)
prediction = predict_with_llm(race_text)
print("=== AI予測結果 ===")
for p in prediction["predictions"]:
print(f"{p['rank']}位: {p['horse_number']}番 {p['horse_name']}")
print(f" 理由: {p['reason']}")
print(f"\n展開予想: {prediction['race_analysis']}")Step 4: 高度なプロンプトエンジニアリング
Chain of Thought(思考の連鎖)を使って、より精度の高い予測を行います。
def predict_with_cot(race_text, model="gpt-4o"):
"""Chain of Thoughtを使った詳細な予測"""
system_prompt = """あなたは競馬予想の専門家です。
以下のステップで分析を行い、最終的な予測を導き出してください。
## 分析ステップ
### Step 1: 能力評価
各馬のIDM、過去成績から基礎能力を評価してください。
### Step 2: 適性評価
今回のコース(距離、馬場状態)への適性を評価してください。
### Step 3: 状態評価
馬体重の増減、休み明け、ローテーションから現在の状態を評価してください。
### Step 4: 展開予想
逃げ・先行馬の数、ペースの予想を行ってください。
### Step 5: 総合判断
上記を踏まえ、各馬の勝利確率を推定し、上位3頭を選出してください。
## 出力形式
各ステップの分析結果を示した後、最終的な予測をJSON形式で出力してください。
最終出力JSON:
{
"step1_ability": {"top3": ["馬名1", "馬名2", "馬名3"], "analysis": "分析"},
"step2_aptitude": {"favored": ["馬名"], "concerns": ["馬名"], "analysis": "分析"},
"step3_condition": {"good": ["馬名"], "bad": ["馬名"], "analysis": "分析"},
"step4_pace": {"prediction": "ハイペース/ミドルペース/スローペース", "key_horses": ["馬名"], "analysis": "分析"},
"final_predictions": [
{"rank": 1, "horse_number": 1, "horse_name": "馬名", "win_probability": 0.25, "reason": "理由"},
{"rank": 2, "horse_number": 2, "horse_name": "馬名", "win_probability": 0.18, "reason": "理由"},
{"rank": 3, "horse_number": 3, "horse_name": "馬名", "win_probability": 0.12, "reason": "理由"}
],
"confidence": "high/medium/low",
"confidence_reason": "自信度の理由"
}"""
response = openai_client.chat.completions.create(
model=model,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": race_text}
],
response_format={"type": "json_object"},
temperature=0.2
)
result = json.loads(response.choices[0].message.content)
return resultStep 5: 機械学習モデルとの組み合わせ
LightGBMの予測結果をLLMに渡して、最終判断を行うハイブリッドアプローチです。
import lightgbm as lgb
def hybrid_prediction(df, model_path='horse_race_model.lgb'):
"""MLモデルとLLMを組み合わせた予測"""
# 1. 機械学習モデルで予測
model = lgb.Booster(model_file=model_path)
feature_columns = [...] # 学習時と同じ特徴量
X = preprocess_features(df, feature_columns)
ml_predictions = model.predict(X, num_iteration=model.best_iteration)
df['ml_probability'] = ml_predictions
df['ml_rank'] = df['ml_probability'].rank(ascending=False)
# 2. ML予測結果を含むテキストを作成
race_text = format_race_info(df)
ml_summary = "\n【機械学習モデルの予測】\n"
ml_top5 = df.nsmallest(5, 'ml_rank')
for _, row in ml_top5.iterrows():
ml_summary += f"・{row['horse_number']}番 {row['horse_name']}: "
ml_summary += f"勝率 {row['ml_probability']*100:.1f}%\n"
combined_text = race_text + ml_summary
# 3. LLMで最終判断
system_prompt = """あなたは競馬予想の専門家です。
レース情報と機械学習モデルの予測結果を参考に、最終的な予測を行ってください。
機械学習モデルの予測は過去データに基づく客観的な評価ですが、
以下の点は考慮されていない可能性があります:
- 当日の馬場状態の変化
- 騎手の戦略的な騎乗
- 展開の有利不利
- 調子の上昇/下降トレンド
これらを考慮して、機械学習の予測を補正してください。
出力形式:
{
"ml_agreement": ["MLの予測に同意する馬番"],
"ml_disagreement": [
{"horse_number": 1, "ml_rank": 3, "my_rank": 1, "reason": "理由"}
],
"final_predictions": [
{"rank": 1, "horse_number": 1, "horse_name": "馬名", "confidence": 0.8},
{"rank": 2, "horse_number": 2, "horse_name": "馬名", "confidence": 0.6},
{"rank": 3, "horse_number": 3, "horse_name": "馬名", "confidence": 0.5}
],
"betting_advice": "馬券のアドバイス"
}"""
response = openai_client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": combined_text}
],
response_format={"type": "json_object"},
temperature=0.3
)
return json.loads(response.choices[0].message.content)ハイブリッドアプローチの利点
- • MLモデル: 大量の数値データから客観的なパターンを学習
- • LLM: 文脈理解、例外的ケースの判断、説明生成
- • 両者の強みを組み合わせることで予測精度を向上
Step 6: 複数レースのバッチ処理
1日分の全レースを効率的に処理する方法です。
import asyncio
from openai import AsyncOpenAI
async_client = AsyncOpenAI(api_key=os.getenv("OPENAI_API_KEY"))
async def predict_race_async(race_text, racekey):
"""非同期でレース予測を行う"""
try:
response = await async_client.chat.completions.create(
model="gpt-4o-mini", # バッチ処理にはコスト効率の良いモデルを使用
messages=[
{"role": "system", "content": PREDICTION_PROMPT},
{"role": "user", "content": race_text}
],
response_format={"type": "json_object"},
temperature=0.3
)
result = json.loads(response.choices[0].message.content)
result['racekey'] = racekey
return result
except Exception as e:
return {"racekey": racekey, "error": str(e)}
async def predict_all_races(racedate):
"""1日分の全レースを予測"""
# 全レースデータを取得
url = f"https://dev.api.bigtime.world/v1/tky?api_key={API_KEY}"
headers = {"Content-Type": "application/json", "x-user-key": AUTH_KEY}
payload = {"racedate": racedate}
response = requests.post(url, json=payload, headers=headers)
df = pd.DataFrame(response.json()["result"]["data"])
# レース毎にグループ化
races = df.groupby('racekey')
# 非同期タスクを作成
tasks = []
for racekey, race_df in races:
race_text = format_race_info(race_df)
tasks.append(predict_race_async(race_text, racekey))
# 並列実行(レート制限に注意)
results = await asyncio.gather(*tasks)
return results
# 実行
# results = asyncio.run(predict_all_races("20250105"))コストとレート制限に注意
- • GPT-4oは高精度だがコストが高い(1レース約$0.05-0.10)
- • GPT-4o-miniはコスト効率が良い(1レース約$0.001-0.005)
- • APIのレート制限に注意(並列数を制限するか遅延を入れる)
- • キャッシュを活用して同じリクエストを繰り返さない
Step 7: 予測精度の評価と改善
予測結果を実際のレース結果と比較して、精度を評価します。
def evaluate_predictions(predictions, actual_results):
"""予測精度を評価"""
metrics = {
'total_races': 0,
'top1_hit': 0, # 1着的中
'top3_hit': 0, # 3着以内的中
'trifecta_hit': 0, # 3連単的中
}
for pred, actual in zip(predictions, actual_results):
metrics['total_races'] += 1
pred_top3 = [p['horse_number'] for p in pred['final_predictions'][:3]]
actual_top3 = actual['top3_horses']
# 1着的中
if pred_top3[0] == actual_top3[0]:
metrics['top1_hit'] += 1
# 3着以内的中(予測1位が3着以内に入っているか)
if pred_top3[0] in actual_top3:
metrics['top3_hit'] += 1
# 3連単的中
if pred_top3 == actual_top3:
metrics['trifecta_hit'] += 1
# 的中率を計算
n = metrics['total_races']
print(f"=== 予測精度評価 ===")
print(f"対象レース数: {n}")
print(f"1着的中率: {metrics['top1_hit']/n*100:.1f}%")
print(f"3着内率: {metrics['top3_hit']/n*100:.1f}%")
print(f"3連単的中率: {metrics['trifecta_hit']/n*100:.1f}%")
return metrics
def improve_prompt_with_feedback(original_prompt, failed_cases):
"""失敗ケースを分析してプロンプトを改善"""
analysis_prompt = f"""以下の競馬予測で失敗したケースを分析してください。
【元のプロンプト】
{original_prompt}
【失敗ケース】
{json.dumps(failed_cases, ensure_ascii=False, indent=2)}
以下の観点で分析し、プロンプトの改善案を提案してください:
1. 共通する失敗パターンはあるか
2. 見落としている評価観点はあるか
3. 具体的なプロンプト修正案
出力形式:
{{
"failure_patterns": ["パターン1", "パターン2"],
"missing_factors": ["要因1", "要因2"],
"improved_prompt": "改善されたプロンプト全文"
}}"""
response = openai_client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": analysis_prompt}],
response_format={"type": "json_object"}
)
return json.loads(response.choices[0].message.content)ベストプラクティス
プロンプト設計
- • 明確な役割設定(競馬予想の専門家)
- • 構造化された出力形式(JSON)
- • 段階的な思考プロセス(CoT)
- • 具体的な評価観点の指定
データ準備
- • 人間が読みやすい形式に変換
- • 重要な情報を優先的に配置
- • 不要な情報は削除(トークン節約)
- • MLモデルの予測結果を含める
運用
- • バッチ処理でコスト削減
- • キャッシュの活用
- • レート制限への対応
- • エラーハンドリング
改善サイクル
- • 予測結果の記録と評価
- • 失敗ケースの分析
- • プロンプトの継続的な改善
- • A/Bテストによる検証
注意事項
- • LLMの予測は100%正確ではありません。馬券購入は自己責任で行ってください。
- • APIコストが発生します。本番運用前にコストを見積もってください。
- • LLMは確率的な出力のため、同じ入力でも結果が異なる場合があります。
- • 最新のモデルや料金体系は各APIプロバイダーのドキュメントを確認してください。