Open
161

読者コミュニティ|競馬予想で始める機械学習

本の感想や質問をお気軽にコメントしてください。ソースコードを挿入したい場合は

```python
挿入したいコード
```

でできます。

YouTubeのメンバーシップに登録していて、Zennのアカウント名と異なる場合には、メンバー登録していることを知らせていただけると助かります!

いつも動画大変勉強なってます。

動画では2019年レースデータをスクレイピングして予想が始まりましたが、
2020年のレースデータを予想で読み込みたいときは、2020年のレースデータをスクレイピングし、
2019年のデータと結合して、新たにファイルをつくるのでしょうか。

動画で2020年のデータも予想に使っていると言っていましたが、どこでデータを更新したのか見つけることができませんでした。教えてください。

ありがとうございます。
動画さがしていました。助かります。。

もう一つ質問なんですが、単勝回収値のグラフを作成すると、
最高が1.9,最低が1.3のおかしなグラフになってしまいます。
考えられる原因ってなにが挙げられますか。教えてください。。

yuyaさん

len()で分割し確認したとこ、おっしゃる通りでした。
コード書きなおし、実行すると動画に近い値となりました。
お騒がせしました。

修正いただきましてありがとうございます。
当方、仕事で機械学習・ディープラーニングを用いたシステム開発をしております。
色々な特徴量で回収率アップトライ中ですので、また別途コメントで紹介させていただけたらと思います。

今後ともどうぞよろしくお願いいたします。

僕自身もまだまだ機械学習の勉強途中なので、何かお気付きの点があればぜひコメントいただけると嬉しいです!

今後ともよろしくお願いいたします。


