【競馬AI-7】学習データに調教情報を付加する

競馬AI

競馬AIを作る際に、レース結果だけでなく調教データを組み込むことは精度を高める大切な要素です。実際の馬はレース直前の追い切り内容で仕上がり具合が変わるため、この情報を学習データに付加しておくと予測がより現実に近づきます。今回は、ウッドチップ・坂路の調教データをレース結果に結合して、新しいテーブルを作成する処理を解説します。

処理概要

この処理では、まず既存の w_joined_race テーブルに対して、調教データ(woodchip_chokyo と hanro_chokyo)を結合します。

  • ウッドチップ調教 → WOOD_ 接頭辞を付けて格納
  • 坂路調教 → HANRO_ 接頭辞を付けて格納

さらに、各レース日の直前に行われた最も近い追い切りを結合するために、pd.merge_asof を活用しています。これにより「レース当日に対して直近で行われた調教」を自動的に紐付けできます。

最終的に、w_joined_race_chokyo という新しいテーブルを作り、元データに調教の列を加えた完全な学習用データセットを生成します。

プログラム全文

以下が実際のコードです。
db/init_work_tables.py に追記する)

def enrich_w_joined_race_bulk():

    wood_columns_with_types = {
        "TRACEN_KUBUN": "CHAR(1)",
        "CHOKYO_NENGAPPI": "CHAR(8)",
        "CHOKYO_JIKOKU": "CHAR(4)",
        "COURSE": "CHAR(1)",
        "BABAMAWARI": "CHAR(1)",
        "YOBI": "CHAR(1)",
        "TIME_GOKEI_10FURLONG": "CHAR(4)",
        "LAPTIME_10FURLONG": "CHAR(3)",
        "TIME_GOKEI_9FURLONG": "CHAR(4)",
        "LAPTIME_9FURLONG": "CHAR(3)",
        "TIME_GOKEI_8FURLONG": "CHAR(4)",
        "LAPTIME_8FURLONG": "CHAR(3)",
        "TIME_GOKEI_7FURLONG": "CHAR(4)",
        "LAPTIME_7FURLONG": "CHAR(3)",
        "TIME_GOKEI_6FURLONG": "CHAR(4)",
        "LAPTIME_6FURLONG": "CHAR(3)",
        "TIME_GOKEI_5FURLONG": "CHAR(4)",
        "LAPTIME_5FURLONG": "CHAR(3)",
        "TIME_GOKEI_4FURLONG": "CHAR(4)",
        "LAPTIME_4FURLONG": "CHAR(3)",
        "TIME_GOKEI_3FURLONG": "CHAR(4)",
        "LAPTIME_3FURLONG": "CHAR(3)",
        "TIME_GOKEI_2FURLONG": "CHAR(4)",
        "LAPTIME_2FURLONG": "CHAR(3)",
        "LAPTIME_1FURLONG": "CHAR(3)"
    }

    hanro_columns_with_types = {
        "TRACEN_KUBUN": "CHAR(1)",
        "CHOKYO_NENGAPPI": "CHAR(8)",
        "CHOKYO_JIKOKU": "CHAR(4)",
        "TIME_GOKEI_4FURLONG": "CHAR(4)",
        "LAP_TIME_4FURLONG": "CHAR(3)",
        "TIME_GOKEI_3FURLONG": "CHAR(4)",
        "LAP_TIME_3FURLONG": "CHAR(3)",
        "TIME_GOKEI_2FURLONG": "CHAR(4)",
        "LAP_TIME_2FURLONG": "CHAR(3)",
        "LAP_TIME_1FURLONG": "CHAR(3)"
    }

    with get_connection() as conn:
        cursor = conn.cursor()
        print("📋 データ取得中...")
        cursor.execute("SELECT * FROM w_joined_race")
        df = pd.DataFrame(cursor.fetchall(), columns=[desc[0] for desc in cursor.description])
        print(f"✅ w_joined_race: {len(df)}件")

        cursor.execute("SELECT * FROM woodchip_chokyo WHERE CHOKYO_NENGAPPI >= '20150101'")
        df_wood = pd.DataFrame(cursor.fetchall(), columns=[desc[0] for desc in cursor.description])

        cursor.execute("SELECT * FROM hanro_chokyo WHERE CHOKYO_NENGAPPI >= '20150101'")
        df_hanro = pd.DataFrame(cursor.fetchall(), columns=[desc[0] for desc in cursor.description])
        print(f"✅ woodchip_chokyo: {len(df_wood)}件, hanro_chokyo: {len(df_hanro)}件")

    # 日付変換
    df["RACE_DATE"] = pd.to_datetime(df["KAISAI_NEN"].astype(str) + df["KAISAI_GAPPI"].astype(str), format="%Y%m%d", errors="coerce")
    df_wood["CHOKYO_NENGAPPI_DT"] = pd.to_datetime(df_wood["CHOKYO_NENGAPPI"], format="%Y%m%d", errors="coerce")
    df_hanro["CHOKYO_NENGAPPI_DT"] = pd.to_datetime(df_hanro["CHOKYO_NENGAPPI"], format="%Y%m%d", errors="coerce")

    # ウッドチップ追い切りデータを結合
    print("🔗 woodchip_chokyoを結合中...")
    df_wood = df_wood.sort_values("CHOKYO_NENGAPPI_DT")
    df = df.sort_values("RACE_DATE")
    
    df = pd.merge_asof(
        df,
        df_wood[["KETTO_TOROKU_BANGO", "CHOKYO_NENGAPPI_DT"] + list(wood_columns_with_types.keys())],
        by="KETTO_TOROKU_BANGO",
        left_on="RACE_DATE",
        right_on="CHOKYO_NENGAPPI_DT",
        direction="backward"
    )

    # 坂路追い切りデータを結合(suffixがつく)
    print("🔗 hanro_chokyoを結合中...")
    df_hanro = df_hanro.sort_values("CHOKYO_NENGAPPI_DT")
    df = pd.merge_asof(
        df,
        df_hanro[["KETTO_TOROKU_BANGO", "CHOKYO_NENGAPPI_DT"] + list(hanro_columns_with_types.keys())],
        by="KETTO_TOROKU_BANGO",
        left_on="RACE_DATE",
        right_on="CHOKYO_NENGAPPI_DT",
        direction="backward",
        suffixes=('', '_HANRO')
    )

    # カラム名リネーム(WOOD_)
    for col in wood_columns_with_types:
        if col in df.columns:
            df.rename(columns={col: f"WOOD_{col}"}, inplace=True)

    # カラム名リネーム(_HANRO → HANRO_)
    for col in hanro_columns_with_types:
        if f"{col}_HANRO" in df.columns:
            df.rename(columns={f"{col}_HANRO": f"HANRO_{col}"}, inplace=True)
        elif col in df.columns:
            df.rename(columns={col: f"HANRO_{col}"}, inplace=True)

    # テーブル作成
    with get_connection() as conn:
        cursor = conn.cursor()
        print("🆕 新テーブル作成中...")
        cursor.execute("DROP TABLE IF EXISTS w_joined_race_chokyo")
        cursor.execute("CREATE TABLE w_joined_race_chokyo LIKE w_joined_race")

        for col, col_type in wood_columns_with_types.items():
            try:
                cursor.execute(f"ALTER TABLE w_joined_race_chokyo ADD COLUMN `WOOD_{col}` {col_type}")
            except Exception as e:
                print(f"⚠️ カラム追加失敗: WOOD_{col} → {e}")

        for col, col_type in hanro_columns_with_types.items():
            try:
                cursor.execute(f"ALTER TABLE w_joined_race_chokyo ADD COLUMN `HANRO_{col}` {col_type}")
            except Exception as e:
                print(f"⚠️ カラム追加失敗: HANRO_{col} → {e}")
        conn.commit()

        # 不要列削除
        df.drop(columns=["RACE_DATE", "CHOKYO_NENGAPPI_DT", "CHOKYO_NENGAPPI_DT_HANRO"], inplace=True, errors="ignore")

        # INSERT対象の列だけを抽出
        all_columns = df.columns.tolist()
        base_cols = [col for col in all_columns if not col.startswith("WOOD_") and not col.startswith("HANRO_")]
        wood_cols = [f"WOOD_{col}" for col in wood_columns_with_types if f"WOOD_{col}" in all_columns]
        hanro_cols = [f"HANRO_{col}" for col in hanro_columns_with_types if f"HANRO_{col}" in all_columns]
        insert_cols = base_cols + wood_cols + hanro_cols

        df = df[insert_cols]
        df = df.where(pd.notnull(df), None)

        print("💾 一括INSERT中...")
        insert_sql = f"INSERT INTO w_joined_race_chokyo ({','.join(insert_cols)}) VALUES ({','.join(['%s'] * len(insert_cols))})"
        data = df.values.tolist()

        for i in tqdm(range(0, len(data), 1000)):
            with conn.cursor() as cursor:
                cursor.executemany(insert_sql, data[i:i + 1000])
            conn.commit()

    print("✅ 全件完了")

