Chapter 09無料公開

【新】ディレクトリ構成と実行コード

競馬予想で始めるデータ分析・機械学習
競馬予想で始めるデータ分析・機械学習
2022.08.17に更新

ソースコードの取得方法

  1. 新verの共同開発リポジトリにアクセスします。(アクセスは書籍購入者限定コミュニティのメンバー限定となっております)
  2. Gitが分かる人はこのリポジトリをクローンして使ってください。分からない方は、緑色の「Code」ボタンから「Download ZIP」を選択し、解凍して使ってください。

ディレクトリ構成

旧バージョンからの一番大きな変更点は、以下のようなディレクトリ構成を作り、ソースコード本体をmodulesに分けて書いていることです。

.
├── main.ipynb                      ・・・実行コード
├── data                            ・・・データを保存
│   ├── html                        ・・・netkeiba.comからスクレイピングしたhtmlを保存
│   │   ├── horse                   ・・・/horse/ページからスクレイピングしたhtmlを保存
│   │   ├── ped                     ・・・/horse/ped/ページからスクレイピングしたhtmlを保存
│   │   └── race                    ・・・/race/ページからスクレイピングしたhtmlを保存
│   ├── master                      ・・・マスタを保存
│   ├── raw                         ・・・rawデータを保存
│   │   ├── results.pickle          ・・・レース結果テーブル(コード実行で生成)
│   │   ├── horse_results.pickle    ・・・馬の過去成績テーブル(コード実行で生成)
│   │   ├── race_info.pickle        ・・・レース情報テーブル(コード実行で生成)
│   │   ├── peds.pickle             ・・・血統テーブル(コード実行で生成)
│   │   ├── return_tables.pickle    ・・・払い戻しテーブル(コード実行で生成)
│   └── tmp                         ・・・一時的なファイルを保存
├── models                          ・・・学習済みモデルを保存
└── modules                         ・・・モジュール(ソースコードが書かれている部分)
    ├── constants                   ・・・定数
    ├── preparing                   ・・・スクレイピング〜rawデータの作成
    ├── preprocessing               ・・・前処理
    ├── training                    ・・・訓練
    ├── policies                    ・・・予測スコアの算出ロジックや、馬券購入戦略
    └── simulation                  ・・・回収率シミュレーション

こうすることで、

  • 実行の際は、シンプルなソースコードが書かれた、main.ipynbを実行するのみでよい
  • 開発・運用が行いやすい

など、様々なメリットがあります。

実行コード

それでは、実行コードであるmain.ipynbを見ていきます。

モジュールインポート

import pandas as pd
import glob
import os
from tqdm.notebook import tqdm

from modules.constants import LocalPaths
from modules.constants import HorseResultsCols
from modules import preparing
from modules import preprocessing
from modules import training
from modules import simulation
from modules import policies
%load_ext autoreload # 補足(※1)

データ取得

レースIDの取得

# 開催日取得。to_の月は含まないので注意。
kaisai_date_2020 = preparing.scrape_kaisai_date(from_="2020-01-01", to_="2021-01-01")
# 開催日からレースIDの取得
race_id_list = preparing.scrape_race_id_list(kaisai_date_2020)

/race/ページからのデータ取得

# db.netkeiba.com/race/のhtmlをスクレイピングして、binファイルとして保存
html_files_race = preparing.scrape_html_race(race_id_list, skip=True)

# レース結果テーブルの作成
results_new = preparing.get_rawdata_results(html_files_race)
# レース情報テーブルの作成
race_info_new = preparing.get_rawdata_info(html_files_race)
# 払い戻しテーブルの作成
return_tables_new = preparing.get_rawdata_return(html_files_race) 

# テーブルの更新。元々のテーブルが存在しない場合は、新たに作成される。
preparing.update_rawdata(filepath=LocalPaths.RAW_RESULTS_PATH, new_df=results_new)
preparing.update_rawdata(filepath=LocalPaths.RAW_RACE_INFO_PATH, new_df=race_info_new)
preparing.update_rawdata(filepath=LocalPaths.RAW_RETURN_TABLES_PATH, new_df=return_tables_new)

/horse/ページからのデータ取得

# スクレイピング対象のhorse_idを取得
horse_id_list = results_new['horse_id'].unique()
# htmlをスクレイピング
# すでにスクレイピングしてある馬をスキップしたい場合はskip=Trueにする
# すでにスクレイピングしてある馬でも、新たに出走した成績を更新したい場合はskip=Falseにする
html_files_horse = preparing.scrape_html_horse_with_master(
    horse_id_list, skip=True
    )

