競馬予想AIの作成③(競馬データの取得③)

機械学習

はじめに

前回前々回のプログラムを結合して、指定期間のレース結果を取得してCSVに保存するプログラムを作成しました。前々回のプログラムの一部を変更しています。
変更したのは、距離/天候/競争種別/馬場状態/開催日/開催場所を追加しています。

AIで予測するデータを「タイム」とすることにしました。
「タイム」を1列目に移動してCSVファイルに保存します。

競馬データの取得先

以下のページから競馬データを取得します。

netkeiba.com – 国内最大級の競馬情報サイト

プログラムの概要

前回作成したプログラムと前々回作成したプログラムを結合したプログラムです。
変更が有った箇所は「変更有り」と記載しています。

該当行 概要
1~12 ライブラリの読み込み。変更有り
17~50 メイン処理。各関数を呼び出して各レース結果をCSVに保存する。変更有り
53~92 レース詳細検索で検索条件を指定して検索する。変更無し
95~127 レース詳細検索の結果から各レース結果のURLを抽出する。変更無し
130~157 レース詳細検索の結果で「次」がある場合、「次」をクリックする。変更無し
160~239 レース結果の詳細を取得する。前々回のプログラムで変更有り

事前準備

2022年の1~5月の芝・ダートのレース結果を取得する場合、6700レースの情報を取得することになります。1秒間に4レースの情報を取得できたとしても、プログラムの実行にはおよそ27分かかります。進捗状況を確認できるように、tqdmを用いて進捗バーを表示します。tqdmはpipでインストールします。

pip install tqdm

tqdmを使用することで、下記のように進捗バーを表示することができるようになります。

作成したプログラム

掲載したプログラムは、前回と前々回から「変更有り」の箇所となります。

ライブラリ読み込み

requests、re、tqdmのライブラリを追加しました。

# ライブラリの読み込み
import pandas as pd
import urllib
import requests
import re

from webdriver_manager.chrome import ChromeDriverManager
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import Select
from bs4 import BeautifulSoup

# プログレスバーを表示するためのライブラリを読み込む
from tqdm import tqdm

メイン処理

取得したレース結果をCSVに保存する処理と、コメントを変更しました。

# メイン
def main():
    # webdriver_managerで最適なchromeのバージョンをインストールして設定する
    browser = webdriver.Chrome(ChromeDriverManager().install())

    # 競馬データベースを開く
    browser.get('https://db.netkeiba.com/?pid=race_search_detail')

    browser.implicitly_wait(10) # 指定した要素が見つかるまでの待ち時間を10秒と設定する
    search_rase(browser)        # 検索条件を設定して検索する
    link_list = []              # リンクページのURLリスト

    no_click_next = 0           # 0:次のページ無し、1:次のページ有り
    count_click_next = 0        # 0:検索結果の1ページ目、1以上:検索結果の2ページ目以上
    while no_click_next == 0:
        # レース結果のリンクページのURLを取得する
        link_list = make_raseURL(browser, link_list)

        # 「次」をクリックし、「次」の有無とクリックした回数を返す
        no_click_next, count_click_next = click_next(browser, count_click_next)

    df_RaceResult = pd.DataFrame()

    # レース結果のリンクページにアクセスして、レース結果を取得する
    # 日本レース以外のレース結果は取得しない
    # 日本以外のレースのリンクページには、/race/2022H1a00108/のようにアルファベットが含まれている
    # isdecimalで文字列が数字のみかを判定している
    print('レース結果の詳細を取得します')
    for url in tqdm(link_list):
        if url.split('/')[-2].isdecimal():
            df_RaceResult = pd.concat([df_RaceResult, output_RaceResult(url)], axis=0)

    # CSVにレース結果を保存する
    df_RaceResult.to_csv('./レース結果.csv', encoding='cp932', header=False, index=False, errors="ignore")

レース結果の詳細を取得する

レース距離、天候、馬場の状態、レース名、開催日、開催場所を前々回から追加して取得します。
「タイム」を1列目に移動したデータフレームを作成します。

