競馬のタイムをスコア化してみた

競馬のタイムを見ても、これって速いの？遅いの？ってところが、なんとなくわかりにくいですよね。競馬歴10年以上の自分が見ても、なんとなくこれぐらいかな・・しかわかってないです。

そこで、~~競馬の収支を上げるべく~~プログラムを使って、競馬の走破タイムをスコア化し、速い遅いを判断してみようと思います。

## 過去データの取得

まず、速いか遅いかを判断するために、標準タイムを計算する必要がありそうです。
標準タイムは過去5年分ぐらいのデータを取得して、平均タイムを出すことで標準タイムとしてみたいと思います。
レースのクラスによってタイムにバラツキはありますが、細かいことは置いておきます。

ということで、過去データを netkeiba さんからスクレイピングして抽出していきます。

:::note warn
注意！
**スクレイピングは、サイトの利用規約に違反する可能性があります。**

netkeiba さんの利用規約を見る限りは、サービスの運営に支障をきたさない範囲であれば問題ないようですが、自己責任でお願いします。

（参考：[netkeiba さんの利用規約](https://support.keiba.netkeiba.com/hc/ja/articles/18841959592857-%E3%83%87%E3%83%BC%E3%82%BF%E3%83%99%E3%83%BC%E3%82%B9%E3%81%AE%E9%96%B2%E8%A6%A7%E3%81%8C%E3%81%A7%E3%81%8D%E3%81%AA%E3%81%84-%E9%80%9A%E4%BF%A1%E5%88%B6%E9%99%90%E3%81%8C%E3%81%8B%E3%81%8B%E3%81%A3%E3%81%9F-%E3%82%B9%E3%82%AF%E3%83%AC%E3%82%A4%E3%83%93%E3%83%B3%E3%82%B0%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6)）
:::

## 過去データのレース条件

レースの条件は幅広いので、できるだけ同条件で絞っていったほうが信頼性ありそうです。
検索条件は、下記で絞ってみたいと思います。

```text: レース検索条件
（固定）
期間: 過去5年、馬場: 良馬場

（変動）引数に設定
競馬場、種別（芝orダート）、距離
```

netkeibaさんで、過去レース一覧を閲覧したときのURLは下記のものになるので、このURLをいじればいけそうです。

```text: 過去レース一覧を閲覧するURL
# 条件: 「東京競馬場」、「芝」、「良馬場」、「1600m」、「2019年1月以降のレース」
https://db.netkeiba.com//?pid=race_list&word=&track%5B0%5D=1&start_year=2019&start_mon=1&end_year=none&end_mon=none&jyo%5B0%5D=05&baba%5B0%5D=1&kyori_min=&kyori_max=&kyori%5B0%5D=1600&sort=date%24list%3D200&page=1
```

## dartでスクレイピング

なんで、dartやねん！！・・・と思われたかもしれませんが、私が普段Flutterを使っているためなので、ご容赦ください。

```text: 使用パッケージ
http
html
euc
```

```dart: タイム一覧を取得
final url = 'https://db.netkeiba.com/?pid=race_list&track%5B%5D=$track&start_year=2019&start_mon=1&end_year=2024&end_mon=12&baba%5B%5D=1&jyo%5B%5D=$jyo&kyori%5B%5D=$kyori&sort=date&list=100&page=1';

// netkeibaにアクセスします
final response = await http.get(Uri.parse(url));

// 日本語が文字化けしてるので、EUC-JPでデコードします
String decodedHtml = EucJP().decode(response.bodyBytes);

// プログラム内で使いやすいように、HTML解析します
var document = parser.parse(decodedHtml);

// テーブル要素を取得します（nk_tb_commonクラスが指定してあるテーブル）
final table = document.querySelector('.nk_tb_common');

final List<String> times = [];

// 一行のデータを取得します
final rows = table.querySelectorAll('tr');

for (int i = 0; i < rows.length; i++) {
  // 一行のデータを列に分解します
  final raceData = rows[i].querySelectorAll('td');
  
  if (raceData.length == 0) continue;

  // タイムがある列（index: 9）からタイムを取得します
  times.add(raceData[9].text.trim());
}

print(times);
// 出力例: [1:35.8, 1:34.7, 1:35.0, 1:34.8, ...]
```

過去のレースのタイム一覧が取得できましたね！
ただ、このままだと使いにくいので、すべて秒数に変換します。

```dart: 秒数に変換
final List<double> seconds = times
    .map(
      (time) {
        final split = time.split(':');
        return double.parse(split[0]) * 60 + double.parse(split[1]);
      }
    )
    .toList();

print(seconds);
// 出力例: [95.8, 94.7, 95.0, 94.8, ...]
```


## 平均タイムと標準偏差を計算

平均タイムは、取得したタイム一覧の合計をレース数で割ることで求められます。
また、「1200mのコースの1秒差」と「3000mのコースの1秒差」の重みが違うので、標準偏差をとることで、それを考慮していきます。

統計のことはよく理解してないですが、なんとなくでやってみましょう。

```dart: 平均タイムと標準偏差の計算
// 平均値を求める
double average(List<double> list) {
  return list.reduce((a, b) => a + b) / list.length;
}

// 標準偏差を求める
double standartDeviation(List<double> list) {
  final avg = average(list);
  final variance = average(list.map((e) => (e - avg) * (e - avg)).toList());
  return sqrt(variance);
}
```

## スコア化

平均タイムと標準偏差が計算できたら、最後に調べたいタイムをスコア化していきます。
どうスコア化したらよいかは難しいところですが、下記のように決めてみました。

- 平均タイムと同じタイム: 50点
- 平均タイムから標準偏差2個分速い: 100点
- 平均タイムから標準偏差2個分遅い: 0点

標準偏差2個分あれば、95%のデータが収まる範囲になります。
標準偏差2個分以上離れたら、100点を超えたりマイナスだったりしますが、とりあえずヨシ！とします。

```dart: スコア化
// スコアを計算する
double calcScore(double time, double average, double standartDeviation) {
  final double diffScoreFromAverage = (average - time) / standartDeviation;

  // diffScoreFromAverageが-2〜2の間に収まることを想定して計算する（95％でこの間に収まる）
  return (diffScoreFromAverage + 2) / 4 * 100;
}
```

## 実行例

これでプログラムは完成です。

早速試してみます。今年（2024年）の日本ダービーのタイムが **2:24.3** だったので、このタイムのスコアを出してみます。

```shell: 実行例
# 引数は、競馬場・競走種別・距離・調べたいタイムの順番です。
# 競馬場: 01:札幌, 02:函館, 03:福島, 04:新潟, 05:東京, 06:中山, 07:中京, 08:京都,　09:阪神, 10:小倉
# 競走種別: 1: 芝, 2: ダート, 3: 障害
$ dart main.dart 05 1 2400 2:24.3

jyo: 東京, track: 芝, kyori: 2400, time: 2:24.3
スコア: 71.60285718968473

```

71点なので平均タイムよりは速いですが、G1にしてはなんとなく物足りないなーっていう印象ですね。展開がスローだったことを踏まえれば、まあいいのかな。

## まとめ

細かいところを言い出すと、タイムだけではなくいろんな要素があるんですが、あくまでざっくりとした判断材料として使えるんじゃないかなと思います。

本記事における、馬券の当たり外れについては一切責任を負いませんので、ご了承ください。