AIを使った競馬の成績はいかがでしょうか?
私の方は半年間動かして単勝回収率100%を少し下回る辺りを推移しています。
AI競馬予想ページも見ていただけると嬉しいです!
半年間で延べ400頭弱が一定閾値を超え、その馬は馬券内率が57%、1着率が26%となかなかの予測が出来ていると思います。
しかし勝つと予測される馬の多くは3番人気以内の馬ばかりで当たっても利益が多く出ないことに悩んできました。
考えれば当たり前のことで、”勝つと予測される馬=過去のレースも強かった馬=みんな買う”という式になってしまいます。
そこで穴馬を予測するモデルを作成してみました。まだまだ多くのレースで試せていないので結果には現れていませんが、計算上回収率は300%以上となっています。
※私の環境での数値ですので、学習データによって回収率の計算値は異なります。
修正箇所は簡単です。
穴馬を予測するモデルの構築
修正箇所
競馬AI③で着順を学習する際に以下のように3着以内を”1″とし、4着以降を”0″としていたと思います。
※リンクの記事も参考にしてみてください
data['着順'] = data['着順'].map(lambda x: 1 if x<4 else 0)
上記の書き方はつまり3着以内に入った馬とそうでない馬を分けて、予測時には3着以内に入る確率を求めています。
ここの1行を修正するだけで穴馬を予測できるようになります。以下のように修正してモデルを作成してみてください。
data['着順'] = ((data['着順'] <= 3) & (data['オッズ'] >= 20)).astype(int)
このように書き換えると、オッズが20倍以上で3着以内に入った馬を”1″と変換します。なので人気馬(オッズ1~19.9倍)が3着以内だった場合でも”0″と分類されるので、人気馬が予測対象となりません。
コード全文
前の記事にも記載はありますが、一応全文載せておきます。
import lightgbm as lgb
import pandas as pd
from sklearn.metrics import roc_auc_score
import numpy as np
def split_date(df, test_size):
sorted_id_list = df.sort_values('日付').index.unique()
train_id_list = sorted_id_list[:round(len(sorted_id_list) * (1-test_size))]
test_id_list = sorted_id_list[round(len(sorted_id_list) * (1-test_size)):]
train = df.loc[train_id_list]
test = df.loc[test_id_list]
return train, test
# データの読み込み
data = pd.read_csv('encoded/encoded_data.csv')
#着順を変換
data['着順'] = ((data['着順'] <= 3) & (data['オッズ'] >= 20)).astype(int)
# 特徴量とターゲットの分割
train, test = split_date(data, 0.3)
X_train = train.drop(['着順','オッズ','人気','上がり','走破時間','通過順'], axis=1)
y_train = train['着順']
X_test = test.drop(['着順','オッズ','人気','上がり','走破時間','通過順'], axis=1)
y_test = test['着順']
# LightGBMデータセットの作成
train_data = lgb.Dataset(X_train, label=y_train)
valid_data = lgb.Dataset(X_test, label=y_test)
params={'objective': 'binary', 'metric': 'binary_logloss', 'verbosity': -1, 'boosting_type': 'gbdt', 'class_weight': 'balanced', 'random_state': 100, 'feature_pre_filter': False, 'lambda_l1': 5.724334023859959, 'lambda_l2': 1.8512445987933976e-08, 'num_leaves': 3, 'feature_fraction': 0.8, 'bagging_fraction': 0.9891729399864875, 'bagging_freq': 5, 'min_child_samples': 20, 'num_iterations': 1000, 'early_stopping_round': None}
lgb_clf = lgb.LGBMClassifier(**params)
lgb_clf.fit(X_train, y_train)
y_pred_train = lgb_clf.predict_proba(X_train)[:,1]
y_pred = lgb_clf.predict_proba(X_test)[:,1]
#モデルの評価
#print(roc_auc_score(y_train,y_pred_train))
print(roc_auc_score(y_test,y_pred))
total_cases = len(y_test) # テストデータの総数
TP = (y_test == 1) & (y_pred >= 0.8) # True positives
FP = (y_test == 0) & (y_pred >= 0.8) # False positives
TN = (y_test == 0) & (y_pred < 0.8) # True negatives
FN = (y_test == 1) & (y_pred < 0.8) # False negatives
TP_count = sum(TP)
FP_count = sum(FP)
TN_count = sum(TN)
FN_count = sum(FN)
accuracy_TP = TP_count / total_cases * 100
misclassification_rate_FP = FP_count / total_cases * 100
accuracy_TN = TN_count / total_cases * 100
misclassification_rate_FN = FN_count / total_cases * 100
print("Total cases:", total_cases)
print("True positives:", TP_count, "(", "{:.2f}".format(accuracy_TP), "%)")
print("False positives:", FP_count, "(", "{:.2f}".format(misclassification_rate_FP), "%)")
print("True negatives:", TN_count, "(", "{:.2f}".format(accuracy_TN), "%)")
print("False negatives:", FN_count, "(", "{:.2f}".format(misclassification_rate_FN), "%)")
# True Positives (TP): 実際に1で、予測も1だったもの
# False Positives (FP): 実際は0だが、予測では1だったもの
# True Negatives (TN): 実際に0で、予測も0だったもの
# False Negatives (FN): 実際は1だが、予測では0だったもの
# モデルの保存
lgb_clf.booster_.save_model('model/model.txt')
# 特徴量の重要度を取得
importance = lgb_clf.feature_importances_
# 特徴量の名前を取得
feature_names = X_train.columns
# 特徴量の重要度を降順にソート
indices = np.argsort(importance)[::-1]
# 特徴量の重要度を降順に表示
# for f in range(X_train.shape[1]):
# print("%2d) %-*s %f" % (f + 1, 30, feature_names[indices[f]], importance[indices[f]]))
まとめ
実際のレースを予測した際の予測値は、通常のモデルと異なり高い値が出づらくなっているので、購入する馬を検討する際の閾値は自分で調整してみてください。
また、今回は”オッズ”で試してみましたが”〇人気以下”といった条件でもできます。様々なパターンを試してみてください。
私のモデルでは単勝オッズ200倍の馬が上位予測され3着以内に入ってきたこともありました。
穴馬予測なので、5頭に1頭馬券内に来たら良い方なので期待しすぎないように注意してください!
コメント