よっしーの私的空間

機械学習を中心に興味のあることについて更新します

競馬のレーティングをしてみた(Elo Rating)

前回はGlicko2 Rating Systemを使用してレーティングを行いましたが、今回はElo Ratingを使用してみました。今回は競走馬だけではなくジョッキーやトレイナーについてもレーティングを行っています。Elo Ratingは通常チェス等の1対1の対戦において使用されるレーティングシステムですが、今回はそれを複数人対戦用に拡張したライブラリを使用してみました。英語ですが解説はこちらです。

1.レーティング結果

2015年以降の全レース結果を対象にイロレーティングを適用しました。結果は以下の通りでした。上位5位までを掲載します。6位以下についても気になる人がいるようであれば開示します。
f:id:t-yoshi-book:20220301005110p:plain

2.ソースコード

from multielo import MultiElo
import pickle
import pandas as pd
pd.set_option('display.max_columns', 100)
import numpy as np
import datetime
from tqdm.notebook import tqdm as tqdm

# 事前に取得した競馬レースデータをロードする。競馬データの取得は以下を参考にした。
# http://houdoukyokucho.com/2020/08/31/post-1617/
with open('df.pickle', 'rb') as f:
    df = pickle.load(f)

# ジョッキーの名前から減量記号を削除
df['jockey_name'] = df['jockey_name'].str.replace("▲","").replace("△","").replace("☆","").replace("★","").replace("◇","")
# 中止レース等を除外
df = df[(df["rank"]!="中止") & (df["rank"]!="除外") & (df["rank"]!="取消") & (df["rank"]!="失格")]
# レートを格納する列を追加
df["horse_rate"]=np.nan
df["jockey_rate"]=np.nan
df["trainer_rate"]=np.nan

# レーティング結果を格納するデータフレームを作成
def createRatingTable(df, colName):
    df = df[[colName]].copy()
    df = df.drop_duplicates()
    df["rating"] = 1500
    return df

df_horse_rate = createRatingTable(df, "horse_name")
df_jockey_rate = createRatingTable(df, "jockey_name")
df_trainer_rate = createRatingTable(df, "trainer")

# レーティング処理を高速化するためにPandasをリストに変換
horse_rates = df_horse_rate.values.tolist()
jockey_rates = df_jockey_rate.values.tolist()
trainer_rates = df_trainer_rate.values.tolist()
race_datas = df.values.tolist()

# 繰り返し計算用に辞書定義
list_dict = {
    "horse_name":horse_rates,
    "jockey_name":jockey_rates,
    "trainer":trainer_rates
}
rating_col_dict = {
    "horse_name":"horse_rate",
    "jockey_name":"jockey_rate",
    "trainer":"trainer_rate"
}

# レース単位に繰り返し計算を行う
elo = MultiElo(k_value=32, d_value=400, score_function_base=2)
finished = []

for i,race_data in enumerate(tqdm(race_datas)):
    target_race_id = race_data[12]
    if target_race_id not in finished:
        finished.append(target_race_id)
        single_race = [rd for rd in race_datas if rd[12]==target_race_id]
        ranks = [int(horse[15]) for horse in single_race]
        
        # 馬・ジョッキー・トレイナーのレートを計算
        for rating_target in ["horse_name","jockey_name","trainer"]:
            # 馬・ジョッキー・トレイナーの名前のリストを作成
            name_col_no = df.columns.get_loc(rating_target)
            names = [single_record[name_col_no] for single_record in single_race]
            # 最新レートを格納するリストを取得
            rating_list=list_dict[rating_target]
            # レートを格納する列番号を取得
            rate_col_no = df.columns.get_loc(rating_col_dict[rating_target])
            
            # 将来的に機械学習等を行うときの説明変数とするために、レース前のレートを記録しておく
            players=[]
            for j, name in enumerate(names):
                # 過去レートを取得
                rate = [rate for rate in rating_list if rate[0]==name][0][1]
                players.append(rate)
                # レースデータのリストに登録
                race_datas[i+j][rate_col_no] = rate
            
            # レートを計算する
            new_player_rates  = elo.get_new_ratings(players, ranks)
            
            # 最新のレート情報を記録
            for name, new_player_rate in zip(names,new_player_rates):
                for j,rate in enumerate(rating_list):
                    if rate[0] == name:
                        rating_list[j][1]=new_player_rate
                        break

# 見やすいようにDataFrameに戻す
def createDataFrame(rated_list, original_df):
    columns=original_df.columns
    df = pd.DataFrame(rated_list, columns=columns)
    return df

df = createDataFrame(race_datas, df)
df_horse_rate = createDataFrame(horse_rates, df_horse_rate)
df_jockey_rate = createDataFrame(jockey_rates, df_jockey_rate)
df_trainer_rate = createDataFrame(trainer_rates, df_trainer_rate)

# 結果表示
display(df_horse_rate.sort_values("rating",ascending=False).head(5))
display(df_jockey_rate.sort_values("rating",ascending=False).head(5))
display(df_trainer_rate.sort_values("rating",ascending=False).head(5))