Pythonでボートレース予想AIを作ってみた

スポンサーリンク

はじめに

この記事は、プログラミングの経験があり、ボートレースや競馬の予想プログラムを作ってみたい方を対象にした内容になってます。

今回はPythonの勉強がてら、ボートレースの予想AIを作ってみました。

ここでいう「AI」とは「重回帰分析」を用いた予想のことを「AI」と呼ぶことにします。

重回帰分析をするにあたって、PythonのLightGBMを使いました。

LightGBMの使い方に関しては以下のサイトを参考にさせて頂きました。ありがとうございました。

https://mathmatical22.xyz/2020/04/11/%E3%80%90%E5%88%9D%E5%AD%A6%E8%80%85%E5%90%91%E3%81%91%E3%80%91lightgbm-%E5%9F%BA%E6%9C%AC%E7%9A%84%E3%81%AA%E4%BD%BF%E3%81%84%E6%96%B9-%E5%A4%9A%E3%82%AF%E3%83%A9%E3%82%B9%E5%88%86%E9%A1%9E%E7%B7%A8/

作ったプログラムは、ボートレースの2連複の組み合わせ15通りのそれぞれがどのくらいの確率で来るかを予想するものです。

確率をもとに、舟券を買うのに必要なオッズを計算して、実際にそのオッズより高ければ買うという判断を出来ます。

確率とオッズの話は以下を参考にしてください。

プログラムと過去データのファイル

Pythonのソースコードと戸田、芦屋、大村の過去4年分のCSVファイルをzipファイルで以下に添付しました。

CSVファイルの説明をすると、「card」は出走表のデータ、「course」は進入コースのデータ、「remark」は返還艇の有無のデータ、「result」は結果のデータが格納されています。

PythonコードとCSVファイルは同階層にないと上手く作動しませんので注意してください。

実行方法

Pythonを使ったことがある方なら問題ないですが、これからPythonを始める人は「Anaconda」をインストールして「Anaconda Prompt」で実行しましょう。

エラーが出た場合は「conda install (パッケージ名)」などで必要なパッケージをインストールしましょう。

実行した時の状態は下のような感じです。

(base) C:\Users\Desktop\boat_gbm_tk>python boat_gbm_tk.py
予想したい場を2桁で入力:24
予想したい日をyyyymmdd型で入力:20210910

(base) C:\Users\Desktop\boat_gbm_tk>

過去のデータは、戸田と芦屋と大村しか準備してないので場コードは2と21と24以外を入れるとエラーが出ます。

他の場の予想をしたい場合は自分で準備して、Pythonファイルと同階層に入れてください。

予想した日を入力して一時待っていると下のような画面が表示されます。

表示されているオッズより実際のオッズが高かったら買うという判断をします。

リアルマネーでの検証結果

自分のリアルマネーで検証したのでご参考にしてください。

詳しくはこちらで見れます。

x.com

「#〇〇2連複のみチャレンジ」で検索かけてみて下さい。

買い方は予想より高いオッズの組をすべて1枚ずつ買っています。

最終的な結果は下の通りです。

  • 大村
    【通算損益】+600円
    【通算購入金額】12,960円
    【通算払戻金】13,560円
    【通算回収率】104.6 %
  • 芦屋
    【通算損益】-920円
    【通算購入金額】4,800円
    【通算払戻金】3,880円
    【通算回収率】80.8%
  • 戸田
    【通算損益】-3,660円
    【通算購入金額】5,300円
    【通算払戻金】1,640円
    【通算回収率】30.9%

大村だけ辛うじてプラスという感じです笑。

戸田は2連複なのに本当に当たる気がしませんでした。

まとめ

AIプログラムを作ったのはいいものの利益を上げることは出来ませんでした。

これを参考にして改良出来る方いたらぜひやってみて下さい。

最後にソースコードを貼っておきます。

ソースコード

#何度も修正したので不要なimportが含まれている可能性があります
import numpy as np
import pandas as pd
import matplotlib.pyplot as pyplot
import seaborn as sns; sns.set()
import lightgbm as lgb
from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
from sklearn.metrics import log_loss
from sklearn.metrics import roc_auc_score
from bs4 import BeautifulSoup
from urllib import request
from urllib.request import urlopen
import time
from sklearn.preprocessing import StandardScaler
import tkinter as tk
import tkinter.ttk as ttk

