競走馬にイロレーティングの概念を取り込んでみた

## はじめに
みなさんイロレーティングってご存じですか？

将棋やサッカーや対戦格闘ゲームなどの1対1の対戦でプレイヤー(またはチーム)の実力を表すために使われる指標です。

これとは別に競走馬には独自のレーティングが用いられていますが、個人的にはイロレーティングのほうが目にする機会も多く、競走馬についてもイロレーティングが導入できれば、競馬の新たな見方が出来たり、個人的に行っている競馬の予測にも利用できるのではないかと考えました。

## 簡易的なイロレーティングの求め方
どうやらイロレーティングというものは、
$\text{R}_A$をAのレーティングとすると、Aの勝率$\text{W}_A$が
$$
\text{W}_A = \frac{1}{1 + 10^{\frac{\text{R}_B - \text{R}_A}{400}}}
$$
となり、Aの対戦結果$\text{r}_A$を
```math
\text{r}_A = 
\begin{cases}
1 & (Aの勝ち) \\
0.5 & (引き分け) \\
0 & (Aの負け)
\end{cases}
```
とすると、この対戦によって増減したレーティングの値$\text{d}_A$は、謎の定数$k$を用いて、
$$
\text{d}_A = k * (\text{r}_A - \text{W}_A)
$$

となり、対戦後のAのレーティング$\text{R}'_A$は、
$$
\text{R}'_A = \text{R}_A + \text{d}_A
$$

となるっぽい。

## 競走馬に拡張する上での問題点
競馬は1対1の対戦ではないため、
$$
\sum\limits_{i} \text{W}_i = 1　(全馬の勝率の和が1)
$$

$$
\sum\limits_{i} \text{r}_i = 1　(全馬の対戦結果の和が1)
$$

$$
\sum\limits_{i} \text{d}_i = 0　(全馬のレーティングの増減値の和が0)
$$

を満たすように、$\text{W}_i$、$\text{r}_i$、$\text{d}_i$を決めるというのが悩みの種。

## こうやって定義してみる
そこで自分なりに工夫してみた結果、$\text{W}_i$、$\text{r}_i$、$\text{d}_i$を次のように定義するとうまくいく気がする。($\text{R}_i$は$i$の対戦前のレーティング)

$$
W_i = \frac{1}{1 + \sum\limits_{j \neq i} 10^{\frac{R_j - R_i}{400}}}
$$

```math
\text{r}_i = 
\begin{cases}
0.53 & (1着) \\
0.21 & (2着) \\
0.13 & (3着) \\
0.08 & (4着) \\
0.05 & (5着) \\
0 & (6着以下)
\end{cases}
```

$$
\text{d}_i = k * (\text{r}_i - \text{W}_i)
$$

$\text{r}_i$は本賞金の総額に占める割合にしてみました。

## 謎の定数 k の取り扱い
$k$を固定にすると、例えば、1勝クラス⇒2勝クラス⇒3勝クラスを連勝した競走馬と、GIを3連勝した競走馬で、レーティングの上昇具合が似たような挙動になってしまう。

そこで、$k$をそのレースの1着賞金を百万で割った値に設定してみました。

## 実装にあたって
ここまでで実装のもとになる情報がそろったので、Pythonで競走馬版イロレーティングを作ってみましょう。

流れは、
A)レース出走によって変動するレーティングの増減値を計算する
B)レースごとに出走全馬のレーティングをAをもとに更新する
といった感じ

## レーティングの増減値の計算
まずは、出走前のレーティング$\text{R}_A$、出走結果$\text{r}_A$、レースによって変動する定数$k$をもとに、出走によって変動するレーティング増減値$\text{d}_A$を返す関数から。
```python
def calculate_ratings_diff(ratings, results, k):

    num_players = len(ratings)
    expected = []
    for i in range(num_players):
        denominator = 0
        for j in range(num_players):
            if j != i:
                denominator += 10 ** ((ratings[j] - ratings[i]) / 400)
        expected_value = 1 / (1 + denominator)
        expected.append(expected_value)

    rating_diffs = []
    for i in range(num_players):
        rating_diff = k * (results[i] - expected[i])
        rating_diffs.append(rating_diff)

    return rating_diffs

```

