プロローグ:始まりは朝の思いつき
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 - モデル改善
アルゴリズムの変更
試したこと
- ロジスティック回帰 → 72.8%
- ランダムフォレスト → 85.3%
- Gradient Boosting → 103.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 |
| AUC | 0.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%で惨敗。でも諦めない理由を全部話す」で、
リアルな結果を包み隠さず公開します。
📌 関連リンク
ここまで読んでいただき、ありがとうございました!
「自分も作ってみたい」
「ここが知りたい」
「こうすればもっと良くなるのでは?」
コメントお待ちしています 💬
次回もお楽しみに 🏇✨
🗨️ コメント欄
- 質問・感想・アドバイス、何でもどうぞ!
- コードの詳細が知りたい方は個別に解説します
- 一緒に改善していきましょう