1
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ユニケージで競馬予想AIの特徴量を生成する

Last updated at Posted at 2023-12-25

趣味で競馬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.レース結果データ」でスクレイピングしたデータになります。)

Lace_Spacing.sh
#!/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

コードは以上。
どういった処理が行われているか、解説していきます。

 

Lace_Spacing.sh
#!/bin/bash

tmp=/tmp/tmp
scrpd=/data/scraping
csvd=/data/csv

today=$(date +%Y%m%d)

こちらはただの変数定義なので、サラっと
以下の内容を設定しています。

tmp : 中間ファイルの出力先
scrpd : スクレイピングしたデータの保管場所
csvd : ユニケージで作成したデータの保管場所

 

Lace_Spacing.sh
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までの数値が入ってます。

 

Lace_Spacing.sh
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の様に、カラム名を指定して記述を行わないので、開発を進めると現在のレイアウトの状況が分かりづらくなるため、この様にコメントで記載します。

 

Lace_Spacing.sh
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 の数値の順番が、少し不思議な順になっていたのはそのためです。

 

Lace_Spacing.sh
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 に保管します。
使い道については、後ほど。
 

Lace_Spacing.sh
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」に各馬の初戦データだけを保管してます。
 

Lace_Spacing.sh
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番目)を除去しています。
 

Lace_Spacing.sh
# ダミーデータの追加(この後の「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番目の日数を引く事で、レース間隔を出しております。

Lace_Spacing.sh
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」という名前で出場しておりました。
結果は、なんとか予選を潜り抜け、最終戦まで進出しました。

1
5
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
1
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?