def merge(self, results, date):    
        dd = self.merged.copy()
        dd = dd[dd["date"]<date]   #今回使用する過去戦績
        df = results[results['date']==date]   #dateで指定された日のレース結果
        horse_id_list = df['horse_id']   #dateで指定された日に出走した馬のリスト
        dict_r = {}
        sample_hi_list = dd["horse_id"].unique()   #初出走ではない、過去戦績のある馬のリスト
                
        for horse_id in horse_id_list:
            if horse_id in sample_hi_list:   #過去戦績をもつ馬について

                horse_r = df[results["horse_id"]==horse_id]
                n_sample = 1   #前走について
                a = dd.copy()
                a = a[a["date"]<date].sort_values('date', ascending=False).groupby(["horse_id"]).get_group(horse_id)[n_sample-1:n_sample]
                a = a.rename(columns={'枠番':'枠番_{}R'.format(n_sample), '騎手':'騎手_{}R'.format(n_sample), ...    #前走競走情報として扱うので列名を_1Rに直す

                n_sample = 2    #前々走について
                b = dd.copy()
                b = b[b["date"]<date].sort_values('date', ascending=False).groupby(["horse_id"]).get_group(horse_id)
                if len(b) >= 2:    #過去戦績が2レース分以上ある場合
                    b = b[n_sample-1:n_sample]
                    b = b.rename(columns={'枠番':'枠番_{}R'.format(n_sample), '騎手':'騎手_{}R'.format(n_sample),...   #前々走競走情報として扱うので列名を_2Rに直す
                else:     #過去戦績が1走分しか無い場合
                    b = b[b["date"]<date].sort_values('date', ascending=False).groupby(["horse_id"]).get_group(horse_id)[0:1]
                    b.loc[0, ['枠番', '騎手'... = 0     #前々走部分は0の値で扱う

                 n_sample = 7     #近7走分について、賞金など平均を求める
                c = dd.copy()
                c = c[c["date"]<date].sort_values('date', ascending=False).groupby(["horse_id"]).get_group(horse_id).head(n_sample)...

                horse_r = horse_r.merge(a, on="horse_id", how="left")
                horse_r = horse_r.merge(b, on="horse_id", how="left")
                horse_r = horse_r.merge(c, on="horse_id", how="left")
                
                dict_r[horse_id] = horse_r
            else:
                horse_r = df[results["horse_id"]==horse_id]
                dict_r[horse_id] = horse_r

        self.merged_df = pd.concat([dict_r[key] for key in dict_r])
        return self.merged_df   

def merge_all(self, results):            
        date_list = results['date'].unique()
        self.merged_df = pd.concat([self.merge(results, date) for date in tqdm(date_list)])

        self.merged_df["中9週"] = (self.merged_df["date"] - self.merged_df["date_1R"]) > "65 days"
        self.merged_df["中9週"] = self.merged_df["中9週"].astype(int)   #ついでに列を20ほど追加
return self.merged_df


質問失礼いたします。

長かったので部分的に端折っておりますが、上記のような形でhr.merge_all を前走・前々走・7走平均で
欲しい特徴量を加えていきました。

このhr.merge_allを2019年分のデータについて(110日分と出ています)行うと、処理に6時間くらいかかる見込みなのですが、merge_allはこのくらい処理時間掛かるものなのでしょうか?

それとも、自作したコードにものすごく時間が掛かる部分が出てきてしまったのでしょうか?
詳しい方教えて頂けるとたすかります!m(__)m

110日分のデータで6時間は結構時間がかかっていますね。
おそらくhorse_id_listを回すfor文の処理が重いのだと思うのですが、ぱっと見ただけではどこに処理時間がかかっているのかわからないので、for文の中のコードを取り出して%%timeitなどをを使って処理時間を計測するか、

import time

def merge(self, results, date): 
    #省略

    t1 = time.time()
    n_sample = 7     #近7走分について、賞金など平均を求める
    c = dd.copy()
    c = c[c["date"]<date].sort_values('date',ascending=False).groupby(["horse_id"]).get_group(horse_id).head(n_sample)...
    t2 = time.time()
    break

などのようにすれば、

t2-t1

で、挟まれた部分の処理にかかった時間がわかるので、これでどのコードに時間がかかっているのか検証して見てください。

ありがとうございます。
良い改善方法が表現できず、とりあえず時間掛けて実行しています。

主さんの製作方法を参考に自分なりに特徴量なり加えて4年分くらいのデータでモデルを学習→今週3日間予想 までやってみたのですが、

データをsplitさせて学習→テストの段階ではpredの結果を見る限り納得できる確率(明らかに来そうな人気馬のpredが0.5〜0.8)が算出されるのですが、

実際に出馬表をmeクラスで予想させると同じような明らかに来そうな人気馬のpredが平気で0.1と算出したり、ちょっと理解できない結果が表示されたのですが、モデル作成の時点と実際の予想とでこんなにも性格が変わることってあるのでしょうか?

過学習を抑えようとパラメータを弄る過程でfeature fractionを0.05〜0.2程度まで低くしていました。kaggleの本を読むには0.6〜0.95くらいが典型的な設定みたいですね...。

これくらいの設定ですと、学習結果がデタラメになるのでしょうか?(auc scoreは80.4/80.0でした)

学習データは予想した日の直前までのものを使っていますか?
実際に予想する日に対して学習データ(特にhorse_resultsデータ)の日にちが離れていると、最近の成績が反映されず、精度の良い学習ができない可能性があります。
なのでテストをするときは訓練データとテストデータを分けますが、実際に使うときは全て訓練データにしたほうが良いかもしれません。

2017〜2020年データを使っているので、一応直近と言えば直近とは思うのですが...。

lightgbmが難しいので一旦、ランダムフォレストに戻ってモデルやり直しています。

また、以前merge_all関数でhorse_idを回すところでかなり時間か掛かっていた件ですが、自分が扱いたかった過去n走前のレコードについては、groupby(horse_id).nth(n)で満足いく処理が出来ることに気が付き、解決しました😅

素晴らしい動画をいつもありがとうございます。

Chapter 04
機械学習モデル作成&学習

を参考にoptunaによるハイパーパラメータチューニングを実行しています。
しかし、optunaを実行するたびにハイパーパラメータの値が変わるため、
その後のLightGBMの学習結果も毎回異なってしまいます。
期待値としては毎回同じ結果が欲しいのですが、そのようなやり方はないのでしょうか?

<実行1回目のlgb_clf_o.params>
{'objective': 'binary',
'random_state': 100,
'feature_pre_filter': False,
'lambda_l1': 2.763814491446932e-07,
'lambda_l2': 4.848539250253659e-07,
'num_leaves': 31,
'feature_fraction': 0.7,
'bagging_fraction': 1.0,
'bagging_freq': 0,
'min_child_samples': 25,
'num_iterations': 1000,
'early_stopping_round': 10}

<実行2回目のlgb_clf_o.params>
{'objective': 'binary',
'random_state': 100,
'feature_pre_filter': False,
'lambda_l1': 3.1893294433877453e-07,
'lambda_l2': 3.534217732516116e-07,
'num_leaves': 31,
'feature_fraction': 0.7,
'bagging_fraction': 1.0,
'bagging_freq': 0,
'min_child_samples': 25,
'num_iterations': 1000,
'early_stopping_round': 10}

そうなんですよね、random_stateを固定しているにもかかわらず結果が変わってしまうのでおかしいと思い、僕も原因を調べているのですが、ちょっとまだ分からないです・・・

返信ありがとうございます。

やはり毎回結果が変わってしまうのですね。
毎回結果が変わるものなのか、固定するやり方があるのか、今の私の知識ではわかりませんが、
もし解決したら教えていただけるとありがたいです。

これからも動画楽しみにしています。

購読させていただいている者です。update_data(old, new)関数の具体的な利用方法を教えていただきたいです!

ご返信ありがとうございます!
無事利用することができました!

重ねてのご質問となるのですが、
本コードではthresholdを0.5としています。
「pred値が〜以上の場合に賭けると良い」といった
thresholdの最適化?のような操作は行わなくてよろしいのでしょうか?
また、具体的な方法があれば教えていただきたいです!
私自身、機械学習初学者であり分かりにくい文章で申し訳ないです…

シミュレーションの章のコードを見ると分かりますが、threshold=0.5はあくまで初期値で、thresholdを0.5から1まで変化させることで賭ける枚数を変化させて回収率をプロットしています。
横軸が賭ける枚数、縦軸が回収率
なので、回収率でthresholdを最適化をすると「thresholdは大きく設定すればするほど良い」ということになります。
実際にはそれだと月に数枚しか賭けないことになるので、どれくらい競馬を楽しみたいかによって賭ける枚数は増やすことになりそうですが。

2021/1/8 馬連BOXによる回収率シミュレーションはどこで見れますか?

05に書いてありましたね。失礼しました!

更新ありがとうございます。
更新箇所はリンクにしたらいかがしょうか?
そうすることで探す手間も省けるかと。

よろしくお願いいたします。

【競馬予想】Pythonでスクレイピングして過去成績を入れた出馬表を作る方法【データ分析】のコメントで
Che Che さんの質問にあるように動画の12分15秒で

1 st = ShutubaTable()
2 st.scrape_shutuba_table(['202006010101'])

を実行すると全く同じエラーが出ていたのですが

Headlessモードを有効にすることで解決しました。

import time
options.set_headless(True)
url = 'https://race.netkeiba.com/race/shutuba.html?race_id=202106010407'
options = ChromeOptions()
sample_driver = Chrome(options=options)
sample_driver.get(url)

いつもありがとうございます。

本や動画を参考に、一部特徴量を除けば目的変数の設定(1着なら1、他は0)、採用モデルという点でほぼほぼ同じような予測モデルでやらしていただいています。

ところが、モデルの評価まではまずまずのスコアが出るのですが、実際に未知データ(2021年のレース)を与えて予測させてみると、大概の馬がpred=0.005とかになり、各レース1頭ずつくらいでpred=0.28の馬がいる、というような予測結果になります。

皆さんはこんな感じの予測結果になることはありますか?

目的変数が1≒8%、0≒92%と不均衡なためこのような予測結果になるのでしょうか。。。

ご教示ください🙇🙇🙇

ありがとうございます。
predはlightGBMで予測された値をそのまま使用しています。
試しにCatBoostでも予測してみたのですが、似たような予測結果になりました。

具体的には、中山ですと内田・横山典。中京ですと幸・和田が騎乗する馬ばかりが上位スコアになり、ほかの馬はすべて一定のきわめて小さな値が算出されています...。

なるほど、騎手だけが重要視されてしまっている感じですね。
feature_imporanceをみるとどうなっていますか?
違う特徴量を使っているということなので、もしかしたらその特徴量の中だと騎手を重要視せざるを得ないという可能性もあります。

もう一つの可能性としては、僕のモデルと同じようにLightGBMのcategorical_featuresを使っている場合、その列を重要視しすぎてしまう場合があります。なので方法としては

  • categorical_featuresを使わずにLabelEncodingで止める
  • early_stoppingを使う

などがあるかなと思います。

質問が2点あります。
①Resultsについて(Chapter3)
Resultsクラスのpreprocessing関数内の馬体重を体重と体重変化に分割する箇所ですが、
馬体重に「計不」があり整数型へキャストできません。
df = df[df["馬体重"] != '計不']
として排除して対応していますがどうするべきなのでしょうか?

②出馬表データの加工について(Chapter3)
no_pedsへの対処ですが、変数pedsはどこで定義しているのでしょうか?

そのようなものはデータ数として少ないでしょうし、排除してしまっても良いと思います。それか、欠損値にするのもありだと思います。

df['体重'] = df['馬体重'].str.split("(", expand=True)[0]
df['体重'] = pd.to_numeric(df['体重'], errors='coerce')

で、キャストできない場合は欠損値にしてくれます。

ご回答ありがとうございます。②については如何でしょうか?

no_pedsへの対処ですが、以下を追加してから処理すれば問題ないと思います。

peds = pd.read_pickle('peds.pickle')

私もまだまだ勉強中ですが、助けになれば幸いです。

実践的な動画で大変勉強なってます。
エラーが出ても挫折することなく続けられる内容になっていますね。
質問なんですが
2019年分と2021年分は正常に変換できるのですが
2020年分だとエラーになってしまいます。
対処方法を教えてください。お願いします。

Returnクラス

def fukusho(self):
fukusho = self.return_tables[self.return_tables[0]=='複勝'][[1,2]]
wins = fukusho[1].str.split('br', expand=True).drop([3], axis=1)
wins.columns = ['win_0', 'win_1', 'win_2']
returns = fukusho[2].str.split('br', expand=True).drop([3], axis=1)
returns.columns = ['return_0', 'return_1', 'return_2']

    df = pd.concat([wins, returns], axis=1)
    for column in df.columns:
        df[column] = df[column].str.replace(',', '')
    return df.fillna(0).astype(int)

ValueError Traceback (most recent call last)
<ipython-input-12-1c79a4487a71> in <module>
1 rt = Return(return_tables_2020)
----> 2 rt.fukusho

<ipython-input-5-fdc8134a3438> in fukusho(self)
61 fukusho = self.return_tables[self.return_tables[0]=='複勝'][[1,2]]
62 wins = fukusho[1].str.split('br', expand=True).drop([3], axis=1)
---> 63 wins.columns = ['win_0', 'win_1']
64 # wins.columns = ['win_0', 'win_1', 'win_2']
65 returns = fukusho[2].str.split('br', expand=True).drop([3], axis=1)

~\anaconda3\lib\site-packages\pandas\core\generic.py in setattr(self, name, value)
5150 try:
5151 object.getattribute(self, name)
-> 5152 return object.setattr(self, name, value)
5153 except AttributeError:
5154 pass

pandas_libs\properties.pyx in pandas._libs.properties.AxisProperty.set()

~\anaconda3\lib\site-packages\pandas\core\generic.py in _set_axis(self, axis, labels)
562 def _set_axis(self, axis: int, labels: Index) -> None:
563 labels = ensure_index(labels)
--> 564 self._mgr.set_axis(axis, labels)
565 self._clear_item_cache()
566

~\anaconda3\lib\site-packages\pandas\core\internals\managers.py in set_axis(self, axis, new_labels)
224
225 if new_len != old_len:
--> 226 raise ValueError(
227 f"Length mismatch: Expected axis has {old_len} elements, new "
228 f"values have {new_len} elements"

ValueError: Length mismatch: Expected axis has 4 elements, new values have 2 elements
1

横から失礼します。自分も同じような現象が起きましtが以下のコードで解決しております。

def fukusho(self):
    fukusho = self.return_tables[self.return_tables[0]=='複勝'][[1,2]]
    wins = fukusho[1].str.split('br', expand=True).drop([3,4], axis=1)
    wins.columns = ['win_0', 'win_1', 'win_2']
    returns = fukusho[2].str.split('br', expand=True).drop([3,4], axis=1)
    returns.columns = ['return_0', 'return_1', 'return_2']
    
    df = pd.concat([wins, returns], axis=1)
    for column in df.columns:
        df[column] = df[column].str.replace(',', '')
    return df.fillna(0).astype(int)

詳しく言うと、
3行目の、wins = fukusho[1].str.split('br', expand=True).drop([3,4], axis=1)
 ↑4を追加
5行目の、wins = fukusho[2].str.split('br', expand=True).drop([3,4], axis=1)
                                ↑4を追加

なぜか2020年度のは不要なカラムがあるためそこを切り落とさないとエラーが起きるようです。

mawari web さん
返信ありがとうございます。
問題が解決しました。助かりました。
何日も悩んでいたのでこれでゆっくり眠れます。
ありがとうございました。

はじめまして。
質問するのも恥ずかしいレベルの初心者です。
競馬Aiを作りたく現在勉強中です。
見よう見まねでコードを打っておりますが、エラーが出てしまい対処方法が
わかりません。
定義されてないというエラーなのですがどういうことなのでしょうか。
何度打ち直してもエラーになります。

def scrape_race_results(race_id_list):
race_results = {}
for race_id in race_id_list:
url = "https://db.netkeiba.com/race/" + race_id
race_rasults[race_id] = pd.read_html(url)[0]
return race_results

race_id_list = ["201901010101","201901010102","201901010103"]
test = scrape_race_results(race_id_list)

NameError Traceback (most recent call last)
<ipython-input-1-101a93d83cbe> in <module>
1 race_id_list = ["201901010101","201901010102","201901010103"]
----> 2 test = scrape_race_results(race_id_list)

NameError: name 'scrape_race_results' is not defined

初歩的な質問で大変恐縮ですがよろしくお願いします。

def scrape_race_results(race_id_list):
race_results = {}
for race_id in race_id_list:
url = "https://db.netkeiba.com/race/" + race_id
race_rasults[race_id] = pd.read_html(url)[0]
return race_results

のコードは下のようにインデントを入れていますか?

def scrape_race_results(race_id_list):
    race_results = {}
    for race_id in race_id_list:
    url = "https://db.netkeiba.com/race/" + race_id
    race_rasults[race_id] = pd.read_html(url)[0]
    return race_results

いつもためになる動画やソースコードをありがとうございます。
毎回大変勉強させていただいております。
今後の開発ロードマップで修正予定とされている単勝適正回収値についてですが、
モデルの評価指標として重要な部分だと思いますので優先的に修正いただけると非常に助かります。
ご参考までに汚いコードですが、修正したものを添付させて頂きます。
一応計算結果に間違いはないと思いますが、その辺りも含めてお返事いただけますと幸いです。

修正しようとしたのですが、よく考えたらレース結果にある単勝オッズのデータはレース終了後に確定しているものなので、払い戻し金額を基準にして計算した場合と同じになります。実際に単勝オッズを100倍したものと払い戻し金額を比較すると全てのデータで一致しました。
実際にはレース開始直前の単勝オッズと終了後の単勝オッズは異なりますが、今取れているデータを使ってシミュレーションするなら今の評価指標のままで良いかなと思います。

コメントありがとうございます。
確かに当たり馬券の単勝オッズは、払い戻し表にある単勝の金額のちょうど1/100になるかと思います。
しかし、払い戻し表には勝ち馬の払い戻し金額しか記載されていないので、ハズレ馬券に関しては払い戻し表からオッズを算出することはできないかと思います。

例えば、以下のような例を考えると分かり易かと思います。
<昨年の第50回高松宮記念>(https://db.netkeiba.com/race/202007010811/)
「レース結果」
1位:モズスーパーフレア(オッズ:32.3倍)
2位:グランアレグリア(オッズ:4.1倍)
3位:ダイアトニック(オッズ:9.2倍)
・・・
「払い戻し」
単勝:3,230円

このレースに関してモデルが「モズスーパーフレア」にかけると判断した場合、賭け金は10,000円/32.3≒300円となります。この場合の32.3は払い戻し表の3,230円を100で割って算出する現行のtansho_return_properでの計算と一致します。

しかし、モデルが「グランアレグリア」にかけると判断した場合、賭け金は10,000円/4.1≒2,400円となりますが、現行のtansho_return_properでは「モズスーパーフレア」にかける場合と同額の300円をかけることになります。

予想する馬券の全てが的中する場合は現在のtansho_return_properでも問題ないかと思いますが、そうではない限りにおいてはやはりレース結果のテーブルにある単勝オッズを用いて算出するべきかと思います。

ありがとうございます!動画楽しみにしております!
今後も質問させていただくこともあろうかと思いますが宜しくお願い致します。

当該修正を行った場合、回収率が芳しくありません。やはりリークが影響しているのでしょうか。。。

次に出す動画で詳しく話しますが、修正すると回収率下がります。
具体的には、今までの計算方法だと「賭ける馬の単勝オッズ」が低い馬にも「結果1着になった馬の単勝オッズ」が高い時には、低い金額を賭けていることになります。
つまり、「あらかじめレースが荒れるかどうかを知っている」ことになっていて、このため今の単勝適正回収値は実際よりも高いものになってしまっています。
この点は完全に見落としていました。。。申し訳ありません。

謝っていただくことではありません!訂正があったとしても競馬予想動画シリーズが素晴らしいものに変わりないので、今後ともよろしくお願いします。

初めまして! 動画を見て本を購読しました、現在勉強中の者です。
本にある通りにコードを打っていき、何とか無事?にoptunaのハイパーパラメータチューニングにまで進みましたが、lgb_clf_o.paramsで実行したら、
TypeError: float() argument must be a string or a number, not 'Timestamp'
というエラーメッセージが表示されました。
検索してみてもサッパリ…。正直手詰まりです。
教えて頂けませんでしょうか。お願いします!

Timestampは日付や時刻を表すときの型なので、おそらくdateの列がデータに入っているのだと思います。なので訓練データとテストデータからdate列を除いてください。

ありがとうございます!
早速試してみます!

こんにちは。
現在、2021/01/24にアップされた「競馬予想AIの単勝回収率が159%に到達!馬の得意なコースを機械学習で判定させる方法とは?」をもとにコードを打ち進めているのですが、エラーが出てしまって止まっています。

クラスの中身を変更したあと下の手順の通り実行しました。
r = Results.read_pickle(['results.pickle'])
r.preprocessing()
hr = HorseResults.read_pickle(['horse_results.pickle'])
r.merge_horse_results(hr, n_samples_list=[5, 9, 'all'])
p = Peds.read_pickle(['peds.pickle'])
p.encode()
r.merge_peds(p.peds_e)
r.process_categorical()
race_id_list = ['2021060108{}'.format(str(i).zfill(2)) for i in range(1, 2, 1)]
st = ShutubaTable.scrape(race_id_list, '2021/01/24')
※↑とりあえず1レース分だけ抜き出しています。
st.preprocessing()

ここまではいままでどおり、いけてたんですが
st.merge_horse_results(hr)
↑を実行したらエラーがでてしまいました。
エラー文は
KeyError: '開催'
となっています。


自分でも調査進めていますが、原因をご教授いただければ幸いです。

よろしくお願いいたします。

たぶん解決しました。
class ShutubaTable(DataProcessor):
  #前処理
def preprocessing(self):
  ~中略~

不要な列を削除→どの列を使うか

df = df[['枠', '馬番', '斤量', 'course_len', 'weather','race_type',
'ground_state', 'date', 'horse_id', 'jockey_id', '性', '年齢',
'体重', '体重変化','開催']]
         ↑ こいつが入っていませんでした。

これでKEYERRORはなくなると思います。

コメントありがとうございます。
ご指摘の通り修正しました。あと本家の動画コメントにあります通りShutubaTableクラスのpreprocessing関数の中で「 df['開催'] = df.index.map(lambda x:str(x)[4:6])」をしたうえで上記の修正内容をfixさせないといけなかったようです。

無事解決できました。ありがとうございました。

いつもありがとうございます。
現在は、馬の情報がメインで学習結果が算出されていると思うのですが、馬の過去レース結果+騎手の過去レース結果みたいな感じでできないものなのでしょうか?
初心者的な考えで申し訳ございません。。

騎手の過去レース結果も入れたいですよね!
特徴量の重要度でjockey_idがかなり上の方に来てますが今のところカテゴリ変数として直接入れているだけなので。
騎手のデータは数ページに渡るので、少しスクレイピングが面倒でまだやっていないですが、そのうちやりたいと思っています。

M1チップのmacで、
この教材のためのimportなどうまくできた方、selenium、numpy、pandasなどのインストールの方法
やり方教えていただけませんでしょうか?
M1チップのmacでも可能でしょうか?
windowsPCの買い替えを検討しています。

著者の方や読者の方でm1のmacを使用されているかたはいらっしゃいますか?
何かと設定が複雑と聞いたので、上記の環境設定について確証を得てから購入したいものです。

僕はM1チップのmac使ってますよ!
インストール方法など基本的には旧モデルのmacと変わらなかったです。

シェルがzshというものになっていることだけ注意すれば、という感じです。(今言われても何のことか分からないかもしれないですが書いておきますね)

以下の定義の追加が必要だと思います。

class HorseResults 中の
#開催場所
df['開催'] = df['開催'].str.extract(r'(\D+)')[0].map(place_dict).fillna('11')
                              ↑
place_dictの定義(動画の中ではありましたが)

#race_type
df['race_type'] = df['距離'].str.extract(r'(\D+)')[0].map(race_type_dict)
                                  ↑
race_type_dict定義

place_dict={
"札幌":"01","函館":"02","福島":"03","新潟":"04","東京":"05",
"中山":"06","中京":"07","京都":"08","阪神":"09","小倉":"10"
}
race_type_dict={
"芝":"芝","ダート":"ダ","障害":"障"
}
を追加ですかね??

その後
#前処理
st.preprocessing()

#馬の過去成績データの追加。新馬はNaNが追加される
st.merge_horse_results(hr)

まで進めましたが、その際に
KeyError Traceback (most recent call last)
<ipython-input-50-f2b92f817292> in <module>
3
4 #馬の過去成績データの追加。新馬はNaNが追加される
----> 5 st.merge_horse_results(hr)

<ipython-input-16-eaf84c8c2324> in merge_horse_results(self, hr, n_samples_list)
14 self.data_h = self.data_p.copy()
15 for n_samples in n_samples_list:
---> 16 self.data_h = hr.merge_all(self.data_h, n_samples=n_samples)
17 self.data_h.drop(['開催'], axis=1, inplace=True)
18

<ipython-input-38-a6232dc9026e> in merge_all(self, results, n_samples)
136 def merge_all(self, results, n_samples='all'):
137 date_list = results['date'].unique()
--> 138 merged_df = pd.concat([self.merge(results, date, n_samples) for date in tqdm(date_list)])
139 return merged_df

<ipython-input-38-a6232dc9026e> in <listcomp>(.0)
136 def merge_all(self, results, n_samples='all'):
137 date_list = results['date'].unique()
--> 138 merged_df = pd.concat([self.merge(results, date, n_samples) for date in tqdm(date_list)])
139 return merged_df

<ipython-input-38-a6232dc9026e> in merge(self, results, date, n_samples)
129 right_index=True, how='left')
130 for column in ['course_len','race_type', '開催']:
--> 131 merged_df = merged_df.merge(self.average_dict[column],
132 left_on=['horse_id', column],
133 right_index=True, how='left')

~\anaconda3\lib\site-packages\pandas\core\frame.py in merge(self, right, how, on, left_on, right_on, left_index, right_index, sort, suffixes, copy, indicator, validate)
7282 from pandas.core.reshape.merge import merge
7283
-> 7284 return merge(
7285 self,
7286 right,

~\anaconda3\lib\site-packages\pandas\core\reshape\merge.py in merge(left, right, how, on, left_on, right_on, left_index, right_index, sort, suffixes, copy, indicator, validate)
71 validate=None,
72 ) -> "DataFrame":
---> 73 op = _MergeOperation(
74 left,
75 right,

~\anaconda3\lib\site-packages\pandas\core\reshape\merge.py in init(self, left, right, how, on, left_on, right_on, axis, left_index, right_index, sort, suffixes, copy, indicator, validate)
625 self.right_join_keys,
626 self.join_names,
--> 627 ) = self._get_merge_keys()
628
629 # validate the merge keys dtypes. We may need to coerce

~\anaconda3\lib\site-packages\pandas\core\reshape\merge.py in _get_merge_keys(self)
1006 join_names.append(None)
1007 else:
-> 1008 left_keys.append(left._get_label_or_level_values(k))
1009 join_names.append(k)
1010 if isinstance(self.right.index, MultiIndex):

~\anaconda3\lib\site-packages\pandas\core\generic.py in _get_label_or_level_values(self, key, axis)
1690 values = self.axes[axis].get_level_values(key)._values
1691 else:
-> 1692 raise KeyError(key)
1693
1694 # Check for duplicates

KeyError: '開催'

となりました。

このクラスでスクレイピングをして、プロセッシングをしても、["開催"]がKeyで入っていないんですよね・・・見た感じだと。

解決しました。
class ShutubaTable(DataProcessor):
  #前処理
def preprocessing(self):
  ~中略~
# 不要な列を削除→どの列を使うか
df = df[['枠', '馬番', '斤量', 'course_len', 'weather','race_type',
'ground_state', 'date', 'horse_id', 'jockey_id', '性', '年齢',
'体重', '体重変化','開催']]
          ↑ こいつが入っていませんでした。

Returnクラスの単勝(複勝等も)が出力できません。
Returnクラスを本と同様に定義したあと

return_tables = Return.read_pickle(['return_tables.pickle'])
rt = Return(return_tables)
rt.tansho #jupyterで表示

とすると以下のエラーが出ます。

TypeError Traceback (most recent call last)
<ipython-input-37-adcad8812fe5> in <module>
1 return_tables = Return.read_pickle(['return_tables.pickle'])
2 rt = Return(return_tables)
----> 3 rt.tansho #jupyterで表示

<ipython-input-32-45ce5e0e672c> in tansho(self)
67 @property
68 def tansho(self):
---> 69 tansho = self.return_tables[self.return_tables[0]=='単勝'][[1,2]]
70 tansho.columns = ['win', 'return']
71

TypeError: 'Return' object is not subscriptable

どなたか原因と解決方法をご教示いただけないでしょうか。

Return.read_pickleはReturnクラスのインスタンスを返しているようなので、
以下でうまくいくのではないでしょうか?

rt = Return.read_pickle(['return_tables.pickle'])
#rt = Return(return_tables)
rt.tansho #jupyterで表示

もしくはReturnクラスのコンストラクタを使うなら以下でしょうか。

#return_tables = Return.read_pickle(['return_tables2.pickle4'])
rt = Return(pd.read_pickle('return_tables.pickle'))
rt.tansho #jupyterで表示

python自体はあまり詳しくないのですが、助けになれば幸いです。

ありがとうございます!無事解決しました。

単勝適正回収率 修正版において、ShutubaTable クラスのスクレイピングを"単勝"オッズをスクレイピングするため、以前のchrome driverを使用したバージョンにしないといけないと思うのですがいかがでしょうか?

そこで、chromedriverを使用して以前の処理+BeautifulSoupバージョンのような処理をすると、以下のような状態が発生します。
1 取消のrowだけcolumnずれを起こします。
  例えば、11列に記載されているはずの情報が、10列に記載されています。
 →これは、preprocessing()の段階で、その行だけ削除すれば問題ありません。

2  障害レースがある場合、本来なら右回り、左周りが書かれている部分に”芝”などの情報が入るため、通常の列+1のサイズのdfが出来てしまいます。
 → こっちのほうは厄介でした。結局、row毎、appendする前に、「障害」がある場合に、del row[17]をしてあげる必要がありました。

ソースを、一応載せておきます。ただし、このソースでスクレイピングすると時間がかかります。私のパソコンで(性能 core i 5 3470)1日分のレース(36レース)だと、8分ぐらいかかりました。


class ShutubaTable(DataProcessor):
    def __init__(self, shutuba_tables):
        super(ShutubaTable, self).__init__()
        self.data = shutuba_tables
    @classmethod
    def scrape(cls, race_id_list, date):
        data = pd.DataFrame()
        options = ChromeOptions()
        options.add_argument('--headless')
        driver = Chrome(options=options)
        for race_id in tqdm(race_id_list):
            url = 'https://race.netkeiba.com/race/shutuba.html?race_id=' + race_id
            driver.get(url)
            elements = driver.find_elements_by_class_name('HorseList')
            for element in elements:
                tds = element.find_elements_by_tag_name('td')
                race_infos = driver.find_elements_by_class_name('RaceData01')
                row = []
                for td in tds:
                    row.append(td.text)
                    if td.get_attribute('class') in ['HorseInfo', 'Jockey']:
                        href = td.find_element_by_tag_name('a').get_attribute('href')
                        row.append(re.findall(r'\d+', href)[0])
                for race_info in race_infos:
                    texts = re.findall(r'\w+', race_info.text)
                    for text in texts:
                        if 'm' in text:
                            row.append(int((re.findall(r'\d+', text)[0])))
                        if text in ["曇", "晴", "雨", "小雨", "小雪", "雪"]:
                            row.append(text)
                        if text in ["良", "稍重", "重"]:
                            row.append(text)
                        if '不' in text:
                            row.append('不良')
                        # 2020/12/13追加
                        if '稍' in text:
                            row.append('稍重')
                        if '障' in text:
                            row.append('障害')
                        if '芝' in text:
                            row.append('芝')
                        if 'ダ' in text:
                            row.append('ダート')
                        
                        #if "障" in text:
                        #    print(row)#del row[17]
                    row.append(date)
                    
                #2/9障害レースの時に削除
                if "障害" in row:
                    del row[17]
                
                data = data.append(pd.Series(row, name=race_id))
            time.sleep(1)
        driver.close()
        return cls(data)
        
    def preprocessing(self):
        df = self.data.copy()
#2/9取消の馬のために2を追加し、印と定義する。
        df = df[[0,1,2,3,4,5,6,7,8,10,11,12,15,16,17,18,19]]
        df.columns = ['枠', '馬番','印', '馬名', 'horse_id', '性齢', '斤量', '騎手', 'jockey_id',
                        '馬体重', '単勝', '人気','course_len','race_type','weather','ground_state','date']
        df = df[df["印"] == '--']#2/9取消の馬を削除
        df['course_len'] = df['course_len'].astype("int")
        df['単勝'] = df['単勝'].astype("float")
        df['人気'] = df['人気'].astype("int")

        df["性"] = df["性齢"].map(lambda x: str(x)[0])
        df["年齢"] = df["性齢"].map(lambda x: str(x)[1:]).astype(int)

        # 馬体重を体重と体重変化に分ける
        df = df[df["馬体重"] != '--']
        df["体重"] = df["馬体重"].str.split("(", expand=True)[0].astype(int)
        df["体重変化"] = df["馬体重"].str.split("(", expand=True)[1].str[:-1]
        # 2020/12/13追加:増減が「前計不」などのとき欠損値にする
        df['体重変化'] = pd.to_numeric(df['体重変化'], errors='coerce')
        df["date"] = pd.to_datetime(df["date"])
        df['枠'] = df['枠'].astype(int)
        df['馬番'] = df['馬番'].astype(int)
        df['斤量'] = df['斤量'].astype(float).astype(int)
        df['開催'] = df.index.map(lambda x:str(x)[4:6])
        df = df[['枠', '馬番', '斤量', 'course_len', 'weather','race_type',
        'ground_state', 'date', 'horse_id', 'jockey_id', '性', '年齢',
        '体重', '体重変化','開催','単勝']]
        print(df)
        self.data_p = df.rename(columns={'枠': '枠番'})

ソースコードありがとうございます!

現在のモデルで実際に賭けることを想定すると、馬体重などが直前でないと出馬表に入ってこないので、レース直前に1レース分スクレイピングして予測し、賭ける馬を決める感じになると思います。
なので今の段階だと直接単勝オッズを見て手動で賭ける金額を決めた方が早い気がします。
ちゃんと回収率を狙おうとするとおそらく賭ける馬は多くても1レースに一頭でしょうし、ChromeDriverだとスクレイピングに時間がかかるので、その間に単勝オッズの値も変化してしまいます。
ただ将来的には単勝オッズの値もスクレイピングして使うかもしれません。

HorseResultsクラス内の実際のコードと説明文で差異がありました。

埋め込まれている実際のコード(HorseResultsクラス内96,97行目)

self.target_list = ['着順', '賞金', '着差', 'first_corner',
                            'first_to_rank', 'first_to_final','final_to_rank']

説明文
"この列と着順、着差、賞金をまとめて処理したいので、preprocessing関数内でリストにして変数に保存しておきます。"

self.target_list = ['着順', '賞金', '着差', 'first_corner', final_corner,
                    'first_to_rank', 'final_to_rank', 'first_to_final']

説明文の方が正しいのかなと思いますが、ご確認よろしくお願い致します。

回収率を上げるための提案として、新馬戦を除くというのはいかがでしょうか?
新馬戦は血統しか活かせず、調教やパドックといった要素が大きくなってしまうからです。
ご検討宜しくお願いします。

いつも勉強させていただいております。

さて、LightGBMによる学習で発生するエラーについてご教示ください。

「ValueError: For early stopping, at least ~」のエラーが生じたため
本誌のとおり、 パラメータからearly_stopping_roundの行を削除しましたが
解決しません。

またearly stoppingに起因するものか全くわかりませんが、複数のエラー表示
が出ております。

恐縮ですが、エラーの対処をご教示ください。よろしくお願いします。


ValueError Traceback (most recent call last)
<ipython-input-48-0f3210d7fe74> in <module>
8
9 lgb_clf = lgb.LGBMClassifier(**lgb_clf_o.params)
---> 10 lgb_clf.fit(X_train.values, y_train.values)

~\anaconda3\lib\site-packages\lightgbm\sklearn.py in fit(self, X, y, sample_weight, init_score, eval_set, eval_names, eval_sample_weight, eval_class_weight, eval_init_score, eval_metric, early_stopping_rounds, verbose, feature_name, categorical_feature, callbacks, init_model)
845 valid_sets[i] = (valid_x, self._le.transform(valid_y))
846
--> 847 super(LGBMClassifier, self).fit(X, _y, sample_weight=sample_weight,
848 init_score=init_score, eval_set=valid_sets,
849 eval_names=eval_names,

~\anaconda3\lib\site-packages\lightgbm\sklearn.py in fit(self, X, y, sample_weight, init_score, group, eval_set, eval_names, eval_sample_weight, eval_class_weight, eval_init_score, eval_group, eval_metric, early_stopping_rounds, verbose, feature_name, categorical_feature, callbacks, init_model)
610 init_model = init_model.booster_
611
--> 612 self._Booster = train(params, train_set,
613 self.n_estimators, valid_sets=valid_sets, valid_names=eval_names,
614 early_stopping_rounds=early_stopping_rounds,

~\anaconda3\lib\site-packages\lightgbm\engine.py in train(params, train_set, num_boost_round, valid_sets, valid_names, fobj, feval, init_model, feature_name, categorical_feature, early_stopping_rounds, evals_result, verbose_eval, learning_rates, keep_training_booster, callbacks)
260 try:
261 for cb in callbacks_after_iter:
--> 262 cb(callback.CallbackEnv(model=booster,
263 params=params,
264 iteration=i,

~\anaconda3\lib\site-packages\lightgbm\callback.py in _callback(env)
217 def _callback(env):
218 if not cmp_op:
--> 219 _init(env)
220 if not enabled[0]:
221 return

~\anaconda3\lib\site-packages\lightgbm\callback.py in _init(env)
187 return
188 if not env.evaluation_result_list:
--> 189 raise ValueError('For early stopping, '
190 'at least one dataset and eval metric is required for evaluation')
191

ValueError: For early stopping, at least one dataset and eval metric is required for evaluation

9 lgb_clf = lgb.LGBMClassifier(**lgb_clf_o.params)
---> 10 lgb_clf.fit(X_train.values, y_train.values)

を見ると削除前のパラメータのようですが、削除後のパラメータを使いましたか?

いつも拝見させて頂いてます。
python歴半年未満のため稚拙な質問でしたら申し訳ありません。
動画を参考に競馬予想できる形にはしましたが、私も最新の動画のように回収率が約80%になりました。
そこで回収率を上げるため以下を行いました。

・threshold値を変更する (0.5より上げる)
 ・predict関数の変更
   ①確率を生のデータに戻し、そこに単勝オッズを賭けた値(期待値)で賭ける賭けないを判定
   ②期待値と確率両方で判定
 ・上記を使い、複勝、三連複で賭ける
 ・特徴量削減(馬の過去データを使用しない)
 ・データ期間変更(2012年以降→2016年以降に変更)

これらをgain関数を使いプロットしましたがどれをやっても回収率80%あたりで収束しています。
別サイト https://result.db-keiba.com/#! で「前走馬体重(525kg以上)」&「脚質(逃)」&「距離増減(-200m)」と設定するだけで単勝回収率は100%を超えるので、本AIが80%にとどまっているのに違和感を感じております。
急ぎませんが、何か見落しや改善策などあればアドバイス頂けるとありがたいです。

このサイトで単勝回収率100%越えるときは、どの期間のものを何枚くらい賭けた時ですか?
この本のモデルでもthresholdの高いところ(賭ける枚数の少ないところ)では100%を超えているので、枚数を絞った買い方になっているのかなと思いました。

返信ありがとうございます。
確かに8年で約100枚に絞った結果でしたので100%を超えてるのだと納得いたしました。

もう一つ質問させてください。
特徴量のひとつに馬齢を使ってますが、私のモデルではfeature_importanceで確認すると最も重要なものになってます。lightgbmの中身を理解してないので的外れな質問かもしれませんが、2歳限定や3歳限定のレースがあるため、2,3歳馬の3着内数が確実に一定数生じるためと考えてます。馬齢はそのまま使っても大丈夫でしょうか?

LigthGBMは交互作用も反映するので、例えば
「馬年齢2歳で、馬の成績が〜の時は勝ちやすい」
という処理をしているかもしれないので、基本的には精度が上がるかどうかで特徴量の良し悪しは判断します。実際に馬齢を省いてみると回収率はどうなりますか?

交互作用を反映するのならあえて無くす必要はないかもしれませんね。実際馬齢省いた場合は回収率は0.01ポイント下がるので効いているようです。今後も気になる特徴量はとりあえず入れて検証するスタンスで進めようと思います。ありがとうございました。

回収率検証の際に実行ごとのぶれが大きいので、TimeSeriesSplitを用いて時系列交差検証を用いると安定かつ汎用化できる気がします(まだ実装ができていませんので検証不十分ですが…)。
ご検討ください。

管理者の方、読者の方、

この一連のコードの作成、これからの作成において、
どのような書籍が参考になるでしょうか??あるいは、皆さんはどのような参考書を使っていますか?
初級、中級、上級と3レベルぐらい本を揃えておきたいのですか。
ご教示お願い致します。

BAさんのプログラミング、競馬の経験値がどれ程か分かりませんが、私は、ユーチューブやネットで本当に基礎的な部分を学びつつやっています。
管理者と全く同じ者を自分で考えてやるとなると、例えばPythonの基礎的な部分から、pandas等のライブラリーに関する部分、統計学等に関する部分、機械学習アルゴリズムに関する部分及びそれらを取り合うライブラリの部分と競馬に関する考え方を機械学習アルゴリズムに落とし込む事を理解出来るような書籍が必要なんだと思います。私にとってはそれぞれがかなり上級の本だと思います。
ただし、それらを自分一人でやるのはほとんど困難だと私は感じるので、ほとんどコピペです。ほとんどはそれで事足ります。

過去レース結果をスクレイピングするとき、
過去動画のようにtqdmで10/150のように10まで進んだと仮定して、
その10のスクレイピング済みのものを再利用するコードにしたいです。
今、この教本にのっているコードではそうなっていないと思います。
過去動画のように、pre_race_resultsを引数に入れたいのですが、、、

今のソースコードだと関数内でDataFrame型に変換してしまっているので、

class Results:
    @staticmethod
    def scrape(race_id_list, pre_race_results=pd.DataFrame()):
        #スクレイピング済みのrace_idを省く
        race_id_list = set(race_id_list) - set(pre_race_results.index)
        race_results = {}
        for race_id in tqdm(race_id_list):
        #同じなので省略
        return pd.concat([pre_race_results, race_results_df]) #pre_race_resultsと結合

とすればできると思います。

oputunaで、gb_clf_o = lgb_o.trainの行を行う際に、
例えば、データに通過コーナーの情報( '1-1-1-1'など)が含まれていると、
could not convert string to float: '1-1-1-1'というエラーが出てしまいます。
しかし、X_trainなどでstringから数値に直しているのではないのでしょうか?
エラーの原因と対処が不明です。ご教示ください。

ひょっとすると、le.corner通過のようなエンコーディングが必要ですか??

ValueError Traceback (most recent call last)
<ipython-input-435-54388b60ed90> in <module>
1 #チューニング実行
----> 2 lgb_clf_o = lgb_o.train(params, lgb_train,
3 valid_sets=lgb_valid,
4 verbose_eval=100,
5 early_stopping_rounds=10)

~/opt/anaconda3/lib/python3.8/site-packages/optuna/integration/_lightgbm_tuner/init.py in train(*args, **kwargs)
32
33 auto_booster = LightGBMTuner(*args, **kwargs)
---> 34 auto_booster.run()
35 return auto_booster.get_best_booster()

~/opt/anaconda3/lib/python3.8/site-packages/optuna/integration/_lightgbm_tuner/optimize.py in run(self)
538 self.sample_train_set()
539
--> 540 self.tune_feature_fraction()
541 self.tune_num_leaves()
542 self.tune_bagging()

~/opt/anaconda3/lib/python3.8/site-packages/optuna/integration/_lightgbm_tuner/optimize.py in tune_feature_fraction(self, n_trials)
563
564 sampler = optuna.samplers.GridSampler({param_name: param_values})
--> 565 self._tune_params([param_name], len(param_values), sampler, "feature_fraction")
566
567 def tune_num_leaves(self, n_trials: int = 20) -> None:

~/opt/anaconda3/lib/python3.8/site-packages/optuna/integration/_lightgbm_tuner/optimize.py in _tune_params(self, target_param_names, n_trials, sampler, step_name)
638 _timeout = None
639 if _n_trials > 0:
--> 640 study.optimize(
641 objective,
642 n_trials=_n_trials,

~/opt/anaconda3/lib/python3.8/site-packages/optuna/study.py in optimize(self, func, n_trials, timeout, n_jobs, catch, callbacks, gc_after_trial, show_progress_bar)
374 If nested invocation of this method occurs.
375 """
--> 376 _optimize(
377 study=self,
378 func=func,

~/opt/anaconda3/lib/python3.8/site-packages/optuna/_optimize.py in _optimize(study, func, n_trials, timeout, n_jobs, catch, callbacks, gc_after_trial, show_progress_bar)
61 try:
62 if n_jobs == 1:
---> 63 _optimize_sequential(
64 study,
65 func,

~/opt/anaconda3/lib/python3.8/site-packages/optuna/_optimize.py in _optimize_sequential(study, func, n_trials, timeout, catch, callbacks, gc_after_trial, reseed_sampler_rng, time_start, progress_bar)
162
163 try:
--> 164 trial = _run_trial(study, func, catch)
165 except Exception:
166 raise

~/opt/anaconda3/lib/python3.8/site-packages/optuna/_optimize.py in _run_trial(study, func, catch)
260
261 if state == TrialState.FAIL and func_err is not None and not isinstance(func_err, catch):
--> 262 raise func_err
263 return trial
264

~/opt/anaconda3/lib/python3.8/site-packages/optuna/_optimize.py in _run_trial(study, func, catch)
209
210 try:
--> 211 value_or_values = func(trial)
212 except exceptions.TrialPruned as e:
213 # TODO(mamu): Handle multi-objective cases.

~/opt/anaconda3/lib/python3.8/site-packages/optuna/integration/_lightgbm_tuner/optimize.py in call(self, trial)
247 kwargs = copy.copy(self.lgbm_kwargs)
248 kwargs["valid_sets"] = self._copy_valid_sets(kwargs["valid_sets"])
--> 249 booster = lgb.train(self.lgbm_params, train_set, **kwargs)
250
251 val_score = self._get_booster_best_score(booster)

~/opt/anaconda3/lib/python3.8/site-packages/lightgbm/engine.py in train(params, train_set, num_boost_round, valid_sets, valid_names, fobj, feval, init_model, feature_name, categorical_feature, early_stopping_rounds, evals_result, verbose_eval, learning_rates, keep_training_booster, callbacks)
229 # construct booster
230 try:
--> 231 booster = Booster(params=params, train_set=train_set)
232 if is_valid_contain_train:
233 booster.set_train_data_name(train_data_name)

~/opt/anaconda3/lib/python3.8/site-packages/lightgbm/basic.py in init(self, params, train_set, model_file, model_str, silent)
2051 break
2052 # construct booster object
-> 2053 train_set.construct()
2054 # copy the parameters from train_set
2055 params.update(train_set.get_params())

~/opt/anaconda3/lib/python3.8/site-packages/lightgbm/basic.py in construct(self)
1319 else:
1320 # create train
-> 1321 self._lazy_init(self.data, label=self.label,
1322 weight=self.weight, group=self.group,
1323 init_score=self.init_score, predictor=self._predictor,

~/opt/anaconda3/lib/python3.8/site-packages/lightgbm/basic.py in _lazy_init(self, data, label, reference, weight, group, init_score, predictor, silent, feature_name, categorical_feature, params)
1121 self.__init_from_csc(data, params_str, ref_dataset)
1122 elif isinstance(data, np.ndarray):
-> 1123 self.__init_from_np2d(data, params_str, ref_dataset)
1124 elif isinstance(data, list) and len(data) > 0 and all(isinstance(x, np.ndarray) for x in data):
1125 self.__init_from_list_np2d(data, params_str, ref_dataset)

~/opt/anaconda3/lib/python3.8/site-packages/lightgbm/basic.py in __init_from_np2d(self, mat, params_str, ref_dataset)
1160 data = np.array(mat.reshape(mat.size), dtype=mat.dtype, copy=False)
1161 else: # change non-float data to float data, need to copy
-> 1162 data = np.array(mat.reshape(mat.size), dtype=np.float32)
1163
1164 ptr_data, type_ptr_data, _ = c_float_array(data)

ValueError: could not convert string to float: '1-1-1-1'

馬の過去成績データの方でもレース結果でもいいと思いますが、例えば、レース結果をlightgbmで学習させると、データベースのレース結果のページに通過の情報があると思います。そのため、目的変数をrankにすると通過の欄はrankを予測するための材料に使用できます。今は、第一コーナーと最終コーナーの位置を使って学習させているとおもいますが、確か前に、lightgbmはpedsなどの文字情報も扱えると動画内であったので、例えば、1-1-1-1とすべてのコーナーにおいて逃げているというようなハイフン付きの情報(1-2-3-4や1-3などの場合もあると思います)も学習できるのではないかと考えたのですが、、、
今のコードでは何かとうまくいかなくてどうすればいいのか悩んでいます。

忘れていました。ありがとうございます。
ぜひとも物理学専攻らしい発想に基づくAIを開発したいものです。(差別化のため。)
今の動画は古典統計学と線形代数であって、量子統計とか重ね合わせ状態などを競馬に置き換えたらどうなるんだろうなど少し思ったりします。
最後に、修士修了されたと思いますので、おめでとうございます!これからも楽しみにしております。

素人で申し訳ないのですが
動画中のソースコード(第1回〜第4回)の一番最初のプログラムを打つと以下のような分が出力されます

HBox(children=(FloatProgress(value=0.0, max=7200.0), HTML(value='')))

これはどういう意味なのでしょうか、
うまく実行できていないということでしょうか。
解決策やどういう意味なのか教えていただけますでしょうか。

返信いただきありがとうございます。

tqdmの方のダウンロードでエラーが出ていました。
そこを改善すると、動画通りプログレスバーの確認ができました。

特徴量に頭数を追加したいのですが、良い方法はありますでしょうか。
HorseResultにはもともと入っているので問題ないのですが、Resultには入っておらず、以下のコードを追加して試しましたがfor文で回しているため膨大な時間がかかります。レースIDが一致している数を数える方法なのですが…

df['num_horse'] = pd.NA
for i in range(len(df)):
    df['num_horse'][i] = (df.index == df.index[i]).sum()

改善案、代替案があればご教示いただきたいです。

私はrace_idごとの馬番の数を数えたdfを作成してマージしています。

tosu = results.groupby(level=0)[['馬番']].count().rename(columns={'馬番': '頭数'})
results = results.merge(tosu, left_index=True, right_index=True, how='left')

もっといいやり方があるかもしれませんが、助けになれば幸いです。

ありがとうございます。参考になります。

お世話になります。初めての書き込み失礼します。

競馬予想で始める機械学習〜完全版〜のChapter5 モデル評価の部分でエラーが出てしまい、解決できませんでしたのでこちらで質問させていただきます。

こちらが該当部分のコードです。

#ModelEvaluatorクラスのオブジェクトを作成
me = ModelEvaluator(lgb_clf,  './data/scraped_data/%s/return_tables.pickle' % year)

#単勝適正回収値=払い戻し金額が常に一定になるように賭けた場合の回収率
g_proper = gain(me.tansho_return_proper, X_test)
#単勝の回収率
g_tansho = gain(me.tansho_return, X_test)

#プロット
plt.plot(g_proper.index, g_proper['return_rate'])
plt.plot(g_tansho.index, g_tansho['return_rate'])
plt.grid()

そしてエラー内容です。

---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
<ipython-input-109-257e9404f607> in <module>
      3 
      4 #単勝適正回収値=払い戻し金額が常に一定になるように賭けた場合の回収率
----> 5 g_proper = gain(me.tansho_return_proper, X_test)
      6 #単勝の回収率
      7 g_tansho = gain(me.tansho_return, X_test)

<ipython-input-82-fe9216fe0c83> in gain(return_func, X, n_samples, min_threshold)
      4     for i in tqdm(range(n_samples)):
      5         threshold = 1 * i / n_samples + min_threshold * (1-(i/n_samples))
----> 6         n_bets, return_rate, n_hits = return_func(X, threshold)
      7         if n_bets > 2:
      8             gain[n_bets] = {'return_rate': return_rate, 

<ipython-input-107-a4af226328d6> in tansho_return_proper(self, X, threshold)
     66     def tansho_return_proper(self, X, threshold=0.5):
     67         #モデルによって「賭ける」と判断された馬たち
---> 68         pred_table = self.pred_table(X, threshold)
     69         n_bets = len(pred_table)
     70 

<ipython-input-107-a4af226328d6> in pred_table(self, X, threshold, bet_only)
     34 
     35     def pred_table(self, X, threshold=0.5, bet_only=True):
---> 36         pred_table = X.copy()[['馬番', '単勝']]
     37         print(pred_table)
     38         pred_table['pred'] = self.predict(X, threshold)

/opt/anaconda3/lib/python3.8/site-packages/pandas/core/frame.py in __getitem__(self, key)
   2906             if is_iterator(key):
   2907                 key = list(key)
-> 2908             indexer = self.loc._get_listlike_indexer(key, axis=1, raise_missing=True)[1]
   2909 
   2910         # take() does not accept boolean indexers

/opt/anaconda3/lib/python3.8/site-packages/pandas/core/indexing.py in _get_listlike_indexer(self, key, axis, raise_missing)
   1252             keyarr, indexer, new_indexer = ax._reindex_non_unique(keyarr)
   1253 
-> 1254         self._validate_read_indexer(keyarr, indexer, axis, raise_missing=raise_missing)
   1255         return keyarr, indexer
   1256 

/opt/anaconda3/lib/python3.8/site-packages/pandas/core/indexing.py in _validate_read_indexer(self, key, indexer, axis, raise_missing)
   1302             if raise_missing:
   1303                 not_found = list(set(key) - set(ax))
-> 1304                 raise KeyError(f"{not_found} not in index")
   1305 
   1306             # we skip the warning on Categorical

KeyError: "['単勝'] not in index"

return_tablesの保存先以外は特にいじっていません。gain関数やModelEvaluator, Returnクラスはそのまま使わせていただいております。また、return_tablesの中身はこのようになっており問題なく思えます。

解決法がわかる方がいらっしゃいましたら、ご教授のほど何卒よろしくお願いいたします。

X_testに単勝の列がないのでは?と思います。
ModelEvaluatorのロジックを見直した回でX_testに単勝を追加していたと思うので、
動画を見直してみれば解決するのではないでしょうか?

助けになれば幸いです。

お世話になります。
早速のお返事ありがとうございます。確かにX_testの中身に単勝の列がなかったみたいです。
動画を参考に確認したところ、Chapter3のデータ加工・前処理編のResultsクラス、126行目に単勝列が削除列として含まれたままだったのでそこを修正したら問題なく動作しました。助かりました。
これからもよろしくお願いいたします。

お世話になっております。

現在、動画(第1回)を見てスクレイピング作業をしているのですが
スクレイピングが途中で終了してしまう問題が発生しております。
特にエラーが出て終了というわけではなく、300件ほどのデータをスクレイピングすると
処理が終わってしまいます。
この問題について、どのような原因が考えられるのか
または、対処法をご存知でしたら教えていただけると助かります。

書いてあるソースコードはほぼ動画のとおりです。
tqdmのみ自分の環境では動作しないことが判明しましたので書いておりません。

また、動画中では2019年度のデータのリストを作成していたと思いますが
2020年度のデータのリストを作成しております。

def scrape_race_results(race_id_list, pre_race_results={}):
    race_results = pre_race_results
    for race_id in race_id_list:
        #途中でまで取得しているデータ分がある場合それらのデータをスキップ
        if race_id in race_results.keys():
            continue
        try:
            time.sleep(1)
            url = 'https://db.netkeiba.com/race/' + race_id
            race_results[race_id] = pd.read_html(url)[0]
            
        except IndexError:
            continue
            
        except:
            break
    return race_results

race_id_list = []
#開催場所は10箇所ある
for place in range(1,11,1):
    #開催場所で最大6回行われる
    for kai in range(1,7,1):
        #1回に付き最大12日行われる
        for day in range(1,13,1):
            #1日に付き12レースが開催される
            for race in range(1,13,1):
                race_id = '2020'\
                         + str(place).zfill(2)\
                         + str(kai).zfill(2)\
                         + str(day).zfill(2)\
                         + str(race).zfill(2)
                race_id_list.append(race_id)

test = scrape_race_results(race_id_list)

返信ありがとうございます。

先程スクレイピングをしたところ、202004020612のkeyを最後に
スクレイピングが終了していました。

202002020612
試しにもう一度やったところ、上記のを最後にスクレイピングが終了していました。

ありがとうございます。

指示通りに例外処理を変更して、再度スクレイピングしたところ
以下のエラーが出ました。
これはスクレイピング時にはよく出るエラーなのでしょうか?

<urlopen error [Errno 50] Network is down>

お世話になります。
2021年のスクレイピングで以下のIndexErrorが出ました。

2020年までは問題なかったのですがサイト側のソースを見ても
解決できなかったので改善案があればよろしくお願いします。

url = 'https://db.netkeiba.com/race/202110020807/'
df = pd.read_html(url)[0]

時間をあけて再度トライしてみたら難なくいけました
原因がよく分かりませんがお騒がせしました。

データ加工をしている時になぜか教科書のように賞金5R 着順5Rを表示することができません。
解決策が見つからないので教えていただきたいです。

表示される列が省略されているだけだと思います!例えば

r.data_h.iloc[:, 10:20]

と実行すれば、r.data_hの10列目〜20列目が表示できるので、これでどうですか?

無事表記することができました。ありがとうございます。
レベルの低い質問をしてすみません。

optunaを利用しようとすると以下エラーが出るようになりました。
AttributeError: partially initialized module 'lightgbm' has no attribute 'Dataset' (most likely due to a circular import)
以前は出ていなかったのですが解決策に心当たりの方がいらっしゃいましたらご教授ください。

ファイル名をlightgbmとしていたためインポート先が不正になっていたことが原因でした。初歩的なミスでした。。。

お世話になっております。
最近勉強を始めた初心者なのですが、ご質問がございます。

第一回の動画を拝見させていただき、動画の内容通りにしたのですが下記エラーが出てきました。
自分なりに調べたのですが、解決できない為ご教授ください。

データ取得中にエラーが出ました。
<urlopen error [WinError 10013] アクセス許可で禁じられた方法でソケットにアクセスしようとしました。>

途中からちょっとずつ取得したら無事にできました。
ありがとうございます。

続けてすみません。
単勝回収率を計算する際に、各レースの指数1位と指数2位の差が一定以上なら賭ける、という条件を加えることで回収率が上がると思い、実装しようと試行錯誤しましたが、力不足でうまくいきませんでした。どなたか、お助けください…。

素晴らしいアイデアだと思います。
スマートではないですが、race_idごとの指数1位、指数2位を算出して、
差を求めるやり方が私はわかりやすいです。

df_1 = pred_table.sort_values(['pred'], ascending=[False]).groupby(level=0)[['pred']].nth(0).add_suffix('_1')
df_2 = pred_table.sort_values(['pred'], ascending=[False]).groupby(level=0)[['pred']].nth(1).add_suffix('_2')
pred_table = pred_table.merge(df_1, left_index=True, right_index=True, how='left')
pred_table = pred_table.merge(df_2, left_index=True, right_index=True, how='left')
pred_table['pred_diff'] = pred_table['pred_1'] - pred_table['pred_2']
pred_table
馬番	pred	pred_1	pred_2	pred_diff

202006020101 8 0.679014 0.679014 0.521127 0.157887
202006020101 7 0.236240 0.679014 0.521127 0.157887
202006020101 9 0.281369 0.679014 0.521127 0.157887
202006020101 6 0.414115 0.679014 0.521127 0.157887
202006020101 12 0.059108 0.679014 0.521127 0.157887
... ... ... ... ... ...
202106020212 3 0.068125 0.422127 0.344886 0.077242
202106020212 4 0.031521 0.422127 0.344886 0.077242
202106020212 2 0.110103 0.422127 0.344886 0.077242
202106020212 1 0.044287 0.422127 0.344886 0.077242
202106020212 10 0.293999 0.422127 0.344886 0.077242
5937 rows × 5 columns

助けになれば幸いです。

ありがとうございます。

pred_tableにpred_diffを追加して回収率をプロットしたところ、以下のようにグラフがおかしくなってしまいました。どんな原因が考えられますか?

おかしいというのは具体的にどの部分のことでしょうか?
グラフが二つ表示されている部分でしょうか?
仮説と結果にギャップがありすぎるということでしょうか?
もう少し詳しく説明(もしくはコードを提示)いただければ答えられるかもしれません。

グラフが2つ表示されている点です。
コードは、dfにpred_diffを定義したあと

df = df[df['pred_diff'] > min_diff]

で範囲を指定しています。

pred_diffは、関数pred_table内で以下のように定義し、returnに追加しています。

pred_1 = pred_table.sort_values(['pred'], ascending=[False]).groupby(level=0)[['pred']].nth(0)
pred_2 = pred_table.sort_values(['pred'], ascending=[False]).groupby(level=0)[['pred']].nth(1)
pred_min = pred_table.sort_values(['pred'], ascending=[False]).groupby(level=0)[['pred']].nth(-1)
pred_table['pred_diff'] =  (pred_1 - pred_2) / (pred_1 - pred_min)

今回の変更でグラフが二つ表示されてしまっているのであれば、

df = df[df['pred_diff'] > min_diff]

の部分をコメントアウトして、以前と同じ結果になるのか確認して、
コード部分に誤りや考慮漏れがないかトレースしてみるのはいかがでしょうか?

私はModelEvaluatorクラスを使った回収率計算ではなく、
自前で回収率計算を行っているので、具体的な修正点を提示するのが難しそうです。
賭けると判断したrace_idのみをX_testから抽出して試してみるのもひとつの手かもしれません。

コメントアウトするとグラフは1つに直りました。どう処理されているかもう一度考え直してみます。

いろいろ試したところ、無事直りました。ありがとうございます。

馬連の回収率シミュレーションを指数1位からの流しにしようとしました。
umaren_return関数の中で以下のコードを追加しようとしたところ、エラーが出て困っています。

pred_table = self.pred_table(X, threshold)
pred = self.predict_proba(X)
pred_table['pred'] = pred

ValueError: cannot reindex from a duplicate axis

こちらの対処法を教えて下さい。

解決しました。

ど素人な質問で申し訳ございませんが、03 データ加工・前処理の部分で以下の様に開催の列がDropされないからErrorになっているのかと思うのですが、対処方法を教えて頂けませんでしょうか。
class DataProcessor:の部分はこちらのZennの内容をそのまま使っております。


三連複軸1頭流し相手5頭の回収率を出してみました。参考までに。
残念ながら回収率はかなり低いですね。複勝回収率も80%程度ですし複系馬券は向いていないのかもしれません。


三連単フォーメーション1-2-5はこんな感じになりました。単系は有望に思えます。

良い数値が出てますね!
横軸ってレース数ですかね?

横軸は賭けた点数ですね。レース数×8です。

返信ありがとうございます!
数値に夢がありますね!
的中率次第ですが、実際に運用する場合この回収率に収束させるために必要な資金がかなり多くなりそうですね。
3連単だと回収率が上がるのは、競馬界では有名なユープロが3連単を大量購入していたのに似ていて面白いですね。

掛け方としては1位の指数のみを閾値で変化させて、指数上位でフォーメーションを組む感じですかね?
(1-2,3-2,3,4,5,6)

賭け方はその通りです。単勝で平均(8割)より少しでも良い回収率が出ていれば、三連単は相乗効果で回収率が更に上がることが多いですね。実際に賭けるとなると、1レース最低600円で、的中率を考えると100連敗しても全然おかしくないと思うので資金と相当な忍耐力が必要です(笑)

>単勝で平均(8割)より少しでも良い回収率が出ていれば、三連単は相乗効果で回収率が更に上がることが多いですね。
なるほど。となると馬単で指数1位→指数2位,指数3位への流しが回収率は3連単より下がりそうですが、的中率が上がって実運用を考えた際は現実的な数値になりそうですね。

再度申し訳ないのですが、第5回の動画内でスプレイピングを実行したのですが
以下のエラーが発生しました。

HTTPSConnectionPool(host='db.netkeiba.com', port=443): Max retries exceeded with url: /race/202010010608 (Caused by NewConnectionError('<urllib3.connection.HTTPSConnection object at 0x00000240BE2BE280>: Failed to establish a new connection: [WinError 10013] アクセス許可で禁じられた方法でソケットにアクセスしようとしました。'))

このエラーは接続が切れたからなのでしょうか?
その場合はscrape_race_results(race_id_list[:])で続きを取得する認識でよろしいでしょうか?

ShutsubaTableのデータ加工でエラーが出るようになりました。対処法をご教示いただきたいです。

ValueError: You are trying to merge on object and float64 columns. If you wish to proceed you should use pd.concat

Resultsは問題なく加工できます。ShutsubaTableとResultsの差異を考えましたが原因がわかりません…

過去レースをmergeするところで['開催']に問題があったようです。無事直りました。

youtubeを含め、楽しく勉強させていただいております。
chapter2について質問させてください。いきなりつまづき、お恥ずかしい限りです

Peds.scrape()の最後、「スクレイピングできたら、同様にpeds.pickleに保存してください」とありますが、どのようなコードを書けば良いのか分かりません。自分なりに模索しましたが、うまくいきませんでした。

おそらくReturn.scrape()でも、同じことが起こるのだと思います。
ご指導いただければ幸いです。よろしくお願いします

Pedsの場合は下記のようにしました。

peds_results = Peds.scrape(horse_id_list)
peds_results.to_pickle("peds.pickle") ←ここで"peds.pickle"に保存しています

コメント、ありがとうございました。
ご指導の通りにやったところ、
ValueError: No objects to concatenate
というのが出ました。

ここまでのコードを一通り確認するなど、対策を考えてみたいと思います

改めて、ありがとうございました

無事に前に進めました。コードを全体的に見直して、うまくいきました。
改めて、ご指導、ありがとうございました

血統データをレース毎または日毎に更新できるようにしたいのですが、どうすればよいでしょうか。

peds_old = Peds.read_pickle(['peds.pickle'])    
peds_new = Peds.scrape(horse_id_list)
peds = update_data(peds_old, peds_new)
peds.to_pickle('peds.pickle')

で実行すると
AttributeError: 'Peds' object has no attribute 'index'
となります。この解決方法がわかりません。

peds_old = pd.read_pickle('peds.pickle')    
peds_new = Peds.scrape(horse_id_list)
peds = update_data(peds_old, peds_new)
peds.to_pickle('peds.pickle')

これでうまくいきました。

いつも楽しく拝見しています。
『競馬予想で始める機械学習~完全版~』を見ながら、動画のコードを実行し、いろいろと試しています。
「第14回 Pythonで単勝回収率122%の競馬予想AIを作る方法」のコードを実行し、2019年のデータでは単勝回収率120%オーバーは再現できたのですが、自分でスクレイピングした2020年のデータで試すと、横軸が4000ぐらいのときに回収率が0.82029ぐらいのグラフとなります。
年によって、これほどまでに成績が変わるものなのでしょうか?
それとも、処理等が間違っているのでしょうか。
同じように2020年や2021年のデータで試された方がいらっしゃいましたら、アドバイスをいただけると嬉しいです。
よろしくお願いいたします。

みなさま、
32bitのPC環境でlightgbmやoptunaを導入できた方はいらっしゃいますか。
この環境が整わない限り、コードが動かない印象です。
何か方法をご存じの方、ご教示お願いします。

皆様こんにちわ。
本書のスクレイピング→データ加工で作成した「r.data_c」と「st.data_c」を使用して
PyTorchを使用したディープラーニングに挑戦しています。
r.data_cをトレーニングデータとテストデータに分ける際にr.data_c.fillna(0)として欠損値を0埋めしているのですがst.data_c.fillna(0)とすると下記のエラーが返ってきます。

ValueError: Cannot setitem on a Categorical with a new category, set the categories first

調べて見たのですがいまいちピンとくる回答がありません。
どなたかお分かりになる方いらっしゃいませんでしょうか?

ログインするとコメントできます