# 馬の過去成績テーブルの作成(補足※2)
horse_results_new = preparing.get_rawdata_horse_results(html_files_horse)
# テーブルの更新。元々のテーブルが存在しない場合は、新たに作成される。
preparing.update_rawdata(LocalPaths.RAW_HORSE_RESULTS_PATH, horse_results_new)

/horse/ped/ページからのデータ取得

# htmlをスクレイピング
html_files_peds = preparing.scrape_html_ped(horse_id_list, skip=True)

# 血統テーブルの作成
peds_new = preparing.get_rawdata_peds(html_files_peds)
# テーブルの更新。元々のテーブルが存在しない場合は、新たに作成される。
preparing.update_rawdata(LocalPaths.RAW_PEDS_PATH, peds_new)

データ加工

前処理

results_processor = preprocessing.ResultsProcessor(
    filepath=LocalPaths.RAW_RESULTS_PATH)
race_info_processor = preprocessing.RaceInfoProcessor(
    filepath=LocalPaths.RAW_RACE_INFO_PATH)
return_processor = preprocessing.ReturnProcessor(
    filepath=LocalPaths.RAW_RETURN_TABLES_PATH)
horse_results_processor = preprocessing.HorseResultsProcessor(
    filepath=LocalPaths.RAW_HORSE_RESULTS_PATH)
peds_processor = preprocessing.PedsProcessor(
    filepath=LocalPaths.RAW_PEDS_PATH)

馬の過去成績を集計しつつ、前処理の済みの全てのテーブルを結合

# 集計する馬の成績を選択
TARGET_COLS = [HorseResultsCols.RANK, HorseResultsCols.PRIZE, HorseResultsCols.RANK_DIFF, 
               'first_corner', 'final_corner',
               'first_to_rank', 'first_to_final','final_to_rank']
# horse_id列と共に、ターゲットエンコーディングの対象にする列(集計のグループ)を選択
GROUP_COLS = ['course_len', 'race_type', HorseResultsCols.PLACE]
data_merger = preprocessing.DataMerger(
        results_processor,
        race_info_processor,
        horse_results_processor,
        peds_processor,
        target_cols=TARGET_COLS,
        group_cols=GROUP_COLS
)
# 処理実行
data_merger.merge()

カテゴリ変数の処理

feature_enginnering = preprocessing.FeatureEngineering(data_merger)\
    .add_interval()\
    .dumminize_ground_state()\
    .dumminize_race_type()\
    .dumminize_sex()\
    .dumminize_weather()\
    .encode_horse_id()\
    .encode_jockey_id()\
    .dumminize_kaisai()\
    .dumminize_around()\
    .dumminize_race_class()

一時保存。20220709は実行時の日付などをつけておくと分かりやすいです。

feature_enginnering.featured_data.to_pickle('data/tmp/featured_data_20220709.pickle')

学習

# モデル作成
keiba_ai = training.KeibaAIFactory.create(feature_enginnering.featured_data)
# パラメータチューニングをして学習
keiba_ai.train_with_tuning()
# 特徴量の重要度を表示
keiba_ai.feature_importance()

(例)

# モデル保存
training.KeibaAIFactory.save(keiba_ai, version_name='model_2018_2021')

models/(実行した日付)/(version_name).pickleに、モデルとデータセットが保存されます。

シミュレーション

シミュレーターに馬券をセット

simulator = simulation.Simulator(return_processor)

シミュレーション実行。

T_RANGE = [0.5, 3.5]
N_SAMPLES = 100

returns = {}
#「馬の勝ちやすさスコア」の閾値を変化させた時の成績を計算
for i in tqdm(range(N_SAMPLES)):
    # T_RANGEの範囲を、N_SAMPLES等分して、thresholdをfor分で回す
    threshold = T_RANGE[1] * i / N_SAMPLES + T_RANGE[0] * (1-(i/N_SAMPLES))
    try:
        # 賭ける馬券を決定
        actions = keiba_ai.decide_action(
                keiba_ai.datasets.X_test, # テストデータ
                policies.StdScorePolicy, #「馬の勝ちやすさ」スコアを決める方針を選択
                policies.BetPolicyTansho, # 賭け方の方針を選択
                threshold=threshold #「馬の勝ちやすさスコア」の閾値
                )
        returns[threshold] = simulator.calc_returns(actions)
    except Exception as e:
        print(e)
        break
returns_df = pd.DataFrame.from_dict(returns, orient='index')
returns_df.index.name = 'threshold'

シミュレーション結果も、models/に保存しておくとわかりやすいです。

