本記事の目的
本記事は、下記事項を目的としています。
前回モデルの回収率
本ページでは、過去に作成したモデルを改良方法とその結果を紹介します。
過去モデルの詳細は「【競馬予測】LightGBM(多クラス分類)で単勝予測|血統情報等の特徴量を追加」で解説しているので、詳しく知りたい方は読んでみてください。
ちなみに過去データでシミュレーションした結果、回収率の最大値は86%位でした。。。

前回モデルからの変更点

本記事では、「過去モデルを改善し、その結果回収率は向上するのか」という観点で解説を行っていきます。
今回は下記2つの変更を加えました。
①特徴量に「スピード指数」を追加
いくつか種類がありますが、今回は「西田式スピード指数」をベースにした方法でスピード指数を計算します。
■西田式スピード指数■
スピード指数 = (基準タイム – 走破タイム) × 距離指数 + 馬場指数 + (斤量 – 55) × 2 + 80
各項の解説は公式サイトにあるので、そちらをご覧ください。
②LightGBMの目的関数を「多クラス分類」から「2値分類」に変更
以前のモデルでは、
の4クラスを分類していました。
しかし、
これらの理由から、「1着」「2着以降」で2値分類することにしました。
ソースコード解説

以降では、今回加えた変更点についてソースコードを交えて解説していきます。
①スピード指数
西田式スピード指数をベースにスピード指数を計算します。
■西田式スピード指数■
スピード指数 = (基準タイム – 走破タイム) × 距離指数 + 馬場指数 + (斤量 – 55) × 2 + 80
以降では、上記式の各項について解説します。
基準タイム
基準タイムは
の4項目の全組み合わせ毎に、上位3着の走破タイムの平均値で計算します。
# 基準タイムを計算(競馬場・レース長・芝/ダート・馬場毎の上位3着の平均値)
horse_df['date'] = horse_df['日付'].map(lambda x: x.replace('/', '')).astype(int)
past_df = horse_df[(horse_df['date'] < df['date'].min())
& (horse_df['date'] >= (df['date'].min() - 50000))
& (horse_df['着順'] <= 3)]
std_time = past_df[['time', 'area_ID', 'race_len', 'race_type', '馬場']]\
.groupby(['area_ID', 'race_len', 'race_type', '馬場'])\
.median()
std_time['time'] = std_time['time'].map(lambda x: x * 10)
走破タイム
名前の通り、競走馬のゴールタイムです。
これは、「netkeiba.com」からスクレイピングしたデータを使用するので、解説は割愛します。
距離指数
レース長ごとに1秒の価値が異なるので、その差を減らすための指数です。
計算方法は単純で
1秒 ÷ 基準タイム × 1000
で計算するだけです。
馬場指数
馬場状態によってもレースの結果は異なるので、その差を減らすための指数です。
この計算方法の詳細は、「西田式スピード指数」で解説されていないため、正直なところどう計算すればよいかが分かりませんでした。
なので今回は、「基準タイム」に馬場状態も含めたパターンを計算し、馬場指数は無視することにしました。
全体ソースコード(スピード指数計算)
スピード指数の各項の解説が終わったので、全体ソースコードを紹介します。
# 基準タイムを計算(競馬場・レース長・芝/ダート・馬場毎の上位3着の平均値)
horse_df['date'] = horse_df['日付'].map(lambda x: x.replace('/', '')).astype(int)
past_df = horse_df[(horse_df['date'] < df['date'].min())
& (horse_df['date'] >= (df['date'].min() - 50000))
& (horse_df['着順'] <= 3)]
std_time = past_df[['time', 'area_ID', 'race_len', 'race_type', '馬場']]\
.groupby(['area_ID', 'race_len', 'race_type', '馬場'])\
.median()
std_time['time'] = std_time['time'].map(lambda x: x * 10)
# スピード指数を計算
print('【スピード指数を計算】')
# 処理を早くするために不要な行を削除
horse_df_aft = horse_df[['time', 'area_ID', 'race_len', 'race_type', '馬場']]
speed_ind = []
for i in tqdm(horse_df.index):
area_ID = int(horse_df_aft.loc[i, 'area_ID'])
race_len = int(horse_df_aft.loc[i, 'race_len'])
race_type = int(horse_df_aft.loc[i, 'race_type'])
baba = str(horse_df_aft.loc[i, '馬場'])
if (area_ID, race_len, race_type, baba) in std_time.index:
# 基準タイム
std = std_time.loc[(area_ID, race_len, race_type, baba), 'time']
# 走破タイム
time = horse_df_aft.loc[i, 'time'] * 10
# 距離指数
len_ind = 1000 / std
# 斤量
weight = horse_df.loc[i, '斤量']
# スピード指数
speed_ind.append((std - time) * len_ind + (weight - 55) * 2 + 80)
else:
speed_ind.append(float('nan'))
horse_df['speed_indicator'] = speed_ind
レース毎に他競走馬のスピード指数を追加
過去5Rのスピード指数の中央値を競争馬毎に計算し、レースに参加する競争馬の平均値・最大値・標準偏差を特徴量に追加します。
# レース毎に競走馬のスピード指数(過去5Rの中央値)の平均値・最大値・標準偏差を追加
print('【他競走馬のスピード指数を追加】')
speed, speed_ave, speed_std, speed_max = [], [], [], []
for race_id in tqdm(df['race_ID'].unique()):
date = int(df.loc[df['race_ID'] == race_id, 'date'].head(1))
horse_list = list(df.loc[df['race_ID'] == race_id, 'horse_ID'])
speed_ind = [horse_df.loc[(horse_df['horse_ID'] == x) & (horse_df['date'] < date)
, 'speed_indicator'].median()\
if len(horse_df.loc[(horse_df['horse_ID'] == x) & (horse_df['date'] < date)
, 'speed_indicator']) <= 5\
else horse_df.loc[(horse_df['horse_ID'] == x) & (horse_df['date'] < date)
, 'speed_indicator'].head(5).median()\
for x in horse_list]
speed += speed_ind
speed_ind = [x for x in speed_ind if not np.isnan(x)]
speed_ave += [np.mean(speed_ind)] * len(horse_list)
speed_std += [np.std(speed_ind)] * len(horse_list)
speed_max += [max(speed_ind)] * len(horse_list)
df['speed'] = speed
df['speed_ave'] = speed_ave
df['speed_std'] = speed_std
df['speed_max'] = speed_max
②目的関数
LightGBMの目的変数を二値分類に設定して、学習させます。
やり方は簡単で、パラメータのobjectiveの値を「binary」に設定するだけです。
また、metricには「AUC」を選択しています。
# パラメータ
params = {'task': 'train',
'boosting_type': 'gbdt',
'objective': 'binary', # この箇所で目的関数を指定
'metric': 'auc',
'learning_rate': 0.01}
# モデル作成
model = lgb.train(params,
lgb_train,
valid_sets=(lgb_train, lgb_valid),
early_stopping_rounds=100,
verbose_eval=100)
回収率
今回の変化によって回収率がどの程度になるか、確認します。
回収率の計算
「予測確率がxx%を超えたら、その競争馬に単勝を賭ける」というロジックで、回収率を計算します。
本処理では、上記xxの値を0%~500%の間で10%刻みで計算しています。
# 予測結果を整理
df_pred = pd.DataFrame(data={'race_ID': x_test['race_ID'],
'y_pred_prob': y_pred_prob,
'y_test': y_test[0],
'win_odds': odds_df_test['win_odds']})
# 回収率を計算
step_list = [x * 0.01 for x in range(0, 31)]
return_rate_list = []
for cnt in range(0, len(step_list)):
step = step_list[cnt]
df_pred['y_pred_class'] = [1 if x >= step else 0 for x in df_pred['y_pred_prob']]
input_amount = len(df_pred.loc[df_pred['y_pred_class'] == 1, 'y_pred_prob'])
hit_odds_list = list(df_pred.loc[(df_pred['y_pred_class'] == df_pred['y_test']) &\
(df_pred['y_pred_class'] == 1),
'win_odds'])
return_amount = sum(hit_odds_list)
if input_amount > 0:
return_rate_list.append(return_amount / input_amount)
elif input_amount == 0:
return_rate_list.append(float('nan'))
回収率を描画
計算した回収率をグラフ化します。
# 回収率を描画
x = step_list
y = return_rate_list
fig, ax = plt.subplots(1, 1, figsize=(10, 8))
ax.plot(x, y, linestyle='solid', color='k', marker='^', label='race_cnt')
plt.title('Return_Rate', fontsize=20)
# 軸ラベルを追加
ax.set_xlabel('y_pred_prob', fontsize=15)
ax.set_ylabel('return_rate', fontsize=15)
# y軸設定
ax.set_ylim(0.5, 1.5)
# 値ラベルを追加
for i, k in zip(x, y):
if math.isnan(k):
continue
elif k < 0.5 :
continue
else:
plt.text(i, k + 0.05, str(round(k * 100)) + '%', fontsize=10)
回収率の結果は以下のようになりました。

最大で125%を達成していますが、これは予測確率が30%以上の競走馬に賭けた場合なので、該当するレースがとても限られます。
この結果だと、あまり現実的ではありませんね。
特徴量の重要度
次に、特徴量の重要度を確認していきます。
lgb.plot_importance(model, max_num_features=20)

今回は新たに、
の4項目を新たに追加しましたが、それぞれが上位に食い込んでいることが分かります。
今回の変更は回収率にはあまり影響がなかったものの、よりよいモデルを作成できました。
コメント