競馬AI予測

【競馬AI #0】朝「作れるかな?」→夜には回収率103%達成した1日の記録

プロローグ:始まりは朝の思いつき

2026年2月のある日の朝。

「競馬の予測、AIでできないかな?」

ふとそう思い立ちました。

機械学習の経験?ほぼゼロ。
競馬の知識?データの見方すらわからない。

でも、やってみよう

そこから始まった10時間のチャレンジが、
予想以上の結果を生み出すことになります。

この記事では、その1日を時系列で振り返ります。


📅 タイムライン:1日の流れ

9:00 AM - プロジェクト開始

目標設定

・競馬の勝敗を予測するAI
・回収率100%超を目指す
・今週末に実戦投入できるレベル

技術スタック(決定)

・Python 3.12
・scikit-learn(機械学習)
・pandas(データ処理)
・TARGET Frontier JV(データ取得)

当時の私:

「機械学習ってscikit-learnでできるらしい。使ったことないけど」


10:00 AM - データ取得の壁

最初の挫折

TARGETでデータを出力したものの…

❌ 1着馬のデータしかない
❌ ヘッダーがない
❌ DS形式とS形式の違いがわからない
❌ 文字コードで文字化け

試行錯誤 その1

  • DS形式で出力 → 過去走データで使えない
  • S形式で出力 → これだ!
  • でもヘッダーなし → 列の意味がわからない
  • ヘッダー付きで再出力 → やっと読める

試行錯誤 その2

# 最初(エラー)
df = pd.read_csv('race.csv')
# → 文字化け

# 修正
df = pd.read_csv('race.csv', encoding='cp932')
# → 読めた!

所要時間: 2時間
結果: JRA 2024年全レース(46,752頭)のデータ取得成功

当時の私:

「データ取得だけで午前中終わった…」


12:00 PM - データ前処理

生データの確認

馬場状態: '良', '稍重', '重', '不良'
芝・ダ: '芝', 'ダ', '障'
性別: '牡', '牝', 'セ', '騸'
オッズ: '(3.2)' とか '340' とか

→ カテゴリ変数と数値がバラバラ

やったこと

カテゴリ変数のエンコーディング

track_condition_map = {'良': 0, '稍重': 1, '重': 2, '不良': 3}
df['track_condition_encoded'] = df['馬場状態'].map(track_condition_map)

sex_map = {'牡': 0, '牝': 1, 'セ': 2, '騸': 2}
df['sex_encoded'] = df['性別'].map(sex_map)

オッズの正規化

# (3.2) → 3.2
# 340 → 3.4
def parse_odds(x):
    if x.startswith('('):
        return float(x[1:-1])
    else:
        return int(x) / 100

派生特徴量の作成

# オッズと人気の乖離
df['odds_popularity_gap'] = df['odds'] - (df['popularity'] * 2)

# 距離カテゴリ
df['distance_category'] = df['distance'].apply(distance_category)

所要時間: 1.5時間
使用した特徴量: 11種類

当時の私:

「これで機械学習できる形になった!」


2:00 PM - モデル構築(第1弾)

ベースライン:ロジスティック回帰

最初のコード

from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)

model = LogisticRegression()
model.fit(X_train, y_train)

# 予測
predicted_probs = model.predict_proba(X_test)[:, 1]

評価

from sklearn.metrics import roc_auc_score

# AUC(予測精度)
auc = roc_auc_score(y_test, predicted_probs)
print(f"AUC: {auc}")
# → AUC: 0.7821

# 回収率計算
df_test['predicted_win_prob'] = predicted_probs
df_test['expected_value'] = (df_test['predicted_win_prob'] * df_test['odds']) - 1
bet_horses = df_test[df_test['expected_value'] >= 0.05]

# 回収率
recovery_rate = bet_horses['結果'].sum() / len(bet_horses) * 100
print(f"回収率: {recovery_rate}%")
# → 回収率: 72.8%

結果: 回収率72.8%

当時の私:

「プラスにはなったけど、目標100%には遠い…」


3:30 PM - モデル改善

アルゴリズムの変更

試したこと

  1. ロジスティック回帰 → 72.8%
  2. ランダムフォレスト → 85.3%
  3. Gradient Boosting103.5%

Gradient Boostingのコード

from sklearn.ensemble import GradientBoostingClassifier

model = GradientBoostingClassifier(
    n_estimators=100,
    learning_rate=0.1,
    max_depth=5,
    random_state=42
)

model.fit(X_train, y_train)
predicted_probs = model.predict_proba(X_test)[:, 1]

結果

AUC: 0.8393
回収率: 103.5%
推奨ベット: テストデータで約30%の馬

所要時間: 1.5時間

当時の私:

「103.5%!? 本当に?何度も確認したけど本当だ!」


5:00 PM - 実用化への道

予測スクリプトの作成

やりたいこと

TARGETでCSV出力
↓
スクリプト実行
↓
推奨馬が表示される

predict.py の作成

import pandas as pd
import pickle
import sys

# データ読み込み
data_file = sys.argv[1]
df = pd.read_csv(data_file, encoding='cp932')

# 前処理
# ... (省略)

