3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ChatGPTと作る競馬予想AI (2)

Last updated at Posted at 2025-09-07

プログラミング初心者がChatGPTを使って競馬予想AIを作ることで、生成AIとプログラミングについて学んでいく企画の第2回です。

前回の記事はこちらから

前回は「どのモデルでどんな予測をするか」、「どんな特徴量が必要か」といった課題をChatGPTとクリアにしていきました。
そこでGPT君から以下のように問われました。

ここで質問です:
手元にあるデータは JRA-VANデータベースのような詳細情報でしょうか? それとも netkeibaのレース結果や出走表をスクレイピングしたデータのようなものですか?

👉 それによって「使える特徴量の範囲」と「前処理の難易度」がかなり変わってきます。

したがって今回はデータ収集を行うスクレイピングについて検討していきます。

どこからデータを入手するの?

先ほどGPT君はJRA-VANとnetkeibaのどちらのデータを持っているかと問われました。
しかしそもそもまだデータはありませんし、競馬に疎い自分はどちらのサイトが良いのかもわかりません。
ここは正直に話し、GPT君から意見をもらいましょう。

プロンプト まだデータは手元にないです。 したがってスクレイピングする必要があると考えていましたが、JRA-VANとnetkeiba、どちらがおすすめでしょうか?
回答 なるほど 👍 まだデータが手元にないなら、「どこから取得するか」を最初に決めるのが大事ですね。 それぞれの選択肢のメリット・デメリットを整理してみます。

🔹 JRA-VAN データベース

  • メリット

    • 公認データベースなので 公式かつ高品質
    • 過去の詳細データ(レース結果、ラップ、血統、調教師、騎手、馬場情報など)が揃っている。
    • API もあり、機械学習向けに加工しやすい。
  • デメリット

    • 有料(月額制)
    • 商用利用には制限あり(研究・個人用途ならOKだが、公開サービスは制限される)。

👉 本格的に競馬AIをやるなら JRA-VANを契約してデータを買うのがベスト
特にラップタイムや血統データも必要になる段階で、データ品質がものを言います。


🔹 netkeiba(スクレイピング)

  • メリット

    • 無料でデータが取れる(スクレイピングすれば過去の成績やオッズが入手可能)。
    • 個人の勉強・実験用途なら十分なデータ量。
    • Pythonでスクレイピング用のサンプルコードが多数ネットにある。
  • デメリット

    • スクレイピングはサイト側の規約に抵触する可能性がある。
    • レース情報がHTML形式で埋め込まれているので、 データ整形に時間がかかる
    • ラップや調教データなどの詳細情報は不足気味。

👉 「とりあえず作ってみたい」「無料で試したい」なら netkeibaスクレイピングが現実的。


🔹 おすすめの進め方

  1. ステップ1

    • 無料で手軽に始めたい → netkeibaをスクレイピングして、
      「過去成績 + 騎手成績 + オッズ」で 単勝・複勝確率予測モデルを作る。
  2. ステップ2

    • ある程度モデルが動いて「もっと精度を上げたい」と思ったら → JRA-VAN契約。
    • ラップタイム・血統・調教データを加えてモデル強化。

💡 つまり最初は netkeibaでPoC(実験)、その後 JRA-VANで本格化 が現実的です。


質問ですが、
👉 ユーザーさんは「最初は無料で試してみたい」ですか? それとも「最初から有料でも精度重視で進めたい」ですか?

2つのサイトは「データの質・量」や「有料/無料」といったところに差がありそうです。
ここはおすすめに従い無料のnetkeiba.comというサイトを使っていきましょう。

どうやってスクレイピングをするの?

それでは早速GPT君にスクレイピングの方法を教わります。

プロンプト わかりやすい説明をありがとう! それでは最初は無料で試してみたいので、netkeibaでスクレイピングを行いたいです。 具体的なコードを教えてください。
回答 いいですね! では、まず「netkeibaからレース結果や出走表をスクレイピングして、pandasのDataFrameにまとめる」基本コードを紹介します。

⚠️ 注意

  • スクレイピングはサイトの利用規約に抵触する可能性があります。自己責任で、アクセス頻度を落とす(time.sleep() を入れるなど)配慮が必要です。
  • 公開アプリや商用利用は避け、あくまで個人学習用にしてください。

必要ライブラリ

bash
pip install requests beautifulsoup4 pandas

サンプルコード:レース結果ページのスクレイピング

以下の例では、netkeibaのレース結果ページから 着順・馬名・騎手・オッズ を取得します。

