見出し画像

競艇の順位と確率を予測する

はじめに

「このブログはAidemy Premiumのカリキュラムの一環で、受講修了条件を満たすために公開しています」
データ分析に興味を持ったけれど、年齢は40歳に近く3児の父であり、これから始めるのは遅いかなと思いつつオンラインスクールでデータ分析コースを受講し、最後の成果物作成となりました。


本記事の概要

20代の頃に、ハマっていた競艇をテーマとしました。
当時は、新聞やWEBなどで情報を集め予想して競艇を楽しんでいましたが、子供が生まれてから自然と競艇を見なくなりました。
まさかこの歳になって再び違う形で競艇に関わることが出来て嬉しく思います。
予測についてですが、
公式HPからデータのダウンロード
 ↓
ダウンロードデータの前処理
 ↓
機械学習でモデルの作成
 ↓
学習済モデルを使って次回のレースを予測する

といった流れになります。

作成したプログラム

データ収集

まずは公式HPからのデータのダウンロードからです。
ダウンロード元:

https://www.boatrace.jp/owpc/pc/extra/data/download.html

ダウンロードデータはテキストファイルとなっているので、ダウンロード後に機械学習が行えるようにデータの前処理が必要です。
公式HPでは、「番組表ダウンロード」と「競争成績ダウンロード」をクリックして1日ずつデータをダウンロードしなくてはなりません。
今回の学習データは2022年10月1日~2023年9月30日の1年間のデータを使用していますが、ダウンロードは手作業でした。

データクレンジング

