はじめに
競艇の予測アプリを作成したので、制作の流れをまとめていきます。
なお、初心者ゆえ、勘違いしている部分等あれば色々教えて突っ込んでいただけるとありがたいです。
あとUIをよくすることに関してはほぼ勉強していないので、原始的な仕上がりとなっています点ご容赦ください。
結論
2022年1月〜2022年6月15日までのレースを学習させ、2022年6月16日〜8月31日の約12,000レースにおいて実践。
結果としては、なんと 回収率98% まで持っていくことができました!!
オッズ1000倍の3連単を当てたりしているので、人間が見ているところとは違う観点から順位を予測することもあるとわかります。
制作の流れ
①データ収集
↓
②データクレンジング
↓
③モデル作成
↓
④モデル学習
↓
⑤検証
①データ収集
特徴量として使えそうなデータを探したところ、以下のサイトから全競艇場の番組表と競走成績がダウンロードできることがわかりました。
テキストファイルなのでこんな感じです。。。
STARTB
24BBGN
ボートレース大 村 1月 1日 ウルトラセブン杯 第 7日
*** 番組表 ***
ウルトラセブン杯
第 7日 2022年 1月 1日 ボートレース大 村
-内容については主催者発行のものと照合して下さい-
1R 一般 H1800m 電話投票締切予定15:15
-------------------------------------------------------------------------------
艇 選手 選手 年 支 体級 全国 当地 モーター ボート 今節成績 早
番 登番 名 齢 部 重別 勝率 2率 勝率 2率 NO 2率 NO 2率 123456見
-------------------------------------------------------------------------------
1 4966田川大貴24長崎53B1 4.76 25.00 5.22 30.98 47 39.47 64 30.43 341 42666 13 8
2 4705吉川勇作33長崎54B1 4.22 19.51 4.54 24.82 30 45.61 32 46.51 4 245 15666 9
3 5055眞鳥章太25長崎53B1 2.88 14.29 3.97 20.93 75 28.44 53 40.00 4 535 45565 6
4 5011高木圭大24長崎52B1 3.65 14.81 3.20 12.36 71 27.87 34 33.33 4 654 44556 5
5 4969町田洸希30長崎54B1 4.07 16.82 4.12 21.01 61 27.87 16 28.35 5 161 4654S 6
6 4299中島浩哉39長崎56B1 4.98 28.57 4.63 26.49 73 21.15 74 36.94 363 616 S62 7
2R 一般 H1800m 電話投票締切予定15:44
-------------------------------------------------------------------------------
艇 選手 選手 年 支 体級 全国 当地 モーター ボート 今節成績 早
番 登番 名 齢 部 重別 勝率 2率 勝率 2率 NO 2率 NO 2率 123456見
-------------------------------------------------------------------------------
1 4248岡本 大39長崎54B1 4.25 20.25 5.15 23.76 52 25.41 27 28.57 5 554 62423 7
2 5086本村 大22長崎51B1 3.56 14.00 2.80 5.88 27 26.67 47 32.14 5 553 3S134 6
3 4498宮本夏樹34長崎54B1 5.16 35.96 5.57 38.76 63 47.17 63 28.57 655 2 216546
4 3151落合敬一57長崎57B1 4.33 17.02 4.35 21.78 37 34.65 18 29.06 5 354 53455 6
5 3843上之晃弘45長崎55B1 4.76 28.87 5.34 36.81 26 38.46 72 21.67 643 353 5222 9
6 3777樋口 亮46長崎52B1 5.44 34.83 5.57 39.71 31 27.05 22 24.53 422 433 1621 8
別 勝率 2率 勝率 2率 NO 2率 NO 2率 123456見
------62 7.57 62.87 76 38.52 75 45.24 16216 121213 7
2 4259真庭明志37長崎53A2 5.99 39.01 5.82 39.90 28 34.13 37 31.50 3 1535213 33 6
3 4315山崎昂介37長崎53B1 5.09 34.04 5.16 31.88 29 46.61 52 37.37 243 33433 13 3
4 4566塩田北斗34福岡52A1 7.26 57.86 6.30 45.00 67 21.74 70 37.17 3 1222514 14 8
5 3527中嶋誠一50長崎51A2 5.90 37.68 5.76 35.71 69 26.55 36 32.26 42125 343344 4
6 4241大串重幸36長崎53B1 5.13 32.98 5.27 34.87 45 33.33 12 25.74 333 4 143444 5
STARTK
24KBGN
大 村[成績] 1/ 1 ウルトラセブン杯 第 7日
*** 競走成績 ***
ウルトラセブン杯
第 7日 2022/ 1/ 1 ボートレース大 村
-内容については主催者発行のものと照合して下さい-
[払戻金] 3連単 3連複 2連単 2連複
1R 1-5-3 5460 1-3-5 1130 1-5 920 1-5 720
2R 2-1-6 12850 1-2-6 1780 2-1 2020 1-2 570
3R 1-2-3 870 1-2-3 470 1-2 330 1-2 280
4R 1-3-4 1110 1-3-4 530 1-3 270 1-3 140
5R 1-2-5 1220 1-2-5 790 1-2 190 1-2 200
6R 4-1-3 2540 1-3-4 300 4-1 680 1-4 140
7R 4-3-1 7090 1-3-4 410 4-3 3190 3-4 2090
8R 1-3-4 2060 1-3-4 1330 1-3 430 1-3 290
9R 1-2-6 1010 1-2-6 460 1-2 330 1-2 270
10R 1-2-4 640 1-2-4 290 1-2 260 1-2 220
11R 1-2-3 670 1-2-3 360 1-2 240 1-2 240
12R 2-4-3 66850 2-3-4 4990 2-4 12380 2-4 3450
1R 一般 H1800m 晴 風 北西 1m 波 1cm
着 艇 登番 選 手 名 モーター ボート 展示 進入 スタートタイミンク レースタイム 逃げ
-------------------------------------------------------------------------------
01 1 4966 田 川 大 貴 47 64 6.84 1 0.12 1.55.0
02 5 4969 町 田 洸 希 61 16 6.83 5 0.12 2.00.5
03 3 5055 眞 鳥 章 太 75 53 6.86 3 0.12 2.03.5
04 4 5011 高 木 圭 大 71 34 6.87 4 0.18 2.04.8
05 6 4299 中 島 浩 哉 73 74 6.86 6 0.09 . .
S0 2 4705 吉 川 勇 作 30 32 6.79 2 0.07 . .
単勝 1 180
複勝 1 230 5 480
2連単 1-5 920 人気 4
2連複 1-5 720 人気 3
拡連複 1-5 310 人気 5
1-3 130 人気 1
3-5 820 人気 14
3連単 1-5-3 5460 人気 18
3連複 1-3-5 1130 人気 6
2R 一般 H1800m 晴 風 北西 1m 波 1cm
着 艇 登番 選 手 名 モーター ボート 展示 進入 スタートタイミンク レースタイム 抜き
-------------------------------------------------------------------------------
01 2 5086 本 村 大 27 47 6.87 2 0.22 1.48.8
02 1 4248 岡 本 大 52 27 6.87 1 0.22 1.49.8
03 6 3777 樋 口 亮 31 22 6.83 6 0.23 1.52.4
04 4 3151 落 合 敬 一 37 18 6.85 4 0.29 1.53.0
05 5 3843 上 之 晃 弘 26 72 6.80 5 0.19 . .
06 3 4498 宮 本 夏 樹 63 63 6.85 3 0.23 . .
単勝 2 840
複勝 2 270 1 140
2連単 2-1 2020 人気 7
2連複 1-2 570 人気 3
拡連複 1-2 320 人気 4
2-6 760 人気 10
1-6 470 人気 7
3連単 2-1-6 12850 人気 44
3連複 1-2-6 1780 人気 9
これらのデータを1日分ずつダウンロードしていきます。これは手作業。
②データクレンジング
このテキストファイルから必要部分をデータフレームに入れ込んでいきます。
この時には以下のような感じでゴリゴリに正規表現を使って必要部分を収集していきました。
import glob
import os
import re
from string import ascii_letters
from string import digits
import pandas as pd
files_K = glob.glob("/content/drive/MyDrive/Colab Notebooks/ブログ/K/*")
files_B = glob.glob("/content/drive/MyDrive/Colab Notebooks/ブログ/B/*")
# Bデータを全て取り込み結合する
df_all_B = pd.DataFrame()
for file in files_B:
#データ読み込み
with open(file, encoding='shift-jis') as f:
data1 = f.readlines()
#レース日付読み込み
date = os.path.basename(file)
date = re.sub(r"\D", "", date)
#余分なスペース、改行を消去
data2 = [s.replace('\u3000', '').replace('\n', '') for s in data1]
#全角文字を半角文字に変換
han = ascii_letters + digits
table = {c + 65248: c for c in map(ord, han)}
data3 = [name.translate(table) for name in data2]
#必要データ抽出
data4 = [row for row in data3 if re.match('^[0-9]', row)]
#行検索用
pattern_place1 = '\d{2}[B][B]'
pattern_race_num1 = '\d+[R]'
pattern_racer1 = '^[1-6]\s\d{4}'
pattern_place_re1 = re.compile(pattern_place1)
pattern_race_num_re1 = re.compile(pattern_race_num1)
pattern_racer_re1 = re.compile(pattern_racer1)
#データ取得用
pattern_place2 = '(\d{2})[B][B]'
pattern_race_num2 = '(\d+)[R]'
pattern_racer2 = '^([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})'
pattern_place_re2 = re.compile(pattern_place2)
pattern_race_num_re2 = re.compile(pattern_race_num2)
pattern_racer_re2 = re.compile(pattern_racer2)
#必要データ取得
values = []
for row in data4:
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 = []
for i in value:
val_li.append(i)
val_li.append(place_elm)
val_li.append(race_num_elm)
val_li.append(date)
values.append(val_li)
#データフレーム作成
column = ['艇番', '選手登番', '選手名', '年齢', '支部', '体重', '級別', '全国勝率', '全国2連率', '当地勝率', '当地2連率', 'モーターNO', 'モーター2連率', 'ボートNO', 'ボート2連率', '開催地', 'レース番号', '日付']
df_B = pd.DataFrame(values, columns=column)
#開催地、レース番号、日付(すべてstr型)を元ににレースIDを追加
df_B['レースID'] = df_B['日付'] + df_B['開催地'] + df_B['レース番号']
df_all_B = pd.concat([df_all_B, df_B])
# Kデータを全て取り込み結合する
df_all_K = pd.DataFrame()
for file in files_K:
#データ読み込み
with open(file, encoding='shift-jis') as f:
data5 = f.readlines()
#レース日付読み込み
date = os.path.basename(file)
date = re.sub(r"\D", "", date)
#余分なスペース、改行を消去
data6 = [s.replace('\u3000', '').replace('\n', '') for s in data5]
#全角文字を半角文字に変換
han = ascii_letters + digits
table = {c + 65248: c for c in map(ord, han)}
data7 = [name.translate(table) for name in data6]
#必要データ抽出
data8 = [row for row in data7 if re.match('[0-9]', row) or re.match('\s\s[0-9]', row) or re.match('\s\s\s[0-9]', row)]
#行検索用
pattern_place1 = '\d{2}[K][B]'
pattern_race_num1 = '\s+\d+[R]'
pattern_racer1 = '\s+\d+\s+[1-6]\s\d{4}'
pattern_place_re1 = re.compile(pattern_place1)
pattern_race_num_re1 = re.compile(pattern_race_num1)
pattern_racer_re1 = re.compile(pattern_racer1)
#データ取得用
pattern_place2 = '(\d{2})[K][B]'
pattern_race_num2 = '\s+(\d+)[R]'
pattern_racer2 = '\s+(\d+)\s+[1-6]\s(\d{4})'
pattern_place_re2 = re.compile(pattern_place2)
pattern_race_num_re2 = re.compile(pattern_race_num2)
pattern_racer_re2 = re.compile(pattern_racer2)
#必要データ取得
values = []
for row in data8:
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 = []
for i in value:
val_li.append(i)
val_li.append(place_elm)
val_li.append(race_num_elm)
val_li.append(date)
values.append(val_li)
#データフレーム作成
column = ['実着順', '選手登番', '開催地', 'レース番号', '日付']
df_K = pd.DataFrame(values, columns=column)
#開催地、レース番号、日付(すべてstr型)を元ににレースIDを追加
df_K['レースID'] = df_K['日付'] + df_K['開催地'] + df_K['レース番号']
df_K = df_K[['実着順', '選手登番', 'レースID']]
df_all_K = pd.concat([df_all_K, df_K])
#レース予定Bとレース結果Kデータ結合
df = df_all_K.merge(df_all_B, how='left', on=['レースID', '選手登番'])
#フライング等着順つかなかったところを埋める
df['実着順'] = df['実着順'].fillna('06')
#体重、年齢、艇番をstr型からint型に変換
df[['体重', '年齢', '艇番', 'レースID']] = df[['体重', '年齢', '艇番', 'レースID']].astype(int)
#不成立レース(有効選手2名以下)を削除
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)
各列の選手がどのレースに出たかを把握するため、レースIDを別途振るようにしてます。
例えば、2022年1月1日桐生(会場コード01)の第2レースだとすると、
202201010102という風にIDを振りました。
ここまで辿り着くのに一番時間が掛かったと思います。
が、後から知ったのですが正規表現チェッカーなるものがいくつもあるらしく、そういったものを使っておけばこんなに時間は掛からなかったと思います。
③モデル作成
今回はカテゴリカルな特徴量が多いこともあるので、CatBoostという勾配ブースティングの手法を利用しました。
当初はこちらのブログを参考にLightGBMでモデル作成していたのですが、プログラミングスクールの先生からアドバイスいただき路線変更しました。
#訓練5ヶ月、検証半月、テスト半月パラメータ設定
date_split_valid = 2206010000 # 訓練データと検証データの分岐点(年+月+日+4桁(会場、レース番号))
date_split_test = 2206160000 # 検証データとテストデータの分岐点(年+月+日+4桁(会場、レース番号))
# 訓練データ・テストデータに分割
df_train = df[df['レースID'] < date_split_valid]
df_valid = df[(df['レースID'] >= date_split_valid) & (df['レースID'] < date_split_test)]
df_test = df[df['レースID'] >= date_split_test]
#説明変数
X_train = df_train.drop(['実着順'], axis=1)
X_valid = df_valid.drop(['実着順'], axis=1)
X_test = df_test.drop(['実着順'], axis=1)
#目的変数
y_train = df_train['実着順']
y_valid = df_valid['実着順']
y_test = df_test['実着順']
from catboost import CatBoostRanker, Pool
cat_features = ['選手登番', '選手名', '支部', '級別', 'モーターNO', 'ボートNO', '開催地', 'レース番号', '日付']
train_pool = Pool(data=X_train, label=y_train, group_id=X_train['レースID'], cat_features=cat_features)
valid_pool = Pool(data=X_valid, label=y_valid, group_id=X_valid['レースID'], cat_features=cat_features)
test_pool = Pool(data=X_test, label=y_test, group_id=X_test['レースID'], cat_features=cat_features)
param = {'loss_function':'YetiRank', 'learning_rate': 0.05, 'iterations': 200,
'depth': 7, 'use_best_model':True}
model = CatBoostRanker(**param)
こんな感じ。
ハイバーパラメーターのdepthとかは色々変えて試した結果、7が一番精度高かったので採用しました。
④モデル学習
これはシンプルですね。
model.fit(train_pool, eval_set=valid_pool)
⑤検証
ここもシンプルです。
pred_score = model.predict(X_test)
予測X_testのデータを読み込んで予測を出すことができました。
あとは算数の力を使って計算すると
的中率:0.09444584488698429
収益率:0.9852617427106966
という個人的には満足できる結果が出てくれました!!!
まとめ
今回はCatBoostを使って競艇予測を行いました。
次は競馬でもチャレンジしてみて、収益率100%越えを狙っていきたいと思います!