はじめに
学生時代にほんの少し競馬をかじったことがあるのですが、競馬の着順予測は考えるべき要素が多く、自分の頭の中でそれらの要素を考慮して予測するのは難しいと感じたので、1年足らずで止めてしまいました。ですが、「StanとRでベイズ統計モデリング」10.2節の棋士の「強さ」を推定する手法を知り、この手法を活用すれば、着順予測をシステマティックに行えるのではないかと考えるようになりました。そこで、以下では着順予測に考慮すべき要素の一つである競走馬の「強さ」を統計モデリングにより推定します。
使用データ
以下のようなレース結果のシミュレーションデータを使用します(1438頭、290レース分)*1。このデータは、後述の統計モデルから生成したものです。
> d <- read.csv(file='input/sim-data.csv',header = TRUE) > d[1:20,c("RaceID","OoA","horseID")] RaceID OoA horseID 1 1 1 625 2 1 2 525 3 1 3 628 4 1 4 343 5 1 5 110 6 1 6 341 7 1 7 527 8 1 8 4 9 1 9 136 10 1 10 6 11 1 11 3 12 1 12 629 13 1 13 624 14 1 14 626 15 1 15 627 16 2 1 495 17 2 2 492 18 2 3 115 19 2 4 496 20 2 5 262
ここで、列名の意味は以下の通りです。
- RaceID:レースのインデックス
- OoA:着順
- horseID:競走馬のインデックス
統計モデル
競馬の着順が決まるメカニズムを以下のように仮定します。
- 各競走馬には固有の「強さ」が存在する(は競走馬のインデックス)。
- レースで発揮できる「強さ」(=パフォーマンス)にはバラつきが存在し、このパフォーマンスの大小でレースの着順が決まる。
パフォーマンスが平均・標準偏差の正規分布に従うものとし、 これらの仮定を統計モデルで表現すると以下のようになります。
ここで、はレースのインデックス、は各レースの着順(を反転したもの)のインデックス、は各レースの競走馬数、は競走馬数のレース数、は競走馬数を表します。また、は各レース・各競走馬のパフォーマンス、は競走馬のインデックス(競走馬数のレースにおいて着だった競走馬)を表します。最終行は、競走馬の「強さ」とそのバラつきを一意に推定できるように事前分布を設定しています*2。上記の統計モデルに基づき、レース結果のデータから、を推定します。
上記の統計モデルのstanでの実装例は以下の通りです。なお、このコードは「StanとRでベイズ統計モデリング」10.2節を参考にしています(というよりほとんど同じです)。レース毎に競走馬数が変わるため、以下では愚直に競走馬数ごとに配列を作成しています。もっとスマートな書き方をご存じの方がいらっしゃれば、ご指摘いただけるとありがたいです。。。
data { int N; // num of horses int G[18]; // num of races int<lower=1, upper=N> LW7[G[7],7]; // order of arrival of each race (7th:LW7[,1],6th:LW[,2],...1st:LW7[,7]) int<lower=1, upper=N> LW8[G[8],8]; int<lower=1, upper=N> LW9[G[9],9]; int<lower=1, upper=N> LW10[G[10],10]; int<lower=1, upper=N> LW11[G[11],11]; int<lower=1, upper=N> LW12[G[12],12]; int<lower=1, upper=N> LW13[G[13],13]; int<lower=1, upper=N> LW14[G[14],14]; int<lower=1, upper=N> LW15[G[15],15]; int<lower=1, upper=N> LW16[G[16],16]; int<lower=1, upper=N> LW17[G[17],17]; int<lower=1, upper=N> LW18[G[18],18]; } parameters { ordered[7] performance7[G[7]]; ordered[8] performance8[G[8]]; ordered[9] performance9[G[9]]; ordered[10] performance10[G[10]]; ordered[11] performance11[G[11]]; ordered[12] performance12[G[12]]; ordered[13] performance13[G[13]]; ordered[14] performance14[G[14]]; ordered[15] performance15[G[15]]; ordered[16] performance16[G[16]]; ordered[17] performance17[G[17]]; ordered[18] performance18[G[18]]; vector[N] mu; real<lower=0> s_mu; vector<lower=0>[N] s_pf; } model { for (r in 2:18){ for (g in 1:G[r]){ for (i in 1:r){ if (r==7) performance7[g,i] ~ normal(mu[LW7[g,i]], s_pf[LW7[g,i]]); else if (r==8) performance8[g,i] ~ normal(mu[LW8[g,i]], s_pf[LW8[g,i]]); else if (r==9) performance9[g,i] ~ normal(mu[LW9[g,i]], s_pf[LW9[g,i]]); else if (r==10) performance10[g,i] ~ normal(mu[LW10[g,i]], s_pf[LW10[g,i]]); else if (r==11) performance11[g,i] ~ normal(mu[LW11[g,i]], s_pf[LW11[g,i]]); else if (r==12) performance12[g,i] ~ normal(mu[LW12[g,i]], s_pf[LW12[g,i]]); else if (r==13) performance13[g,i] ~ normal(mu[LW13[g,i]], s_pf[LW13[g,i]]); else if (r==14) performance14[g,i] ~ normal(mu[LW14[g,i]], s_pf[LW14[g,i]]); else if (r==15) performance15[g,i] ~ normal(mu[LW15[g,i]], s_pf[LW15[g,i]]); else if (r==16) performance16[g,i] ~ normal(mu[LW16[g,i]], s_pf[LW16[g,i]]); else if (r==17) performance17[g,i] ~ normal(mu[LW17[g,i]], s_pf[LW17[g,i]]); else if (r==18) performance18[g,i] ~ normal(mu[LW18[g,i]], s_pf[LW18[g,i]]); } } } mu ~ normal(0, s_mu); s_pf ~ gamma(10, 10); }
推定結果
推定された馬の「強さ」(の中央値)上位5頭と、実際の馬の「強さ」上位5頭を以下に示します。 horseIDが616と1324の馬は、実際の上位5頭と推定された上位5頭の両方に含まれていることから、ある程度適切に推定できています。 一方で、horseIDが1080, 955, 1246の馬は、実際の上位5頭には含まれているにも関わらず、推定された上位5頭には含まれていません。
> # 推定された上位5頭 > d_top5[,c("horseID","mu_p50")] horseID mu_p50 956 956 2.352053 616 616 2.287300 1324 1324 2.197555 142 142 2.196556 118 118 2.171983 > # 実際の上位5頭 > d2 <- dplyr::distinct(d,horseID,.keep_all = TRUE) > d2[order(-d2$mu)[1:5],c("horseID","mu")] horseID mu 457 616 3.890667 1063 1324 3.179124 1042 1080 2.996332 1107 955 2.806721 1003 1246 2.773523
これは、horseIDが1080, 955, 1246の馬のデータが少ないことが原因と考えられます。実際、これらの馬の出走レース数は1レースしかありません(下表のn_race参照)。
> # 実際の上位5頭のレース数 > d %>% + dplyr::select(horseID,RaceID,OoA) %>% + dplyr::filter(horseID == 616 | horseID == 1324 | horseID == 1080 | horseID == 955 | horseID == 1246) %>% + dplyr::group_by(horseID) %>% + dplyr::summarise(n_race = n()) # A tibble: 5 x 2 horseID n_race <int> <int> 1 616 4 2 955 1 3 1080 1 4 1246 1 5 1324 3
なお、horseIDが1080, 955, 1246の馬の「強さ」の90%ベイズ信用区間を算出すると、データ数が多い馬(horseID:616,1324)に比べて信用区間が広く、 データが少ないことに起因する推定値の不確かさを評価できていることが確認できます。
> # 実際の上位5頭の「強さ」の推定値 > d_qua %>% + dplyr::filter(horseID == 616 | horseID == 1324 | horseID == 1080 | horseID == 955 | horseID == 1246) %>% + dplyr::mutate(BayesConf_90 = mu_p95 - mu_p05) horseID mu_p05 mu_p50 mu_p95 BayesConf_90 1 616 1.2021932 2.287300 3.343293 2.141100 2 955 -0.4223376 1.045755 2.420237 2.842574 3 1080 -0.2466671 1.358909 2.843493 3.090160 4 1246 -0.3242929 1.123751 2.506408 2.830701 5 1324 1.0066156 2.197555 3.477100 2.470484
まとめ
競走馬の「強さ」によって着順が決まる統計モデルを作成し、シミュレーションデータで競走馬の「強さ」とその不確かさを推定できることを確認しました。ただし、今回作成したモデルは、騎手・脚質・枠順等、実際のレースで着順に影響を与える要素を考慮していないので、まだまだ改善の余地があると考えています。
更新履歴
- 2019年11月25日
- 馬を表す英単語のスペルが誤っていたため修正しました(誤:horce→正:horse)
- コードをGitHubにUPしましたhttps://github.com/walkingsdbg/horseracing
- 2019年12月03日
- 数式中の一部変数名を変更しました(→)