# モデル読み込み
with open('gbm_model.pkl', 'rb') as f:
    model = pickle.load(f)

# 予測
predicted_probs = model.predict_proba(X)[:, 1]
df['expected_value'] = (predicted_probs * df['odds']) - 1

# 推奨馬表示
bet_horses = df[df['expected_value'] >= 0.05]
print(bet_horses[['馬名', 'オッズ', '期待値']])

所要時間: 1時間


6:00 PM - Windows対応の苦労

問題発覚

研究用Pythonスクリプトは動く。
でも、Windowsでは…

❌ 日本語ファイル名で文字化け
❌ バッチファイルが動かない
❌ ZIPファイルが展開できない
❌ エラーメッセージが文字化け

解決プロセス

問題1: 日本語ファイル名

フォルダ名: 競馬予測AI_実用版/
↓ Windows で展開すると文字化け

解決: 全て英語名に変更
keiba_ai/

問題2: バッチファイル

@echo off
chcp 65001 >nul
python predict.py %filename%

問題3: エンコーディング

# すべてのファイルでUTF-8を明示
df = pd.read_csv(file, encoding='cp932')  # 読み込み
df.to_csv(file, encoding='utf-8-sig')     # 書き込み

所要時間: 2時間

当時の私:

「動けばいいじゃん、から実用レベルは遠い…」


8:00 PM - パッケージ完成

最終的なフォルダ構成

keiba_ai/
├── quick_start/
│   ├── RUN.bat                 # 実行用
│   ├── PRACTICE.bat            # 練習用
│   ├── predict.py              # 予測スクリプト
│   ├── practice.py             # 練習スクリプト
│   └── gbm_model.pkl           # AIモデル
├── dev_scripts/                # 開発用
├── documents/                  # ドキュメント
├── sample_data/                # サンプル
└── README.md                   # 使い方

機能

✅ ダブルクリックで実行
✅ 練習モード搭載
✅ レース情報自動表示
✅ エラー処理完備
✅ 日本語対応
✅ ベットリスト出力

所要時間: 1.5時間


9:30 PM - 完成!

最終テスト

練習モードで確認

2024/10/05のデータ(458頭)
↓
24頭推奨
期待値5%以上

結果表示

馬名: シュラザック
オッズ: 15.2倍
期待値: 90.0%
→ 単勝100円

動作確認: ✅


📊 成果まとめ

技術的成果

項目結果
学習データJRA 2024年 46,752頭
特徴量11種類
アルゴリズムGradient Boosting
AUC0.8393
回収率103.5%
開発時間約10時間

システム的成果

✅ Windows対応完了
✅ ワンクリックで実行可能
✅ レース情報自動表示
✅ エラーハンドリング
✅ 練習モード搭載


💡 学んだこと

1. データが全て

学び:
機械学習の8割はデータ準備。
正しいデータを取得できれば、後は意外と簡単。

苦労した点:

  • DS形式とS形式の違い
  • ヘッダーの有無
  • 文字コードの違い
  • オッズの表記ゆれ

所要時間: 全体の30%


2. アルゴリズム選択の重要性

試したもの:

ロジスティック回帰: 72.8%
ランダムフォレスト: 85.3%
Gradient Boosting: 103.5% ← これ!

学び:
同じデータでも、アルゴリズムで結果が大きく変わる。
試行錯誤が大事。


3. 実用化の壁

研究レベル: Jupyter Notebookで動けばOK
実用レベル: 誰でも使える形に

やったこと:

  • バッチファイル作成
  • エラー処理
  • 文字コード対応
  • ドキュメント作成
  • テスト

所要時間: 全体の40%

学び:
「動く」と「使える」は全然違う。


4. 試行錯誤の価値

失敗の数:

  • データ出力: 5回以上
  • モデル構築: 3アルゴリズム
  • Windows対応: 無数のエラー

学び:
失敗から学ぶことが最も多い。
エラーを恐れず試すことが重要。


🎯 できたこと・できなかったこと

✅ できたこと

AI的には

  • 回収率103.5%達成
  • 統計的に有意な結果
  • 期待値計算の実装

システム的には

  • Windows対応
  • 実用的なUI/UX
  • エラーハンドリング

記録的には

  • 10時間で完成
  • ゼロから実用レベル
  • 週末に実戦投入可能

❌ できなかったこと(今後の課題)

データ面

  • 1年分のデータのみ(3-5年欲しい)
  • 過去走データなし
  • 騎手・調教師データなし
  • 馬場指数なし

モデル面

  • 重賞レース専用モデルなし
  • アンサンブル未実装
  • ハイパーパラメータ調整未完

システム面

  • 自動ベット機能なし
  • リアルタイムオッズ未対応
  • Web UI未実装

🤔 なぜ1日で作れたのか

1. スコープを絞った

やったこと:

  • 単勝予想のみ
  • 基本特徴量のみ
  • シンプルなモデル

やらなかったこと:

  • 複勝、馬連、3連単
  • 複雑な特徴量エンジニアリング
  • 深層学習

2. 既存ツールを活用

使ったもの:

  • TARGET(データ取得)
  • scikit-learn(機械学習)
  • pandas(データ処理)