# レース結果の詳細を取得する
def output_RaceResult(url):
    res = requests.get(url) # 指定したURLからデータを取得する
    soup = BeautifulSoup(res.content, 'html.parser') # content形式で取得したデータをhtml形式で分割する

    # レース名を取得する
    race_name = soup.find_all('h1')
    race_name = race_name[1].text

    # 開催日、開催場所を取得する
    race_base_info = soup.find('p', attrs={'class': 'smalltxt'})
    rase_base_info_text = race_base_info.text.replace(u'\xa0', u' ')
    words = rase_base_info_text.split(' ')
    race_date = words[0]
    race_place = words[1]

    # レース情報を取得する
    race_info = soup.find('diary_snap_cut')
    race_info_text = race_info.text.replace(u'\xa0', u' ')
    race_info_text = race_info.text.replace(u'\xa5', u' ')
    words = race_info_text.split('/')

    race_info_distance = int(re.sub(r'\D', '', words[0]))   # レース距離だけを取り出してint型で保存する
    race_info_weather = words[1].split(':')                 # 天候
    race_info_condition = words[2].split(':')               # 馬場の状態

    # tableデータを抽出する
    tables = soup.find('table', attrs={'class': 'race_table_01'})
    tables = tables.find_all('tr')

    # 取得したデータからindex名を抽出する
    indexs = tables[0].text.split('\n')

    # レース情報/結果を取得する
    tmp = []
    df = pd.DataFrame()
    df_tmp1 = pd.DataFrame()

    for table in tables[1:]:
        tmp = table.text.split('\n')
        df_tmp1 = pd.Series(tmp)
        df = pd.concat([df, df_tmp1], axis=1)

    # 学習に必要な情報のみを抽出する
    # 着順:要素No.1、馬名:要素No.5、性齢:要素No.7、斤量:要素No.8、騎手:要素No.10、
    # タイム:要素No.12、上り:要素No.21、単勝:要素No.23、人気:要素No.24、馬体重:要素No.25の情報を抽出する
    df_tmp2 = pd.DataFrame()
    df_tmp3 = pd.DataFrame()
    for i in (1, 5, 7, 8, 10, 12, 21, 23, 24, 25):
        df_tmp2 = df.iloc[i]
        df_tmp3 = pd.concat([df_tmp3, df_tmp2], axis=1)

    # カラム名を設定する
    tmp = ['着順', '馬名', '性齢', '斤量', '騎手', 'タイム', '上り', '単勝', '人気', '馬体重']
    df_columns = pd.Series(tmp)
    df_tmp3.columns = df_columns  # index名を設定する

    df_tmp4 = pd.DataFrame()

    try:
        # 距離、天候、馬場、状態の列を追加する
        df_tmp3['距離'] = race_info_distance
        df_tmp3['天候'] = race_info_weather[1]
        df_tmp3['馬場'] = race_info_condition[0]
        df_tmp3['状態'] = race_info_condition[1]
        df_tmp3['開催日'] = race_date
        df_tmp3['レース名'] = race_name
        df_tmp3['開催場所'] = race_place

        # 説明変数として使用しない列を削除する
        df_tmp3.drop(['着順', '上り'], axis=1, inplace=True)

        # 目的変数にするタイムを1列に変更する
        df_tmp4 = df_tmp3.reindex(columns=['タイム', '馬名', '性齢', '斤量', '騎手', '単勝',
                                           '人気', '馬体重', '距離', '天候', '馬場', '状態',
                                           '開催日', 'レース名', '開催場所'])
    except:
        print('レース情報を取得できませんでした')

    return df_tmp4

プログラムの実行結果

プログラム実行のカレントフォルダに「レース結果.csv」が作成されます。
また、標準出力では下記のメッセージが表示されます。
中止になったレース情報を参照した場合、「レース情報を取得できませんでした」と表示されます。

====== WebDriver manager ======
Current google-chrome version is 102.0.5005
Get LATEST chromedriver version for 102.0.5005 google-chrome
Driver [C:\Users\xxxx\.wdm\drivers\chromedriver\win32\102.0.5005.61\chromedriver.exe] found in cache
C:\Users\ xxxx\Google ドライブ\Web_Scraping\xxxx.py:20: DeprecationWarning: executable_path has been deprecated, please pass in a Service object
  browser = webdriver.Chrome(ChromeDriverManager().install())
検索条件を設定します
取得したHTMLからレース結果のURLを抽出します
URLを抽出しました
~中略~

取得したHTMLからレース結果のURLを抽出します
URLを抽出しました
取得したHTMLからレース結果のURLを抽出します
URLを抽出しました
取得したHTMLからレース結果のURLを抽出します
URLを抽出しました
取得したHTMLからレース結果のURLを抽出します
URLを抽出しました
次のページ無し
レース結果の詳細を取得します
 98%|█████████▊| 6559/6700 [36:36<00:50,  2.80it/s]レース情報を取得できませんでした
