SnowflakeのNotebookでXGBoostを用いた順位予測モデルの作成
- 公開日:
- 最終更新日:
機械学習の分野では、予測モデルの精度を向上させるためにさまざまなアルゴリズムが使用されています。その中でも、XGBoostは人気のあるアルゴリズムの一つです。
本記事では、競艇を例にSnowflake NotebookでXGBoostを用いて、競艇の順位予測モデルを作成し、実際のレースでの回収率をみていきます。
前提:競艇のルール
まず、競艇の基本的なルールですが、1周600メートルのコースを3周し、スタートからゴールまでの速さを競います。各レースには6艇が出場します。
XGBoostとは
XGBoostの概要
XGBoostは、勾配ブースティング(Gradient Boosting)*アルゴリズムの一種で、特に大規模なデータセットや高次元のデータに対して高い性能を発揮します。XGBoostは次の4つの特徴が挙げられます。
- 高速な学習速度と高い予測精度:XGBoostは並列処理を活用して高速に学習を行い、高い予測精度
- 過学習の防止:正則化(Regularization)を導入することで、過学習を防げる
- 柔軟性:回帰、分類、ランキングなどさまざまなタスクに対応可能
- 欠損値の処理:欠損値を自動的に処理する機能がある
*勾配ブースティング:予測誤差を順次補正していくことで、高精度な予測モデルを構築する機械学習手法
XGBoostの技術的詳細
XGBoostは、以下の技術的な特徴を持っています
- 並列処理:XGBoostは、データの分割や木の構築などの処理を並列に実行することで、学習速度を大幅に向上させる
- ブロック構造:データをブロック単位で処理することで、メモリ効率を高め、大規模データセットに対してもスケーラブルに対応
- スパースデータの効率的な処理:スパースデータ(欠損値が多いデータ)を効率的に処理するための最適化が施されている
- カスタマイズ可能な目的関数と評価指標:ユーザーが独自の目的関数や評価指標を定義可能
Snowflake Notebookとは
Snowflake Notebookの概要
Snowflake Notebookは、データサイエンスや機械学習のプロジェクトを効率的に進めるためツールです。Snowflake Notebookには、次の4点のメリットがあります。
- 統合環境:データの取り込み、前処理、モデルの学習、評価、デプロイまで一貫して行うことが可能
- スケーラビリティ:Snowflakeのクラウドインフラを活用することで、大規模なデータセットに対してもスケーラブルに処理可能
- コラボレーション:チームメンバーとリアルタイムでノートブックを共有し、共同作業が可能
- セキュリティ:データのセキュリティとプライバシーを確保
順位予測モデルの構築
データの準備
それでは、実際にSnowflake Notebookで、XGBoostを用いた順位予測モデルを作成していきます。
競艇の公式サイトから過去の成績と、当日の出走表をダウンロードし、Snowflakeにデータを取り込みます。
データ加工
データの前処理
データの前処理は、以下の記事を参考に実施しました。
https://qiita.com/yyyyyy666/items/1a28cc2f84ea24d6ab4a
ライブラリのインポート
import glob
import os
import re
import pandas as pd
import xgboost as xgb
import numpy as np
from sklearn.metrics import accuracy_score
from sklearn.model_selection import train_test_split, GridSearchCV, RepeatedKFold
from string import ascii_letters, digits
出走表情報
# ファイル一覧取得
files_race = glob.glob('race/*')
df_all_race = pd.DataFrame()
# 各ファイルを処理
for file in files_race:
# ファイルを読み込み
with open(file, encoding='shift-jis') as f:
data = f.readlines()
# スペースや改行を削除し
data = [s.replace('\u3000', '').replace('\n', '') for s in data] half = ascii_letters + digits table = {c + 65248: c for c in map(ord, half)} data = [name.translate(table) for name in data] # ファイル名から日付を抽出 date = os.path.basename(file) date = re.sub(r'\D', '', date) # 数値のみ抽出 data = [row for row in data if re.match('^[0-9]', row)] # 必要な値のみを抽出 pattern_place_re1 = re.compile('\d{2}[B][B]') pattern_race_num_re1 = re.compile('\d+[R]') pattern_racer_re1 = re.compile('^[1-6]\s\d{4}') pattern_place_re2 = re.compile('(\d{2})[B][B]') pattern_race_num_re2 = re.compile('(\d+)[R]') pattern_racer_re2 = re.compile('^([1-6])\s(\d{4})([^0-9]+)(\d{2})([^0-9]+)(\d{2})([AB]\d{1})\s(\d.\d{2})\s*(\d+.\d{2})\s*(\d+.\d{2})\s*(\d+.\d{2})\s*(\d+)\s*(\d+.\d{2})\s*(\d+)\s*(\d+.\d{2})') values = [] place_elm = race_num_elm = None for row in data: if re.match(pattern_place_re1, row): place = re.match(pattern_place_re2, row).groups() place_elm = place[0] elif re.match(pattern_race_num_re1, row): race_num = re.match(pattern_race_num_re2, row).groups() race_num_elm = race_num[0].zfill(2) elif re.match(pattern_racer_re1, row): value = re.match(pattern_racer_re2, row).groups() val_li = list(value) + [place_elm, race_num_elm, date] values.append(val_li) # データフレーム作成 columns = ['艇番', '選手登番', '選手名', '年齢', '支部', '体重', '級別', '全国勝率', '全国2連率', '当地勝率', '当地2連率', 'モーターNO', 'モーター2連率', 'ボートNO', 'ボート2連率', '開催地', 'レース番号', '日付'] df_race = pd.DataFrame(values, columns=columns) df_race['レースID'] = df_race['日付'] + df_race['開催地'] + df_race['レース番号'] df_all_race = pd.concat([df_all_race, df_race], ignore_index=True) df_all_race
◇出力結果
| 艇番 | 選手番号 | 選手名 | 年齢 | 支部 | 体重 | 級別 | 全国勝率 | 全国2勝率 | 当地勝率 | 当地2勝率 | モーターNO | モーター2連率 | ボートNO | ボート2連率 | 開催地 | レース番号 | 日付 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
1 |
3290 |
倉谷和信 |
61 |
大阪 |
55 |
B1 |
4.60 |
22.86 |
0.00 |
0.00 |
25 |
27.16 |
38 |
32.94 |
24 |
01 |
241106 |
|
2 |
4463 |
三苫晃幸 |
38 |
福岡 |
54 |
A2 |
5.79 |
39.81 |
5.85 |
41.51 |
16 |
30.12 |
33 |
28.74 |
24 |
01 |
241106 |
|
3 |
4346 |
前田健太 |
39 |
福岡 |
56 |
B1 |
4.07 |
25.00 |
5.05 |
29.31 |
62 |
44.71 |
16 |
36.14 |
24 |
01 |
241106 |
レース結果データ
# ファイル一覧取得
files_result = glob.glob('result/*')
df_all_result = pd.DataFrame()
# 各ファイルを処理
for file in files_result:
# ファイルを読み込み
with open(file, encoding='shift-jis') as f:
data = f.readlines()
# 余分なスペースや改行を削除
data = [s.replace('\u3000', '').replace('\n', '') for s in data]
# 全角文字を半角文字に変換
half = ascii_letters + digits
table = {c + 65248: c for c in map(ord, half)}
data = [name.translate(table) for name in data]
# ファイル名から日付を抽出
date = os.path.basename(file)
date = re.sub(r'\D', '', date)
# 必要なデータのみを抽出
data = [row for row in data if re.match('[0-9]', row) or re.match('\s\s[0-9]', row) or re.match('\s\s\s[0-9]', row)]
# データから必要な値を抽出
pattern_place_re1 = re.compile('\d{2}[K][B]')
pattern_race_num_re1 = re.compile('\s+\d+[R]')
pattern_racer_re1 = re.compile('\s+\d+\s+[1-6]\s\d{4}')
pattern_place_re2 = re.compile('(\d{2})[K][B]')
pattern_race_num_re2 = re.compile('\s+(\d+)[R]')
pattern_racer_re2 = re.compile('\s+(\d+)\s+[1-6]\s(\d{4})')
values = []
place_elm = race_num_elm = None
for row in data:
if re.match(pattern_place_re1, row):
place = re.match(pattern_place_re2, row).groups()
place_elm = place[0]
elif re.match(pattern_race_num_re1, row):
race_num = re.match(pattern_race_num_re2, row).groups()
race_num_elm = race_num[0].zfill(2)
elif re.match(pattern_racer_re1, row):
value = re.match(pattern_racer_re2, row).groups()
val_li = list(value) + [place_elm, race_num_elm, date]
values.append(val_li)
# データフレーム作成
columns = ['実着順', '選手登番', '開催地', 'レース番号', '日付']
df_result = pd.DataFrame(values, columns=columns)
df_result['レースID'] = df_result['日付'] + df_result['開催地'] + df_result['レース番号']
df_result = df_result[['実着順', '選手登番', 'レースID']]
df_all_result = pd.concat([df_all_result, df_result], ignore_index=True)
df_all_result
◇出力結果(上位3位)
| 実着順 | 選手登板 | レースID |
|---|---|---|
|
01 |
4760 |
2412032301 |
|
02 |
3708 |
2412032301 |
|
03 |
5352 |
2412032301 |
出走表データと、レース結果データを結合
# レースID,選手登番をキーに出走表データとレース結果データを結合
df = df_all_race.merge(df_all_result, how='left', on=['レースID','選手登番'])
# 級別を数値に変換(説明変数として使用したいため)
df['等級'] = df['級別'].apply(lambda x: 1 if x=='A1' else (2 if x=='A2' else(3 if x=='B1' else 4)))
# 着順なしのデータを補正
df['実着順'] = df['実着順'].fillna('06')
# 無効レースを削除
df_del = df.groupby('レースID').count()['実着順']
df_del = df_del[df_del <= 2]
for i in df_del.index:
df.drop(df[df['レースID'] == i].index, inplace=True)
# データ型を変換
df[['艇番', '選手登番', 'レースID']] = df[['艇番', '選手登番', 'レースID']].astype(int)
df[['全国勝率', '全国2連率', '当地勝率', '当地2連率', 'モーター2連率', 'ボート2連率']] = df[['全国勝率', '全国2連率', '当地勝率', '当地2連率', 'モーター2連率', 'ボート2連率']].astype(float)
# 不要なカラムを削除
df = df.drop(['選手名', '年齢', '支部', '体重', 'モーターNO', 'ボートNO', '開催地', 'レース番号', '日付','級別'], axis=1)
# カラム名を変更
df = df.rename(columns={
'実着順': '着順', '選手登番': '選手番号', '艇番': '艇番',
})
# # 着順をマッピングする
class_mapping = {'01': 0, '02': 1, '03': 2, '04': 3, '05': 4, '06': 5}
df['着順'] = df['着順'].map(class_mapping)
df
◇出力結果(上位3件)
| 艇番 | 選手番号 | 全国勝率 | 全国2連率 | 当地勝率 | 当地2連率 | モーター2連率 | ボート2連率 | 着順 | 等級 |
|---|---|---|---|---|---|---|---|---|---|
| 1 | 4,760 | 7.28 | 52.78 | 8.71 | 65.71 | 40.32 | 22.56 | 0 | 1 |
| 2 | 5,054 | 4.86 | 25 | 5.38 | 34.48 | 40 | 34.15 | 5 | 3 |
| 3 | 3,708 | 5.2 | 32.82 | 5.15 | 30.77 | 53.06 | 28.92 | 1 | 3 |
学習
パラメータ探索
最適なパラメータを見つけるため、グリッドサーチでパラメータ探索します。
# 説明変数
# 説明変数
df_x = df.drop(['着順'],axis=1)
# 目的変数
df_y = df['着順']
x_train, x_test, y_train, y_test = train_test_split(df_x,df_y,random_state=1)
model = xgb.XGBClassifier()
params = { 'booster':['gbtree']
,'max_features':['sqrt', 'log2', 'auto', None]
,'random_state':[1]
,'objective':['multi_sofmax']
}
gcv = GridSearchCV(estimator=model, param_grid=params,
scoring='f1_micro')
gcv.fit(x_train, y_train)
print('best score: {}'.format(gcv.score(x_test, y_test)))
print('best params: {}'.format(gcv.best_params_))
print('best val score: {}'.format(gcv.best_score_))
モデルの構築
こちらをもとにパラメータを設定し、モデルを構築していきます。
model=xgb.XGBClassifier(
booster= "gbtree",
max_features='sqrt',
objective="multi:softmax",
fandom_state=1
)
model.fit(x_train, y_train)
モデル評価
y_test_pred = model.predict(x_test)
print(classification_report(y_test,y_test_pred))
◇出力結果
precision recall f1-score support
0 0.52 0.56 0.54 1128
1 0.25 0.26 0.25 1152
2 0.18 0.18 0.18 1094
3 0.22 0.19 0.20 1161
4 0.20 0.20 0.20 1095
5 0.34 0.35 0.35 1264
accuracy 0.29 6894
macro avg 0.28 0.29 0.29 6894
weighted avg 0.29 0.29 0.29 6894
実践
今回、リアルタイムのレースに投票するのではなく、2024年のボートレース鳴門 G1大渦大賞 開設71周年記念でシミュレーションしてみました。
投票数 : 55件
的中 : 2件
回収率 : 67.27%
競艇の期待値は75%なので、今回のシミュレーションは期待値を下回る結果となってしまいました。
まとめ
残念ながら、2024年のSnowflake NotebookでXGBoostを用いた順位予測モデルは精度は高くありませんでした。データの質、モデルの選定、ハイパーパラメータなど、まだまだ調整する必要がありそうです。
また、今回の構築したモデルでは、艇番の影響がかなり大きかったです。
今後は、リアルタイムで予測を行う機能、Streamlitを活用し、作成したモデルを使ったアプリケーションを開発していこうと思います。
目指せ、回収率100%越え!!
本記事は、Snowflakeを中心としたデータエンジニアリング関連の技術的な情報を、弊社技術者が公開しているテックブログhttps://zenn.dev/p/datatechblogより引用しており、公開時(2024/12/7)の情報をもとに作成しています。
製品・サービスに関する詳しいお問い合わせは、弊社Webサイトからお問い合わせください。
https://data-management.dentsusoken.com/snowflake/inquiry/