#AIの予想結果を取得するためのクラス
class gbm_predict:

    def __init__(self):
        self.get_jcd()
        self.my_list=[]
        self.get_predict_day()
        self.read_csv()
        self.process()
        self.gbm()
        self.predict()

    #予想したい日を取得
    def get_predict_day(self):
        self.predict_day=input('予想したい日をyyyymmdd型で入力:')

    #予想したい場のコードを取得
    def get_jcd(self):
        self.jcd=input('予想したい場を2桁で入力:')

    #CSVファイルの読み込み
    def read_csv(self):

        #出走表のデータ
        self.df_card=pd.read_csv(self.jcd+'card.csv',index_col=0)

        #進入コースのデータ
        self.df_course=pd.read_csv(self.jcd+'course.csv',index_col=0)

        #返還艇に関するデータ
        self.df_remark=pd.read_csv(self.jcd+'remark.csv',index_col=0)

        #結果のデータ
        self.df_result=pd.read_csv(self.jcd+'result.csv',index_col=0)

    #AIでデータ処理を行うためにデータの加工を行う
    def process(self):
        self.df_card=self.remove_course_remark(self.df_card)
        self.df_result=self.remove_course_remark(self.df_result)
        self.remove_unnes_card()
        self.display_result_number()

    #進入コースが枠なりでないレースと返還艇のあるレースを削除する
    def remove_course_remark(self,df):

        #枠なりでないレースを削除する
        for index, item in self.df_course.iterrows():
            if item['1c']!=1 or item['2c']!=2 or item['3c']!=3 or item['4c']!=4 or item['5c']!=5 or item['6c']!=6:
                df=df.drop(index,axis=0)

        #返還艇のあるレースを削除する
        try:
            for index, item in self.df_remark.iterrows():
                if len(item['remark'])!=1:
                    df=df.drop(index,axis=0)
        except KeyError:
            pass
        return df
    
    #出走表のデータの内、全国と当地の勝率、2連率、3連率以外の列を削除する
    def remove_unnes_card(self):
        for i in range(1,7):
            self.df_card=self.df_card.drop([str(i*15-6),str(i*15-5),str(i*15-4),str(i*15-3),str(i*15-2),str(i*15-1),str(i*15-13),str(i*15-15),str(i*15-14),],axis=1)

    #例:結果[3-1-4]を2連複表記[13]で表す
    def display_result_number(self):
        self.df_result['result']=0
        for index, item in self.df_result.iterrows():
            item['result']=int(str(min(item['rank1'],item['rank2']))+str(max(item['rank1'],item['rank2'])))
        self.df_result=self.df_result.drop(['rank1','rank2','rank3'],axis=1)

    #AIの準備
    def gbm(self):

        #入力値に出走表のデータをセット
        x=self.df_card.to_numpy()

        #出力値にレース結果をセット
        y=self.df_result.to_numpy()[:,0]

        x_train,x_test,y_train,y_test = train_test_split(x,y,test_size=0.01,random_state=0,stratify=y)

        #データを正規化する
        self.scaler=StandardScaler()
        self.scaler.fit(x_train)
        x_train=self.scaler.transform(x_train)

        #AIモデルの準備と回帰分析の実施
        self.model = lgb.LGBMClassifier()
        self.model.fit(x_train,y_train)
    
    #予想したい日の出走表のデータから予想する
    def predict(self):
        for rno in range(1,13):
            url='https://www.boatrace.jp/owpc/pc/race/racelist?rno='+str(rno)+'&jcd='+str(self.jcd).zfill(2)+'&hd='+str(self.predict_day)
            time.sleep(1)
            response=request.urlopen(url)
            soup=BeautifulSoup(response,'html.parser')
            response.close()

            #1レースの出走表のデータが1行でまとめられたもの
            table_sum=[]
            for i in [1,2,6,7,11,12,16,17,21,22,26,27]:
                table = soup.find_all('td',class_='is-lineH2')[i].get_text()
                table = table.replace(' ','')
                table = table.strip()
                table = table.replace('\r\n',',')
                table = table.split(',')
                table_sum=table_sum+table
            table_sum = [float(x) for x in table_sum]
            table_sum = [np.array(table_sum)]

            #AIに入れるまえに正規化を行う
            table_sum=self.scaler.transform(table_sum)

            #各出力値(12,13,...,64,65)に対応する確率を計算
            proba_list=self.model.predict_proba(table_sum)[0]

            #確率の逆数から必要なオッズを計算
            odds_list=[int(1/self.solve_zero_problem(proba)*10+1)/10 for proba in proba_list]

            odds_list_str=list('0' for i in range(15))

            #オッズが800を超えたら買わないので「-」の表記に変更
            for i in range(len(odds_list)):
                if odds_list[i]>=800:
                    odds_list_str[i]='-'
                else:
                    odds_list_str[i]=str(odds_list[i])
            self.my_list.append(odds_list_str)

    #確率の逆数を返す
    def solve_zero_problem(self,proba):
        if proba==0:
            proba=1/10000
        return proba