## レースごとにレーティングを更新する
そして、実際のレース情報をもとにレーティングを更新する部分を作ってみる。
ただし、関数に引き渡すデータフレームdf_dataとdf_race_kは以下の構造になっています。
<table border="1">
    <thead>
        <tr>
            <th>race_id</th>
            <th>race_ymd</th>
            <th>horse_id</th>
            <th>placing_order</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>201501010101</td>
            <td>2015-08-01</td>
            <td>2013102966</td>
            <td>1</td>
        </tr>
        <tr>
            <td>201501010101</td>
            <td>2015-08-01</td>
            <td>2013106468</td>
            <td>2</td>
        </tr>
        <tr>
            <td>201501010101</td>
            <td>2015-08-01</td>
            <td>2013105712</td>
            <td>3</td>
        </tr>
        <tr>
            <td>201501010101</td>
            <td>2015-08-01</td>
            <td>2013106165</td>
            <td>4</td>
        </tr>
        <tr>
            <td>201501010101</td>
            <td>2015-08-01</td>
            <td>2013104911</td>
            <td>5</td>
        </tr>
        <tr>
            <td>201501010101</td>
            <td>2015-08-01</td>
            <td>2013106308</td>
            <td>6</td>
        </tr>
    </tbody>
</table>
<table border="1">
    <thead>
        <tr>
            <th>race_id</th>
            <th>k</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>201501010101</td>
            <td>5.0</td>
        </tr>
        <tr>
            <td>201501010102</td>
            <td>4.6</td>
        </tr>
        <tr>
            <td>201501010103</td>
            <td>4.6</td>
        </tr>
        <tr>
            <td>201501010104</td>
            <td>4.6</td>
        </tr>
        <tr>
            <td>201501010105</td>
            <td>7.0</td>
        </tr>
        <tr>
            <td>201501010106</td>
            <td>4.6</td>
        </tr>
    </tbody>
</table>

次の関数にこの2つのデータフレームを渡せば、レーティングを計算してくれます。
```python
def update_horse_ratings(df_data, df_race_k):

    # 初期レーティング設定
    horse_ratings = {}
    for horse_id in df_data["horse_id"].unique():
        horse_ratings[horse_id] = 1500

    # placing_order に基づく result 値
    placing_to_result = {1: 0.53, 2: 0.21, 3: 0.13, 4: 0.08, 5: 0.05}

    # 競走日順にデータをソート
    df_data = df_data.sort_values(["race_ymd", "race_id"])

    # race_id を race_ymd 順に取得
    sorted_race_ids = df_data.drop_duplicates("race_id").sort_values("race_ymd")["race_id"].tolist()

    # 各競走ごとにレーティング更新
    for race_id in sorted_race_ids:
        race_group = df_data[df_data["race_id"] == race_id]
        race_ymd = race_group["race_ymd"].iloc[0]

        # 現在処理中のレース日とレースIDを出力
        print(f"Processing race_ymd: {race_ymd}, race_id: {race_id}")

        # kを設定
        k = df_race_k.loc[df_race_k['race_id'] == race_id, 'k'].values[0]

        # レースごとの競走馬と順位を取得
        horses = race_group["horse_id"].tolist()
        placings = race_group["placing_order"].tolist()

        # 各競走馬の現在のレーティング
        current_ratings = []
        for horse in horses:
            current_ratings.append(horse_ratings[horse])

        # 順位に基づいて result を計算
        results = []
        for place in placings:
            results.append(placing_to_result.get(place, 0))

        # レーティング変動を計算
        rating_diffs = calculate_ratings_diff(current_ratings, results, k)

        # 各競走馬のレーティングを更新
        for horse, rating_diff in zip(horses, rating_diffs):
            horse_ratings[horse] += rating_diff

    # 最終レーティングをデータフレーム化
    final_ratings_list = []
    for horse, rating in horse_ratings.items():
        final_ratings_list.append({"horse_id": horse, "rating": rating})

    final_ratings = pd.DataFrame(final_ratings_list)

    return final_ratings
```

