プログラミング初心者がChatGPTを使って競馬予想AIを作ることで、生成AIとプログラミングについて学んでいく企画の第15回です。
↓前回の記事
馬の過去成績には以下の列が存在し、前回からいくつかの特徴量について加工を行いました。
特にタイムや着差はそのまま使うとレースを予測する時点で、既にその結果を知っているような状態になってしまうので注意が必要でした。
(オッズや人気も直前までわからない不安定な特徴量になります。)
〇 ... 追加済み、△ ... 追加無し、× ... 未追加
"""
〇 オッズ(odds)
〇 人気(popularity)
〇 最終順位(finish_position)
〇 斤量(weight)
〇 タイム(time)
△ 着差(margin)
〇 上り(final_3f)
× 賞金(prize_money)
× 通過順位1(passing_1st)
× 通過順位2(passing_2st)
× 通過順位3(passing_3st)
× 通過順位4(passing_4st)
× ペース1(pace_1st)
× ペース2(pace_2nd)
△ 馬体重(body_weight)
△ 差分(body_diff)
"""
まだまだ追加していない特徴量がたくさんあるため、今回はそれらを追加し予測精度への影響を確認しましょう。
賞金(prize_money)
賞金はそのレースのクラス水準を直接的に表す指標です。
同着・下位入選でも一定額が入るため、「勝ちきれなくても強い相手と戦ってきた履歴」となります。
回収率的には「前走高賞金×今回低人気」が最も歪みやすいらしいのですが、今回はシンプルに対数に変換して直近N走のローリング特徴量にしましょう。
# 対数変換(賞金の歪み対策)
df['log_prize_money'] = np.log1p(df['prize_money'])
# 直近N走平均(例:3走)
df['log_prize_avg3'] = (
df.groupby('horse_id')['log_prize_money']
.rolling(3).mean()
.reset_index(level=0, drop=True)
)
通過順位(passing_1st ~ _4th)
この指標は馬の位置取り戦略・脚質・レース内適応力を表します。
どの位置だったかは当然出走馬の頭数に関係しますので、頭数で割って相対位置に変換してあげましょう。
passing_cols = ['passing_1st','passing_2nd','passing_3rd','passing_4th']
# 頭数で割って相対位置に変換
for c in passing_cols:
df[c + '_rel'] = (df['num_of_horses'] - df[c]) / df['num_of_horses']
# 直近N走平均(例:3走)
df[c + '_rel' + '_avg3'] = (df.sort_values('race_date')
.groupby('horse_id')[c + '_rel']
.rolling(3).mean().
reset_index(level=0, drop=True)
)
予測してみよう
これらの特徴量を加えて予測しましょう。
今回はテーブルに既に存在していた出走頭数も追加してみます。
といいたいところですが、今回も相変わらず的中率は0%のままでした…
(結果を載せるのも憚られるため、省略で失礼します)
多種多様な原因が考えられるのですが、とりあえずデータ量が少なすぎて追加した特徴量がほとんど欠損値 になってしまうのはどうにかしたいです。
これでは追加した特徴量を正しく評価できていないません。
試しにローリング特徴量のn試合平均がどれだけ欠損値になっているか、調べてみました。
# log_odds_avgの各カラムの欠損値をチェック
nan_counts = {
'log_odds_avg3': df_clean['log_odds_avg3'].isna().sum(),
'log_odds_avg5': df_clean['log_odds_avg5'].isna().sum(),
'log_odds_avg10': df_clean['log_odds_avg10'].isna().sum(),
}
total_rows = len(df_clean)
print(f"全行数: {total_rows}")
print("\n各カラムの欠損値数:")
for col, count in nan_counts.items():
percentage = (count / total_rows) * 100
print(f"{col}: {count} ({percentage:.2f}%)")
全行数: 28506
各カラムの欠損値数:
log_odds_avg3: 21585 (75.72%)
log_odds_avg5: 27146 (95.23%)
log_odds_avg10: 28506 (100.00%)
3試合平均ですら7割以上のデータが欠損値になっており、10試合平均は100%です。
(数か月で10試合に出ている馬なんてそうそういるわけないですよね)
次回はデータを拡充するとともに、クラスを使ってコードの保守管理も高めていこうと思います。