returns_df.to_pickle('models/20220705/tansho.pickle')

回収率をプロット

simulation.plot_single_threshold(returns_df, N_SAMPLES, label='tansho')

実際に賭ける時の実行コード

例として2022年1月8日に開催されるレースを実際に予想する場合を考えます。

事前準備

出走する馬が発表されたら、馬の過去成績テーブルと血統テーブルを更新します。前日などにやっておくのが良いでしょう。

# レースidを取得
race_id_list = preparing.scrape_race_id_list(['20220108'])
# 出走するhorse_idの取得
horse_id_list = preparing.scrape_horse_id_list(race_id_list)

# horse_resultsテーブルの更新
# 直近レースが更新されている可能性があるので、skip=Falseにして上書きする
html_files_horse = preparing.scrape_html_horse_with_master(horse_id_list, skip=False)
horse_results_20220108 = preparing.get_rawdata_horse_results(html_files_horse)
preparing.update_rawdata(LocalPaths.RAW_HORSE_RESULTS_PATH, horse_results_20220108)

# pedsテーブルの更新
html_files_peds = preparing.scrape_html_ped(horse_id_list, skip=True)
peds_20220108 = preparing.get_rawdata_peds(html_files_peds)
preparing.update_rawdata(LocalPaths.RAW_PEDS_PATH, peds_20220108)

# processorの更新
horse_results_processor = preprocessing.HorseResultsProcessor(
    filepath=LocalPaths.RAW_HORSE_RESULTS_PATH)
peds_processor = preprocessing.PedsProcessor(filepath=LocalPaths.RAW_PEDS_PATH)

レース当日

# 学習済みモデルをロードして準備(学習時の処理から再起動などをしていない場合はスキップ可)
keiba_ai = training.KeibaAIFactory.load('models/(日付)/(version_name).pickle')

レース直前に馬体重が発表されたら、出馬表を取得します。馬体重発表前に実行すると正しく動かないので注意してください。

# 一時的に出馬表を保存するパスを指定
filepath = 'data/tmp/shutuba.pickle'
# 出馬表の取得
preparing.scrape_shutuba_table(race_id_list[0], '2022/1/8', filepath)

学習データと同様の処理を行ってモデルにインプットするデータを作成します。

# 出馬表の加工
shutuba_table_processor = preprocessing.ShutubaTableProcessor(filepath)

# テーブルのマージ
shutuba_data_merger = preprocessing.ShutubaDataMerger(
    shutuba_table_processor,
    horse_results_processor,
    peds_processor,
    target_cols=TARGET_COLS,
    group_cols=GROUP_COLS
)
shutuba_data_merger.merge()

# カテゴリ変数の処理
feature_enginnering_shutuba = preprocessing.FeatureEngineering(shutuba_data_merger)\
    .add_interval()\
    .dumminize_ground_state()\
    .dumminize_race_type()\
    .dumminize_sex()\
    .dumminize_weather()\
    .encode_horse_id()\
    .encode_jockey_id()\
    .dumminize_kaisai()\
    .dumminize_around()\
    .dumminize_race_class()

# 最終的にインプットするデータ
X = feature_enginnering_shutuba.featured_data.drop(['date'], axis=1)

学習済みモデルに入れると、予測スコアが出力されます。

keiba_ai.calc_score(X, policies.StdScorePolicy).sort_values('score', ascending=False)

(例)

補足

・(※1)%load_ext autoreloadは、モジュールを更新した際、notebookに反映させるために使用します。この行を入れておくことで、モジュールを更新した際、

%autoreload

を実行すると、インポートしてあるモジュールの更新がmain.ipynbに反映されるので、main.ipynbの再起動の必要がなくなります。

・(※2)main.ipynbを再起動して変数がリセットされた時などは、以下のコードを実行することで保存してあるhtmlのパスを取得することができます。

# スクレイピングした日付を指定
target_date = '2022-06-25'
# 更新情報マスタの読み込み
update_master = pd.read_csv(
    os.path.join(LocalPaths.MASTER_DIR, 'horse_results_updated_at.csv'),
    dtype=object
    )
# target_dateにスクレイピングしたhorse_idに絞り込む
filter = pd.to_datetime(update_master['updated_at']).dt.strftime('%Y-%m-%d') == target_date
horse_id_list = update_master[filter]['horse_id']
# binファイルのパスを取得
html_files_horse = []
for horse_id in tqdm(horse_id_list):
    file = glob.glob(os.path.join(LocalPaths.HTML_HORSE_DIR, horse_id+'*.bin'))[0]
    html_files_horse.append(file)