## 実際にやってみた
2015～2024年のJRAの全競走に出走した競走馬を対象にレーティングを計算してみると、こんな感じ。
(上位20頭のみピックアップし、horse_idは対応した馬名に付け替えています)
<table border="1">
    <thead>
        <tr>
            <th>馬名</th>
            <th>レーティング</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>イクイノックス</td>
            <td>2056.22</td>
        </tr>
        <tr>
            <td>ドウデュース</td>
            <td>2013.88</td>
        </tr>
        <tr>
            <td>アーモンドアイ</td>
            <td>1948.12</td>
        </tr>
        <tr>
            <td>キタサンブラック</td>
            <td>1925.75</td>
        </tr>
        <tr>
            <td>コントレイル</td>
            <td>1881.19</td>
        </tr>
        <tr>
            <td>リスグラシュー</td>
            <td>1838.00</td>
        </tr>
        <tr>
            <td>グランアレグリア</td>
            <td>1834.07</td>
        </tr>
        <tr>
            <td>クロノジェネシス</td>
            <td>1816.88</td>
        </tr>
        <tr>
            <td>ソングライン</td>
            <td>1764.56</td>
        </tr>
        <tr>
            <td>レガレイラ</td>
            <td>1762.95</td>
        </tr>
        <tr>
            <td>リバティアイランド</td>
            <td>1743.82</td>
        </tr>
        <tr>
            <td>スワーヴリチャード</td>
            <td>1740.28</td>
        </tr>
        <tr>
            <td>タイトルホルダー</td>
            <td>1732.65</td>
        </tr>
        <tr>
            <td>レモンポップ</td>
            <td>1725.88</td>
        </tr>
        <tr>
            <td>フィエールマン</td>
            <td>1722.92</td>
        </tr>
        <tr>
            <td>ソウルラッシュ</td>
            <td>1713.34</td>
        </tr>
        <tr>
            <td>シャフリヤール</td>
            <td>1706.19</td>
        </tr>
        <tr>
            <td>ラッキーライラック</td>
            <td>1703.88</td>
        </tr>
        <tr>
            <td>エフフォーリア</td>
            <td>1703.15</td>
        </tr>
        <tr>
            <td>モーリス</td>
            <td>1701.67</td>
        </tr>
    </tbody>
</table>

## 早速金杯でチェック
2025年のレースでどうなるか事後ですが金杯で確認。
中山金杯はう～んな感じでしたが、京都金杯の出走馬のレーティング上位5頭はそれなりにいい感じ。
<table border="1">
    <thead>
        <tr>
            <th>馬名</th>
            <th>レーティング</th>
            <th>結果</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>サクラトゥジュール</td>
            <td>1546.85</td>
            <td><span style="color: red; font-weight: bold;">6番人気1着</span></td>
        </tr>
        <tr>
            <td>シャドウフューリー</td>
            <td>1540.35</td>
            <td>1番人気6着</td>
        </tr>
        <tr>
            <td>ロジリオン</td>
            <td>1532.1</td>
            <td><span style="color: red; font-weight: bold;">2番人気3着</span></td>
        </tr>
        <tr>
            <td>アスクコンナモンダ</td>
            <td>1528.66</td>
            <td>5番人気4着同着</td>
        </tr>
        <tr>
            <td>セオ</td>
            <td>1524.5</td>
            <td>8番人気4着同着</td>
        </tr>
    </tbody>
</table>

## 最後に
レーティングを計算するにあたって、$\text{W}_i$、$\text{r}_i$、$\text{d}_i$、$k$だけでなく、レーティングの初期値、レーティング集計開始日、対象レース、などによって激しく変化するので、ぜひ自分なりのいい感じの定義を見つけて競馬ライフを楽しんでみてください。おまけでイクイノックスとドウデュースの同期2頭のレーティング推移を。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3982189/fa368f53-c87d-8338-699d-bd7d90082de5.png)