ENGINEER BLOG

ENGINEER BLOG

身の回りのデータで機械学習してみた

こんにちは。イノベーション本部の樋口です。

今回は身近にあるデータをスクレイピングで収集して、機械学習を行ってみたのでご紹介します。

前提

今回は競馬のデータを収集してレースの着順を予想してみました。

競馬の予測といっても色々あると思うので、今回はシンプルに上位(1~3着)、中位(4~9着)、下位(10着以下)の3クラスに分けて、マルチクラス分類を行います。

環境

  • Python 3.6.9
  • Google Colaboratory

手順

  1. レースデータを収集
  2. データの前処理
  3. LightGBM で学習を行いモデルを作成
  4. モデルの評価、チューニング
  5. レースを予測し結果を比較する

データ収集

今回は、 netkeiba.comさんより2020年4月~2020年8月までのレースデータをスクレイピングしました。

スクレイピングの詳しい方法については今回は省略します。
スクレイピング方法についてはこちらのサイトを参考にしました。

PythonとBeautiful Soupでスクレイピング

※スクレイピングを行なう際は、利用規約等で禁止しているサイトもあるため事前に確認しておきましょう。

  • スクレイピングしたデータはこんな感じです。

    pic_01

  • 特徴量

    列名 説明
    popularity 人気
    win 単勝
    impost 斤量
    post_position 枠番
    horse 馬名
    horse_num 馬番
    Jockey 騎手
    course_len コースの長さ
    weather 天気
    race_type 芝orダート
    ground_state グラウンドの状態
    sex
    age 年齢
    weight 体重
    weight_change 体重増減
    rank 着順

「rank(着順)」を目的変数、それ以外の情報を説明変数として学習させ、着順を求めていきます。

データの前処理

着順の処理

前提で述べたように、着順を上位=0、中位=1、下位=2になるように変換していきます。

numpyのdigitize()関数を使います。

import numpy as np

# 4未満、4以上10未満、10以上のグループに分ける用の範囲を決めます。
bins = np.array([4, 10])

# rank(着順)を渡して、複数の値をまとめて判定します。
data['rank'] = np.digitize(data['rank'], bins)

上手く分かれているか確認

data['rank'].value_counts()

```
2    11640  #下位
1     9843  #中位
0     5946  #上位
Name: rank, dtype: int64
```

カテゴリカル変数の処理

基本カテゴリ変数はそのまま使わず数値に置き換えます。
今回は単純にラベルエンコードを行います。

  • ラベルエンコーディング
    • 与えられたカテゴリ変数に数字を割り当てる手法

