JvLink To Importer

Pythonで競馬予測モデルを作る流れ

Pythonで競馬予測モデルを作る流れ

競馬データベースを構築したら、Pythonから直接データベースに接続して予測モデルを作れます。この記事では、PostgreSQLの競馬データを使ってLightGBMで予測モデルを構築するまでの全体の流れを紹介します。


全体の流れ

  1. データベースからデータを取得
  2. 特徴量を作る
  3. 学習データとテストデータに分割
  4. モデルを学習
  5. 予測して評価

1. データベースからデータを取得

psycopg2やSQLAlchemyを使ってPostgreSQLに接続し、pandasのDataFrameとしてデータを取得します。

import pandas as pd
from sqlalchemy import create_engine

engine = create_engine('postgresql://user:pass@localhost:5432/keiba')

query = """
SELECT u.race_code, u.ketto_toroku_bango, u.umaban, u.wakuban,
u.futan_juryo, u.bataiju, u.zogen_sa, u.tansho_odds,
u.kakutei_chakujun, u.soha_time, u.kohan_3f,
u.corner4_juni, u.kishu_code,
r.keibajo_code, r.kyori, r.track_code,
r.shiba_babajotai_code, r.kaisai_nen, r.kaisai_gappi
FROM umagoto_race_joho u
JOIN race_shosai r USING (race_code)
WHERE r.kaisai_nen >= '2020'
AND u.kakutei_chakujun BETWEEN '01' AND '18'
"""

df = pd.read_sql(query, engine)

2. 特徴量を作る

生データのままではモデルに入れられません。過去の成績を集約して「この馬がどれくらい強いか」を表す数値を作ります。

よく使う特徴量の例

# 着順を数値化
df['finishing_pos'] = df['kakutei_chakujun'].astype(int)

# 目的変数: 3着以内なら1
df['target'] = (df['finishing_pos'] <= 3).astype(int)

# 過去5走の平均着順(馬ごとに計算)
df = df.sort_values(['ketto_toroku_bango', 'race_code'])
df['avg_pos_5'] = df.groupby('ketto_toroku_bango')['finishing_pos'].transform(
lambda x: x.shift(1).rolling(5, min_periods=1).mean()
)

# 過去5走の上がり3Fの平均
df['avg_3f_5'] = df.groupby('ketto_toroku_bango')['kohan_3f'].transform(
lambda x: x.shift(1).rolling(5, min_periods=1).mean()
)

# 同コースでの複勝率
df['course_key'] = df['keibajo_code'] + '_' + df['kyori'].astype(str)
df['course_place_rate'] = df.groupby(['ketto_toroku_bango', 'course_key'])['target'].transform(
lambda x: x.shift(1).expanding().mean()
)

ポイントは shift(1) で「未来の情報を使わない」ようにすることです。これを忘れるとリーク(データ漏洩)が起き、実際の予想では使えないモデルになります。

特徴量の一覧例

特徴量 説明
avg_pos_5 過去5走の平均着順
avg_3f_5 過去5走の上がり3F平均
best_3f 過去の上がり3F最速値
course_place_rate 同コースでの複勝率
jockey_win_rate 騎手の勝率
weight_diff 馬体重の前走比
odds 単勝オッズ
draw 枠番
distance 距離
field_condition 馬場状態

3. 学習データとテストデータに分割

競馬データは時系列なので、ランダム分割ではなく時間で区切るのが正しいやり方です。

# 2020〜2024年を学習、2025年をテスト
train = df[df['kaisai_nen'] <= '2024']
test = df[df['kaisai_nen'] == '2025']

feature_cols = ['avg_pos_5', 'avg_3f_5', 'course_place_rate',
'futan_juryo', 'bataiju', 'zogen_sa', 'tansho_odds',
'wakuban', 'kyori']

X_train = train[feature_cols]
y_train = train['target']
X_test = test[feature_cols]
y_test = test['target']

4. モデルを学習

LightGBMは競馬予測で最もよく使われるモデルの一つです。学習が速く、精度も高い。

import lightgbm as lgb

lgb_train = lgb.Dataset(X_train, y_train)
lgb_valid = lgb.Dataset(X_test, y_test, reference=lgb_train)

params = {
'objective': 'binary',
'metric': 'binary_logloss',
'learning_rate': 0.05,
'num_leaves': 31,
'feature_fraction': 0.8,
'bagging_fraction': 0.8,
'bagging_freq': 5,
'verbose': -1,
}

model = lgb.train(
params,
lgb_train,
num_boost_round=1000,
valid_sets=[lgb_valid],
callbacks=[lgb.early_stopping(50)],
)

5. 予測して評価

モデルが出力するのは「3着以内に入る確率」です。

test['pred_prob'] = model.predict(X_test)

# レースごとに予測確率で順位付け
test['pred_rank'] = test.groupby('race_code')['pred_prob'].rank(ascending=False)

評価指標

単純な正解率よりも、競馬予想では回収率が重要です。

# 予測1位の馬の単勝を買い続けた場合の回収率
top_picks = test[test['pred_rank'] == 1]
total_bets = len(top_picks)
wins = top_picks[top_picks['finishing_pos'] == 1]
returns = wins['tansho_odds'].sum() / 10  # オッズは10倍で格納されている

print(f'レース数: {total_bets}')
print(f'勝率: {len(wins) / total_bets * 100:.1f}%')
print(f'回収率: {returns / total_bets * 100:.1f}%')

回収率が100%を超えていればプラス収支です。


精度を上げるためのヒント

  • 特徴量を増やす: 血統データ、調教データ、騎手との相性など、使えるデータは多い
  • ハイパーパラメータの調整: Optunaなどの自動チューニングツールを活用
  • 馬券種を工夫: 単勝だけでなく、複勝やワイドも含めて期待値を計算
  • 対象レースを絞る: すべてのレースで勝つ必要はない。得意な条件だけに絞ることで回収率が上がる

まとめ

ステップ ポイント
データ取得 pandasでDB直結、SQLで必要なデータだけ抽出
特徴量作成 shift(1)でリーク防止、過去成績の集約がカギ
データ分割 時系列分割(ランダムはNG)
モデル学習 LightGBMが定番、early_stoppingで過学習防止
評価 勝率よりも回収率で判断

データベースにデータさえあれば、上記のコードをそのまま動かして予測モデルを試せます。

JvLink To Importer でデータベースを構築して、予測モデル作りに挑戦してみてください。

この記事をシェア

Post