解説

ここで注目すべきは pd.merge_asof を使った調教データの結合 です。通常の JOIN では「同じ日付」しか結合できませんが、merge_asof を使うと「レース日以前で最も近い調教データ」を自動で引っ張ってきてくれます。

  • woodchip_chokyo は WOOD_ プレフィックスで整理
  • hanro_chokyo は HANRO_ プレフィックスで整理

これにより、カラム名の衝突を避けつつ、情報を一目で区別できるようになります。

また、MySQL 側でも 新しいテーブル w_joined_race_chokyo をDROP→CREATE する流れを入れておくことで、毎回フレッシュな状態で処理をやり直せます。

ポイント

  • 直近の追い切りだけを結合
    レース日に最も近い調教情報を効率的に紐付け。
  • カラム名を統一管理
    WOOD_ と HANRO_ の接頭辞を使い、調教種別がすぐに分かる構成。
  • バッチ挿入で安定化
    データ件数が膨大なため、1000件ごとの分割 INSERT を採用。処理失敗を防ぎ、リソース負荷を抑えます。

コードの呼び出し方

この関数は前回のコードと同じくinit_db.py から実行します。
複数の作業テーブルをまとめて作成する流れの中で enrich_w_joined_race_bulk() が呼ばれます。以下のようなコードになります。

from db.init_work_tables import (
    create_ranked_race_base,
    create_ranked_race_temp,
    create_joined_race_table,
    enrich_w_joined_race_bulk
)

if __name__ == "__main__":
    print("🛠️ ranked_race_base を作成中...")
    create_ranked_race_base()

    print("🛠️ ranked_race_temp を作成中...")
    create_ranked_race_temp()

    print("🛠️ joined_race を作成中...")
    create_joined_race_table()

    print("🛠️ joined_race に調教データ追加中...")
    enrich_w_joined_race_bulk()

    print("✅ すべて完了しました。")

まとめ

学習データに調教情報を組み込むことで、仕上がり具合を考慮した予測が可能になります。

  • merge_asof による直近データの結合
  • WOOD_/HANRO_ プレフィックスで明確化
  • バッチ処理による安定したINSERT

この工夫を積み重ねることで、より実戦に即した競馬AIモデルの構築ができるようになります。

コメント