はじめに
MYJLab Advent Calendar 2022の9日目の記事です。
Pythonで機械学習(XGBoostによる分類)を行って競馬の予想してみようという内容になっています。
先にネタバレしますと何とも言えない結果となりました。
環境
データ取得はTARGET frontier JV
というアプリケーション(2022年12月8日時点での最新版)を使用します。
(注:JRA-VAN データラボ
の契約が必要です)
機械学習・モデル作成はGoogle Colaboratory
を使用します。
データ取得
2022年12月4日のチャンピオンズカップを予想したいので、同条件である中京ダート1800mのレース結果等のデータを集めます。
TARGET frontier JV
から、2012年1月1日~2022年11月31日の中京ダート1800mのデータを一走前のデータも含めてCSV形式でダウンロードします。これを学習データにしてモデルを構築します。
2022年12月4日のチャンピオンズカップの、一走前のデータも含めた出馬表のデータもCSV形式でダウンロードします。構築したモデルを使って、このデータで予想します。
ダウンロードしたCSVファイルはGoogle Colaboratory
で使用できるようにGoogle Drive
にアップロードしておきます。
前処理
まずはデータの前処理を行って機械学習が出来るように準備します。
Google DriveからCSVファイルを使えるようにします。
このとき,文字化け防止のためにencoding="Shift-JIS"
で文字コードを指定してエンコードします。
from google.colab import drive
drive.mount('/content/drive')
df = pd.read_csv('/content/drive/MyDrive/〇〇/〇〇.csv', encoding="Shift-JIS")
前走のデータがない場合のデータの処理を行います。前走馬体重は該当レースの馬体重にし、上り3Fタイムや通過順はNaN
にします。
scikit-learnが文字列対応していないのでOne-Hotエンコーディングで性別と前走が芝かダートかどうかについてのダミー変数を作成します。
また、乗り替わりの有無も説明変数とし用いたいので、df['騎手名']
とdf['前走騎手名']
が一致した場合は1を、そうでない場合には0を返す関数を定義し、それを用いてdf['乗り替わり']
という列を新たに作ります。
# 前走が地方や海外の場合、前走馬体重が0になってしまうため、前走馬体重を(今回の)馬体重と同じにする
df.loc[df['前走馬体重'] == 0,'前走馬体重'] =df.loc[df['前走馬体重'] == 0,'馬体重']
# 前走が地方や海外の場合データが無いため、該当部分をNaNにする
for element in ['前走上がり3Fタイム','前走通過順1', '前走通過順2','前走通過順3', '前走通過順4']:
df.loc[df[element] == 0,element] = np.NaN
# 性別のone-hot化
one_hot_vector = pd.get_dummies(df[['性別']],drop_first=False)
df = pd.concat([df, one_hot_vector], axis=1)
# 前走芝・ダのone-hot
one_hot_vector = pd.get_dummies(df[['前走芝・ダ']],drop_first=False)
df = pd.concat([df, one_hot_vector], axis=1)
# 乗り替わりの有無
def categorize(x):
if x['騎手名'] == x['前走騎手名']:
return 0
else:
return 1
# '乗り替わり'という列を新たに作る
df['乗り替わり'] = df.apply(categorize, axis=1)
データをシャッフル、サンプリングしてから、hold-out法で訓練用とテスト用のデータに分割します。
df.loc[:,~df.columns.isin(['xxx','yyy'])]
と書くことで、'xxx','yyy'
の列を取り除くことが出来ます。これで不要な列を取り除きます。
# データをシャッフルして半分をサンプリング
df = df.sample(frac=0.5, random_state=0).reset_index(drop=True)
# 説明変数を取り出す
x_data = df.loc[:,~df.columns.isin(['入線着順','騎手名','前走騎手名','異常コード','前走異常コード','前走芝・ダ','性別'])]
# 目的変数は'入線着順'
y_data = df.loc[:,['入線着順']]
from sklearn.model_selection import train_test_split
x_train, x_test, y_train, y_test = train_test_split(x_data, y_data, train_size = 0.8)
学習
学習する前にグリッドサーチでパラメータ探索して、最適なパラメータを見つけていきます。
from sklearn.model_selection import RepeatedKFold
cv = RepeatedKFold(n_splits=5, n_repeats=3, random_state=0)
import xgboost as xgb
model = xgb.XGBClassifier()
from sklearn.model_selection import GridSearchCV
params = {
'booster': ['gbtree'], 'n_estimators': [10, 30, 50],'max_depth': [3,5,7], 'learning_rate': [0.3, 0.5, 0.7],
'colsample_bytree': [0.3, 0.5, 0.7],'random_state': [0], "objective":["multi:softmax"],"num_class": [16]
}
gs = GridSearchCV(estimator=model, param_grid=params, cv=cv, scoring = 'f1_micro')
gs.fit(x_train, y_train)
print('best score: {:0.3f}'.format(gs.score(x_test, y_test)))
print('best params: {}'.format(gs.best_params_))
print('best val score: {:0.3f}'.format(gs.best_score_))
best score: 0.090
best params: {'booster': 'gbtree', 'colsample_bytree': 0.5, 'learning_rate': 0.5, 'max_depth': 3, 'n_estimators': 50, 'num_class': 16, 'objective': 'multi:softmax', 'random_state': 0}
best val score: 0.102
これを元にパラメータを設定し、学習を行いモデルを構築していきます。
model=xgb.XGBClassifier(
booster= 'gbtree', colsample_bytree= 0.5, learning_rate= 0.25,max_depth= 3, n_estimators= 50,
num_class= 16, objective='multi:softmax', random_state= 0
)
model.fit(x_train, y_train)
精度確認
sklearn.metrics
のclassification_report
で分類の精度を確認していきます。
y_test_pred = model.predict(x_test)
from sklearn.metrics import classification_report
print(classification_report(y_test, y_test_pred))
precision recall f1-score support
1 0.12 0.22 0.16 41
2 0.13 0.10 0.11 52
3 0.15 0.12 0.13 48
4 0.11 0.12 0.12 41
5 0.04 0.10 0.06 20
6 0.03 0.03 0.03 39
7 0.02 0.02 0.02 46
8 0.03 0.03 0.03 36
9 0.23 0.13 0.17 38
10 0.13 0.11 0.12 38
11 0.09 0.12 0.10 32
12 0.00 0.00 0.00 22
13 0.07 0.05 0.05 22
14 0.09 0.04 0.05 27
15 0.07 0.06 0.06 16
16 0.18 0.15 0.17 13
accuracy 0.09 531
macro avg 0.09 0.09 0.09 531
weighted avg 0.10 0.09 0.09 531
めちゃくちゃ精度が悪い
とはいえ、機械学習等で競馬の予想をする場合、そのほとんどが馬券を当てるということがメインになっています。なので、投資した金額を配当額が上回ったかどうかという、回収率という評価指数を用いる必要がありそうです。
いよいよチャンピオンズカップの予想をしてみる
df_test = pd.read_csv('/content/drive/MyDrive/〇〇/〇〇.csv', encoding="Shift-JIS")
学習データと同じように前処理を行います。
カラム名が学習データと違っている場合はdf.rename(columns={'xxx':'XXX'})
などでカラム名の変更を行い、学習データのカラム名と一致させる必要があります。
test_pred = model.predict(test_data)
pred_df = pd.Series(test_pred, index=df_test['馬名'])
print(pred_df)
出力結果が下になります。右側の数字は着順です。
グロリアムンディ 16
サンライズホープ 1
ハピ 3
スマッシングハーツ 3
ジュンライトボルト 5
レッドガラン 12
オーヴェルニュ 11
サンライズノヴァ 8
ノットゥルノ 11
クラウンプライド 1
バーデンヴァイラー 15
テーオーケインズ 12
シャマル 3
タガノビューティー 1
サクラアリュール 9
レッドソルダード 1
結果
実際のチャンピオンズカップの結果は、5着と予想したジュンライトボルトが優勝し、1着と予想したクラウンプライドが2着に、3着と予想したハピが3着に来るという結果でした。
これだけだったら結構よさそうな感じに見えるのですが、他に1着だと予想したサンライズホープは6着、タガノビューティーは10着、レッドソルダードは16着という散々な結果になりました。
XGBoost以外の分析手法を用いたり、前走だけではなく、2走前、3走前のデータを用いたりすることで精度を改善できそうです。また、データがない場合の処理を見直す必要もありそうです。
競馬で大儲けするのはまだまだ先になりそうです。
参考文献