本記事の概要
本サイトでは、競馬AIの作成方法について解説しています。
そして、過去に作成した競馬AIでは直接「1着になる競走馬か否か」を予測しており、いずれも回収率は80%台に留まっています。

※横軸が「1着になる確率」、縦軸が「y_pred_probがある値を超えた競争馬に単勝を賭けた時の回収率」を表しています。
そこで今回は少し見方を変えて、
競走馬の走破タイム
を予測し、その結果をもとに着順および回収率を計算していきます。
特徴量の重要度や回収率について、どのような違いが見られるのかをチェックしていきます!
前提条件

続いて、競馬AI作成における前提条件を説明します。
①特徴量
大きく分けて、「レース情報」「競争馬情報(絶対情報)」「競走馬情報(相対情報)」の3つを特徴量としています。
特徴量の入手・加工方法
上記特徴量は、「netkeiba.com」から入手したデータをそのまま、もしくは加工しています。
入手・加工のソースコードについては、本記事で解説していないので過去記事を参照してください。
カテゴリ | 対象データ | URL |
データ入手 (スクレイピング) |
レース結果のURL | https://ryohei22.com/scraping-get_url-central/ |
レース結果 | https://ryohei22.com/scraping-race_result/ | |
競走馬の競争成績 | https://ryohei22.com/scraping-horse_result/ | |
データ加工 | 大部分の特徴量 | https://ryohei22.com/scraping-edit_data/ |
血統情報 | https://ryohei22.com/win-prediction4/ | |
スピード指数 | https://ryohei22.com/win-prediction3/ |
②使用する機械学習アルゴリズム
今回使用する機械学習アルゴリズムは、
LightGBM
です。
LightGBMとは、Microsoftが開発した分散型勾配ブースティングフレームワークのことで、詳細な解説は「最近流行りのLightGBMとは」を参照してください。
LightGBMには下記の特徴があり、実装の容易性と高い回収率が期待できると考え採用しました。
ソースコード解説

