リアルタイム予測システム

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_today

Step 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 df

Step 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の強み

  • • 基準オッズ(適正オッズ)
  • • ブリンカー・馬具変更フラグ
  • • パドック評価(印・気配・馬体)
  • • 調教評価

活用ポイント

  • • 事前予測と当日情報を統合
  • • 過去データで調整値を検証
  • • 情報更新タイミングを把握
  • • 異常値の検出と対処

次のステップ