python初学者がボートレースを本格的に予想する。

記事
IT・テクノロジー
中川です。。

お久しぶりになってしまい、申し訳ないです。

今回は本当に大変でした。。

まず、データの取得が大変だったことと、あとたくさんのイレギュラーなレーサー情報に対応しなければならず、とてもてこずってしまいました。

前回までのデータはこちらです。


今回は、レーサー情報のコース別侵入率、三連対立、コース別平均スタートタイミング、コース別スタートタイムを取得し、それらのデータと前回取得した全国成績のデータを使って機械学習を行うことが今回の目標です。

まず、欲しいそれらの情報ですが、

スクリーンショット (6).png

ボートレースのURL/owpc/pc/data/racersearch/course?toban="レーサの番号"
のURLにありました。この例ではレーサーの番号は”4200”です。

例として、レーサー番号が4200番の早川尚人さんが出走番号2番でレースに出る場合は、上記四つの情報の2番だけが欲しいことになります。

ですがその前に、そもそもレーサーの番号を取得してくる必要がありますね。

レーサー番号を取得するためには

スクリーンショット (4).png
url = ボートレースのURL/owpc/pc/data/racersearch/course?toban="レーサの番号"

から、マーカーの部分の情報を抽出します。

マーカー部は'div'  タグの  classは "is-fs11"で指定されていたので。

res = requests.get(url)
soup = BeautifulSoup(res.text , "html.parser")
racers = soup.find_all('div' , class_ = "is-fs11")

で取得することができます。そして、文字列で取得したデータは一文字ずつリスト型で格納されているという特徴がありますのでそれを利用して

racer_1 = int(racers[0].text[0:4])
racer_2 = int(racers[2].text[0:4])
racer_3 = int(racers[4].text[0:4])
racer_4 = int(racers[6].text[0:4])
racer_5 = int(racers[8].text[0:4])
racer_6 = int(racers[10].text[0:4])

racer_list = [racer_1,racer_2,racer_3,racer_4,racer_5,racer_6]

.text[0:4]は取得した文字列のインデックス番号0から3番目、つまり1文字目から4文字目を取得してくれます。

そして、手に入れた情報をリストに納入することで for 文を使った繰り返し分によって1番から6番までの艇番のレーサーの情報を取得します。

そして、ここで得たレーサー番号の情報を使ってレーサー情報のコース別侵入率、三連対立、コース別平均スタートタイミング、コース別スタートタイムを取得していきます。

上記の通りレーサー情報はボートレースのURL/owpc/pc/data/racersearch/course?toban="レーサの番号"にあります。

このURLを使って

res_racer = requests.get(url_racer)
soup_racer = BeautifulSoup(res_racer.text , "html.parser")
course_rates = soup_racer.find_all('span',class_ = "table1_progress2Label")

必要な情報はすべて 'span',class_ = "table1_progress2Label" で取得できますので、ここから、

course_rates[0].text.split()[0][:-1]


と、記述することで1番のコース別進入率を取得することができます。

この形に至るまでにすごい時間を要してしまいました。

course_rates の中には、各四つの情報の一番から六番までのデータ、つまり24個のデータ入っています。

スクリーンショット (6).png
この24個のデータすべてが course_rates に格納されています。

ここでは1番のコース別進入率を取得したいので、一番最初の情報を[0]で取得し、その情報をcourse_rates[0].text.split()[0]で取得します。
これで必要な部分の数字がリストに格納されたもの取得することができるのですが、末尾の%まで取ってきてしまいます。そこで、

course_rates[0].text.split()[0][:-1]とすることで、[:-1]の部分のおかげで最後の一文字、つまり%を除いた値を取得することができます。

そして、データは24個に分けられているので、

course_rates[0].text.split()[0][:-1]
course_rates[6].text.split()[0][:-1]
course_rates[12].text.split()[0][:-1]
course_rates[18].text.split()[0][:-1]

と、記述すれば、

スクリーンショット (7).png

マークの部分を抽出することができます。

そして、ループ回数ごとに艇番を+していけばよいので


#ここまでは前回と同じ 

       for r in rno:

            url='ボートレースのサイト名/owpc/pc/race/racelist?rno={r}&jcd=12&hd=2021{m_2}{d_2}'.format(r = r, m_2 = m_2, d_2 = d_2)
            res = requests.get(url)
            soup = BeautifulSoup(res.text , "html.parser")
            racers = soup.find_all('div' , class_ = "is-fs11")
            racer_1 = int(racers[0].text[0:4])
            racer_2 = int(racers[2].text[0:4])
            racer_3 = int(racers[4].text[0:4])
            racer_4 = int(racers[6].text[0:4])
            racer_5 = int(racers[8].text[0:4])
            racer_6 = int(racers[10].text[0:4])
            print(racer_1)
            print(racer_2)
            print(racer_3)
            print(racer_4)
            print(racer_5)
            print(racer_6)
            racer_list = [racer_1,racer_2,racer_3,racer_4,racer_5,racer_6]

            race_num += 1

            racer_number = 0

            course_rate_in_list_origin = [] #事前にリストを作りその中にデータを入れる
            course_rate_in3_list_origin = []
            course_rate_starttime_list_origin = []
            course_rate_startrate_list_origin = []

            print(str(race_num) + "レース目")