レース情報を取得できませんでした
 98%|█████████▊| 6561/6700 [36:37<00:52,  2.62it/s]レース情報を取得できませんでした
レース情報を取得できませんでした
 98%|█████████▊| 6563/6700 [36:37<00:51,  2.65it/s]レース情報を取得できませんでした
レース情報を取得できませんでした
 98%|█████████▊| 6565/6700 [36:38<00:49,  2.74it/s]レース情報を取得できませんでした
レース情報を取得できませんでした
 98%|█████████▊| 6567/6700 [36:39<00:42,  3.11it/s]レース情報を取得できませんでした
レース情報を取得できませんでした
100%|██████████| 6700/6700 [37:25<00:00,  2.98it/s]

プロセスは終了コード 0 で終了しました

プログラム作成時にハマったところ

レースが中止になった場合のデータを取得するとプログラムが異常終了

中止になったレースの情報も情報としてサイトに掲載されています。そのレースをプログラムでデータ取得した際、空のデータとして取得することになります。空のデータをpandasで処理しようとすると、exceptionが発生しました。

基本的なことですが、今後は、異常処理が発生することを想定してプログラムを作成するようにします。

CSVファイルにデータを保存しようとしたときにエンコードでエラー

取得したデータをCSVファイルに保存するタイミングで下記のエラーが出ました。

UnicodeEncodeError: 'cp932' codec can't encode character '\xa0' in position 47: illegal multibyte sequence

対処としては、replaceを使って問題となっている文字列を変換しました。

race_info_text = race_info.text.replace(u'\xa0', u' ')
race_info_text = race_info.text.replace(u'\xa5', u' ')

また、CSVファイルに保存する際のオプションで「errors=”ignore”」を追加しました。

# CSVにレース結果を保存する
df_RaceResult.to_csv('./レース結果.csv', encoding='cp932', header=False, index=False, errors="ignore")

次回、試すこと

AIモデルを作成する前に、まずは取得したデータの全体像を把握します。
取得したデータから馬ごと、騎手ごと、開催場所ごと、様々な要素ごとにデータを整理してみます。

コメント

  1. tty より:

    有益な記事ありがとうございます。
    コードを活用させて頂いております。ありがとうございます。
    こちらのコードを実行すると、下記のエラーが出たのでclass名が変わったのかなと思ったのでデベロッパーツールで確認したところ『nk_tb_common race_table_01』というクラスに変わっていました。
    その後、94行目をtable_data = soup.find(class_=’nk_tb_common race_table_01′) と変更して実行してみました。
    しかし、下記のエラーが再度でました。コードの一部を変えてみたりしたのですが、結局下記のエラーが主に出てきました。
    宜しければお助けいただけないでしょうか。
    返信お待ちしております。

    ———————————————————————–
    browser = webdriver.Chrome(ChromeDriverManager().install())
    検索条件を設定します
    検索を行います
    取得したHTMLからレース結果のURLを抽出します
    Traceback (most recent call last):
    File “/Users//Desktop/memo.txt/keiba.py”, line 155, in
    main()
    File “/Users//Desktop/memo.txt/keiba.py”, line 30, in main
    link_list = make_raseURL(browser, link_list)
    File “/Users//Desktop/memo.txt/keiba.py”, line 96, in make_raseURL
    for element in table_data.find_all(‘a’):
    AttributeError: ‘NoneType’ object has no attribute ‘find_all’
    ———————————————————————–

    • sakurater より:

      エラーメッセージを見るに、netkeibaから情報を取得できていないように見受けられます。
      掲載しているプログラムの動作確認をしてみました。
      正常に動作していることを確認しましたので、プログラムの実行環境に影響しているかもしれません。

      プログラムを実行するとchromeが自動インストールされます。
      そのあとにchromeが自動起動して取得処理をseleniumで行います。
      netkeibaの情報取得に時間が掛かっているかもしれません。
      main関数内の下記の箇所を10から30に変更すると、うまくいくかもしれません。

      browser.implicitly_wait(10) → browser.implicitly_wait(30)

      • tty より:

        迅速な返信ありがとうございます。
        教えてくださったコードを書き換えてみたところ、おっしゃる通りnetkeibaから情報を取得できていなかったらしくて正常に実行することが出来ました。
        ありがとうございました!

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