はじめに
競馬AIを作る人が最初にハマる罠のひとつが「AUCが高ければROIも高いはず」という思い込みです。
実際には、AUCが高くてもROIは低いことがあります。その理由と、ROIを改善するための「期待値(EV)」の計算方法を解説します。
AUCとROIの違い
**AUC(Area Under the Curve)**は、モデルが「当たる馬と外れる馬を正しい順序で並べられるか」を測る指標です。0.5がランダム、1.0が完璧な予測。
**ROI(Return on Investment)**は、実際に賭けたときの回収率です。投資100円に対して払戻が120円なら、ROI = 120%。
なぜ両者がズレるのか?
AUCは「順序の正確さ」を測りますが、実際に儲けるためには「正しい金額を正しいタイミングで賭ける」必要があります。
例えば、単勝1.1倍の馬を80%の確率で的中させるモデルがあるとします。
- AUCは高い
- 期待値 = 0.8 × 1.1 = 0.88 → ROIはマイナス
一方、単勝10倍の馬を15%の確率で的中させるモデルがあるとします。
- AUCへの貢献は少ない
- 期待値 = 0.15 × 10 = 1.5 → ROIはプラス
重要なのは「当たる確率 × オッズ」が1を超えるかどうかです。
期待値の計算式
期待値(EV) = 予測勝率 × 実際に支払われるオッズ
EVが1.0より大きいとき、理論的には長期的にプラス収支が期待できます。
ただし注意:競馬のオッズには控除率(JRAは約25%)が含まれているため、ランダムに買い続けるとROI 75%になります。EV > 1.0の馬だけを選ぶことで、市場平均を上回るROIを狙います。
Pythonでの期待値計算
import pandas as pd
import numpy as np
def calculate_expected_value(
prob: float,
odds: float
) -> float:
"""
期待値(EV)を計算する。
Args:
prob: モデルの予測確率(0〜1)
odds: オッズ(例:単勝5倍なら5.0)
Returns:
期待値(1.0以上で理論的にプラス)
"""
return prob * odds
def calculate_ev_for_dataframe(
df: pd.DataFrame,
prob_col: str,
odds_col: str
) -> pd.DataFrame:
"""
DataFrameに期待値列を追加する。
"""
df = df.copy()
df['ev'] = df[prob_col] * df[odds_col]
return df
単勝・複勝・馬連の期待値計算
単勝の期待値
def calculate_win_ev(win_prob: float, win_odds: float) -> float:
"""単勝の期待値"""
return win_prob * win_odds
# 例:単勝5倍の馬をモデルが25%の確率と予測
ev = calculate_win_ev(0.25, 5.0)
print(f"単勝EV: {ev:.2f}") # 1.25 → EV > 1.0なので買い候補
複勝の期待値
複勝は「3着以内に入る確率」を使います。
def calculate_place_ev(place_prob: float, place_odds: float) -> float:
"""複勝の期待値(3着以内確率 × 複勝オッズ)"""
return place_prob * place_odds
# 例:複勝1.5倍の馬をモデルが60%の確率で3着以内と予測
ev = calculate_place_ev(0.60, 1.5)
print(f"複勝EV: {ev:.2f}") # 0.90 → EV < 1.0なので見送り
馬連の期待値
馬連は「2頭の組み合わせの期待値」なので、少し複雑です。
def calculate_exacta_ev(
prob_horse_a_top2: float, # 馬Aが2着以内の確率
prob_horse_b_top2: float, # 馬Bが2着以内の確率
exacta_odds: float, # 馬連オッズ
n_horses: int = 16 # 出走頭数(近似計算用)
) -> float:
"""
馬連の期待値を近似計算する。
正確な計算はモデルの同時確率が必要だが、
近似として独立仮定を使う。
"""
# 馬AとBの両方が2着以内に入る確率の近似
# (厳密には独立ではないが、近似として使える)
joint_prob = prob_horse_a_top2 * prob_horse_b_top2 * n_horses / (n_horses - 1)
return joint_prob * exacta_odds
EV閾値の設計
「EVがいくら以上のときに買うか」という閾値(min-EV)の設計は、ROIに大きく影響します。
def analyze_ev_roi_by_threshold(
df: pd.DataFrame,
ev_col: str = 'ev',
odds_col: str = 'odds',
label_col: str = 'won',
bet_amount: int = 100
) -> pd.DataFrame:
"""
EV閾値ごとのROIを分析する。
"""
thresholds = [1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 2.0]
results = []
for threshold in thresholds:
filtered = df[df[ev_col] >= threshold]
if len(filtered) == 0:
continue
investment = len(filtered) * bet_amount
payout = (filtered[label_col] * filtered[odds_col] * bet_amount).sum()
roi = payout / investment if investment > 0 else 0
results.append({
'min_ev': threshold,
'n_bets': len(filtered),
'investment': investment,
'payout': int(payout),
'roi': round(roi, 4)
})
return pd.DataFrame(results)
# 使い方
results = analyze_ev_roi_by_threshold(df)
print(results.to_string(index=False))
出力例:
min_ev n_bets investment payout roi
1.0 108339 10833900 10872390 1.0036
1.1 50000 5000000 5200000 1.0400
1.2 30000 3000000 3270000 1.0900
1.3 15000 1500000 1720000 1.1467
私のシステムでは、EV 1.0〜1.1帯のROIが89〜93%と赤字だったため、min-EV 1.2以上という設定に変更しました。
AUCが高くてもROIが低い理由のまとめ
- AUCは確率の「順序」を評価する → 人気馬を正しく上位に置けばAUCは高い
- 人気馬のオッズは低い → EV = 確率 × オッズが1を超えにくい
- AIが人気馬を正確に予測するほど、人気馬のオッズが下がる → 市場との悪循環
解決策:
- 高EV(穴馬)に絞って賭ける
- min-EVの閾値を適切に設定する
- モデルの確率とオッズを両方活用する設計にする
詳細な実装について
EV計算の完全実装(複数の券種対応)や、EVフィルタの設計思想は**Part1:設計編**で解説しています。また、実際のバックテストでのEV分布とROIの関係も詳細に分析しています。