作らなかったもの:

  • データ収集システム
  • 機械学習フレームワーク
  • データベース

3. 完璧を求めなかった

方針:

  • まず動くものを作る
  • 改善は後で
  • 実戦投入しながら改良

結果:

  • 初日で実用レベル達成
  • 週末に実戦投入
  • 改善サイクルを回せる

📈 当日の感情の推移

9:00 - 開始

「やってみよう!」
期待 ★★★★☆
不安 ★★☆☆☆

12:00 - データ取得完了

「思ったより大変...」
期待 ★★★☆☆
不安 ★★★☆☆

14:00 - 初回モデル (72.8%)

「プラスにはなった!」
期待 ★★★★☆
不安 ★★☆☆☆

15:30 - Gradient Boosting (103.5%)

「え、103.5%!?本当に!?」
期待 ★★★★★
不安 ★☆☆☆☆
興奮 ★★★★★

18:00 - Windows対応で苦戦

「なんで動かないんだ...」
期待 ★★★☆☆
不安 ★★★★☆
疲労 ★★★★☆

21:00 - 完成

「やった!できた!」
達成感 ★★★★★
期待 ★★★★★
疲労 ★★★★★

💻 技術詳細(コード例)

特徴量エンジニアリング

# オッズと人気の乖離を特徴量に
df['odds_popularity_gap'] = df['odds'] - (df['popularity'] * 2)

# 距離カテゴリ
def distance_category(dist):
    if dist < 1400:
        return 0  # 短距離
    elif dist < 2000:
        return 1  # 中距離
    else:
        return 2  # 長距離

df['distance_category'] = df['distance'].apply(distance_category)

# オッズカテゴリ
def odds_category(odds):
    if odds < 2.0:
        return 0  # 本命
    elif odds < 5.0:
        return 1  # 対抗
    elif odds < 10.0:
        return 2  # 中穴
    else:
        return 3  # 大穴

df['odds_category'] = df['odds'].apply(odds_category)

モデル構築

from sklearn.ensemble import GradientBoostingClassifier
from sklearn.model_selection import train_test_split

# データ分割
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)

# モデル訓練
model = GradientBoostingClassifier(
    n_estimators=100,
    learning_rate=0.1,
    max_depth=5,
    random_state=42
)

model.fit(X_train, y_train)

# 予測
predicted_probs = model.predict_proba(X_test)[:, 1]

# 期待値計算
df_test['predicted_win_prob'] = predicted_probs
df_test['expected_value'] = (df_test['predicted_win_prob'] * df_test['odds']) - 1

# 推奨馬抽出(期待値5%以上)
bet_horses = df_test[df_test['expected_value'] >= 0.05]

# 回収率計算
total_investment = len(bet_horses) * 100
total_return = df_test[df_test['expected_value'] >= 0.05]['払戻'].sum()
recovery_rate = (total_return / total_investment) * 100

print(f"回収率: {recovery_rate:.1f}%")
# → 回収率: 103.5%

評価指標

from sklearn.metrics import roc_auc_score, classification_report

# AUC(予測精度)
auc = roc_auc_score(y_test, predicted_probs)
print(f"AUC: {auc}")
# → AUC: 0.8393

# 分類レポート
print(classification_report(y_test, (predicted_probs > 0.5).astype(int)))

🎬 スクリーンショット

データ取得画面

[TARGETの画面]
→ CSV出力
→ 馬データ★画面(CSV形式)

予測結果画面

================================================================================
💰 ベット推奨馬(期待値5%以上)
================================================================================

馬名                 | 人気 | オッズ | 予測勝率 | 期待値
---------------------|------|--------|----------|--------
ワイドリューレント    |   8  |  55.7  |   24.2%  | 1246.6%
セントラルヴァレー    |   5  |   7.4  |   38.8%  |  187.2%
...

📝 まとめ:1日でできたこと

成果

項目結果
開発時間10時間
データ数46,752頭
回収率103.5%
システム実用レベル
状態実戦投入可能

所感

良かった点:

  • スコープを絞ったこと
  • 既存ツールを使ったこと
  • 完璧を求めなかったこと

反省点:

  • Windows対応に時間かかりすぎ
  • エラー処理が甘い部分がある
  • ドキュメントが不十分

学び:

  • データさえあれば機械学習は意外と簡単
  • 実用化の方が大変
  • 試行錯誤が最も重要

🔜 次のステップ

次回予告:実戦投入

このAIを実際に使って競馬予想をしてみます。

テストでは回収率103.5%

でも、実戦では?

次回「【競馬AI #1】初週は回収率67%で惨敗。でも諦めない理由を全部話す」で、
リアルな結果を包み隠さず公開します。


📌 関連リンク


ここまで読んでいただき、ありがとうございました!

「自分も作ってみたい」
「ここが知りたい」
「こうすればもっと良くなるのでは?」

コメントお待ちしています 💬

次回もお楽しみに 🏇✨


🗨️ コメント欄

  • 質問・感想・アドバイス、何でもどうぞ!
  • コードの詳細が知りたい方は個別に解説します
  • 一緒に改善していきましょう

-競馬AI予測