stockedge.jpの技術メモ

http://stockedge.jp/の中の人が書いてる技術メモ

競馬の解析をガチでやったら回収率が100%を超えた件

f:id:stockedge:20160111142317j:plain
記事のタイトル通り、競馬で回収率100%を超える方法を見つけたので、その報告をする。
ちなみに、この記事では核心部分はぼかして書いてあるため、読み進めたとしても「競馬で回収率100%を超える方法」が具体的に何なのかを知ることはできない。(私は本当に有効な手法を何もメリットが無いのに公開するほどお人好しではないので)
本当に有効な手法を見つけたいのであれば、あなた自身がデータと向き合う以外の道は無い。
ただし、大まかな仕組み(あと多少のヒントも)だけは書いておくので、もしあなたが独力でデータ解析を行おうという気概のある人物なのであれば、この記事はあなたの助けとなるだろう。


ちなみに、これは前回の記事の続きなので、読んでない方はこちらからどうぞ。
stockedge.hatenablog.com

オッズの歪みを探す

さて、前回からの続きである。
前回の記事のブコメで「回収率を上げたいならオッズの歪みを探したほうが良い」という指摘を複数いただいた。私も同意見なので、その方法で儲けられるかためしてみることにしよう。

穴馬バイアス

実は経済学の文献の中では、競馬のオッズには「穴馬バイアス」と呼ばれる歪みが存在することが知られている。これは、馬券購入者達は穴馬(オッズが高い馬)を過剰に好むので相対的に穴馬の馬券が割高になり、逆に本命馬(オッズの低い馬)の馬券は割安になる、というアノマリーである。このアノマリーは世界各地の競馬で恒常的に観測されており、統計的にその存在が確かめられているものである。(参考:「穴馬への過剰な選好 (longshot bias)」に関するサーベイ


まずは、netkeiba.comから引っ張ってきたデータを使って実際に穴馬バイアスが存在するのかどうか確かめてみよう。
ちなみにnetkeiba.comのデータは以下のスクリプトを使えばダウンロードできる。
GitHub - stockedge/netkeiba-scraper: netkeiba.comをスクレイピングして、競馬予測の素性を作成する。

まず最初に、比較対象として何も考えず全ての馬の単勝馬券を均一に購入した場合の回収率を見てみる。

sqlite> .headers on
sqlite> .mode column
sqlite> select
   ...>   count(*) as 買い目数,
   ...>   avg(case when order_of_finish = 1 then
   ...>         odds
   ...>       else
   ...>         0
   ...>       end) * 100.0 as 回収率
   ...> from
   ...>   (select * from race_result limit 100000)
   ...> where
   ...>   -- 出走取消や競走除外を取り除く
   ...>   cast(order_of_finish as int) <> 0;
買い目数    回収率
----------  ----------------
99076       73.5393031612094

回収率は約73%となっている。*1

次に、レース毎の馬の人気度別回収率を調べてみた。結果が以下である。

sqlite> select
   ...>   popularity as 人気,
   ...>   count(*) as 買い目数,
   ...>   avg(case when order_of_finish = 1 then
   ...>         odds
   ...>       else
   ...>         0
   ...>       end) * 100.0 as 回収率
   ...> from
   ...>   (select * from race_result limit 100000)
   ...> where
   ...>   cast(order_of_finish as int) <> 0
   ...> group by
   ...>   popularity;
人気        買い目数    回収率
----------  ----------  ----------------
1           7016        76.1929874572406
2           7025        80.7274021352312
3           7024        84.6597380410024
4           7003        79.850064258175
5           7009        84.5398773006135
6           7007        92.1749678892536
7           7001        78.2573918011712
8           6952        69.7827963176065
9           6828        75.9138840070299
10          6601        73.9160733222239
11          6285        77.7438345266507
12          5835        69.9194515852614
13          5206        76.0583941605839
14          4581        42.2877101069635
15          3769        26.869196073229
16          2827        35.9851432614078
17          627         85.7735247208931
18          480         20.25

人気度上位の回収率はほとんどが73%を上回っている。そして人気度が下に行くほどに回収率も下がっている。やはり日本の競馬にも穴馬バイアスが存在しているようだ。

馬齢による歪み

他にも、競馬ファンの間では「若い馬の方が儲かる」というアノマリーが知られているようだ。
競馬は若い馬の方が儲かる。馬の年齢と回収率・的中率の関連性 | ブエナの競馬ブログ〜馬券で負けないための知識
これも同様にnetkeiba.comのデータで確認してみよう。

sqlite> select
   ...>   age as 馬齢,
   ...>   count(*) as 買い目数,
   ...>   avg(case when order_of_finish = 1 then
   ...>         odds
   ...>       else
   ...>         0
   ...>       end) * 100.0 as 回収率
   ...> from
   ...>   (select * from race_result limit 100000)
   ...> where
   ...>   cast(order_of_finish as int) <> 0
   ...> group by
   ...>   age;
馬齢        買い目数    回収率
----------  ----------  ----------------
2           13675       72.4409506398537
3           43586       70.9782957830496
4           18167       76.9940001100897
5           12412       85.0217531421206
6           6787        57.6926477088552
7           3140        68.7261146496815
8           1007        121.072492552135
9           246         72.479674796748
10          42          0.0
11          8           40.0
12          5           44.0
13          1           0.0

5歳が回収率のピークで、そこから馬齢が高くなるほど回収率が悪くなる傾向が確認できる。


まぁ、こんな感じでデータとにらめっこしていけば、回収率と関係がある変数を見つけていくことができるわけである。

「おいしさ指数」を計算するモデルを組み立てる

人気度だけあるいは馬齢だけを単独で考慮しても、確かに回収率が高くはなるものの、100%を超えることは無い。回収率100%を超えるには、これだけでは足りないのだ。
ではどうするか。
人気度や馬齢など、回収率が上がりそうな複数の変数を総合的に考慮してオッズの割安度を評価するしかない。
(この辺りの話は卍氏の本にも詳しく書かれているので一読をおすすめする)
本記事ではオッズの歪みの度合いを、卍氏の本にならい「おいしさ指数」と呼ぶことにする。おいしさ指数が高いとは、オッズの割安度が大きいことを意味する。
おいしさ指数を計算する統計モデルを作成するために、私は三つの段階を踏んだ。

  1. 使う素性を決める
  2. おいしさ指数を計算するためのモデルを決めて、そのモデルのパラメータを学習データから推定する
  3. 2のモデルを使って計算したおいしさ指数がある閾値以上のときだけ馬券を買うとどうなるかをテストデータを使ってシミュレーションする

これらについて、上から順に解説していく。ここからは少しぼかした書き方になってしまうがご容赦いただきたい。

素性に関して

気を付けるべきなのは、着順の予測に使える素性と、オッズの歪みを見つけるのに使える素性は違うということだ(とはいえ全くの別物というわけではなく共通部分もある。このことについては後記)。オッズの歪みを捉えるためには、競馬をよく知ることも大事だが、むしろ人間の心理学的バイアスに詳しくなる必要がある。
ひとつだけヒントを言っておくと「前走は○○だった」(○○には何らかの「特殊な出来事」が入る)というパターンの素性には使えるものが多い。
例えば、前走でハナ差やクビ差で負けている馬の回収率は高い。着差がハナ差やクビ差の場合、勝った馬と負けた馬の実力はほぼ同じだったはずである。しかし人間は勝ち負けという結果を重視してしまうため、負けた方の馬が過小評価されることになる。

モデルとパラメータ推定に関して

今回はモデルのパラメータ推定には教師あり学習が使えないので、別の方法を使う必要がある。
なぜなら、教師あり学習は教師となるデータがなければ使うことができないからだ。
今回予測したいのは「オッズに歪みがあるかどうか」であり、そしてオッズが歪んでいるかどうかは、レースが終わった後でさえわからない。*2
(前回のように「レースの着順」を予測するのであれば、レースが終われば教師となるデータが得られるので、教師あり学習を使うことができるのだけど)
オッズに歪みがあるかどうかを調べるには、何らかの「馬券購入ルール」を想定し、その通りに馬券を買うと回収率が高くなるということを確認しなければならない。そうすることで、初めて歪みの存在を認識することができる。
つまり、個々のデータに対してではなく、ルールに対してしか報酬(という良し悪しの尺度)を与えることができないわけである。
このような状況では、教師あり学習ではなく、強化学習や遺伝的アルゴリズムを使って学習する必要がある。
私は実数値遺伝的アルゴリズムを使ってモデルのパラメータ推定を行っていた。
まず最初は、少し複雑なモデルのパラメータを実数値遺伝的アルゴリズム*3を使って最適化させていた。学習データのサンプル数は307364。遺伝的アルゴリズムの適応度は回収率の高さとした。
しかし、この方法はうまく行かなかった。学習データでは100%を超える高い回収率を出せるのだが、テストデータでは回収率が100%を下回ってしまっていた。要は過学習していたわけである。
遺伝的アルゴリズム過学習を回避するテクニック*4をいくつか試してみたけど上手く行かなかったので、思い切って予測モデルを非常に単純なものに変えてみたところ、テストデータでの回収率が100%を超えるようになった。月並みな結論だが、試すなら極力単純な方法から試していった方がいいということだ。

テストデータでのシミュレーションに関して

私はテストデータ(サンプル数は10000)を使って、おいしさ指数がある閾値以上になったときだけ馬券を買うと回収率がどう変わるのかシミュレーションを行った。結果が以下である。

おいしさ指数: 30以上, 買い目数: 10000, 的中率:  7%, 回収率: 73.4%, 平均オッズ:65.02
おいしさ指数: 35以上, 買い目数:  9998, 的中率:  7%, 回収率: 73.4%, 平均オッズ:64.95
おいしさ指数: 40以上, 買い目数:  9993, 的中率:  7%, 回収率: 73.4%, 平均オッズ:64.86
おいしさ指数: 45以上, 買い目数:  9975, 的中率:  7%, 回収率: 73.5%, 平均オッズ:64.60
おいしさ指数: 50以上, 買い目数:  9947, 的中率:  7%, 回収率: 73.7%, 平均オッズ:64.12
おいしさ指数: 55以上, 買い目数:  9878, 的中率:  7%, 回収率: 73.9%, 平均オッズ:63.18
おいしさ指数: 60以上, 買い目数:  9783, 的中率:  7%, 回収率: 74.5%, 平均オッズ:62.42
おいしさ指数: 65以上, 買い目数:  9601, 的中率:  7%, 回収率: 75.8%, 平均オッズ:60.63
おいしさ指数: 70以上, 買い目数:  9314, 的中率:  7%, 回収率: 76.0%, 平均オッズ:58.20
おいしさ指数: 75以上, 買い目数:  8947, 的中率:  7%, 回収率: 76.6%, 平均オッズ:55.36
おいしさ指数: 80以上, 買い目数:  8392, 的中率:  8%, 回収率: 78.3%, 平均オッズ:51.96
おいしさ指数: 85以上, 買い目数:  7786, 的中率:  8%, 回収率: 80.8%, 平均オッズ:48.12
おいしさ指数: 90以上, 買い目数:  7070, 的中率:  8%, 回収率: 79.8%, 平均オッズ:45.26
おいしさ指数: 95以上, 買い目数:  6224, 的中率:  9%, 回収率: 78.1%, 平均オッズ:40.44
おいしさ指数:100以上, 買い目数:  5214, 的中率:  9%, 回収率: 78.3%, 平均オッズ:35.63
おいしさ指数:105以上, 買い目数:  4234, 的中率: 10%, 回収率: 76.9%, 平均オッズ:32.17
おいしさ指数:110以上, 買い目数:  3372, 的中率: 11%, 回収率: 83.6%, 平均オッズ:28.69
おいしさ指数:115以上, 買い目数:  2525, 的中率: 11%, 回収率: 84.0%, 平均オッズ:25.84
おいしさ指数:120以上, 買い目数:  1783, 的中率: 11%, 回収率: 88.1%, 平均オッズ:23.04
おいしさ指数:125以上, 買い目数:  1211, 的中率: 11%, 回収率: 94.5%, 平均オッズ:20.21
おいしさ指数:130以上, 買い目数:   758, 的中率: 13%, 回収率: 97.0%, 平均オッズ:17.56
おいしさ指数:135以上, 買い目数:   453, 的中率: 14%, 回収率:113.2%, 平均オッズ:15.25
おいしさ指数:140以上, 買い目数:   224, 的中率: 15%, 回収率:133.8%, 平均オッズ:13.67
おいしさ指数:145以上, 買い目数:   108, 的中率: 19%, 回収率:185.5%, 平均オッズ:13.77
おいしさ指数:150以上, 買い目数:    43, 的中率: 19%, 回収率:181.9%, 平均オッズ:14.27
おいしさ指数:155以上, 買い目数:    17, 的中率: 29%, 回収率:274.1%, 平均オッズ:14.15
おいしさ指数:160以上, 買い目数:     4, 的中率: 75%, 回収率:680.0%, 平均オッズ: 9.68

おいしさ指数が135を上回った時点で、回収率が100%を超えている。
注目してもらいたいのは、回収率が上昇するのに比例して的中率も上昇している点だ。
つまり競馬で回収率を高めるためには、やはり的中率の高さも重要だということだ。
競馬ファンには「的中率は重要ではない。本当に重要なのは回収率だ」という考えの人が多く見受けられるように思う。私も本当に重要なのは回収率だということは否定しないが、このシミュレーション結果を見る限りでは、的中率も同時に高めていかなければ回収率を高めることは難しいのではないか、と私は思う。
ちなみにおいしさ指数に反比例するように平均オッズも下がっているので、おいしさ指数が高い買い目の中には本命馬が多く含まれているようだ。この結果は穴馬バイアスと整合的である。

まとめ

というわけで、競馬の解析をガチでやったら回収率100%を超える予測モデルを作ることができた。一番重要なのは素性に何を使うかである。自身で競馬の解析をやってみようと思う方は私の記事を参考にして欲しい。
ちなみに、はずれ馬券裁判で話題になった卍氏が作成したモデルの性能がこの本に書いてあるのだけれど、私のモデルの方が少し負けている(私のモデルの方が買い目の数が少ない)ので、まだ伸びしろがあるようだ。なので、一月中はこのモデルをブラッシュアップすることに費やす予定。
その後は、何らかの形で予測を公開していこうかなと思ってます。
お楽しみに~。

*1:単勝馬券の控除率は20%なので、回収率は80%になるはずなのだが、実際に計算すると73%という低い値が出てしまう。おそらくこの辺の話が関係しているのだと思うのだが… 追記:id:pre21さんからご指摘を頂きました

*2:厳密に言うと、一物一価の法則が成立しているかどうかを見ることで、オッズが歪んでいるかを確認することができる。一物一価の不成立については「穴馬への過剰な選好 (longshot bias)」に関するサーベイを見よ

*3:実際に使っていたのは、実数値遺伝的アルゴリズムの改良版であるCMA-ESというアルゴリズム。Rにcmaesというパッケージがあるので簡単に使える

*4:これに書かれているselection periodを使う方法とか