以降では、競馬AIの作成方法についてソースコードを交えて解説していきます。
①データ分割(訓練データ/検証データ/テストデータ・説明変数/目的変数)
この箇所のソースコードは下記のとおりです。
# パラメータ
date_split_train = 20200401 # テストデータの開始点(年+月+日)
date_split_valid = 20210801 # 訓練データと検証データの分岐点(年+月+日)
date_split_test = 20211101 # 検証データとテストデータの分岐点(年+月+日)
# 訓練データ・検証データ・テストデータに分割
train = df[df['date'] < date_split_valid]
valid = df[(df['date'] >= date_split_valid) & (df['date'] < date_split_test)]
test = df[df['date'] >= date_split_test]
odds_df_test = odds_df[odds_df['date'] >= date_split_test]
# 説明変数
drop_list = ['time']
x_train = train.drop(drop_list, axis=1)
x_valid = valid.drop(drop_list, axis=1)
x_test = test.drop(drop_list, axis=1)
# 目的変数
y_train = train['time']
y_valid = valid['time']
y_test = test['time']
y_test_org = pd.DataFrame(test['rank'])
以降で、各要素について解説していきます。
訓練データ・検証データ・テストデータに分割
まずは、データを訓練データ・検証データ・テストデータの3つに分けます。
訓練データ
→用途:モデルを作成するために使用するデータ
→データ期間:2022/4/1 ~ 2021/8/1
検証データ
→用途:ハイパーパラメータを調整するために使用するデータ
→データ期間:2021/8/1 ~ 2021/11/1
テストデータ
→用途:モデルの汎用性を評価するために使用するデータ
→データ期間:2021/11/1 ~ 2022/4/1
# パラメータ
date_split_train = 20200401 # テストデータの開始点(年+月+日)
date_split_valid = 20210801 # 訓練データと検証データの分岐点(年+月+日)
date_split_test = 20211101 # 検証データとテストデータの分岐点(年+月+日)
# 訓練データ・検証データ・テストデータに分割
train = df[df['date'] < date_split_valid]
valid = df[(df['date'] >= date_split_valid) & (df['date'] < date_split_test)]
test = df[df['date'] >= date_split_test]
odds_df_test = odds_df[odds_df['date'] >= date_split_test]
説明変数と目的変数に分ける
説明変数は「①特徴量」のデータで、目的変数は「走破タイム」です。
# 説明変数
drop_list = ['time']
x_train = train.drop(drop_list, axis=1)
x_valid = valid.drop(drop_list, axis=1)
x_test = test.drop(drop_list, axis=1)
# 目的変数
y_train = train['time']
y_valid = valid['time']
y_test = test['time']
y_test_org = pd.DataFrame(test['rank'])
②モデル作成
この箇所のソースコードは下記のとおりです。
""" 2. モデル作成 """
# ライブラリ
import optuna.integration.lightgbm as lgb
# LightGBM用のデータセットに変換
lgb_train = lgb.Dataset(x_train, y_train)
lgb_valid = lgb.Dataset(x_valid, y_valid, reference=lgb_train)
# ハイパーパラメータ
params = {'task': 'train',
'boosting_type': 'gbdt',
'objective': 'regression',
'metric': 'rmse',
'learning_rate': 0.01,
'random_state': 777}
# モデル作成
model = lgb.train(params,
lgb_train,
valid_sets=[lgb_train, lgb_valid],
early_stopping_rounds=50,
verbose_eval=100)
以降で、各要素について解説していきます。
学習用のデータセットを作成
今回作成するLightGBMのモデルは、Python APIを使用します。
そのため、「lgb.Dataset()」で専用のデータセットを事前に作成します。
# ライブラリ
import optuna.integration.lightgbm as lgb
# LightGBM用のデータセットに変換
lgb_train = lgb.Dataset(x_train, y_train)
lgb_valid = lgb.Dataset(x_valid, y_valid)
モデル作成
続いて、ハイパーパラメータを指定してモデルを作成します。
# ハイパーパラメータ
params = {'boosting_type': 'gbdt',
'objective': 'regression', # 回帰モデルを作成する場合は「regression」を指定する必要があります。
'metric': 'rmse',
'learning_rate': 0.01,
'random_state': 777}
# モデル作成
model = lgb.train(params,
lgb_train,
valid_sets=[lgb_train, lgb_valid],
early_stopping_rounds=50,
verbose_eval=100)
今回は「Optuna」を利用して、LightGBMの学習モデルを作成しています。
自動でパラメータチューニングしてくれるため
なお、自動チューニングしてくれるパラメータは下記の7つです。
- lambda_la
- lambda_l2
- num_leaves
- feature_fraction
- bagging_fraction
- bagging_freq
- min_child_samples
また、上記以外のパラメータはこちらで設定する必要があるので、「params」という変数で指定しています。
以降で、今回設定したパラメータを解説していきます。
boosting_type
ブースティングのアルゴリズムを選択できます。
基本は勾配ブースティングを行うことが多いので、デフォルトの ‘gbdt’ でよいと思います。
objective
目的関数を選択できます。
今回は回帰モデルを作成するため、’regression’ を指定しています。
metric
評価指標を選択できます。
回帰モデルの場合は ‘rmse’、’auc’ が代表的ですが、今回は’rmse’を採用しています。