python
import requests
from bs4 import BeautifulSoup
import pandas as pd
import time

def scrape_race(race_id: str):
"""
netkeibaのレース結果ページからデータを取得
race_id: 例 '202101010101' (2021年1月1日 第1競走)
"""
url = f"https://race.netkeiba.com/race/result.html?race_id={race_id}"
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"
}
res = requests.get(url, headers=headers)
res.raise_for_status()

soup = BeautifulSoup(res.text, "html.parser")

# テーブルを取得
table = soup.find("table", class_="RaceTable01")
rows = table.find_all("tr")[1:]  # 1行目はヘッダーなので除外

data = []
for row in rows:
    cols = row.find_all("td")
    if len(cols) < 5:  # データが空の行はスキップ
        continue
    rank = cols[0].get_text(strip=True)   # 着順
    horse = cols[3].get_text(strip=True)  # 馬名
    jockey = cols[6].get_text(strip=True) # 騎手
    odds = cols[7].get_text(strip=True)   # 単勝オッズ
    data.append([rank, horse, jockey, odds])

df = pd.DataFrame(data, columns=["着順", "馬名", "騎手", "単勝オッズ"])
return df

--- 動作確認 ---

race_id = "202101010101" # 例: 2021年1月1日 第1レース
df = scrape_race(race_id)
print(df)

出力イメージ

着順 馬名 騎手 単勝オッズ
0 1 サンプルホース ルメール 2.3
1 2 テストホース 川田 5.6
2 3 データホース 福永 8.9
...

拡張アイデア

  • レースIDをループさせて、1年分まとめて取得する
  • 「出走表ページ」から騎手・枠順・斤量・馬体重などを取得する
  • 「オッズページ」から複勝オッズも取る

👉 最初は「レース結果」だけ集めてみて、学習データとして加工すると進めやすいです。


質問です:
👉 まずは「単一レースの結果を取る」ところから始めたいですか?
それとも「複数レースを一括でスクレイピングしてCSV保存」まで進めたいですか?

必要なライブラリとコードを教えてくれました!
まずは各レースの「着順」「馬名」「騎手」「単勝オッズ」をまとめたテーブルを作るようです。
実装して動かしてみます。

import requests
from bs4 import BeautifulSoup
import pandas as pd
import time

def scrape_race(race_id: str):
    """
    netkeibaのレース結果ページからデータを取得
    race_id: 例 '202101010101' (2021年1月1日 第1競走)
    """
    url = f"https://race.netkeiba.com/race/result.html?race_id={race_id}"
    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"
    }
    res = requests.get(url, headers=headers)
    res.raise_for_status()
    
    soup = BeautifulSoup(res.text, "html.parser")
    
    # テーブルを取得
    table = soup.find("table", class_="RaceTable01")
    rows = table.find_all("tr")[1:]  # 1行目はヘッダーなので除外
    
    data = []
    for row in rows:
        cols = row.find_all("td")
        if len(cols) < 5:  # データが空の行はスキップ
            continue
        rank = cols[0].get_text(strip=True)   # 着順
        horse = cols[3].get_text(strip=True)  # 馬名
        jockey = cols[6].get_text(strip=True) # 騎手
        odds = cols[7].get_text(strip=True)   # 単勝オッズ
        data.append([rank, horse, jockey, odds])
    
    df = pd.DataFrame(data, columns=["着順", "馬名", "騎手", "単勝オッズ"])
    return df

# --- 動作確認 ---
race_id = "202101010101"  # 例: 2021年1月1日 第1レース
df = scrape_race(race_id)
print(df)
着順               馬名       騎手   単勝オッズ
0    1    �ǥ����Х���饤��  ������̭  1:08.7
1    2            �Х�٥륯   ������  1:08.8
2    3         ��ĥ�����     ����  1:08.9
3    4  ����ޥ����ƥ�����    �����  1:09.0
4    5   �ڥ����㥱���ƥ���    �����  1:09.0
5    6           ���ꥷ��      ��˭  1:09.0
6    7          �ǥ��顼��   ������  1:09.2
7    8          �ץ쥸�㥸��    ����Ȼ  1:09.2
8    9          ���ƥ���     ����  1:09.6
9   10      ��ȥ��������   ������  1:09.6
10  11     �ˡ����Х�����  ������ΰ  1:09.7
11  12       �ޥ��ͥ�˥���      ð��  1:09.9
12  13      �롼�����ӥ���   ������  1:09.9
13  14  �������ɥ�������      ��ƣ  1:09.9
14  15         �ߥ��Υ٥��      ɩ��  1:10.2
15  16    ���å���å���å�    ���Ĺ�  1:12.3

