LoginSignup
15
18

More than 3 years have passed since last update.

ボートレースの3連単をランキング学習で予測してみる

Posted at

はじめに

この記事は、自身で製作しWeb公開にまで至ったボートレース3連単予測サイト「きょう、ていの良い予想は当たるだろうか」の内部コード解説となります。今回は機械学習モデルに関してまとめていきます。そもそものデータ取得や整形に関しては別記事を作成しますので、そちらをご覧ください。

・取得した競艇テキストデータからデータフレームを作成する

※コードの書き方は我流なので、アドバイスいただけると有り難いです。

ランキング学習とは?

使えるようになるにあたり、以下の記事を大変参考にしました。

・競馬予想AI再び -前編- 〜LambdaRank編〜

ランキング学習とは、相対的な順序関係を学習するための方法と言われております。
上記リンクのように、競馬や競艇などのような複数人(馬)の相対的な強さを学習するのに向いているんじゃないかな、と思い取り組んでみました。

論文はまだ積ん読されていますが(笑)、まずは使ってみようと思います。
使用するライブラリはlightgbmです。

Queryデータセットを準備する

今回は2020年1月〜4月を学習データ、2020年5月のデータを検証データとします。

ランキング学習の特徴として、「Queryデータ」があります。このQueryデータは、1つのレースに含まれている学習データの数を表します。ボートレースの場合はトラブルがなければ6艇でレースが行われるので、

Queryデータの箱 = [6,6,6,...,6]

といった、(欠場者がいなければ)”6”がレース数分だけ並んだリストができあがるはずです。
image.png

ということで以下のコードでQueryデータの箱をつくっていきます。

%%time #時間を測る
target_cols = ["Position"]
feature_cols = ["Name","Lane","Round","Month","Place"]

train = pd.read_csv(train_file)
train_shuffle = pd.DataFrame([],columns=train.columns)

train_group =[]
for i,k in enumerate(train["Round"]):
    if i == 0:
        temp = k
        temp2 = i

    else:
        if k == temp:
            pass
        else:
            train_group.append(i-temp2)
            #↓ .sampleでシャッフルしたデータにする。
            train_shuffle=train_shuffle.append(train[temp2:i].sample(frac=1))
            temp = k
            temp2 = i

#最後の組が含まれていないのでを追加
train_group.append(i+1-temp2)
train_shuffle=train_shuffle.append(train[temp2:i+1].sample(frac=1))

train_y = train_shuffle[target_cols].astype(int)
train = train_shuffle[feature_cols]
print(train.shape)

read_csvで読んでいるtrain fileは取得した競艇テキストデータからデータフレームを作成するの記事をもとにできあがっています。

同じレース(Round)の数をかぞえ、train_groupのリストに格納しています。そして参考記事を読んだところ、このグループ内の順序をシャッフルしておかないとヤバいことになる、、との事だったので、train_shuffleに格納する際に.sampleでシャッフル処理をしています。

上記のコードを検証用データセットにも行い、検証用Queryデータセットを作成しておきます。

LightGBMを使用する

欠損処理やFeature engineering、One-hot encodingなどは割愛させていただきますが、学習データセット、Queryデータセットが準備できれば今の世の中、機械学習の実行は簡単です。ただ一点、lightgbmの仕様なのか、日本語がカラムに入っているとエラーが発生します。そのため以下のような処理を加えました。

column_list = []
for i in range(len(comb_onehot.columns)):
    column_list.append(str(i)+'_column')

comb_onehot.columns = column_list

※comb_onehotというDataFrame型はTrain datasetとValid datasetを結合し、One-hot encodingを処理した際につくられたデータフレームです。この処理の後、

train_onehot = comb_onehot[:len(train)]
val_onehot = comb_onehot[len(train):]

として、学習用と検証用に再分離しました。
さて、機械学習を実施します。

import lightgbm as lgb

lgbm_params =  {
    'task': 'train',
    'boosting_type': 'gbdt',
    'objective': 'lambdarank', #←ここでランキング学習と指定!
    'metric': 'ndcg',   # for lambdarank
    'ndcg_eval_at': [1,2,3],  # 3連単を予測したい
    'max_position': 6,  # 競艇は6位までしかない
    'learning_rate': 0.01, 
    'min_data': 1,
    'min_data_in_bin': 1,
#     'num_leaves': 31,
#     'min_data_in_leaf': 20,
#     'max_depth':35,
}
lgtrain = lgb.Dataset(train_onehot, train_y,  group=train_group)
lgvalid = lgb.Dataset(val_onehot, val_y,group=val_group)
lgb_clf = lgb.train(
    lgbm_params,
    lgtrain,
    num_boost_round=250,
    valid_sets=[lgtrain, lgvalid],
    valid_names=['train','valid'],
    early_stopping_rounds=20,
    verbose_eval=5
)

num_leavesなどのハイパーパラメータは調整するべきですが、ここでは考えず進みましょう。
検証データに対する予測はこんな感じ。ほんと、便利な時代です..。

y_pred = lgb_clf.predict(val_onehot,group=val_group, num_iteration=lgb_clf.best_iteration)

結果は...

ランキング学習による3連単予測は以下のようになりました。3連単は8.15%..!!

image.png

ちなみに上記の的中率(特に2連単や3連単)を手に入れるため、以下のようなコードを書きました。う〜ん、冗長!

#Validデータ的中率の算出
j = 0
solo_count = 0
doub_count = 0
tri_count = 0
for i in val_group:
    result = y_pred[j:j+i]
    ans = val_y[j:j+i].reset_index()

    result1st = np.argmin(result)
    if len(np.where(result==sorted(result)[1])[0])>1:
        result2nd = np.where(result==sorted(result)[1])[0][0]
        result3rd = np.where(result==sorted(result)[1])[0][1]
    else:
        if i > 1:
            result2nd = np.where(result==sorted(result)[1])[0][0]
        if i > 2:
            result3rd = np.where(result==sorted(result)[2])[0][0]

    ans1st = int(ans[ans["Position"]==1].index.values)
    if len(ans[ans["Position"]==2].index.values)>1:
        ans2nd = int(ans[ans["Position"]==2].index.values[0])
        ans3rd = int(ans[ans["Position"]==2].index.values[1])
    else:
        if i > 1:
            ans2nd = int(ans[ans["Position"]==2].index.values[0])
        if i > 2:
            ans3rd = int(ans[ans["Position"]==3].index.values[0])

    if ans1st==result1st:
        #print(ans1st,result1st)
        solo_count = solo_count+1

    if i > 1:
        if (ans1st==result1st)&(ans2nd==result2nd):
            doub_count = doub_count+1

    if i > 2:
        if (ans1st==result1st)&(ans2nd==result2nd)&(ans3rd==result3rd):
            tri_count = tri_count+1 
    j=j+i

print("単勝的中率:",round(solo_count/len(val_group)*100,2),"%")
print("2連単的中率:",round(doub_count/len(val_group)*100,2),"%")
print("3連単的中率:",round(tri_count/len(val_group)*100,2),"%")

さいごに

上記の結果は何も考えずに買うよりは高い的中率です。(最も高い頻度で起こる3連単の組み合わせが"1-2-3"であり、その頻度は7%程度)

ただ、この結果だけだと的中率として心許ないので、もう一工夫が必要と感じました。
そこのところはまた別記事にてまとめたいと思います。

15
18
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
18