RMSEとは二乗平均平方根誤差のことで、上記式で表されます。
この値が小さいほど、誤差の小さいモデルであると言えます。
learning_rate
学習率を指定できます。
値を大きくするほど処理時間は短くなりますが、精度は落ちます。
一般的に、0.01を設定することが多いです。
③モデル評価
この箇所のソースコートは下記のとおりです。
""" 3. モデルの評価 """
# テストデータの走破タイムを予測
y_pred_time = model.predict(x_test)
# 予測した走破タイムから着順を算出
df_pred = pd.DataFrame(data={'race_ID': x_test['race_ID'],
'horse_ID': x_test['horse_ID'],
'win_odds': odds_df_test['win_odds'],
'actual_time': y_test,
'pred_time': y_pred_time,
'actual_rank': y_test_org['rank']
})
df_pred['pred_rank'] = df_pred[['race_ID', 'pred_time']].groupby('race_ID').rank(ascending=True)
# 的中率を計算
df_pred['hit_mark'] = df_pred.apply(lambda x: 1 if (x['actual_rank'] == 1 and\
x['actual_rank'] == x['pred_rank'])\
else 0, axis=1)
acc_rate = df_pred['hit_mark'].sum() / len(df_pred['race_ID'].unique()) * 100
# 回収率を計算
return_rate = df_pred.loc[df_pred['hit_mark'] == 1, 'win_odds'].sum() / len(df_pred['race_ID'].unique()) * 100
以降で、各要素について解説していきます。
走破タイムを予測
まずは、作成したモデルを用いてテストデータの走破タイムを予測します。
「モデル.predect()」を記述するだけで、簡単に予測できますよ。
# テストデータの走破タイムを予測
y_pred_time = model.predict(x_test)
予測した走破タイムから着順を算出
予測結果と実際の結果(走破タイム・着順)を比較しやすいように、DataFrameを作成します。
下記のように記述することで、列名を指定しながらDataFrameを作成できます。
pd.DataFrame( data = {‘列名’: 値, ・・・} )
その後、「groupby()」と「rank()」を使用してレース内の順位を算出しています。
# 予測した走破タイムから着順を算出
df_pred = pd.DataFrame(data={'race_ID': x_test['race_ID'],
'horse_ID': x_test['horse_ID'],
'win_odds': odds_df_test['win_odds'],
'actual_time': y_test,
'pred_time': y_pred_time,
'actual_rank': y_test_org['rank']
})
df_pred['pred_rank'] = df_pred[['race_ID', 'pred_time']].groupby('race_ID').rank(ascending=True)
回収率を計算
まずは、下記の2条件を満足する行にマーク(1:的中、0:外れ)をつけます。
その際に「apply()」を使用して、DataFrameの行ごとに判定処理を行っています。
その後、下記の計算式で回収率を計算しています。
的中率 = 的中数 / レース数 × 100
# 回収率を計算
df_pred['hit_mark'] = df_pred.apply(lambda x: 1 if (x['actual_rank'] == 1 and\
x['actual_rank'] == x['pred_rank'])\
else 0, axis=1)
return_rate = df_pred.loc[df_pred['hit_mark'] == 1, 'win_odds'].sum() / len(df_pred['race_ID'].unique()) * 100
# 回収率を出力
print(acc_rate)
>>> 80.50925925925927
回収率の計算結果は約80.5%でした。
過去に作成した2値分類モデルの回収率は、最高で88%だったので若干性能は劣るようです。

しかしながら、今回の賭け方は「1位と予測された全ての競走馬に賭ける」という単純なロジックを採用したので、賭け方を改善することで回収率は改善できそうです。
④特徴量の重要度
今回作成したモデルについて、各特徴量がどの程度影響及ぼしているかを確認します。
""" 4. 特徴量の重要度 """
lgb.plot_importance(model, max_num_features=20)