文字化けしておりますが(GPT君の)想定通りの出力が出ています!
とりあえず、そのまま実行するだけでも動くコードになっていてびっくり。
文字化けの部分もGPT君に聞いて直していきます。

プロンプト 教えてもらったコードを実行したところ文字化けしてしまいます。これはどうしたら?

着順 馬名 騎手 単勝オッズ
0 1 �ǥ����Х���饤�� ������̭ 1:08.7
1 2 �Х�٥륯 ������ 1:08.8
2 3 ��ĥ����� ���� 1:08.9
3 4 ����ޥ����ƥ����� ����� 1:09.0
4 5 �ڥ����㥱���ƥ��� ����� 1:09.0
5 6 ���ꥷ�� ��˭ 1:09.0
6 7 �ǥ��顼�� ������ 1:09.2
7 8 �ץ쥸�㥸�� ����Ȼ 1:09.2
8 9 ���ƥ��� ���� 1:09.6
9 10 ��ȥ�������� ������ 1:09.6
10 11 �ˡ����Х����� ������ΰ 1:09.7
11 12 �ޥ��ͥ�˥��� ð�� 1:09.9
12 13 �롼�����ӥ��� ������ 1:09.9
13 14 �������ɥ������� ��ƣ 1:09.9
14 15 �ߥ��Υ٥�� ɩ�� 1:10.2
15 16 ���å���å���å� ���Ĺ� 1:12.3

回答 なるほど 👍 文字化けしているのは **レスポンスのエンコーディング** が正しく指定されていないのが原因です。

Python の requests は自動で encoding を推測するのですが、netkeiba のページは Shift_JIS ベースなので、うまく判別できずに「�」が出てしまいます。


✅ 修正方法

scrape_race 関数内で res.encoding を明示的に設定してください。

python
res = requests.get(url, headers=headers)
res.encoding = res.apparent_encoding # ← これを追加

または res.apparent_encoding ではなく res.encoding = "EUC-JP" と固定してもOKです。


修正版コード(文字化け対応)

python
import requests
from bs4 import BeautifulSoup
import pandas as pd

def scrape_race(race_id: str):
url = f"https://race.netkeiba.com/race/result.html?race_id={race_id}"
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"
}
res = requests.get(url, headers=headers)
res.encoding = res.apparent_encoding # ← 文字化け対策

soup = BeautifulSoup(res.text, "html.parser")

table = soup.find("table", class_="RaceTable01")
rows = table.find_all("tr")[1:]

data = []
for row in rows:
    cols = row.find_all("td")
    if len(cols) < 5:
        continue
    rank = cols[0].get_text(strip=True)   # 着順
    horse = cols[3].get_text(strip=True)  # 馬名
    jockey = cols[6].get_text(strip=True) # 騎手
    odds = cols[7].get_text(strip=True)   # 単勝オッズ
    data.append([rank, horse, jockey, odds])

df = pd.DataFrame(data, columns=["着順", "馬名", "騎手", "単勝オッズ"])
return df

動作確認

race_id = "202101010101"
df = scrape_race(race_id)
print(df)


補足

  • res.encoding = res.apparent_encoding → requests が自動判別したエンコーディングを使う。
  • res.encoding = "EUC-JP" → netkeibaはEUC-JPを使っているので、これで強制してもよいです。

👉 これで日本語(馬名・騎手名など)が正しく表示されるはずです。

試してみて、まだ文字化けするようなら EUC-JP に固定する方法も試しますか?

JISをrequestモジュールがうまく識別できていなかったようですね。
GPT君の指示通りに修正して実行した結果がこちら

着順         馬名    騎手   単勝オッズ
0    1  ディーバサンライズ  △秋山稔  1:08.7
1    2      バンベルク   横山和  1:08.8
2    3      リツィタル    大野  1:08.9
3    4  クレマチステソーロ   鮫島駿  1:09.0
4    5  ペイシャケイティー   古川吉  1:09.0
5    6       セリシア    武豊  1:09.0
6    7      デクラーク   横山武  1:09.2
7    8    プレジャジンク   吉田隼  1:09.2
8    9     メルティング    団野  1:09.6
9   10    リトルロータス   △山田  1:09.6
10  11    ニースバローズ  ▲横山琉  1:09.7
11  12    マイネルニゲラ    丹内  1:09.9
12  13   ルーチェビアンカ   荻野琢  1:09.9
13  14  ワンダードゥヴァン    加藤  1:09.9
14  15     ミエノベルル    菱田  1:10.2
15  16  キッズリッカリッカ   岩田康  1:12.3

