競馬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モデルの構築ができるようになります。
コメント