#以下は取得したレーサー番号で for 文
            for racer in racer_list:

                url_racer = "ボートレースのサイト名/owpc/pc/data/racersearch/course?toban={racer}".format(racer=racer)
                res_racer = requests.get(url_racer)
                soup_racer = BeautifulSoup(res_racer.text , "html.parser")
                course_rates = soup_racer.find_all('span',class_ = "table1_progress2Label")



                    if course_rates:

                        #[course_rate.text.split() for course_rate in course_rates]
                        course_rate_in = float(course_rates[0 + racer_number].text.split()[0][:-1])
                        course_rate_in3 = float(course_rates[6 + racer_number].text.split()[0][:-1])
                        course_rate_starttime = float(course_rates[12 + racer_number].text.split()[0][:-1])
                        course_rate_startrate = float(course_rates[18 + racer_number].text.split()[0][:-1])
                        print(racer_number+1) 
                        print("コース別進入率" + str(course_rate_in))
                        print("コース別3連対率" + str(course_rate_in3))
                        print("コース別平均スタートタイミング" + str(course_rate_starttime))
                        print("コース別スタートタイム" + str(course_rate_startrate))
#もしデータがなければ、からの値をリストに代入。
                    else:
                        course_rate_in = np.nan
                        course_rate_in3 = np.nan
                        course_rate_starttime = np.nan
                        course_rate_startrate = np.nan
                        print(racer_number+1) 
                        print("コース別進入率" + str(course_rate_in))
                        print("コース別3連対率" + str(course_rate_in3))
                        print("コース別平均スタートタイミング" + str(course_rate_starttime))
                        print("コース別スタートタイム" + str(course_rate_startrate)) course_rate_in_list_origin = [np.nan,float(6.01),6,6,6,float('nan')]
print("一番高い侵入率は. " + str(course_rate_in_list_origin.index(np.nanmin(course_rate_in_list_origin)) + 1))
print(np.nanmin(course_rate_in_list_origin))

すごく長くなってしまいましたが、これでできるはず、、、

と、思ったのですが、エラーになってしまいました。。

どうやらレーサーが指定のコースで走ったことがないとき、数字のところが -- という表記がされ、それを無理やりfloat型に変更しようとしているためエラーになっていました。

しかし、どうやってもこのエラーデータを条件分岐で分けることができずに、1週間が経過します。。。


いろいろと探して、見つけ出したのが

try:

except:


を使った、条件分岐!!

なぜ、これを今まで知らなかったのでしょうか

これは、エラーが起こった時に except 以下の分を実行させる魔法の構文です。

これを使えば、今まで嫌いだったエラーとお友達になれます。

これを使って、どう対処したかというと、

                try:

                    if course_rates:

      #以下は同じなので省略

                except:
                        course_rate_in = np.nan
                        course_rate_in3 = np.nan
                        course_rate_starttime = np.nan
                        course_rate_startrate = np.nan
                        print(racer_number+1) 
                        print("コース別進入率" + str(course_rate_in))
                        print("コース別3連対率" + str(course_rate_in3))
                        print("コース別平均スタートタイミング" + str(course_rate_starttime))
                        print("コース別スタートタイム" + str(course_rate_startrate))

として、except 文と try 文の中に処理を入れただけです。

except 文の中には空の値を入れる命令をしているのですが、これですべてのイレギュラーに対応することができました。

そして末尾に、

                course_rate_in_list_origin.append(course_rate_in)#try,exceptと同じインデント。
                course_rate_in3_list_origin.append(course_rate_in3)
                course_rate_starttime_list_origin.append(course_rate_starttime)
                course_rate_startrate_list_origin.append(course_rate_startrate)




                racer_number += 1
#一つインデントをあげる。
            course_rate_in_list.append(course_rate_in_list_origin.index(np.nanmax(course_rate_in_list_origin)) + 1)
            course_rate_in3_list.append(course_rate_in3_list_origin.index(np.nanmax(course_rate_in3_list_origin)) + 1)
            course_rate_starttime_list.append(course_rate_starttime_list_origin.index(np.nanmin(course_rate_starttime_list_origin)) + 1)
            course_rate_startrate_list.append(course_rate_startrate_list_origin.index(np.nanmin(course_rate_startrate_list_origin)) + 1)
                                     score_top.append(np.nan)

