リアルタイム予測システム
JRDBオリジナルデータを活用した当日リアルタイム予測システムを構築します。
このチュートリアルで学ぶこと
- • 基準オッズと実際オッズの乖離分析
- • ブリンカー着脱・馬具変更による成績変化の分析
- • パドック印・気配コードの活用
- • 当日情報を統合したリアルタイム予測スコアの構築
Step 1: 基準オッズと実際オッズの乖離分析
JRDBが算出する「基準オッズ」と、実際の締切前オッズを比較することで、 過剰人気・過小評価を検出できます。
import pandas as pd
import requests
# ダッシュボードで取得した値をそれぞれ貼り付け
API_KEY = "API_KEY" # APIキー(アプリ共通)
AUTH_KEY = "YOUR_AUTH_KEY" # 認証キー (x-user-key)
BASE_URL = "https://dev.api.bigtime.world/v1"
def fetch(endpoint, payload):
"""任意のエンドポイントを叩いて DataFrame で返す"""
url = f"{BASE_URL}/{endpoint}?api_key={API_KEY}"
headers = {
"Content-Type": "application/json",
"x-user-key": AUTH_KEY
}
response = requests.post(url, json=payload, 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 analyze_odds_gap(racedate):
"""基準オッズ(/tky)と当日オッズ(/tpd)の乖離を分析"""
# 基準オッズは競走馬データ(前日確定)に入っている
ky = fetch("tky", {"racedate": racedate})
# 当日の単勝オッズは直前情報から取る
pd_ = fetch("tpd", {"racedate": racedate})
df = ky.merge(
pd_[['racekey', 'horse_number', 'win_odds', 'paddock_index']],
on=['racekey', 'horse_number'],
how='inner'
)
# 基準オッズ(JRDBが算出した適正オッズ)
df['criteria_win_odds'] = pd.to_numeric(df['criteria_win_odds'], errors='coerce')
# 実際のオッズ(直前情報の取得時点)
df['win_odds'] = pd.to_numeric(df['win_odds'], errors='coerce')
# 乖離率を計算(正: 過小評価、負: 過剰人気)
df['odds_gap'] = (df['criteria_win_odds'] - df['win_odds']) / df['win_odds'] * 100
# 乖離カテゴリ
df['odds_category'] = pd.cut(
df['odds_gap'],
bins=[-float('inf'), -30, -10, 10, 30, float('inf')],
labels=['大幅過剰人気', '過剰人気', '適正', '過小評価', '大幅過小評価']
)
return df
# データ取得と分析
df = analyze_odds_gap("20250105")
# 過小評価馬(穴馬候補)を抽出
undervalued = df[df['odds_gap'] > 20].sort_values('odds_gap', ascending=False)
print("=== 過小評価馬(穴馬候補) ===")
print(undervalued[['horse_name', 'win_odds', 'criteria_win_odds', 'odds_gap']].head(10))ポイント
- • 基準オッズ > 実際オッズ: 過剰人気(期待値が低い可能性)
- • 基準オッズ < 実際オッズ: 過小評価(穴馬候補)
- • 乖離率30%以上は特に注目
Step 2: ブリンカー着脱・馬具変更の影響分析
ブリンカーの着脱や馬具変更は、馬のパフォーマンスに大きな影響を与えることがあります。 前日確定のblinker_cd(/tky / /dky)と、 当日直前のchange_equipment_flag(/tpd)を組み合わせて分析します。
利用できるフィールドと時制
- •
blinker_cd(/tky・/dky)… ブリンカーの着用有無。前日確定なので前日選定に使えます。 - •
change_equipment_flag(/tpd)… 馬具変更の有無(0:なし / 1:通常 / 2:特注)。当日直前の情報です。 - •
underfoot_info(/tpd)… 脚元の判定(0:平行線 / 1:良化 / 2:疑問 / 3:悪化)。 - • ハミの種類そのもの(馬具コード)は現在 API のレスポンスには含まれていません。 変更の有無までが取得できる範囲です。
def analyze_blinker_change(df_history):
"""ブリンカー着脱パターン別の成績分析
df_history: /dky(過去競走馬データ)と /dse(成績データ)を
racekey + horse_number で結合したもの
"""
# 馬ごと・日付順に並べて前走と比較する
df_history = df_history.sort_values(['horse_id', 'racedate'])
# blinker_cd は "0"/空 = なし、"1" = 着用
worn = df_history['blinker_cd'].fillna('0').replace('', '0')
df_history['blinker_on'] = (worn == '1')
prev_on = df_history.groupby('horse_id')['blinker_on'].shift(1)
df_history['blinker_put_on'] = df_history['blinker_on'] & (prev_on == False)
df_history['blinker_take_off'] = (~df_history['blinker_on']) & (prev_on == True)
for label, mask in [
('ブリンカー装着(前走なし→今走あり)', df_history['blinker_put_on']),
('ブリンカー外し(前走あり→今走なし)', df_history['blinker_take_off']),
('変化なし', ~(df_history['blinker_put_on'] | df_history['blinker_take_off'])),
]:
sub = df_history[mask]
if len(sub) == 0:
continue
print(f"{label}: 平均着順 {sub['order_of_finish'].mean():.2f} "
f"/ 勝率 {(sub['order_of_finish'] == 1).mean() * 100:.1f}% "
f"/ n={len(sub)}")
return df_history
def analyze_equipment_change(df_pd, df_result):
"""当日の馬具変更フラグ別の成績分析
df_pd: /tpd もしくは過去日の直前情報
df_result: /tse(成績データ)
"""
df = df_pd.merge(
df_result[['racekey', 'horse_number', 'order_of_finish']],
on=['racekey', 'horse_number'],
how='inner'
)
labels = {0: '馬具変更なし', 1: '馬具変更(通常)', 2: '馬具変更(特注)'}
stats = df.groupby('change_equipment_flag').agg(
avg_finish=('order_of_finish', 'mean'),
win_rate=('order_of_finish', lambda s: (s == 1).mean() * 100),
count=('order_of_finish', 'size'),
).round(2)
stats.index = stats.index.map(lambda k: labels.get(k, k))
print("=== 馬具変更フラグ別成績 ===")
print(stats)
return stats
def create_equipment_features(df_today, df_history):
"""当日データに馬具まわりの特徴量を付ける"""
# 前走のブリンカー着用状況を引く
last = (
df_history.sort_values('racedate')
.groupby('horse_id')['blinker_cd']
.last()
.fillna('0')
.replace('', '0')
)
df_today['prev_blinker'] = df_today['horse_id'].map(last).fillna('0')
now = df_today['blinker_cd'].fillna('0').replace('', '0')
df_today['blinker_changed'] = (now != df_today['prev_blinker']).astype(int)
df_today['blinker_put_on'] = ((now == '1') & (df_today['prev_blinker'] != '1')).astype(int)
# 当日直前の馬具変更(/tpd をマージ済みの前提)
df_today['equipment_changed'] = (
df_today['change_equipment_flag'].fillna(0).astype(int) > 0
).astype(int)
return df_todayStep 3: パドック情報の活用
JRDBのパドック評価(印コード)、気配コード、馬体コードを活用して、 当日の馬の状態を数値化します。
def create_paddock_features(df):
"""パドック情報の特徴量を作成
df: /tky(paddock_mark)と /tpd(this_horse_mind_state,
this_horse_body_state, paddock_index)をマージしたもの
"""
# 印コード(良い順)。9=☆ はパドック印でのみ使われる
mark_score_map = {
'1': 5, # ◎
'2': 4, # ○
'3': 3, # ▲
'4': 2, # 注
'5': 1, # △
'6': 1, # △
'9': 2, # ☆
}
# 気配コード(this_horse_mind_state)
mind_score_map = {
'1': 2, # 状態良
'5': 2, # 気合良
'2': 0, # 平凡
'3': -1, # 不安定
'6': -1, # 気不足
'4': -2, # イレ込
'7': -2, # チャカ
'8': -3, # イレチ(イレ込+チャカつき)
}
# 馬体コード(this_horse_body_state)
body_score_map = {
'3': 2, # 良い
'6': 2, # 張り
'4': 0, # 普通
'2': -1, # 余裕
'7': -1, # 緩い
'1': -2, # 太い
'5': -2, # 細い
}
# スコア変換
df['paddock_score'] = df['paddock_mark'].map(mark_score_map).fillna(0)
df['mind_score'] = df['this_horse_mind_state'].map(mind_score_map).fillna(0)
df['body_score'] = df['this_horse_body_state'].map(body_score_map).fillna(0)
# 総合パドックスコア
df['paddock_total'] = (
df['paddock_score'] * 2 + # パドック印を重視
df['mind_score'] +
df['body_score']
)
return df
def analyze_paddock_performance(df_history):
"""パドック評価と成績の関係を分析
df_history: 過去日の /dky(paddock_mark)と /dse(order_of_finish)を
racekey + horse_number で結合したもの
"""
paddock_stats = df_history.groupby('paddock_mark').agg(
avg_finish=('order_of_finish', 'mean'),
win_rate=('order_of_finish', lambda s: (s == 1).mean() * 100),
count=('order_of_finish', 'size'),
)
print("=== パドック印別成績 ===")
print(paddock_stats.round(2))
return paddock_stats
# パドック・馬体重などの当日直前データは /tpd から取得する
# (/tka は開催単位の馬場状態・クッション値で、馬ごとの情報は入っていない)
def fetch_paddock_data(racedate):
"""直前情報(パドック指数・馬体重・気配/馬体コード)を取得"""
return fetch("tpd", {"racedate": racedate})JRDBパドック評価の特徴
- • 専門家による客観的な評価
- • 気配(精神状態)と馬体(体調)を分離して評価
- • 過去データとの比較で傾向を分析可能
Step 4: 馬体重変動の分析
馬体重の増減は、馬の調子を判断する重要な指標です。 適正体重からの乖離を分析します。
def analyze_weight_change(df, df_history):
"""馬体重変動の分析
df: 当日の /tpd(horse_weight, weight_cycling)に
/tky の horse_id をマージしたもの
df_history: 過去の /dse(horse_weight, racedate, horse_id)
"""
# 過去の平均体重を計算
avg_weight = df_history.groupby('horse_id')['horse_weight'].mean()
df['avg_weight'] = df['horse_id'].map(avg_weight)
# 当日体重との差
df['weight_from_avg'] = df['horse_weight'] - df['avg_weight']
# 前走との差は weight_cycling(馬体重増減)がそのまま使える
df['weight_diff'] = pd.to_numeric(df['weight_cycling'], errors='coerce')
# 体重変動カテゴリ
df['weight_category'] = pd.cut(
df['weight_diff'],
bins=[-float('inf'), -10, -4, 4, 10, float('inf')],
labels=['大幅減', '減', '変化なし', '増', '大幅増']
)
return df
def create_weight_features(df):
"""馬体重関連の特徴量を作成"""
# 体重変動の絶対値
df['weight_diff_abs'] = df['weight_diff'].abs()
# 適正体重からの乖離率
df['weight_deviation'] = (
(df['horse_weight'] - df['avg_weight']) / df['avg_weight'] * 100
)
# 大幅変動フラグ
df['is_weight_abnormal'] = (df['weight_diff_abs'] > 10).astype(int)
return dfStep 5: リアルタイム統合スコアの構築
事前予測と当日情報を統合して、最終的な予測スコアを算出します。
def create_realtime_score(df, base_prediction):
"""リアルタイム統合スコアを構築"""
# 事前予測スコア(機械学習モデルの出力)
df['base_score'] = base_prediction
# 当日情報による調整
adjustments = pd.DataFrame(index=df.index)
# 1. オッズ乖離による調整
adjustments['odds_adj'] = df['odds_gap'].clip(-50, 50) / 100 # -0.5〜+0.5
# 2. パドック評価による調整
adjustments['paddock_adj'] = df['paddock_total'] / 10 # -0.5〜+0.5
# 3. 馬体重変動による調整(大幅変動はマイナス)
adjustments['weight_adj'] = -df['weight_diff_abs'] / 20 # 0〜-0.5
adjustments['weight_adj'] = adjustments['weight_adj'].clip(-0.3, 0)
# 4. 馬具変更による調整
adjustments['equip_adj'] = (
df['equipment_changed'] * 0.05 + df['blinker_put_on'] * 0.1
)
# 総合調整値
df['total_adjustment'] = (
adjustments['odds_adj'] * 0.3 +
adjustments['paddock_adj'] * 0.4 +
adjustments['weight_adj'] * 0.2 +
adjustments['equip_adj'] * 0.1
)
# 最終スコア
df['realtime_score'] = df['base_score'] + df['total_adjustment']
df['realtime_score'] = df['realtime_score'].clip(0, 1)
return df
def rank_horses(df):
"""レース毎に馬をランキング"""
df['rank'] = df.groupby('racekey')['realtime_score'].rank(
ascending=False, method='min'
)
# 上位3頭を推奨
df['is_recommended'] = df['rank'] <= 3
return df
def display_predictions(df, racekey):
"""予測結果を表示"""
race_df = df[df['racekey'] == racekey].sort_values('realtime_score', ascending=False)
print(f"\n=== レース: {racekey} ===")
print(f"{'馬番':>4} {'馬名':<12} {'基本':>6} {'調整':>6} {'最終':>6} {'推奨':>4}")
print("-" * 50)
for _, row in race_df.iterrows():
rec = "★" if row['is_recommended'] else ""
print(f"{row['horse_number']:>4} {row['horse_name']:<12} "
f"{row['base_score']:>6.1%} {row['total_adjustment']:>+6.1%} "
f"{row['realtime_score']:>6.1%} {rec:>4}")Step 6: 完全な実行例
import lightgbm as lgb
from datetime import datetime
def run_realtime_prediction(racedate, model_path='horse_race_model.lgb'):
"""リアルタイム予測の完全な実行フロー"""
print(f"=== {racedate} リアルタイム予測 ===\n")
# 1. 事前学習済みモデルを読み込み
model = lgb.Booster(model_file=model_path)
print("✓ モデル読み込み完了")
# 2. 当日データを取得し、基準オッズ乖離まで計算する
df = analyze_odds_gap(racedate) # /tky + /tpd をマージ済み
df_tpd = fetch_paddock_data(racedate) # 直前情報(気配・馬体・馬体重)
print(f"✓ 当日データ取得完了 ({len(df)}頭)")
# 3. 直前情報の残りの列をマージ
df = df.merge(
df_tpd[['racekey', 'horse_number', 'this_horse_body_state',
'this_horse_mind_state', 'change_equipment_flag',
'underfoot_info', 'horse_weight', 'weight_cycling']],
on=['racekey', 'horse_number'],
how='left'
)
# 4. 過去データを取得(ブリンカー着脱・馬体重推移の分析用)
end_date = racedate
start_date = (datetime.strptime(racedate, "%Y%m%d") - timedelta(days=180)).strftime("%Y%m%d")
df_history = fetch_training_data(start_date, end_date)
print("✓ 過去データ取得完了")
# 5. 特徴量を作成
df = create_paddock_features(df)
df = analyze_weight_change(df, df_history)
df = create_equipment_features(df, df_history)
print("✓ 特徴量作成完了")
# 6. 基本予測を実行
feature_columns = [...] # 学習時と同じ特徴量
X = preprocess_features(df, feature_columns)
base_prediction = model.predict(X, num_iteration=model.best_iteration)
print("✓ 基本予測完了")
# 7. リアルタイムスコアを計算
df = create_realtime_score(df, base_prediction)
df = rank_horses(df)
print("✓ リアルタイムスコア計算完了\n")
# 8. 結果を表示
for racekey in df['racekey'].unique():
display_predictions(df, racekey)
return df
# 実行
result = run_realtime_prediction("20250105")運用上の注意
- • パドック情報は発走約30分前に更新されます
- • オッズは締切直前まで変動するため、タイミングに注意
- • 馬体重は当日の計量後に反映されます
- • 調整値の重みは過去データで検証して調整してください
まとめ
JRDBのオリジナルデータを活用することで、他のデータソースでは得られない 当日の馬の状態を予測に反映できます。
JRDBの強み
- • 基準オッズ(適正オッズ)
- • ブリンカー・馬具変更フラグ
- • パドック評価(印・気配・馬体)
- • 調教評価
活用ポイント
- • 事前予測と当日情報を統合
- • 過去データで調整値を検証
- • 情報更新タイミングを把握
- • 異常値の検出と対処