ダウンロードが完了したらデータフレームの作成です。
データフレーム作成に関しまして、@yyyyyy666(N Y)記事を参考とさせていただきました。

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/K2307/K*")
files_B = glob.glob("/content/drive/MyDrive/Colab Notebooks/B2307/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_B.merge(df_all_K, how='left', on=['レースID', '選手登番'])
df = df.drop(["選手名", "年齢", "支部", "体重", "級別", "モーターNO", "ボートNO", "開催地", "レース番号", "日付"], axis=1)
#フライング等着順つかなかったところを埋める
df['実着順'] = df['実着順'].fillna('06')
#不成立レース(有効選手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)
df[["艇番", "選手登番", "レースID"]] = df[["艇番", "選手登番", "レースID"]].astype(int)
df[["全国勝率", "全国2連率", "当地勝率", "当地2連率", "モーター2連率", "ボート2連率"]] = df[["全国勝率", "全国2連率", "当地勝率", "当地2連率", "モーター2連率", "ボート2連率"]].astype(float)
df = df.rename(columns={'レースID': 'レースID', '実着順': '着順', '選手登番': '選手番号', '艇番': '艇番',
                        '全国勝率': '全国勝率', '全国2連率': '全国2連率', '当地勝率': '当地勝率',
                        '当地2連率': '当地2連率', 'モーター2連率': 'モーター2連率', 'ボート2連率': 'ボート2連率'})
class_mapping = {'01': 0, '02': 1, '03': 2, '04': 3, '05': 4, '06': 5}
df['着順'] = df['着順'].map(class_mapping)
# レースIDごとに行をまとめる
df1 = df.groupby('レースID').agg(list).reset_index()
# 最も要素数が多いリストの長さを取得
max_length = max(max(df1['着順'].apply(len)), max(df1['艇番'].apply(len)), max(df1['選手番号'].apply(len)), max(df1['全国勝率'].apply(len)), max(df1['全国2連率'].apply(len)), max(df1['当地勝率'].apply(len)), max(df1['当地2連率'].apply(len)), max(df1['モーター2連率'].apply(len)), max(df1['ボート2連率'].apply(len)))

# 足りない要素をNaNで埋める
df1['着順'] = df1['着順'].apply(lambda x: x + [None]*(max_length-len(x)))
df1['艇番'] = df1['艇番'].apply(lambda x: x + [None]*(max_length-len(x)))
df1['選手番号'] = df1['選手番号'].apply(lambda x: x + [None]*(max_length-len(x)))
df1['全国勝率'] = df1['全国勝率'].apply(lambda x: x + [None]*(max_length-len(x)))
df1['全国2連率'] = df1['全国2連率'].apply(lambda x: x + [None]*(max_length-len(x)))
df1['当地勝率'] = df1['当地勝率'].apply(lambda x: x + [None]*(max_length-len(x)))
df1['当地2連率'] = df1['当地2連率'].apply(lambda x: x + [None]*(max_length-len(x)))
df1['モーター2連率'] = df1['モーター2連率'].apply(lambda x: x + [None]*(max_length-len(x)))
df1['ボート2連率'] = df1['ボート2連率'].apply(lambda x: x + [None]*(max_length-len(x)))

# 新しいカラムを作成
old_cols=["着順", "艇番", "選手番号","全国勝率", "全国2連率", "当地勝率", "当地2連率", "モーター2連率", "ボート2連率"]
for i in range(max_length):
  for col_ in old_cols:
    df1[f'{col_}{i+1:02d}']=df1[col_].apply(lambda x: x[i])

# 元のカラムは削除する場合
df1.drop(columns=['着順', '艇番', '選手番号', '全国勝率', '全国2連率', '当地勝率', '当地2連率', 'モーター2連率', 'ボート2連率'], inplace=True)


レースIDを作成して1つのレースを1行に納めました。
レースIDの見方
例)2310190101
23/10/19/01/01
23・・・23年
10・・・10月
19・・・19日
01・・・桐生(#01)競艇場コード
01・・・第1レース

機械学習でモデルを作成

今回のモデルはXGBoostを使用します。
XGBoostとは、アンサンブル学習の一つで、ブースティングと決定木を組み合わせた手法です。

import xgboost as xgb
from sklearn.metrics import accuracy_score
from sklearn.model_selection import train_test_split
import numpy as np

# 説明変数 X と目的変数 y の設定
X = df1.drop(columns=["着順01", "着順02", "着順03", "着順04", "着順05", "着順06"])  # 艇番以外の特徴量
y_01 = df1["着順01"]
y_02 = df1["着順02"]
y_03 = df1["着順03"]
y_04 = df1["着順04"]
y_05 = df1["着順05"]
y_06 = df1["着順06"]

# データの分割(各実着順ごとに行う)
X_train_01, X_test_01, y_train_01, y_test_01 = train_test_split(X, y_01, test_size=0.2, random_state=42)
X_train_02, X_test_02, y_train_02, y_test_02 = train_test_split(X, y_02, test_size=0.2, random_state=42)
X_train_03, X_test_03, y_train_03, y_test_03 = train_test_split(X, y_03, test_size=0.2, random_state=42)
X_train_04, X_test_04, y_train_04, y_test_04 = train_test_split(X, y_04, test_size=0.2, random_state=42)
X_train_05, X_test_05, y_train_05, y_test_05 = train_test_split(X, y_05, test_size=0.2, random_state=42)
X_train_06, X_test_06, y_train_06, y_test_06 = train_test_split(X, y_06, test_size=0.2, random_state=42)

model_01 = xgb.XGBClassifier(objective='multi:softmax')
model_01.fit(X_train_01, y_train_01)
y_pred = model_01.predict(X_test_01)
accuracy = accuracy_score(y_test_01, y_pred)
print(f'予測精度: {accuracy}')

model_02 = xgb.XGBClassifier(objective='multi:softmax')
model_02.fit(X_train_02, y_train_02)
y_pred = model_02.predict(X_test_02)
accuracy2 = accuracy_score(y_test_02, y_pred)
print(f'予測精度: {accuracy2}')

model_03 = xgb.XGBClassifier(objective='multi:softmax')
model_03.fit(X_train_03, y_train_03)
y_pred = model_03.predict(X_test_03)
accuracy3 = accuracy_score(y_test_03, y_pred)
print(f'予測精度: {accuracy3}')

model_04 = xgb.XGBClassifier(objective='multi:softmax')
model_04.fit(X_train_04, y_train_04)
y_pred = model_04.predict(X_test_04)
accuracy4 = accuracy_score(y_test_04, y_pred)
print(f'予測精度: {accuracy4}')

model_05 = xgb.XGBClassifier(objective='multi:softmax')
model_05.fit(X_train_05, y_train_05)
y_pred = model_05.predict(X_test_05)
accuracy5 = accuracy_score(y_test_05, y_pred)
print(f'予測精度: {accuracy5}')

model_06 = xgb.XGBClassifier(objective='multi:softmax')
model_06.fit(X_train_06, y_train_06)
y_pred = model_06.predict(X_test_06)
accuracy6 = accuracy_score(y_test_06, y_pred)
print(f'予測精度: {accuracy6}')

NaNがあります。とエラーが発生するとその都度補完していきます。

各modelの予測精度は、
model_01
予測精度: 0.5249756011001686
model_02
予測精度: 0.2596930174784846
model_03
予測精度: 0.23893177180374411
model_04
予測精度: 0.2506432437228285
model_05
予測精度: 0.2768166089965398
model_06
予測精度: 0.3265016413805341
です。
(競艇はインコースが有利なのでmodel_01の予測精度が高いのでしょうか!?)

一通りこれで、model_01からmodel_06までのモデルの準備が完了しました。

学習でモデルを使って次回レースを予測

データクレンジングで使用したコードを少し修正して次回のレースデータを準備します。

import glob
import os
import re
from string import ascii_letters
from string import digits
import pandas as pd

files_Y = glob.glob("/content/drive/MyDrive/Colab Notebooks/翌日開催レース表/B*")

# Bデータを全て取り込み結合する
df_all_Y = pd.DataFrame()

for file in files_Y:

#データ読み込み

  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_Y = pd.DataFrame(values, columns=column)

#開催地、レース番号、日付(すべてstr型)を元ににレースIDを追加
  df_Y['レースID'] = df_Y['日付'] + df_Y['開催地'] + df_Y['レース番号']

  df_all_Y = pd.concat([df_all_Y, df_Y])
df2 = df_all_Y.drop(["選手名", "年齢", "支部", "体重", "級別", "モーターNO", "ボートNO", "開催地", "レース番号", "日付"], axis=1)
df2[["艇番", "選手登番", "レースID"]] = df2[["艇番", "選手登番", "レースID"]].astype(int)
df2[["全国勝率", "全国2連率", "当地勝率", "当地2連率", "モーター2連率", "ボート2連率"]] = df2[["全国勝率", "全国2連率", "当地勝率", "当地2連率", "モーター2連率", "ボート2連率"]].astype(float)
df2 = df2.rename(columns={'レースID': 'レースID', '選手登番': '選手番号', '艇番': '艇番',
                        '全国勝率': '全国勝率', '全国2連率': '全国2連率', '当地勝率': '当地勝率',
                        '当地2連率': '当地2連率', 'モーター2連率': 'モーター2連率', 'ボート2連率': 'ボート2連率'})
# レースIDごとに行をまとめる
df2 = df2.groupby('レースID').agg(list).reset_index()
# 最も要素数が多いリストの長さを取得
max_length = max(max(df2['艇番'].apply(len)), max(df2['選手番号'].apply(len)), max(df2['全国勝率'].apply(len)), max(df2['全国2連率'].apply(len)), max(df2['当地勝率'].apply(len)), max(df2['当地2連率'].apply(len)), max(df2['モーター2連率'].apply(len)), max(df2['ボート2連率'].apply(len)))

# 足りない要素をNaNで埋める

df2['艇番'] = df2['艇番'].apply(lambda x: x + [None]*(max_length-len(x)))
df2['選手番号'] = df2['選手番号'].apply(lambda x: x + [None]*(max_length-len(x)))
df2['全国勝率'] = df2['全国勝率'].apply(lambda x: x + [None]*(max_length-len(x)))
df2['全国2連率'] = df2['全国2連率'].apply(lambda x: x + [None]*(max_length-len(x)))
df2['当地勝率'] = df2['当地勝率'].apply(lambda x: x + [None]*(max_length-len(x)))
df2['当地2連率'] = df2['当地2連率'].apply(lambda x: x + [None]*(max_length-len(x)))
df2['モーター2連率'] = df2['モーター2連率'].apply(lambda x: x + [None]*(max_length-len(x)))
df2['ボート2連率'] = df2['ボート2連率'].apply(lambda x: x + [None]*(max_length-len(x)))

# 新しいカラムを作成
old_cols=["艇番", "選手番号","全国勝率", "全国2連率", "当地勝率", "当地2連率", "モーター2連率", "ボート2連率"]
for i in range(max_length):
  for col_ in old_cols:
    df2[f'{col_}{i+1:02d}']=df2[col_].apply(lambda x: x[i])

# 元のカラムは削除する場合
df2.drop(columns=['艇番', '選手番号', '全国勝率', '全国2連率', '当地勝率', '当地2連率', 'モーター2連率', 'ボート2連率'], inplace=True)


これで着順の入っていないデータフレームの完成です。
あとは学習したモデルと準備したデータフレームを合わせて予測を行います。


y_pred_01 = model_01.predict(df2)
y_pred_02 = model_02.predict(df2)
y_pred_03 = model_03.predict(df2)
y_pred_04 = model_04.predict(df2)
y_pred_05 = model_05.predict(df2)
y_pred_06 = model_06.predict(df2)

result=pd.DataFrame({'レースID': df2['レースID'], '着順01': y_pred_01,'着順02': y_pred_02,'着順03': y_pred_03,'着順04': y_pred_04,'着順05': y_pred_05,'着順06': y_pred_06})

これでresultすると・・・

わかりにくいですね・・・。
もう少し手を加えます。

predictions_01 = model_01.predict_proba(df2)
predictions_02 = model_02.predict_proba(df2)
predictions_03 = model_03.predict_proba(df2)
predictions_04 = model_04.predict_proba(df2)
predictions_05 = model_05.predict_proba(df2)
predictions_06 = model_06.predict_proba(df2)

formatted_numbers_01 = [[f'{prob:.3f}' for prob in class_probs] for class_probs in predictions_01]
formatted_numbers_02 = [[f'{prob:.3f}' for prob in class_probs] for class_probs in predictions_02]
formatted_numbers_03 = [[f'{prob:.3f}' for prob in class_probs] for class_probs in predictions_03]
formatted_numbers_04 = [[f'{prob:.3f}' for prob in class_probs] for class_probs in predictions_04]
formatted_numbers_05 = [[f'{prob:.3f}' for prob in class_probs] for class_probs in predictions_05]
formatted_numbers_06 = [[f'{prob:.3f}' for prob in class_probs] for class_probs in predictions_06]

result=pd.DataFrame({'レースID': df2['レースID'], '艇番01': formatted_numbers_01,'艇番02':formatted_numbers_02,'艇番03': formatted_numbers_03,'艇番04': formatted_numbers_04,'艇番05': formatted_numbers_05,'艇番06': formatted_numbers_06})


出力するとこんな感じになります。
レースID 2310190101
艇番01 [0.694, 0.082, 0.061, 0.040, 0.042, 0.080]
艇番02 [0.108, 0.110, 0.286, 0.132, 0.213, 0.150]
艇番03 [0.100, 0.083, 0.167, 0.113, 0.448, 0.090]
艇番04 [0.043, 0.332, 0.253, 0.198, 0.125, 0.049]
艇番05 [0.102, 0.113, 0.221, 0.079, 0.170, 0.315]
艇番06 [0.042, 0.115, 0.220, 0.281, 0.165, 0.178]

1号艇の選手が1着になる確率は69.4%ってことになります。
2着は4号艇ですね。

結果は・・・


的中です。
2連単いただきです。

おわりに

自分でテーマを決めてここに至るまで大変でした。
アウトプットすることで、自分の理解していないところなどを再確認することができました。
受講期間の都合上ひとまずここで終わりとさせていただきます。

しかし、残りの受講期間を利用して試してみたいことが、
予測精度を上げることと回収率の100%超えです。
・競艇には階級があるのでその階級をワンホットエンコーディングしてみる
・ダウンロードデータを増やしてみる。(テスト段階で1か月分のデータを使用して予測をしていたが、1年分にデータを増やすと予測精度が各モデル2~3%上がったため)

競艇で賭けたことがある人なら誰もが気になる回収率。
その回収率を今回の予測と掛け合わせて100%超えができたらと考えています。
そのためには、買い方やレースの選別などがあります。

自分の好きなテーマだったので楽しみながら作成することができました。

最後まで読んでいただきありがとうございました。


この記事が気に入ったらサポートをしてみませんか?