と、記述することで、

リストにそのレースの出場選手の中で最も成績がよい選手の出走艇番を挿入します。

course_rate_in3_list_origin.index(np.nanmax(course_rate_in3_list_origin)) + 1

の部分は、リストの中の最も大きい番号のインデックスを取得しそれに+1をしてやることで、インデックス番号が0から始まる仕組みによるずれを修正します。

最後に、

rate_racer = rate_racer = {"コース別進入率":course_rate_in_list,"コース別3連対率":course_rate_in3_list,"コース別スタートタイム":course_rate_startrate_list}

df_racer = pd.DataFrame(rate_racer)

print(df_racer)

あまり結果に影響しないため平均スタートタイミングを除いた3つをデータに格納することにしました。

すると、

     コース別進入率 コース別3連対率 コース別スタートタイム
0  6  3  3
1  6  1  3
2  5  1  2
3  4  5  5 
4  5  1  1
.. ... ... ...

のように、無事データを取得できました!!

これで、やっと機械学習に入ることができます。

all_data_test = pd.concat([df_kekka,df_score,df_racer],axis = 1)

今までのデータを格納し、

ちなみに、CSVファイルをpandasのデータフレームに変換するには

df = pd.read_csv('csvファイル名')

とすることで、機械学習用にpandasのデータフレームに変換できます。

今回は scikit-learn を使います。

from sklearn import tree

で機械学習モデルの中の決定木分析モジュールをimportします。

今回は2021年6月のデータを使って2021年7月のデータを予測します。

6月のデータを df_all

7月のデータを all_data_test 

に挿入し、

col = ["全国成績","コース別進入率","コース別3連対率","コース別スタートタイム"]
x_l = df_all[col]
t = ["一着"]
y_l = df_all[t]

model = tree.DecisionTreeClassifier(max_depth = 5,random_state = 0, class_weight = 'balanced')

model.fit(x_l,y_l)

#以下はテストデータ

col = ["全国成績","コース別進入率","コース別3連対率","コース別スタートタイム"]
x_l_test = all_data_test[col]
t = ["一着"]
y_l_test = all_data_test[t]

all_data_test.head(2)

今回特筆すべき部分は特にmodel = tree.DecisionTreeClassifier(max_depth = 5,random_state = 0, class_weight = 'balanced')
だけです。

max_depth = 5 というのは、機械学習の決定木の深さ、これは値を大きくすればするほどテストデータの特徴を分析してテストデータの値予測に特化していくのですが、値を大きくしすぎてしまうとテストデータ以外のデータ予測精度が下がってしまうので注意がいるようです。

random_state = 0 というのは、機械学習の再現性を保つためのシード値です。

class_weight = 'balanced' は、サンプルごとの変数の重みを自動で調整、つまりデータが不均衡なものには有効なようです。

そして、正解データと、テストデータは一着の数字のみに絞っています。

そして、これを使ってデータを予測!><


model.score(X=x_l_test,y=y_l_test)

Out:0.23333333333333334

正解率、何と、23%

すばらしいです。

6つしか正解がないというのに23%

もしも、てきとーに値を選んだとして

なんのデータも見ずに当てずっぽうでも

1÷6で 17%はあります。

なんと、6%しか変わりません。

なにが、いけなかったのでしょうか。

試しに depthを変えてみたのですが、

depth = 12 の時に一番高くなり、32.5%でした。

他の機械学習アプローチも試してみます。

from sklearn.linear_model import LinearRegression

model = LinearRegression()

LinearRegression()を使って、結果を出力

model.score(X=x_l_test,y=y_l_test)

すると、正解率は驚異の7%でした。

恐らくこれは、学習法にデータが全く適合していないためだと考えられます。

他のデータもいろいろ試しましたが、結局

model = tree.DecisionTreeClassifier(max_depth = 12,random_state = 0, class_weight = 'balanced')

のモデルを利用したときが、一番高くなりました。

32.5%と停滞してしまった理由としては、データが機械学習に向いていなかったこと、あとデータの数の少なさが要因なのかもしれません。

特にコース別成績はデータ取得にすごく時間を要してしまいますので、時間を見つけてまた挑戦したいです。

とにかく、ようやく自分で集めたボートレースのデータで機械学習をすることができました。

機械学習といってもほとんどの時間をデータ収集に割いてしまいましたので、ほぼスクレイピングの勉強になってしまいましたが、python初学者としての大きな一歩を踏み出すことができました!

ボートレースの機械学習シリーズはいったんここまでとさせていただきます。

また、進展があれば報告いたしますのでよろしくお願いします!

長くなってしまいましたが、最後まで見て頂いた方が一人でもいればすごく幸せです。

本当にありがとうございました。
サービス数40万件のスキルマーケット、あなたにぴったりのサービスを探す