正しく文字が表示されましたが、実行結果を確認すると「単勝オッズ」の列「タイム」になっていることに気づきます。
実際のnetkeiba.comのページを確認し、なぜこのようなことになるのか突き止めましょう。

GPT君のコードの修正

GPT君が出力したコードを改めて確認すると、以下の流れでスクレイピングを行っているようです。

  1. URL作成:レース結果が記載されているページのURLを作成
  2. サイトアクセス:requestでサイトにアクセス&HTMLを保存
  3. データテーブル取得:beautifulsoupでデータテーブルを取得
  4. 特徴量抽出:テーブル内の特徴量を変数"data"に格納
  5. データ成型:pandasのDataFrameにまとめる
    --- 以上がscrape_race関数の機能 ---
  6. 動作確認:scrape_race関数に指定のレース(race_id)を与え、得られた結果を出力

というか生成AIとコーディングするときは、実行する前にコードは一通り読んで、各行何を行っているかを理解してから実行した方が良いですよね…
革命的に思えたバイブコーディングも、理解せずに雰囲気だけでコードを生成するエンジニアが増えたから批判的な意見があるんでしょうね。
ポエムはさておき、「4.特徴量抽出」で抽出する列が間違っていると予想できます。
オッズが書いてある正しい列はどこか、実際にnetkeiba.comのサイトから確認しましょう。
image.png
プログラムに使われたURLをブラウザに打ち込むと上記のページが表示されます。
単勝オッズは7列目ではなく10列目にありますね。

これまでの内容を修正したコードが以下になります。

def scrape_race(race_id: str):
    """
    netkeibaのレース結果ページからデータを取得
    race_id: 例 '202101010101' (2021年1月1日 第1競走)
    """
    # 1. URL作成:レース結果が記載されているページのURLを作成
    url = f"https://race.netkeiba.com/race/result.html?race_id={race_id}"
    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"
    }
    # 2. サイトアクセス:requestでサイトにアクセス&HTMLを保存
    res = requests.get(url, headers=headers)
    res.encoding = res.apparent_encoding # 文字化け対策
    
    # 3. データテーブル取得:beautifulsoupでデータテーブルを取得
    soup = BeautifulSoup(res.text, "html.parser")
    
    table = soup.find("table", class_="RaceTable01")
    rows = table.find_all("tr")[1:]  # 1行目はヘッダーなので除外
    
    # 4. 特徴量抽出:テーブル内の特徴量を変数"data"に格納
    data = []
    for row in rows:
        cols = row.find_all("td")
        if len(cols) < 5:  # データが空の行はスキップ
            continue
        rank = cols[0].get_text(strip=True)   # 着順
        horse = cols[3].get_text(strip=True)  # 馬名
        jockey = cols[6].get_text(strip=True) # 騎手
        odds = cols[10].get_text(strip=True)   # 単勝オッズ
        data.append([rank, horse, jockey, odds])
    
    # 5. データ成型:pandasのDataFrameにまとめる
    df = pd.DataFrame(data, columns=["着順", "馬名", "騎手", "単勝オッズ"])
    return df

# 6. 動作確認:scrape_race関数に指定のレース(race_id)を与え、得られた結果を出力
race_id = "202101010101"  # 例: 2021年1月1日 第1レース
df = scrape_race(race_id)
print(df)
着順         馬名    騎手  単勝オッズ
0    1  ディーバサンライズ  △秋山稔   13.5
1    2      バンベルク   横山和    3.9
2    3      リツィタル    大野  124.7
3    4  クレマチステソーロ   鮫島駿    7.6
4    5  ペイシャケイティー   古川吉    7.5
5    6       セリシア    武豊    3.7
6    7      デクラーク   横山武    6.1
7    8    プレジャジンク   吉田隼   25.0
8    9     メルティング    団野   56.6
9   10    リトルロータス   △山田  275.3
10  11    ニースバローズ  ▲横山琉  142.9
11  12    マイネルニゲラ    丹内   57.7
12  13   ルーチェビアンカ   荻野琢   56.9
13  14  ワンダードゥヴァン    加藤   95.4
14  15     ミエノベルル    菱田   10.4
15  16  キッズリッカリッカ   岩田康   69.9

目標としていた特徴量が正しく得られるようになりました!
次回はforループを使って長期間のデータを一気にスクレイピングできるようにしていきます。

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?