🎯 オッズ非依存・純粋レース分析特化シミュレーション完全ガイド
競馬レースの着順分布を最高精度で予測することに特化した、オッズ・人気を一切使用しないモンテカルロシミュレーション設計書
📑 目次
- 設計思想
- データソースの選定
- スコアリングの完全数値化
- ノイズ設計の精緻化
- 共通ショックの導入
- ペース・展開の動的生成
- 着順生成プロセスの2段階化
- キャリブレーション
- モデル評価指標
- アンサンブル化
- 出力フォーマット
- 実装ロードマップ
- 本質的価値と留意点
1. 設計思想
オッズを捨てることの意味
「期待値分析」を捨て、「このレースはどう決着するか」を最高精度で予測することに特化した設計。
| 観点 | 期待値モデル | 純粋分析モデル |
|---|---|---|
| 目的 | 妙味馬を見つける | 真の着順分布を当てる |
| 入力 | スコア+オッズ | 客観データのみ |
| 評価軸 | ROI | ブライアスコア・的中率 |
| バイアス | 「人気の歪み」を狙う | バイアスを排除する |
| 出力 | 買い目 | 着順確率分布 |
オッズを捨てると 「市場の総意という補助線」を失う代わりに、純粋な実力評価ができる ようになります。
基本原則
- 主観評価の極力排除:「追い切りS評価」などは使わない
- 数値データ優先:すべて観測可能な数値の関数として定義
- 再現性の確保:誰が実行しても同じ結果が出る設計
- 不確実性の可視化:単一の答えではなく確率分布を出力
2. データソースの選定
採用すべき客観データ
| カテゴリ | 具体指標 | 取得元 |
|---|---|---|
| スピード指数 | 過去全レースの指数 | JRA-VAN、TARGET、netkeiba |
| ラップ実績 | 各レースの3F〜区間ラップ | 公式記録 |
| コース実績 | 同コース・同距離・同回り着順履歴 | 公式記録 |
| 血統スコア | 父・母父別コース別勝率/連対率 | netkeiba血統情報 |
| 調教時計 | 最終追い切り・1週前の数値タイム | 各サイトの実測値 |
| 馬体重推移 | 過去全レースの馬体重・前走比 | 公式記録 |
| 騎手指標 | 同コース・同距離での騎手別勝率 | 公式記録 |
| 厩舎指標 | 同レース・同条件での厩舎別連対率 | 公式記録 |
| 枠順 | 番号 | 確定情報 |
| 斤量 | 数値 | 確定情報 |
排除すべきデータ
- 追い切り評価のS/A/B(主観混入)
- 関係者コメント(ポジショントーク)
- 「期待大」「絶好調」などの表現
- 想定オッズ・人気
例外:客観事実として採用すべき当日情報
オッズは排除するが、以下は「市場心理ではなく客観事実」なので採用する:
- 当日の馬体重(公式計測値)
- 最終馬場発表(クッション値・含水率)
- 当日のパドック気配(数値化できる範囲で)
3. スコアリングの完全数値化
主観の入る余地を消すため、すべて観測可能な数値の関数として定義する。
完全数値ベースのスコア構造
def calc_objective_score(horse):
"""主観排除・完全数値ベースのスコア算出"""
# === 1. スピード能力(40点) ===
# 過去5走のスピード指数の上位3走平均
top3_speed = sorted(horse["speed_indices"][-5:], reverse=True)[:3]
speed_score = np.mean(top3_speed) * 0.4
# === 2. コース・距離適性(25点) ===
course_score = 0
if horse["same_course_runs"] >= 1:
course_score += horse["same_course_win_rate"] * 10
course_score += horse["same_course_place_rate"] * 5
course_score += horse["similar_distance_avg_finish"] * 0.5
course_score += horse["same_direction_win_rate"] * 5
# === 3. 上がり性能(15点) ===
agari_score = (
(38.0 - horse["best_agari"]) * 2 +
(38.0 - horse["avg_agari"]) * 1.5 +
horse["agari_top3_rate"] * 5
)
# === 4. 持続力指標(10点) ===
decel_score = (1 - horse["last_2f_decel_rate"]) * 10
# === 5. 状態指標(10点:客観のみ) ===
state_score = 0
state_score += max(0, 5 - horse["weight_std"])
state_score += horse["recent_trend_slope"] * 5
total = speed_score + course_score + agari_score + decel_score + state_score
return total
スコア配分の根拠
| 要素 | 配点 | 理由 |
|---|---|---|
| スピード能力 | 40点 | 競馬で最も再現性の高い指標 |
| コース・距離適性 | 25点 | 該当条件での実績は強い |
| 上がり性能 | 15点 | 直線勝負レースで重要 |
| 持続力 | 10点 | ラスト2F減速耐性 |
| 状態 | 10点 | 客観的に測定できる範囲のみ |
4. ノイズ設計の精緻化
現状の問題
全馬に同一の正規分布σ=12を適用すると、経験値の異なる馬を同じバラツキで扱ってしまう。
馬別σを観測データから算出
def get_individual_sigma(horse):
"""過去レース成績のバラツキから個別σを算出"""
# その馬の過去スピード指数の標準偏差
if len(horse["speed_indices"]) >= 3:
sigma_base = np.std(horse["speed_indices"])
else:
sigma_base = 15 # データ不足時の保守的な値
# 経験補正
if horse["career_starts"] < 3:
sigma_base *= 1.3 # 若駒は不安定
# 距離経験補正
if horse["target_distance_experience"] == 0:
sigma_base *= 1.2 # 初距離は不安定
# 道悪経験補正
if horse["wet_track_experience"] == 0 and forecast_wet:
sigma_base *= 1.15
return sigma_base
分布形状の変更
正規分布は外れ値を過小評価する。実際の競馬は外れ値が頻発するため、t分布を採用。
# t分布(自由度4):外れ値が出やすい現実に近い
noise = np.random.standard_t(df=4) * sigma
# 歪正規分布:本命馬は下振れリスク、穴馬は上振れ余地
from scipy.stats import skewnorm
# 実績馬は下振れしやすい(a=-2)、未知馬は上振れ余地大(a=+2)
noise = skewnorm.rvs(a=-2 if is_proven else +2) * sigma
5. 共通ショックの導入
概念
全馬が独立にノイズを引くと「展開で前が止まる」「内ラチが伸びない」といった相関効果が表現できない。1試行ごとに全馬に影響する共通ショックを導入する。
実装
def simulate_one_trial(horses):
# === 試行ごとに「共通ショック」を引く ===
# 馬場ショック(雨で稍重化など)
track_condition_shock = np.random.normal(0, 3)
# ペースショック(隊列の崩れ)
pace_shock = np.random.normal(0, 4)
# 内外バイアスショック
inner_outer_shock = np.random.normal(0, 3)
# 各馬への適用
finish_scores = []
for h in horses:
score = h["base_score"]
# 馬場ショックの影響度(道悪適性で決まる)
score += track_condition_shock * h["wet_track_sensitivity"]
# ペースショックの影響度(脚質で決まる)
# 差し馬は+1、逃げ馬は-1
score += pace_shock * h["pace_sensitivity"]
# 内外バイアスの影響度(枠順で決まる)
position_factor = (h["waku"] - 4.5) / 4.5 # -1〜+1
score += inner_outer_shock * position_factor
# 個別ノイズ
score += np.random.normal(0, h["individual_sigma"])
finish_scores.append((h["no"], score))
return sorted(finish_scores, key=lambda x: -x[1])
共通ショックの効果
これにより以下が確率的に発生する:
- 「人気馬総崩れの日」
- 「内ラチ伸びない日」
- 「ハイペースで先行馬全滅の日」
現実の競馬に近い変動が再現できる。
6. ペース・展開の動的生成
静的シナリオ分類の限界
「シナリオA 40%・B 30%・C 10%...」と分析者が決め打ちするのは主観混入の温床。出走馬構成から自動算出する方が客観的。
動的ペース生成
def calc_pace_distribution(horses):
"""逃げ馬数・先行馬の質からペース分布を算出"""
nigeuma = [h for h in horses if h["脚質"] == "逃げ"]
senkou = [h for h in horses if h["脚質"] == "先行"]
# 逃げ馬がいない or 単騎 → スロー寄り
if len(nigeuma) <= 1:
pace_mean = -5 # マイナスはスロー寄り
pace_std = 4
# 逃げ馬2頭以上 → ハイペース寄り
elif len(nigeuma) >= 2:
pace_mean = +5
pace_std = 5
# 中間
else:
pace_mean = 0
pace_std = 4
# 先行馬が多いほどミドル化
pace_mean += (len(senkou) - 4) * 0.5
return pace_mean, pace_std
def simulate_with_dynamic_pace(horses):
pace_mean, pace_std = calc_pace_distribution(horses)
for trial in range(N_TRIALS):
# その試行のペースを連続変数として動的決定
this_pace = np.random.normal(pace_mean, pace_std)
# this_pace が大きい→ハイペース→差し有利
for h in horses:
score = h["base_score"] + this_pace * h["pace_sensitivity"]
# ...残りの処理
メリット
- シナリオを5つに無理矢理分類しないので、連続的なバリエーション網羅
- 出走馬の構成変化に自動追従
- 分析者によるシナリオ確率の主観が排除される
7. 着順生成プロセスの2段階化
1段階モデルの限界
「総合スコア+ノイズで1発ソート」では、競馬の実際のプロセス(位置取り→直線伸び)が反映されない。
2段階モデルの実装
def simulate_race_realistic(horses, pace):
"""2段階モデル:4角通過→直線伸び"""
# === Stage 1: 4コーナー通過順位 ===
corner_positions = []
for h in horses:
# 脚質スコア + 枠順 + ゲート巧拙 + ノイズ
position_value = (
h["脚質_value"] # 逃げ=1, 先行=2, 中団=3, 後方=4
+ (h["waku"] - 4.5) * 0.1 # 内枠ほど前に行きやすい
+ h["gate_skill"] # ゲート巧拙
+ np.random.normal(0, 1.0) # 位置取りのバラツキ
)
corner_positions.append((h["no"], position_value))
corner_positions.sort(key=lambda x: x[1])
# === Stage 2: 直線での伸び ===
final_scores = []
for rank, (no, pos_val) in enumerate(corner_positions):
h = get_horse(no)
# 直線の伸び量 = 上がり性能 + ノイズ
agari_performance = h["agari_score"] + np.random.normal(0, h["individual_sigma"])
# 位置取りペナルティ:ハイペースなら前が苦しい、スローなら後ろが届かない
if pace > 0: # ハイペース
position_penalty = -rank * 0.3 # 前にいた馬がマイナス
else: # スロー
position_penalty = +rank * 0.3 # 後ろにいた馬がマイナス
final_score = agari_performance + position_penalty
final_scores.append((no, final_score))
final_scores.sort(key=lambda x: -x[1])
return final_scores
2段階モデルの強み
以下の競馬特有の現象が自然に再現される:
- 先行馬が直線で粘る
- 差し馬が届かない
- スローで前残り決着
- ハイペースで差し決着
- 大外捲りの長距離移動による負荷
8. キャリブレーション
概念
シミュレーション勝率が「実際の勝率」と一致しているかを必ず検証する。最重要だが地味な作業。
検証方法
def calibrate_model(past_races):
"""過去レースで予測→実績の対応を検証"""
all_predictions = []
all_actuals = []
for race in past_races:
# 過去レースをシミュレーション(その時点で入手可能だった情報のみ使用)
sim_result = run_simulation(race["pre_race_data"])
for horse_no in race["horses"]:
predicted_win_prob = sim_result[horse_no]["win_rate"]
actual_winner = 1 if race["winner"] == horse_no else 0
all_predictions.append(predicted_win_prob)
all_actuals.append(actual_winner)
# キャリブレーション曲線
from sklearn.calibration import calibration_curve
prob_true, prob_pred = calibration_curve(
all_actuals, all_predictions, n_bins=10
)
# 補正関数を学習(等張回帰)
from sklearn.isotonic import IsotonicRegression
calibrator = IsotonicRegression()
calibrator.fit(all_predictions, all_actuals)
return calibrator
# 本番予測時に補正
raw_win_prob = simulation_result["win_rate"]
calibrated_win_prob = calibrator.predict([raw_win_prob])[0]
キャリブレーションの効果
例:
- シミュ勝率30%の馬が実際には20%しか勝っていない
- → 補正関数で30% → 20%に変換
- → シミュレーション勝率が現実と一致する
これにより「シミュ勝率30%は実際の30%を意味する」状態を作れる。
9. モデル評価指標
オッズ非依存の評価軸
期待値(ROI)を捨てるなら、別の評価軸が必要。
def evaluate_model(predictions, actuals):
"""オッズ非依存の評価指標"""
# === 1. ブライアスコア(多クラス分類の精度) ===
# 0に近いほど良い
brier = np.mean([(p - a)**2 for p, a in zip(predictions, actuals)])
# === 2. ログロス ===
# 確信を持って外すと大きく罰せられる
log_loss = -np.mean([
a*np.log(p+1e-10) + (1-a)*np.log(1-p+1e-10)
for p, a in zip(predictions, actuals)
])
# === 3. 順位相関(スピアマン) ===
from scipy.stats import spearmanr
rank_corr, _ = spearmanr(predicted_ranks, actual_ranks)
# === 4. Top-N的中率 ===
top3_hit = sum(1 for r in races if r["actual_top3"] in r["predicted_top3"])
top3_rate = top3_hit / len(races)
return {
"brier": brier,
"log_loss": log_loss,
"rank_correlation": rank_corr,
"top3_capture": top3_rate,
}
各指標の意味
| 指標 | 意味 | 良い値 |
|---|---|---|
| ブライアスコア | 予測確率と実績の二乗誤差 | 0に近いほど良い |
| ログロス | 確信を持って外すペナルティ | 小さいほど良い |
| 順位相関 | 予測順位と実順位の相関 | 1に近いほど良い |
| Top-3的中率 | 3着以内の的中数 | 高いほど良い |
10. アンサンブル化
単一モデルの過信を防ぐ
複数の独立アプローチで予測し、加重平均を取る。
def ensemble_prediction(race):
"""4つの独立モデルで予測し、加重平均"""
# Model 1: スピード指数ベース
pred1 = speed_index_model(race)
# Model 2: 上がり3F重視モデル
pred2 = agari_focused_model(race)
# Model 3: 血統・コース適性モデル
pred3 = pedigree_course_model(race)
# Model 4: 過去類似レースKNN
pred4 = knn_similar_races(race, k=10)
# 過去検証で得られた重みで加重平均
final_pred = (
0.35 * pred1 +
0.25 * pred2 +
0.25 * pred3 +
0.15 * pred4
)
# 4モデルの分散も出す(不確実性の指標)
model_variance = np.std([pred1, pred2, pred3, pred4], axis=0)
return final_pred, model_variance
アンサンブルの副産物
- 4モデル全部が推す馬:確度が高い
- モデル間で意見が割れる馬:不確実性が高い
この二次情報は単一モデルでは得られない。
11. 出力フォーマット
オッズが無いので、買い目ではなく着順確率分布を主成果物とする
═══════════════════════════════════════════
■ レース分析結果(オッズ非依存)
═══════════════════════════════════════════
【着順別出現確率マトリクス】
1着 2着 3着 圏内率
ラフター 36.6% 21.2% 15.1% 75.9%
ドリーム 18.4% 22.3% 18.9% 63.6%
スター 22.8% 17.5% 12.7% 53.0%
エンネ 9.4% 14.8% 13.6% 39.8%
...
【決着パターン確率】
- 18-12-13系: 12.6%(最頻パターン)
- 10-12-18系: 9.8%
- 18-13-12系: 7.7%
- その他: 69.9%
【ペース感応度マップ】
スロー ミドル ハイ
ラフター +3.1 +5.2 +6.8
スター +6.5 +3.1 -2.4
ドリーム +2.5 +4.8 +4.2
【予測着順 95%信頼区間】
ラフター: 1〜5着 [中央値: 2.1着]
ドリーム: 2〜6着 [中央値: 3.4着]
スター: 1〜8着 [中央値: 3.8着]
【モデル間合意度】
ラフター上位: 4/4モデル一致 → 高確度
スター上位: 3/4モデル一致 → 中確度
ドリーム上位:4/4モデル一致 → 高確度
出力で重視すべき情報
- 着順別確率マトリクス:単一の勝率ではなく1〜3着の分布
- 決着パターン確率:最頻出の三連系
- ペース感応度:シナリオ別の優位性
- 信頼区間:予測の幅
- モデル間合意度:確度の指標
12. 実装ロードマップ
| 段階 | 作業 | 期間目安 | 効果 |
|---|---|---|---|
| Phase 1 | スピード指数データの蓄積(過去3年G1) | 2週間 | 基盤構築 |
| Phase 2 | スコアリング完全数値化 | 1週間 | 主観排除 |
| Phase 3 | 馬別σ・共通ノイズ実装 | 3日 | 精度+10% |
| Phase 4 | 2段階着順生成モデル | 1週間 | 精度+15% |
| Phase 5 | キャリブレーション検証 | 1週間 | 確度可視化 |
| Phase 6 | アンサンブル化 | 2週間 | 安定性向上 |
| Phase 7 | 評価指標による継続改善 | 永続 | 長期最適化 |
優先順位の根拠
実装コストと効果のバランス:
| 優先 | 改善項目 | コスト | 効果 |
|---|---|---|---|
| 🥇 | 馬別σ+共通ノイズ項 | 低 | 大 |
| 🥈 | キャリブレーション検証 | 中 | 大 |
| 🥉 | 試行回数10倍+信頼区間 | 低 | 中 |
| 4 | ペース分布の動的生成 | 中 | 中 |
| 5 | 2段階着順生成モデル | 高 | 中 |
| 6 | アンサンブル化 | 高 | 大 |
13. 本質的価値と留意点
オッズ非依存モデルの4つの強み
1. 「人気の罠」に陥らない
「人気だから強い」ではなく、「強いから人気」と分離して評価できる。
2. 「正解の着順分布」を学習データに蓄積できる
来年以降のレース予測精度向上に直結。
3. モデル自体の評価ができる
「ブライアスコア0.18」など客観指標で進歩を測れる。
4. 馬券外の用途に転用可能
- 育成シミュレーション
- ローテーション判断
- 馬主向け分析
- 馬産関係者向けレポート
重要な留意点
モデル化不可能な要素は15〜25%存在する
どれだけ精度を上げても以下はモデル化できない:
- パドックでの突発的な気配悪化
- ゲート出遅れ
- 直前の落馬で気性が荒れる
- 騎手のミスジャッジ
- 天候の急変
**シミュレーションは「これらを差し引いた残り75〜85%を最適化する道具」**と割り切る。
当日の客観事実は反映すべき
オッズを捨てる=市場心理を捨てるのであって、客観データまで捨ててはいけない:
- 馬体重(公式計測値)
- 最終馬場発表
- パドック気配(数値化できる範囲で)
キャリブレーションは継続作業
過去データでの検証は1回で終わらない。新しいレース結果が出るたびに:
- キャリブレーション曲線を更新
- パラメータを再学習
- モデル間の重みを見直す
📌 設計思想のまとめ
オッズ非依存モデル = 客観データ + 数値化 + 確率分布 + 検証
NOT: 単一の答えを出す
YES: 着順分布を出す
NOT: 主観評価を信じる
YES: 観測可能な数値だけ使う
NOT: 結果ROIで評価
YES: ブライアスコアで評価
NOT: 「この馬が来る」
YES: 「この馬は1着X%、2着Y%、3着Z%」
🎯 最終結論
「人気の歪み」を逆手に取らない代わり、レースの本質的な決着可能性を最も忠実に再現する——これがオッズ非依存モデルの最大の強みです。
買い目を出すためのモデルではなく、**「レースを理解するためのモデル」**として運用することで、競馬予想を主観的な賭けから客観的な分析へと昇華させることができます。
📚 参考実装の構成
project/
├── data/
│ ├── historical_races.csv # 過去レースデータ
│ ├── speed_indices.csv # スピード指数
│ ├── pedigree.csv # 血統データ
│ └── conditions.csv # 馬場・天候履歴
│
├── models/
│ ├── base_score.py # スコアリング
│ ├── noise_model.py # 馬別σ・共通ノイズ
│ ├── pace_generator.py # 動的ペース生成
│ ├── two_stage_simulator.py # 2段階着順生成
│ ├── ensemble.py # アンサンブル
│ └── calibrator.py # キャリブレーション
│
├── evaluation/
│ ├── brier_score.py
│ ├── log_loss.py
│ └── backtest.py # 過去レース検証
│
├── output/
│ ├── probability_matrix.py # 確率マトリクス出力
│ └── visualization.py # 可視化
│
└── main.py # メイン実行
このドキュメントは継続的に更新されることを前提としています。新しいレース結果や検証結果に応じて、各セクションのパラメータ・手法を見直してください。