#AIの予想値を表示するクラス
class window:
    jcd_name_list=['桐生','戸田','江戸川','平和島','多摩川','浜名湖','蒲郡','常滑','津','三国','びわこ','住之江','尼崎','鳴門','丸亀','児島','宮島','徳山','下関','若松','芦屋','福岡','唐津','大村']

    def __init__(self,master):
        self.master=master

        #AI予想結果を取得するクラスのインスタンス化
        self.gbm_predict=gbm_predict()

        self.odds_frame_list=[]
        self.master.title(self.jcd_name_list[int(self.gbm_predict.jcd)-1]+self.gbm_predict.predict_day)
        self.master.state('zoomed')
        self.create_odds_frame()

    #結果を表示する部分を作成
    def create_odds_frame(self):
        frame1=tk.Frame(self.master)

        #1レースごとに予想値が入った枠組みを表示していく
        for i in range(12):
            self.odds_frame_list.append(tk.Frame(frame1))
            self.set_odds(self.odds_frame_list[i],self.gbm_predict.my_list[i],i)
            self.odds_frame_list[i].grid(row=int(i/4),column=i%4)
        frame1.pack(side='left',anchor='nw')

    #ボートNoに対応する色を設定
    def num_color(self,i):
        if i==1:
            bg='white'
            fg='black'
        elif i==2:
            bg='black'
            fg='white'
        elif i==3:
            bg='red'
            fg='white'
        elif i==4:
            bg='blue'
            fg='white'
        elif i==5:
            bg='yellow'
            fg='black'
        elif i==6:
            bg='green'
            fg='white'
        return {'bg':bg, 'fg':fg}

    #レース1つ分の予想値を表示するための枠組みを作成
    def set_odds(self,frame,odds_list,k):

        #上部に何Rかを表示する
        tk.Label(frame,text=str(k+1)+'R').grid(column=1,row=0,columnspan=8,sticky='nsew')

        for i in range(1,4):
            tk.Label(frame,text='  '+str(i)+'  ',bg=self.num_color(i)['bg'],fg=self.num_color(i)['fg'],borderwidth=1,relief="solid").grid(column=3*i-2,row=1,columnspan=2,sticky='nsew')
        for i in range(2,7):
            tk.Label(frame,text=str(i),bg=self.num_color(i)['bg'],fg=self.num_color(i)['fg'],borderwidth=1,relief="solid").grid(column=1,row=2*i-1,sticky='nsew')
            tk.Label(frame,text=odds_list[i-2],borderwidth=1,relief="solid",anchor='e').grid(column=2,row=2*i-1,sticky='nsew')
        for i in range(3,7):
            tk.Label(frame,text=str(i),bg=self.num_color(i)['bg'],fg=self.num_color(i)['fg'],borderwidth=1,relief="solid").grid(column=4,row=2*i-3,sticky='nsew')
            tk.Label(frame,text=odds_list[i+2],borderwidth=1,relief="solid",anchor='e').grid(column=5,row=2*i-3,sticky='nsew')
        for i in range(4,7):
            tk.Label(frame,text=str(i),bg=self.num_color(i)['bg'],fg=self.num_color(i)['fg'],borderwidth=1,relief="solid").grid(column=7,row=2*i-5,sticky='nsew')
            tk.Label(frame,text=odds_list[i+5],borderwidth=1,relief="solid",anchor='e').grid(column=8,row=2*i-5,sticky='nsew')
        for i in range(4,6):
            tk.Label(frame,text=str(i),bg=self.num_color(i)['bg'],fg=self.num_color(i)['fg'],borderwidth=1,relief="solid").grid(column=3*i-11,row=13,columnspan=2,sticky='nsew')
        for i in range(5,7):
            tk.Label(frame,text=str(i),bg=self.num_color(i)['bg'],fg=self.num_color(i)['fg'],borderwidth=1,relief="solid").grid(column=1,row=2*i+5,sticky='nsew')
            tk.Label(frame,text=odds_list[i+7],borderwidth=1,relief="solid",anchor='e').grid(column=2,row=2*i+5,sticky='nsew')
        for i in range(6,7):
            tk.Label(frame,text=str(i),bg=self.num_color(i)['bg'],fg=self.num_color(i)['fg'],borderwidth=1,relief="solid").grid(column=4,row=15,sticky='nsew')
            tk.Label(frame,text=odds_list[i+8],borderwidth=1,relief="solid",anchor='e').grid(column=5,row=15,sticky='nsew')

win=tk.Tk()
window=window(win)
win.mainloop()

コメント

タイトルとURLをコピーしました