趣味で競馬AIを作り始めて、2年ほど経ったので
自分の為の記録の意味も込めて、作成したものの一部を記載しようと思う。
1.はじめに
最近、XなどのSNSを見ていても、競馬AIの作成に取り組んでいる方を多く見受けられます。
その方たちの中には、機械学習で利用する膨大なデータの取り扱いに難儀した方も多くいるのでは無いでしょうか。自分もその中の一人で、学習データを5年→10年に延ばしてみたり、思い付いた特徴量をどんどん追加していくと、個人のPCでは処理速度がみるみるうちに低下していき、終いには1日ではデータの更新が終わらない、、なんて事も。
そこで私は、特徴量の生成を当初は比較的処理速度の遅いPythonで全て行ってましたが、ユニケージで作成する方法に切り替える事にしました。
2.ユニケージ開発手法とは
ざっくり言うと、既存のDBMSを利用せず、UNIX系のシェルスクリプとコマンドでその機能を補うという思想で、開発コストが安価で容易に行える事が可能で、最大の特徴が処理速度の速さです。
私はこれを取り入れた事により、特徴量を1000まで追加しても運用が可能な状態に持っていく事が出来ました。
3.前準備(1):ユニケージコマンドのインストール
ユニケージコマンド(Personal Tukubai)は、下記から購入可能で非商用だと6ヶ月の利用で2000円となってます。
料金を支払い終えると、メールでコマンドとインストールの方法などをまとめたメモを送ってもらえるので、手順に沿って一緒に送られて来るバッチを実行するだけで、環境は整います。
(Windowsで開発する際は、UNIX環境を整えるため色々とその他設定が必要になります。)
4.前準備(2):データの収集
データについては、ネット競馬のデータをスクレイピングする形で用意しており、主に3つに分けて抽出、利用しています。因みに私は、過去10年分を抽出して利用しております。
(スクレイピングの方法は、youtubeなど探せば出て来るので割愛します。)
1.競走馬の出走データ
上記のページにある各馬の競争データを下記の形式で抽出します。
※の項目は、有料会員のみ取得可能な情報となっている。
| No. | 項目名 | 補足説明 |
|---|---|---|
| 1 | horse_id | 各馬を識別するためのID |
| 2 | 日付 | レースが行われた日付 |
| 3 | 開催 | 第何回目のどの競馬場の何レース目か |
| 4 | 天気 | |
| 5 | R | レース番号 |
| 6 | レース名 | |
| 7 | ※映像 | |
| 8 | 頭数 | |
| 9 | 枠番 | |
| 10 | 馬番 | |
| 11 | オッズ | |
| 12 | 人気 | |
| 13 | 着順 | |
| 14 | 騎手 | |
| 15 | 斤量 | |
| 16 | 距離 | |
| 17 | 馬場 | |
| 18 | ※馬場指数 | |
| 19 | タイム | |
| 20 | 着差 | |
| 21 | ※タイム指数 | |
| 22 | 通過 | |
| 23 | ペース | |
| 24 | 上り | |
| 25 | 馬体重 | |
| 26 | ※厩舎コメント | |
| 27 | ※備考 | |
| 28 | 勝ち馬(2着馬) | |
| 29 | 賞金 |
2.レース結果データ
上記のページにある各レース結果のデータを下記の形式で抽出します。
| No. | 項目名 | 補足説明 |
|---|---|---|
| 1 | race_id | 各レースを識別するためのID |
| 2 | 着順 | |
| 3 | 枠番 | |
| 4 | 馬番 | |
| 5 | 馬名 | |
| 6 | 性齢 | |
| 7 | 斤量 | |
| 8 | 騎手 | |
| 9 | タイム | |
| 10 | 着差 | |
| 11 | 単勝オッズ | |
| 12 | 人気 | 単勝人気 |
| 13 | 馬体重 | |
| 14 | 調教師 | |
| 15 | horse_id | 各馬を識別するためのID |
| 16 | jockey_id | 各騎手を識別するためのID |
| 17 | 距離 | |
| 18 | 天気 | |
| 19 | race_type | 芝 or ダート or 障害 |
| 20 | 馬場 | |
| 21 | 日付 |
3.血統データ
上記のページにある各馬の血統データを下記の形式で抽出します。
| No. | 項目名 | 補足説明 |
|---|---|---|
| 1 | horse_id | 各馬を識別するためのID |
| 2 | peds_0 | 父 |
| 3 | peds_1 | 母 |
| 4 | peds_2 | 父父 |
| 5 | peds_3 | 父母 |
| : | : | : |
| : | : | : |
| 62 | peds_60 | 母母母母父 |
| 63 | peds_61 | 母母母母母 |
これだけでも、100以上の項目となるが、ここからユニケージを利用して新たな特徴量を生み出していきます。
5.特徴量を生成してみる
予想をする上で、いつ以来の出走か?重視されている方も多くいるかと思うので、まず「レース間隔」についてのデータを生成してみようと思う。
無敗の三冠馬、コントレイルを例に今回作るデータイメージを下記に記載しました。
※は、データのイメージを分かり易くする為に記載しただけで、実際には用意しない項目です。
| 日付 | ※レース名 | horse_id | レース間隔(日数) |
|---|---|---|---|
| 2019/09/15 | 新馬戦 | 2017101835 | 0 |
| 2019/11/16 | 東京スポーツ杯2歳S(G3) | 2017101835 | 62 |
| 2019/12/28 | ホープフルステークス(G1) | 2017101835 | 42 |
| 2019/04/19 | 皐月賞(G1) | 2017101835 | 113 |
| 2019/05/31 | 東京優駿(G1) | 2017101835 | 42 |
| : | : | : | : |
| : | : | : | : |
| 2021/11/28 | ジャパンカップ(G1) | 2017101835 | 28 |
以下が実際のコードです。
(「race_results.csv」が、「2.レース結果データ」でスクレイピングしたデータになります。)
#!/bin/bash
tmp=/tmp/tmp
scrpd=/data/scraping
csvd=/data/csv
today=$(date +%Y%m%d)
#----------------------------------------
# 前準備
#----------------------------------------
cat << FIN | self 1 > $tmp-tyaku_list
1
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
FIN
cat ${scrpd}/race_results.csv |
fromcsv > $tmp-race_results
# 1:race_id 2:着順 3:枠番 4:馬番 5:馬名 6:性齢 7:斤量 8:騎手 9:タイム 10:着差 11:単勝 12:人気 13:馬体重 14:調教師
# 15:horse_id 16:jockey_id 17:cource_len 18:weather 19:race_type 20:ground_state 21:date
#----------------------------------------
# 重複行と除外、取り消しがあったデータを取り除く
#----------------------------------------
cat $tmp-race_results |
# 1:race_id 2:着順 3:枠番 4:馬番 5:馬名 6:性齢 7:斤量 8:騎手 9:タイム 10:着差 11:単勝 12:人気 13:馬体重 14:調教師
# 15:horse_id 16:jockey_id 17:cource_len 18:weather 19:race_type 20:ground_state 21:date
LANG=C sort -k 2,2 |
join0 key=2 $tmp-tyaku_list > $tmp-race_results_uniq
#----------------------------------------
# horse_id_listの作成
#----------------------------------------
cat $tmp-race_results_uniq |
# 1:race_id 2:着順 3:枠番 4:馬番 5:馬名 6:性齢 7:斤量 8:騎手 9:タイム 10:着差 11:単勝 12:人気 13:馬体重 14:調教師
# 15:horse_id 16:jockey_id 17:cource_len 18:weather 19:race_type 20:ground_state 21:date
lineup 15 > $tmp-horse_list
# 1:horse_id
#----------------------------------------
# 初戦データ
#----------------------------------------
cat $tmp-race_results_uniq |
# 1:race_id 2:着順 3:枠番 4:馬番 5:馬名 6:性齢 7:斤量 8:騎手 9:タイム 10:着差 11:単勝 12:人気 13:馬体重 14:調教師
# 15:horse_id 16:jockey_id 17:cource_len 18:weather 19:race_type 20:ground_state 21:date
self 15 21 |
# 1:horse_id 2:date
LANG=C sort |
getfirst 1 1 |
awk '{ print $1,$2,0 }' > $tmp-syosen_d
# 1:horse_id 2:date 3:Lace_Spacing
#----------------------------------------
# ダミーデータの作成
#----------------------------------------
cat $tmp-horse_list |
# 1:horse_id
awk '{ print $1,"2000-01-01",0}' > $tmp-dummy
# 1:horse_id 2:ダミー日付 3:0
#----------------------------------------
# メイン処理
#----------------------------------------
cat $tmp-race_results_uniq |
# 1:race_id 2:着順 3:枠番 4:馬番 5:馬名 6:性齢 7:斤量 8:騎手 9:タイム 10:着差 11:単勝 12:人気 13:馬体重 14:調教師
# 15:horse_id 16:jockey_id 17:cource_len 18:weather 19:race_type 20:ground_state 21:date
awk '{ print $15,$21,
'"${today}"',
substr($21,1,4)substr($21,6,2)substr($21,9,2)
}' |
# 1:horse_id 2:date 3:処理日 4:date(8桁)
uniq |
mdate -f 3 4 |
# 1:horse_id 2:date 3:処理日 4:date(8桁) 5:処理日とdateの差
delf 3 4 |
# ダミーデータの追加(この後の「ychange」で列が崩れないようにする為)
cat - $tmp-dummy |
# 1:horse_id 2:date 3:処理日とdateの差
LANG=C sort -r |
ychange num=1 |
# 1:horse_id 2:date 3:処理日とdateの差 4:1レース前のdate 5:1レース前の処理日とdateの差
awk '{ print $1,$2, $5-$3}' |
# 1:horse_id 2:date 3:Lace_Spacing
LANG=C sort |
upl key=1/2 - $tmp-syosen_d |
# yyyymmddの形式に変換
awk '{ print substr($2,1,4)substr($2,6,2)substr($2,9,2),
$1,$3 }' |
LANG=C sort |
tocsv > ${csvd}/Lace_Spacing.csv
# 1:date 2:horse_id 3:Lace_Spacing
コードは以上。
どういった処理が行われているか、解説していきます。
#!/bin/bash
tmp=/tmp/tmp
scrpd=/data/scraping
csvd=/data/csv
today=$(date +%Y%m%d)
こちらはただの変数定義なので、サラっと
以下の内容を設定しています。
tmp : 中間ファイルの出力先
scrpd : スクレイピングしたデータの保管場所
csvd : ユニケージで作成したデータの保管場所
cat << FIN | self 1 > $tmp-tyaku_list
1
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
FIN
race_results(レース結果データ)に出走除外があった馬の情報なども含まれているので、それを取り除くための、前準備です。
tmp-tyaku_list に1〜18までの数値が入ってます。
cat ${scrpd}/race_results.csv |
fromcsv > $tmp-race_results
# 1:race_id 2:着順 3:枠番 4:馬番 5:馬名 6:性齢 7:斤量 8:騎手 9:タイム 10:着差 11:単勝 12:人気 13:馬体重 14:調教師
# 15:horse_id 16:jockey_id 17:cource_len 18:weather 19:race_type 20:ground_state 21:date
ここで、スクレイピングしてきてcsv で保管していたレース結果データを、「fromcsv」というコマンドで、ユニケージで取り扱うための形式に変換します。
具体的にはカンマ区切りで保管されているデータを、スペース区切りに変換します。
コメントで、# 1:race_id 2:着順 〜 と記載しているのは、データのレイアウトです。
SQLの様に、カラム名を指定して記述を行わないので、開発を進めると現在のレイアウトの状況が分かりづらくなるため、この様にコメントで記載します。
cat $tmp-race_results |
# 1:race_id 2:着順 3:枠番 4:馬番 5:馬名 6:性齢 7:斤量 8:騎手 9:タイム 10:着差 11:単勝 12:人気 13:馬体重 14:調教師
# 15:horse_id 16:jockey_id 17:cource_len 18:weather 19:race_type 20:ground_state 21:date
LANG=C sort -k 2,2 |
join0 key=2 $tmp-tyaku_list > $tmp-race_results_uniq
この処理で着順に1〜18までの数値が設定されたレコードのみに絞り、除外などがあった馬の情報を取り除いています。
1点ポイントですが、join を利用する際は、トラン(tmp-race_results)とマスタ(tmp-tyaku_list)の情報がソートされている必要が有ります。
tmp-tyaku_list の数値の順番が、少し不思議な順になっていたのはそのためです。
cat $tmp-race_results_uniq |
# 1:race_id 2:着順 3:枠番 4:馬番 5:馬名 6:性齢 7:斤量 8:騎手 9:タイム 10:着差 11:単勝 12:人気 13:馬体重 14:調教師
# 15:horse_id 16:jockey_id 17:cource_len 18:weather 19:race_type 20:ground_state 21:date
lineup 15 > $tmp-horse_list
# 1:horse_id
「lineup」のコマンドで、指定した15番目の項目(horse_id)がユニークされた状態で全ての種類 tmp-horse_list に保管します。
使い道については、後ほど。
cat $tmp-race_results_uniq |
# 1:race_id 2:着順 3:枠番 4:馬番 5:馬名 6:性齢 7:斤量 8:騎手 9:タイム 10:着差 11:単勝 12:人気 13:馬体重 14:調教師
# 15:horse_id 16:jockey_id 17:cource_len 18:weather 19:race_type 20:ground_state 21:date
self 15 21 |
# 1:horse_id 2:date
LANG=C sort |
getfirst 1 1 |
awk '{ print $1,$2,0 }' > $tmp-syosen_d
# 1:horse_id 2:date 3:Lace_Spacing
「self」のコマンドで、指定した番号の項目を取り出します。(「horse_id」と「date」)
その後、LANG=C sort でデータをソートした後、「getfirst」で同一キーの最初の行を出力します。
データのイメージとしては、こんな感じ。
getfirst前
| horse_id | 日付 |
|---|---|
| 2017101835 | 2019/09/15 |
| 2017101835 | 2019/11/16 |
| 2017101835 | 2019/12/28 |
| 2017101835 | 2019/04/19 |
| 2017101835 | 2019/05/31 |
| : | : |
| : | : |
| 2017101835 | 2021/11/28 |
getfirst後
| horse_id | 日付 |
|---|---|
| 2017101835 | 2019/09/15 |
これらの処理を経た結果、「tmp-syosen_d」に各馬の初戦データだけを保管してます。
cat $tmp-race_results_uniq |
# 1:race_id 2:着順 3:枠番 4:馬番 5:馬名 6:性齢 7:斤量 8:騎手 9:タイム 10:着差 11:単勝 12:人気 13:馬体重 14:調教師
# 15:horse_id 16:jockey_id 17:cource_len 18:weather 19:race_type 20:ground_state 21:date
awk '{ print $15,$21,
'"${today}"',
substr($21,1,4)substr($21,6,2)substr($21,9,2)
}' |
# 1:horse_id 2:date 3:処理日 4:date(8桁)
uniq |
mdate -f 3 4 |
# 1:horse_id 2:date 3:処理日 4:date(8桁) 5:処理日とdateの差
delf 3 4 |
ここからがメインの処理。
ポイントは「mdate -f 3 4 」で、3項目目の処理日と4項目目のdata(レース日)との日数差を5項目目に出力しています。
データイメージはこんな感じ
| horse_id | data | 処理日 | data(8桁) | 処理日とdateの差 |
|---|---|---|---|---|
| 2017101835 | 2019/09/15 | 20231224 | 20190915 | 1561 |
| : | : | : | : | |
| : | : | : | : | |
| 2017101835 | 2021/11/28 | 20231224 | 20211128 | 756 |
そして「delf」のコマンドですが、こちらで指定した項目(3番目と4番目)を除去しています。
# ダミーデータの追加(この後の「ychange」で列が崩れないようにする為)
cat - $tmp-dummy |
# 1:horse_id 2:date 3:処理日とdateの差
LANG=C sort -r |
ychange num=1 |
# 1:horse_id 2:date 3:処理日とdateの差 4:1レース前のdate 5:1レース前の処理日とdateの差
awk '{ print $1,$2, $5-$3}' |
# 1:horse_id 2:date 3:Lace_Spacing
ここのポイントは「ychange num=1」で、このコマンドで以下の様に変化しております。
ychange前
| horse_id | 日付 | 処理日とdateの差 |
|---|---|---|
| 2017101835 | 2019/09/15 | 1561 |
| 2017101835 | 2019/11/16 | 1499 |
| 2017101835 | 2019/12/28 | 1457 |
| : | : | : |
ychange後
| horse_id | 日付 | 処理日とdateの差 | 1レース前のdate | 1レース前の処理日とdateの差 |
|---|---|---|---|---|
| 2017101835 | 2019/09/15 | 1561 | 2000/01/01 | 0 |
| 2017101835 | 2019/11/16 | 1499 | 2019/09/15 | 1561 |
| 2017101835 | 2019/12/28 | 1457 | 2019/11/16 | 1499 |
| : | : | : | : | : |
「cat - $tmp-dummy」でダミーデータを投入しているのは、1戦しか走っていない馬のデータがこの処理を行った際、レイアウトが崩れてしまうので、それを防ぐために行っております。
そして、3番目から5番目の日数を引く事で、レース間隔を出しております。
LANG=C sort |
upl key=1/2 - $tmp-syosen_d |
# yyyymmddの形式に変換
awk '{ print substr($2,1,4)substr($2,6,2)substr($2,9,2),
$1,$3 }' |
LANG=C sort |
tocsv > ${csvd}/Lace_Spacing.csv
初出走のレコードに対しては、レース間隔を「0」に設定する為、「upl key=1/2 - $tmp-syosen_d 」の処理を行ってます。
「upl」コマンドは、同一キーでマージを行い、キーフィールドの値が同一の最終行を抽出します。
データは以下の様に変化しております。
「tmp-syosen_d」の中身
| horse_id | 日付 | レース間隔 |
|---|---|---|
| 2017101835 | 2019/09/15 | 0 |
| : | : | : |
「upl key=1/2 - $tmp-syosen_d」の処理
| horse_id | 日付 | レース間隔 |
|---|---|---|
| 2017101835 | 2019/09/15 | 62 |
| 2017101835 | 2019/09/15 | 0 |
| 2017101835 | 2019/11/16 | 42 |
| : | : | : |
「upl key=1/2 - $tmp-syosen_d」の処理後
| horse_id | 日付 | レース間隔 |
|---|---|---|
| 2017101835 | 2019/09/15 | 0 |
| 2017101835 | 2019/09/15 | 42 |
| : | : | : |
そしてawkでデータを整理して、「tocsv」コマンドで、スペース区切りのデータを、カンマ区切りのcsvに変換して完成です。
6.最後に
最初の方に触れていた処理速度についてですが、過去10年以上のデータを扱い、この処理で70万件、16M程のファイルサイズの出力を行ってますが、15秒で処理が完了しています。
使用しているマシンのスペックについては、こんな感じ。
スペック
MacBook Pro(13-inch,2018,Four Thunderbolt 3 Prots)
プロセッサ:2.3GHz クアッドコア Intel Core i5
メモリ:8G 2133 MHz LPDDR3
最近、既存のDBMSをあまり利用してないのですが、比較的これはかなり速い、、はず。
余裕があれば作っている他の特徴量についても、記載したいと思います。
ネット競馬様主催のAI競馬予想マスターズにも「mag」という名前で出場しておりました。
結果は、なんとか予選を潜り抜け、最終戦まで進出しました。