今回モデル作成に用いるLightGBMというアルゴリズムは決定木ベースなので、正規化などの前処理が必要ないと言われています。(参考:決定木は本当に変換に依存しないのか?

from sklearn.preprocessing import LabelEncoder

# カテゴリカルなカラムの名前
categorical_col=["horse","jockey","weather","race_type","ground_state","age","sex"]

# LabelEncoderのインスタンスを生成
le = LabelEncoder()
for c in categorical_col:
  le = le.fit(data[c])
  # カテゴリカルデータを整数に変換
  data[c] = le.transform(data[c])

モデル作成

今回はLightGBMを用いてモデルを作成します。

  • LightGBMとは
    • 決定木アルゴリズムに基づいた勾配ブースティング(Gradient Boosting)の機械学習フレームワーク
    • Kaggle等で注目度の高い機械学習手法

まず、学習用データを訓練用データとテストデータに分割します。

from sklearn.model_selection import train_test_split

# 訓練用データとテストデータに分割
train_set, test_set = train_test_split(sample, test_size=0.2, random_state=0)

# 訓練データを説明変数と目的変数に分割
X_train = train_set.drop('rank', axis=1)
y_train = train_set['rank']

# テストデータを説明変数と目的変数に分割
X_test = test_set.drop('rank', axis=1)
y_test = test_set['rank']

train()へパラメータ、訓練データ、テストデータを渡してモデル訓練を開始

import lightgbm as lgb

lgb_train = lgb.Dataset(X_train, y_train)
lgb_eval = lgb.Dataset(X_test, y_test, reference=lgb_train)

params = {
        'task': 'train',
        'boosting_type': 'gbdt',
        'objective': 'multiclassova',
        'num_class': 3,
        'metric': {'multi_error'}
}

model = lgb.train(params,
        train_set=lgb_train, # 訓練データの指定
        valid_sets=lgb_eval, # 検証データの指定
        verbose_eval=10
)

モデル評価

モデル作成時に設定した検証データを使ってモデルの精度を計算してくれるのでそちらを見ます。

# 学習経過
```
[10]    valid_0's multi_error: 0.435654
[20]    valid_0's multi_error: 0.437842
[30]    valid_0's multi_error: 0.436566
[40]    valid_0's multi_error: 0.438753
[50]    valid_0's multi_error: 0.4393
[60]    valid_0's multi_error: 0.436566
[70]    valid_0's multi_error: 0.434925
[80]    valid_0's multi_error: 0.434743
[90]    valid_0's multi_error: 0.435472
[100]   valid_0's multi_error: 0.438206
```

モデルの精度はだいたい正答率57%位でした。

もうちょっと精度を上げたいので、パラメータチューニングをしていきます。
ハイパーパラメーターを自動でチューニングしてくれるoptunaをつかいます。

LightGBMでoptunaを使いたいときは、lightgbmをimportするのではなく、optunaを通してimportします。
後は同じように実行すると自動でパラメータをチューニングしてくれます。

# import lightgbm as lgb
# ↓

from optuna.integration import lightgbm as lgb

model.paramsでチューニングしてくれたパラメータが見れます。

model.params

```
{'bagging_fraction': 1.0,
 'bagging_freq': 0,
 'boosting_type': 'gbdt',
 'feature_fraction': 0.4,
 'feature_pre_filter': False,
 'lambda_l1': 0.0,
 'lambda_l2': 0.0,
 'metric': 'multi_error',
 'min_child_samples': 10,
 'num_class': 3,
 'num_leaves': 31,
 'objective': 'multiclassova',
 'task': 'train'}
```

さて、精度はどうでしょう。

model.best_score

```
defaultdict(dict,{'valid_0': {'multi_error': 0.42599343784177907}})
```

若干良くなりましたね。。。

あまり良いモデルとは言えませんが、
このモデルを使って、早速予測をしていきたいと思います!

予測

作成したモデルで実際にレースの1,2,3位の着順を予測してみます。

9月13日に行なわれた「第5回紫苑ステークス(G3)」の
1、2、3着を予測しました。

pic_02

race_pred = model.predict(race_shion)
print(race_pred)


```
[[0.09498449 0.35376843 0.46072428]
 [0.23040056 0.45858191 0.24268427]
 [0.11946333 0.32861667 0.50903119]
 [0.04788258 0.19144857 0.67812379]
 [0.03750438 0.16934383 0.74449447]
 [0.21729804 0.43076399 0.30676997]
 [0.05190986 0.19097434 0.71611271]
 [0.15289972 0.45721926 0.30898854]
 [0.07239629 0.2878886  0.57067259]
 [0.22467073 0.43094351 0.31518758]
 [0.50645025 0.36337569 0.19296039]
 [0.06347555 0.26250014 0.63614264]
 [0.28643455 0.39625746 0.24677016]
 [0.12306013 0.28729929 0.59023578]
 [0.03356086 0.10710864 0.84435289]
 [0.33091491 0.33314675 0.20959785]
 [0.44760867 0.29619812 0.22646527]
 [0.1338794  0.24786876 0.57226198]]

```

model.predict()で予測すると、各クラスに分類される確率が返ってきます。
左から0(1~3着以内),1(4~9着),2(10着以下)です。

今回は1~3着以内に入る確率が高いものを1~3番目まで選んで、1,2,3着の予想とします。

↓今回のモデルで予測した結果がこちらです。

順位 馬名
1着 ウインマイティー
2着 スカイグルーヴ
3着 シーズンズギフト

↓実際のレースの着順はこちらです。

順位 馬名
1着 マルターズディオサ
2着 パラスアテナ
3着 シーズンズギフト

当たっていたのは、「シーズンズギフト」の一頭だけでした・・・
ウインマイティー、スカイグルーヴも当日、人気の馬だったので惜しい予測だったのでしょうか。

まとめ

今回、身の回りのデータを収集して機械学習で予測モデルを作ることが出来ました。

単純なモデルの作成だったので、特徴量を増やすなど、まだまだ改善の余地はありそうです。
過去のレース順位や回収率なども考慮したモデル作成も今後試して、一山当ててみたいですね。