特に重要度の高い特徴量は、下記の4つでした。
全体ソースコード
最後に、全体ソースコードを掲載します。
""" 走破タイムを予測 """
# ライブラリ
import pandas as pd
import datetime
# パラメータ
date_split_train = 20200401 # テストデータの開始点(年+月+日)
date_split_valid = 20210801 # 訓練データと検証データの分岐点(年+月+日)
date_split_test = 20211101 # 検証データとテストデータの分岐点(年+月+日)
file_directory = 'C:/Users/kouhe/OneDrive/01_Documents/02_機械学習/データ'
file_name = 'race_result.csv'
# データ読み込み
race_result_total = pd.read_csv(file_directory + '/race_result.csv')
horse_result = pd.read_csv(file_directory + '/horse_result.csv', index_col=0)
horse_ped = pd.read_csv(file_directory + '/horse_peds.csv', index_col=0)
return_table = pd.read_csv(file_directory + '/return_table.csv', index_col=0)
race_result = race_result_total[race_result_total['date'] >= date_split_train]
# データ加工
df_edit = edit_data(race_result, horse_result, horse_ped)
# オッズを取得
odds_df = pd.DataFrame(data={'date': df_edit['date'],
'win_odds': df_edit['win_odds']})
odds_test = odds_df.loc[odds_df['date'] >= date_split_test, 'win_odds']
# 不要な行を削除
drop_list = ['rank']
df = df_edit.drop(drop_list, axis=1)
# 訓練データ・テストデータに分割
train = df[df['date'] < date_split_valid]
valid = df[(df['date'] >= date_split_valid) & (df['date'] < date_split_test)]
test = df[df['date'] >= date_split_test]
odds_df_test = odds_df[odds_df['date'] >= date_split_test]
# 説明変数
drop_list = ['time']
x_train = train.drop(drop_list, axis=1)
x_valid = valid.drop(drop_list, axis=1)
x_test = test.drop(drop_list, axis=1)
# 目的変数
y_train = train['time']
y_valid = valid['time']
y_test = test['time']
y_test_org = pd.DataFrame(test['rank'])
""" 2. モデル作成 """
# ライブラリ
import optuna.integration.lightgbm as lgb
# LightGBM用のデータセットに変換
lgb_train = lgb.Dataset(x_train, y_train)
lgb_valid = lgb.Dataset(x_valid, y_valid, reference=lgb_train)
# ハイパーパラメータ
params = {'task': 'train',
'boosting_type': 'gbdt',
'objective': 'regression',
'metric': 'rmse',
'learning_rate': 0.01,
'random_state': 777}
# モデル作成
model = lgb.train(params,
lgb_train,
valid_sets=[lgb_train, lgb_valid],
early_stopping_rounds=50,
verbose_eval=100)
""" 3. モデルの評価 """
# テストデータの走破タイムを予測
y_pred_time = model.predict(x_test)
# 予測した走破タイムから着順を算出
df_pred = pd.DataFrame(data={'race_ID': x_test['race_ID'],
'horse_ID': x_test['horse_ID'],
'win_odds': odds_df_test['win_odds'],
'actual_time': y_test,
'pred_time': y_pred_time,
'actual_rank': y_test_org['rank']
})
df_pred['pred_rank'] = df_pred[['race_ID', 'pred_time']].groupby('race_ID').rank(ascending=True)
# 回収率
df_pred['hit_mark'] = df_pred.apply(lambda x: 1 if (x['actual_rank'] == 1 and\
x['actual_rank'] == x['pred_rank'])\
else 0, axis=1)
return_rate = df_pred.loc[df_pred['hit_mark'] == 1, 'win_odds'].sum() / len(df_pred['race_ID'].unique()) * 100
""" 4.特徴量の重要度 """
lgb.plot_importance(model, max_num_features=20)
考察(特徴量の重要度を受けて)

今回のモデルは走破タイムを目的変数に設定したので、「race_len」「race_ID」の重要度が大きくなっているのは納得の結果でした。
一方で、「jockey_ID」「ped_0」の重要度が大きくなっているのは意外でした。

というのも、前回モデルにおける特徴量の重要度を見ると、「jockey_ID」や「ped_0」はあまり大きな重要度ではなかったからです。

今回モデルで影響が大きかった「jockey_ID」と「ped_0」はどちらもカテゴリ変数で、その他にもいくつかカテゴリ変数を使用しているので、
CatBoost
を採用した方が回収率が上がるかもしれませんね。
CatBoostとは?
CatBoostとは「Category Boosting」の略称で、LightGBMと同様に勾配ブースティングの一種です。
その名の通り、カテゴリ変数の多いデータに強い特徴があります。
その代わりに処理速度は遅くなりそうですが、、、
下記の記事によると、LightGBMと同様にCatBoostでも「Oputuna」でパラメータチューニングができそうなので、これは嬉しいですね。
コメント