メインコンテンツへスキップ
見出し画像

【保存版】完成した競馬AIの使い方を徹底解説〜データ収集編〜【#38】

    これまで作成方法を1から解説してきた競馬AIですが、先日ついにデータ取得からAIによる自動投票まで、本格的に運用できるシステムが完成しました。

    今回は、完成した競馬AIシステムの使い方に焦点を当てて、実際にソースコードを動かす方法を詳しく解説していきます。

    ソースコードのダウンロード方法については、記事下部の「ソースコードのダウンロード」の章で案内しているので、記事を読んで興味を持った方は是非使ってみてください。

    この記事で解説するのは学習時のデータ収集の部分、具体的にはnetkeiba.comから「スクレイピング」という技術を使ってデータを集めてくる工程です。

    スクレイピングとは、Webサイトからデータを自動収集する技術のことです。人間がブラウザでWebページを見て手動でデータをコピー&ペーストする作業を、プログラムが自動で行ってくれるイメージです。

    これを利用することで、過去数年分に渡る膨大な競馬データについて、これから紹介するコードをそのまま実行するだけで自動取得することができます。


    ここからは実際に、具体的な使い方をみていきましょう。
    今回の内容は、以下の動画でも解説しているので併せてご覧いただくと理解が深まると思います。

    1. 取得するデータの全体像

    ここからの内容は、動画の0:58〜でも解説しています。

    データ収集の実行コードは、ダウンロードしたソースコード中のcommon/src/main.ipynbに全てまとまっているので、このnotebookを上から順に実行していきます。

    ▼common/src/main.ipynb

    # まずは必要なライブラリをインポート
    import pandas as pd
    import scraping
    import create_rawdf
    import create_prediction_population
    %load_ext autoreload

    先に全体像を見ておくと、以下のようなデータの流れで必要なデータを作成していきます。(クリックして拡大できます)。

    画像

    図に書かれているように、最終的にはdata/rawdf/配下に8種類のCSVファイルが生成されます。

    1. results.csv - レース結果テーブル(メインデータ)

    2. race_info.csv - レース情報テーブル(天気、距離、グレード等)

    3. return_tables.csv - 払い戻しテーブル(シミュレーション用)

    4. horse_results.csv - 馬の過去成績テーブル

    5. peds.csv - 血統テーブル

    6. jockey_leading.csv - 騎手リーディングテーブル

    7. trainer_leading.csv - 調教師リーディングテーブル

    8. sire_leading.csv - 種牡馬リーディングテーブル

    一番メインとなるのは以下のraceページから取得するレース結果テーブルです。

    画像
    https://db.netkeiba.com/race/202306010101/ 

    このテーブルには各レースの着順、出走する馬のhorse_id、騎手のjockey_idなどの情報が入っています。
    このテーブルに存在する馬や騎手に対して「馬の過去成績テーブル」「騎手リーディングテーブル」などを取得して紐付け、予測材料としてAIに学習させることで「どの馬がどれくらい勝ちやすいのか」を予測できるようになる、という仕組みになります。

    2. raceページからのデータ取得

    ここからの内容は、動画の3:44〜でも解説しています。

    まずは全体像のうち、以下の部分のデータ作成について見ていきましょう。

    画像

    まずは一時保存ディレクトリを定義します。

    ▼common/src/main.ipynb

    # 一時保存ディレクトリ
    TMP_DIR = scraping.DATA_DIR / "tmp"

    このディレクトリには、処理の途中で作成される開催日リストやレースIDリストが保存されます。

    2.1. race_id一覧の特定

    最初のステップは、どのレースのデータを取得するかを決めることです。
    netkeiba.comの「カレンダーページ」から開催日一覧を取得し、そこから各レースのIDを特定していきます。

    画像
    詳細データフロー

    ▼common/src/main.ipynb

    # 開催日一覧の取得
    kaisai_date_list = scraping.scrape_kaisai_date(
        from_="2024-01", to_="2024-02", save_dir=TMP_DIR
    )

    この`scrape_kaisai_date()`関数は、カレンダーページから指定した期間内でレースが開催された日付を一覧で取得し、`kaisai_date_list`として返します

    画像
    netkeiba.comのカレンダーページの例
    https://race.netkeiba.com/top/calendar.html?year=2024&month=1 

    kaisai_date_listには、例えば以下のような開催日のリストが返ってきます:

    ['20240106', '20240107', '20240108', '20240113', '20240114', ...]

    次に、取得した`kaisai_date_list`を使って、各開催日のレース一覧ページから全レースのIDを取得します:

    ▼common/src/main.ipynb

    # スクレイピング対象レースのid取得
    race_id_list = scraping.scrape_race_id_list(
        kaisai_date_list, save_dir=TMP_DIR
    )

    この`scrape_race_id_list()`関数は、各開催日のレース一覧ページ(図中の「各開催日のレース一覧ページ」)からレースIDが格納されているリンクを一覧で取得し、`race_id_list`として返します。

    画像
    各開催日のレース一覧ページの例
    https://race.netkeiba.com/top/race_list.html?kaisai_date=20240106 

    race_id_listには、例えば以下のようなリストが返ってきます。

    ['202401060101', '202401060102', '202401060103', ...]

    2.2. HTMLデータの収集

    次に、取得した`race_id_list`を使って、各レースページのHTMLデータを収集していきます。

    ▼common/src/main.ipynb

    # raceページのhtmlをスクレイピング
    html_paths_race = scraping.scrape_html_race(
        race_id_list=race_id_list, skip=False
    )

    この`scrape_html_race()`関数は、図に示されているように、以下の処理を行います。

    1. 各レースIDに対応するnetkeiba.comのレースページ(`netkeiba.com/race/{race_id}`にアクセス)

    2. ページ全体のHTMLデータを取得

    3. `data/html/race/{race_id}.bin`というバイナリファイルに保存

    画像
    再掲

    実際にフォルダを確認すると、`data/html/race/`の中に以下のようなファイルが作成されます:

    202401060101.bin
    202401060102.bin
    202401060103.bin
    ...

    これらのファイルには、レース結果のテーブルや払い戻し情報など、そのレースページに含まれる全ての情報が保存されています。

    【補足】
    ノートブックの再起動などで変数リセットされた場合は、tmpファイルから読み込むこともできます。

    ▼common/src/main.ipynb

    # race_id_listが失われた場合はファイルから読み込む
    if "race_id_list" not in locals():
        with open(TMP_DIR / "race_id_list.txt", "r") as f:
            race_id_list = f.read().splitlines()

    2.3. DataFrameの作成

    スクレイピングが完了したら、そこから必要な情報を抽出してテーブル形式に整理していきます。raceページからは3種類のテーブルを作成し、最終的に`data/rawdf/`フォルダに保存します。

    ■レース結果テーブル(results.csv)の作成
    `create_results()`関数でレース結果テーブルを作成します。

    ▼common/src/main.ipynb

    results = create_rawdf.create_results(html_path_list=html_paths_race)
    画像
    resultsの中身(例)

    このテーブルには、各レースの着順、馬名、騎手、タイム、オッズなどの基本的な競走結果が格納され、`data/rawdf/results.csv`として保存されます。

    ■レース情報テーブル(race_info.csv)の作成
    次に、
    `create_race_info()`関数でレース情報テーブルを作成します。

    ▼common/src/main.ipynb

    race_info = create_rawdf.create_race_info(html_path_list=html_paths_race)

    このテーブルには、レースの基本情報(距離、天気、グレード等)が格納され、`race_info.csv`として保存されます。

    画像
    race_infoの中身(例)

    ■払い戻しテーブル(return_tables.csv)の作成
    最後に、`create_return_tables()`関数で払い戻しテーブルを作成します。

    ▼common/src/main.ipynb

    return_tables = create_rawdf.create_return_tables(html_path_list=html_paths_race)

    このテーブルには、単勝、複勝、馬連、三連単などの払い戻し情報が格納され、`return_tables.csv`として保存されます。後でシミュレーションを行う際に、「この予測で馬券を買っていたらどれくらいの収益が出たか」を計算するために使用します。

    画像
    return_tablesの中身(例)

    これで、raceページからのデータ取得が完了し、全てのテーブルは自動的に`data/rawdf/`フォルダにCSVファイルとして保存されます。

    3. horseページからのデータ取得

    ここからの内容は、動画の13:13〜でも解説しています。

    次は、レース結果テーブルに含まれるhorse_idについて、horseページからのデータを取得していきます。

    画像
    画像
    horseページの例
    https://db.netkeiba.com/horse/2020103575/ 

    詳細なデータの流れは以下のようになります。

    画像

    3.1. 馬の過去成績テーブルの取得

    まずは、取得したい馬のhorse_idリストを作成しましょう。

    ▼common/src/main.ipynb

    horse_id_list = results["horse_id"].unique()
    len(horse_id_list)

    次に、各馬の過去成績ページをスクレイピングしていきます。

    ▼common/src/main.ipynb

    html_paths_horse = scraping.scrape_html_horse(
        horse_id_list=horse_id_list, skip=False
    )

    この`scrape_html_horse()`関数は、図中に示されているように、各馬の過去成績ページ(`db.netkeiba.com/horse/result/{horse_id}`)からHTMLを取得し、`data/html/horse/result/{horse_id}.bin`として保存します。

    画像
    https://db.netkeiba.com/horse/result/2020103575/ 

    【補足】
    2025年8月頭あたりにnetkeiba.comの仕様変更があり、https://db.netkeiba.com/horse/{horse_id}から直接テーブルを取得できなくなったため、horse/result/{horse_id}から取得する形にアップデートしています。
    これによって、保存先も `data/html/horse/{horse_id}.bin` → `data/html/horse/result/{horse_id}.bin` に変更しています。

    3.2. 処理が途中で止まった時の対処法

    ここからの内容は、動画の15:28〜でも解説しています。

    馬の過去成績データの取得は、場合によっては数万件の馬のページにアクセスする必要があるため、処理時間が長くなりがちです。また、ネットワークエラーなどで処理が途中で止まってしまうことがよくあるので、このような場合の処理について解説しておきます。

    プログレスバーを見ると「何番目までは取得できているか」が分かるので、例えば22個目で止まってしまったことが分かった場合、

    ▼common/src/main.ipynb

    # 21個まではスクレイピングが完了しているので、20番目から再開
    html_paths_horse = scraping.scrape_html_horse(
        horse_id_list=horse_id_list[21:], skip=False
    )

    リストのスライス(`[21:]`)を使って、途中から再開できます。念のため19や20番目から再開しても良いでしょう。

    【処理時間について】
    スクレイピング処理では、サーバーの負荷を減らすために、一つのページをスクレイピングするごとに3秒ずつ待機時間を入れています。複数年分のデータを取得する場合はかなり時間がかかってしまいますが、netkeiba.comのサーバーに負荷をかけないよう、気長に待ちましょう。

    HTMLファイルの収集が完了したら、過去成績テーブルを作成します。

    ▼common/src/main.ipynb

    # 馬の過去成績テーブルの作成
    horse_results = create_rawdf.create_horse_results(html_paths_horse)

    このテーブルには各馬の過去の出走記録が時系列で格納され、`horse_results.csv`として保存されます。

    画像
    horse_resultsの中身(例)

    3.3. 血統テーブルの取得

    次に、血統データ(図の右側)を取得していきます。

    画像
    再掲

    ▼common/src/main.ipynb

    html_paths_ped = scraping.scrape_html_ped(horse_id_list=horse_id_list)

    この処理も馬の過去成績と同様に、各馬の血統ページ(`db.netkeiba.com/horse/ped/{horse_id}`)からHTMLを取得し、`data/html/horse/ped/{horse_id}.bin`として保存します。

    HTMLファイルの収集が完了したら、血統テーブルを作成します。

    ▼common/src/main.ipynb

    # 血統テーブルの作成
    peds = create_rawdf.create_peds(html_paths_ped)

    このテーブルには、各馬の血統情報が5代前までの祖先馬のIDとして格納されます。血統IDは競馬予想の重要な特徴量の一つとなります。

    画像
    pedsの中身(例)

    これで、horseページからのデータ取得が完了し、最終的に馬の過去成績テーブルは`horse_results.csv`、血統テーブルは`peds.csv`として`data/rawdf/`フォルダに保存されます。

    4. リーディングページからのデータ取得

    ここからの内容は、動画の21:37〜でも解説しています。

    最後に、騎手・調教師・種牡馬のリーディング(成績)データを取得していきます。

    画像

    このステップでは、各リーディングページから年別・ページ別にデータを収集します。

    画像
    騎手リーディングページの例
    画像


    4.1. 各リーディングデータの取得

    まずは騎手リーディングデータを取得していきます。

    ▼common/src/main.ipynb

    for year in [2018, 2019, 2020, 2021, 2022, 2023, 2024]:
        _ = scraping.scrape_html_leading(
            leading_type="jockey",
            year=year,
            pages=[1, 2, 3, 4],
        )
        
    # 2017年のみ5ページ目まである
    _ = scraping.scrape_html_leading(
        leading_type="jockey",
        year=2017,
        pages=[1, 2, 3, 4, 5],
    )

    この処理では、各年の騎手リーディングページ(例:https://db.netkeiba.com/jockey/jockey_leading_jra.html?year=2024)から、1〜4ページ(2017年のみ5ページ)のデータを取得します。

    HTMLファイルの収集が完了したら、騎手リーディングテーブルを作成します。

    ▼common/src/main.ipynb

    # スクレイピングしたhtmlファイルのパスを取得
    html_paths_jockey_leading = list(
        (scraping.HTML_LEADING_DIR / "jockey").glob("*.bin")
    )
    # rawデータの作成
    jockey_leading = create_rawdf.create_jockey_leading(html_paths_jockey_leading)

    このテーブルには、各騎手の年間成績(勝利数、勝率、収得賞金等)が格納され、`jockey_leading.csv`として保存されます。

    画像

    調教師、種牡馬リーディングデータについても、同様に取得します。

    ▼common/src/main.ipynb

    for year in [2017, 2019, 2020, 2021, 2022, 2023, 2024]:
        _ = scraping.scrape_html_leading(
            leading_type="trainer",
            year=year,
            pages=[1, 2, 3, 4, 5],
        )
        
    # 2018年のみ6ページ目まである
    _ = scraping.scrape_html_leading(
        leading_type="trainer",
        year=2018,
        pages=[1, 2, 3, 4, 5, 6],
    )
    # スクレイピングしたhtmlファイルのパスを取得
    html_paths_trainer_leading = list(
        (scraping.HTML_LEADING_DIR / "trainer").glob("*.bin")
    )
    # rawデータの作成
    trainer_leading = create_rawdf.create_trainer_leading(html_paths_trainer_leading)

    このテーブルは`trainer_leading.csv`として保存されます。

    ▼common/src/main.ipynb

    for year in range(2017, 2025):
        _ = scraping.scrape_html_leading(
            leading_type="sire",
            year=year,
            pages=range(1, 10),
        )
    # スクレイピングしたhtmlファイルのパスを取得
    html_paths_sire_leading = list(
        (scraping.HTML_LEADING_DIR / "sire").glob("*.bin")
    )
    
    # rawデータの作成
    sire_leading = create_rawdf.create_sire_leading(html_paths_sire_leading)

    このテーブルは`sire_leading.csv`として保存されます。

    4.2. 取得するリーディングデータの期間について

    現在のバージョンでは、予測したい年の前年のリーディングデータを使用しています。例えば、

    • 2025年のレースを予想する場合 → 2024年のリーディングデータを使用

    • 2024年のレースを予想する場合 → 2023年のリーディングデータを使用

    リーディングデータは年ごとにまとまっているので、もし2024年1月の予測に2024年のデータを使ってしまうと、入手タイミングによっては「未来のデータ」を学習に使ってしまうことになるためです。(このようなミスを「リーク」と言います)

    また、今回のコードでは2017年から2024年までのデータを取得しているので、2018年から2025年のレース予想に対応できます。もしより古い年度のデータでの学習や、より長期間のデータが必要な場合は、取得年度の範囲を調整してください。

    5. データの更新方法

    ここからの内容は、動画の25:48〜でも解説しています。

    5.1. 新しい期間のデータ追加方法

    一度データを取得した後、新しい期間のデータを追加したい場合の手順を解説します。

    例えば、2024年1〜2月のデータを取得済みで、3〜6月のデータを追加したい場合、以下のように追加分の期間を指定します。

    ▼common/src/main.ipynb

    # 新しい期間の開催日一覧を取得
    kaisai_date_list_new = scraping.scrape_kaisai_date(
        from_="2024-03", to_="2024-06", save_dir=TMP_DIR
    )
    
    # race_id一覧を取得
    race_id_list_new = scraping.scrape_race_id_list(
        kaisai_date_list_new, save_dir=TMP_DIR
    )

    その後は、先ほどと同じようにHTMLの取得からテーブル作成まで実行することで、最終的に出力されるrawdf/xx.csvに、自動的にデータが追加されていくようになっています。

    またこの際、例えばhores_results.csvであればhorse_id単位で重複チェックが行われ、既に存在するレースIDのデータは上書きされる仕組みになっています。
    簡単な追加イメージを示すと、例えば以下のようなテーブルが元々あったとして、

    📁 horse_results.csv
    ┌──────────┬─────────┬───────┬─────────┐
    │ horse_id │   日付  │ レース名 │  着順   │
    ├──────────┼─────────┼───────┼─────────┤
    │   馬A    │ 2024/02 │ 東京3R  │    3    │
    │   馬A    │ 2024/01 │ 中山1R  │    1    │
    │   馬B    │ 2024/01 │ 阪神2R  │    2    │
    └──────────┴─────────┴───────┴─────────┘

    馬A(既存の馬の成績更新)と馬C(新規)のデータを新たに追加する場合、

    📁 horse_results.csv(更新後)
    ┌──────────┬─────────┬───────┬─────────┐
    │ horse_id │   日付  │ レース名 │  着順   │
    ├──────────┼─────────┼───────┼─────────┤
    │   馬B    │ 2024/01 │ 阪神2R  │    2    │← 保持
    │   馬A    │ 2024/04 │ 新潟1R  │    2    │← 🆕追加
    │   馬A    │ 2024/02 │ 東京3R  │    3    │← 上書き  
    │   馬A    │ 2024/01 │ 中山1R  │    1    │← 上書き
    │   馬C    │ 2024/03 │ 小倉2R  │    1    │← 🆕新規
    └──────────┴─────────┴───────┴─────────┘

    のように追加される仕組みになっています。

    5.2. skipパラメータの使い分け

    基本的に、一度スクレイピングしたページは一度.binファイルの形で保存しておけば再スクレイピングの必要性がないので、data/html/ディレクトリ内を探して、すでに存在するidについては処理をスキップする機能が入っています。

    ただし、馬の過去成績テーブルについては、同一ページに最新成績が追加されていく仕様のため、データ更新の際には対象horse_idを全て最新化できるよう、skip=Falseを指定することでスキップ機能を発動しない設定にできるように作っています。

    画像
    馬の過去成績テーブル(最新の競走成績が上に追加されていく)
    html_paths_horse = scraping.scrape_html_horse(
        horse_id_list=horse_id_list, skip=False
    )

    以上で、学習時のデータ収集の全工程が完了です。取得した8種類のCSVファイルを使って、次回は機械学習モデルに最終的にインプットするデータを作成する工程に進む予定です。

    6. ソースコードのダウンロード

    ソースコードは以下の記事に最新版が添付されているので、そこからダウンロードすることができます。

    上記記事には、今回の記事で紹介したソースコード含め、データ収集・機械学習モデル構築・サーバー上での自動投票など、本格的な競馬AIの運用に必要な全工程のコードが含まれています。

    また、ソースコードを1から作成する方法を以下のマガジンで詳しく解説しているので、興味がある方はぜひ覗いてみてください。


     
     
    競馬予想AIを作ることを通して、機械学習・データサイエンスの勉強になるコンテンツの発信や、筆者が行った実験の共有などを行っていきます。

    あなたへのおすすめ