機械学習を使って地方競馬を予測する

最近競馬を始めたのですが、競馬の世界には統計学やAIを使って競馬予測をしている人たちがいるらしいです。自分も機械学習を使えば予測できるのではと思ったので、どれぐらい当たるのかやってみました！


#目的
複勝の控除率は20%なので、ランダムに馬券を買い続けた場合、回収率は80%に収束するはずです。
[競馬はランダムに買い続けても儲かるのか？ - Qiita](https://qiita.com/gassann/items/5dcfff7fc63904c7e3e5)
実際に試したところ、確かに80%に近い値になりました。
なので、今回のゴールは機械学習を使って回収率80%を超えるAIを作ることです！

#データの取得
まずは過去レースのデータを集めます。中央競馬のスクレイピングコードはあがっているので、今回は地方競馬を対象にしました。地方競馬のデータは以下のサイトをスクレイピングして取得します。
http://www.keiba.go.jp/index.html

上記サイトのスクレイピングコードを公開します。
<a href="https://github.com/naokikinugasa/keiba">
![https://qiita-image-store.s3.amazonaws.com/0/266477/820cf73f-64cf-cd3b-079b-125c21335163.png](https://qiita-image-store.s3.amazonaws.com/0/266477/820cf73f-64cf-cd3b-079b-125c21335163.png)
</a>


コードの詳細や使い方はこちらをご覧ください。
[地方競馬データをスクレイピングで取得する方法 - Qiita](https://qiita.com/gassann/items/4743f1996596da9a4452)

#特徴選択
予測に必要なデータの特徴を選びます。ここで関連の高い特徴を上手く選べると精度が上がります。
とりあえず以下の特徴を使います。精度が低かった場合、主成分分析などを使って使用する特徴を選びなおします。

|特徴|変数名|
|---|---|
|馬体重|weight|
|体重変化量|dhweight|
|前走速度|veloity1|
|前々走速度|veloity2|
|3走前速度|veloity3|
|4走前速度|veloity4|
|過去4走平均速度|avg_velocity|


#前処理
次はこのデータに対して前処理をしていきます。

####欠損値の穴埋め

競馬データの欠損値は大きく二つあります。

- 過去走がない

サイトには5走前まで載っていますが、デビューして間もない馬だと前走がなかったり、3走前までしかなかったりします。

- 出走取消になった

馬は出場前に暴れたり、体調が優れなかったりなどで出走取消になる場合があります。
過去走が4走ない馬、出場取り消しになった馬はNull値を代入したあとに`df.dropna(how="any")`で行ごと削除します。


####標準化
 
標準化とはデータの分布を平均0、分散1になるように変換することです。
なぜ標準化するのかというと、例えば馬の体重450kg、タイム90秒といった場合、体重の方が値が大きいため寄与率が大きくなってしまいます。
標準化することで単位の違いに影響されずにすみます。
変換式は以下のように書けます。

$x' = \frac{x - μ}{σ}$


```python
#dataはDataframe
new_data = (data - data.mean()) / data.std()
```

このようなデータに対して、

```
          weight  dhweight  velocity1  velocity2 
0         470     -1        16.611528  16.564185
1         436     -4        15.658752  15.398208  
2         455      2        16.014924  14.445180 
3         420      1        17.001165  15.941180 
```
標準化をするとこうなります。

```
          weight     dhweight  velocity1  velocity2
0         0.467774  -0.064991   0.700931   0.611528 
1        -0.656654  -0.459089  -0.658752  -0.398208 
2        -0.005594   0.267160  -0.014924  -0.445180
3        -0.918316   0.001165   0.941180  -0.256707
```

#予測
まずはk近傍法(k-NN)を使ってみます。
k-NNはクラスがわかっている学習サンプルと予測をしたいサンプルとの特徴ベクトルの距離を測り、k番目までの近くにあるサンプルで多数決をとり、サンプル数が多い方のクラスを割り当てる単純なアルゴリズムです.
今回は複勝に入ったかどうかを予測したいので、レース結果が1~3位の馬には1をそれ以外には0を教師データとして与えます。

KNeighborsClassifier()のパラメータn_neighborsは付近何番目までを見るかのkを表しています。
以下のコードで精度が最大になるパラメータを調べてみるとk=30付近で最も高い精度がでています。

```python
accuracy = []
k_range = np.arange(1,100)
for k in k_range:
       knn = KNeighborsClassifier(n_neighbors=k) # インスタンス生成。
       knn.fit(train_X, train_y)                 # モデル作成実行
       pred_y = knn.predict(test_X)              # 予測実行
       accuracy.append(metrics.accuracy_score(test_y, pred_y)) # 精度格納
plt.plot(k_range, accuracy)
plt.show()
```

![k-nn.png](https://qiita-image-store.s3.amazonaws.com/0/266477/0671488a-f16c-be8d-ede2-2ef3d8281a2b.png)


予測の際は対象レースのURLを入力します。
何月何日の第3レースを予測した結果はこちらです。
predictが1になっている馬が複勝に入ると予測しています。

```
   horsename  result
0       グラーネ       1
1  ココナッツクッキー       0
2    アポロゴールド       1
3       ヴァルト       1
4   アマトリチャーナ       0
5  ヴァーミリアグラン       0
6    ホッカイノソラ       0
7   コウエイリョウマ       1
8     アルアンデス       1
```



#制度評価
モデルの制度の良さを表す指標には以下のようなものがあります。
適合率(precision)：複勝と予測されたデータのうち、実際に複勝であるものの割合
再現率(recall)：実際の複勝のデータのうち、複勝と予測できたものの割合
F値：適合率と再現率の調和平均
競馬はギャンブルなので、複勝と予測したものがどれぐらい当たっているかが重要です。
なので適合率を評価指標に使います。
`sklearn`の`precision_score('実際の値', '予測値')`を使うと適合率が求められます。

#結果
2018年の園田競馬のデータで予測してみました。
訓練データ：650
予測データ：163
複勝と予測した馬を全て100円分購入した場合

```python

def evaluate(data_csv):
    df = pd.read_csv(data_csv)
    df = df.dropna(how="any")
    train_X, test_X, train_y, test_y = train_test_split(
        df, df.result, test_size=0.2)
    train_X = train_X.loc[:,'velocity1':'dhweight']
    knn = KNeighborsClassifier(n_neighbors=30) # インスタンス生成。
    knn.fit(train_X, train_y)                 # モデル作成実行
    pred_y = knn.predict(test_X.loc[:,'velocity1':'dhweight'])
    sales  = (pred_y * test_X.multipul_wins).sum()
    cost = pred_y.sum() * 100
    profits = sales - cost
    recovery_rate = sales/cost
    print(sales, cost, profits, recovery_rate)
```

||掛け金|払戻金|適合率|回収率|
|---|---|---|---|---|
|k-NN|1770円|1460円|64.7%|82.5%|

#おわりに
回収率が82.5%と80は超えているものの、微妙なラインなのでもっと精度を高めます。
・主成分分析で特徴選択
・他のモデルで試す
・データ数を増やす
・買い方を変える
これらを試した結果を次回の記事で書いていきます。