Open401

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

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

動画では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を用いて時系列交差検証を用いると安定かつ汎用化できる気がします(まだ実装ができていませんので検証不十分ですが…)。
ご検討ください。

過去レース結果をスクレイピングするとき、
過去動画のように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と結合

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

素人で申し訳ないのですが
動画中のソースコード(第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の内容をそのまま使っております。

再度申し訳ないのですが、第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[:])で続きを取得する認識でよろしいでしょうか?

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
というのが出ました。

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

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

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

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

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

いつも参考になる動画をアップロードして下さり、ありがとうございます。
購読させていただき、実際にノートブックを作成しているのですが1点質問させてください。
「競馬予想で始める機械学習〜完全版〜、06実際に賭けてみた」の章でレースデータを取得しモデルのpredictを行う際、下記の様に記載されております。

#馬が勝つ確率を予測
pred = me.predict_proba(st.data_c.drop(['date'], axis=1))

手元のノートブックで試したところ、「st.data_c.drop(['date'], axis=1)」を実行した場合、特徴量の順番が学習時データセットの特徴量の順番と異なっている気がするのですが認識あってますでしょうか。
lightGBMを利用しているので、学習時の特徴量順番と合わせなければ、正しく予測が出来ないと思い質問させていただきました。

以下のように出力して確認してみるのがいいかもしれませんね。
romacさんの質問で私自身、いくつかコード誤りを発見できました。
ありがとうございます。

for i in range(0, len(X_test.columns), 10):
    display(X_test.iloc[0:2, i:i+10])
for i in range(0, len(st.data_c.drop(['date'], axis=1).columns), 10):
    display(st.data_cc.drop(['date'], axis=1).iloc[0:2, i:i+10])

助けになれば幸いです。

お返事ありがとうございます。
公開されているコードをコピペして自身のノートブックで試した場合、カラムの順番が誤っていたので、他の方々はどうなのかと思い質問いたしました。
なので自身のノートブックでは以下のように修正しております。
col = list(X_test.columns)
st.data_c[col]

初めてコメントします。素晴らしい教材で勉強の意欲が掻き立てられます。
質問なのですが「動画中のソースコード(1~4)」の「第4回 ロジスティック回帰で競馬予想してみた」のRandomUnderSamplerを使うところでこんなエラーがでました。

AttributeError Traceback (most recent call last)
<ipython-input-4-c8c2b746a92a> in <module>
30 sampling_strategy={1: rank_1, 2: rank_2, 3: rank_3, 4: rank_1}, random_state=71
31 )
---> 32 X_train_rus, y_train_rus = rus.fit_sample(X_train.values, y_train.values)
33
34 from sklean.linear_model import LogisticRegression

AttributeError: 'RandomUnderSampler' object has no attribute 'fit_sample'

imblearnのインストールで少してこずったので、もしかしら上手くインストールできていないせいかもしれいと思いコメントしました。
今までのコメントでこの箇所のこと出てきてないってことは私の環境で何かしらの不具合が出たってことかなと思っています。
_sampleを消して実行すると、
cannot unpack non-iterable RandomUnderSampler objectと見たことのないエラーが出てきてちょっと悩困っています。
手がかりのようなものでも構いません。何かご教授願います。

fit_sample → fit_resample
にして解決するようです。

Nobu Sさんわざわざ返信ありがとうございます。
早速試してみます。
いろいろ試してAnacondaをアンインストールして、もう一度初めからscikit-learnとかも導入し直しました。scikit-learnのバージョンを変えたらうまくいったという情報も見かけたのでまた試してみます。

ここの教科書を買って動画を見ながら勉強しているのですがなかなかうまくいかず、いつも第5回か6回で対処できないことにぶつかってしまいます。
質問の第4回を飛ばして第6回のAUCスコアをだす直前のROC曲線のところでまた問題がでまして。
いきなりグラフが直角ででるんですよね。原因をいろいろ考えましたがうまくrace_infosのDataFrameと結合できなかったかもしれないし、そもそもうまくスクレイピングできていたのかもあやしいのです。

Index: 47118 entries, 201901010101 to 201910021212
Data columns (total 17 columns):

Column Non-Null Count Dtype


0 着順 47118 non-null int32
1 枠番 47118 non-null int64
2 馬番 47118 non-null int64
3 馬名 47118 non-null object
4 斤量 47118 non-null float64
5 騎手 47118 non-null object
6 単勝 47118 non-null float64
7 人気 47118 non-null float64
8 course_len 47118 non-null object
9 weather 47118 non-null object
10 race_type 47118 non-null object
11 ground_state 47118 non-null object
12 date 47118 non-null datetime64[ns]
13 性 47118 non-null object
14 年齢 47118 non-null int32
15 体重 47118 non-null int32
16 体重変化 47118 non-null int32
dtypes: datetime64ns, float64(3), int32(4), int64(2), object(7)
memory usage: 4.3+ MB

第5回の最後に保存したresults_addinfoのinfoです。int32とかもありますし、47118っていう数字がそもそもおかしいかもしれません。動画のなかの数字より常に自分がスクレイピングした時の方が多いです。第一回でも動画の中では4800でしたが自分のプログレスバーでは7200とでてきます。

ほとんど前に進めない状況なのでどんなコメントでもありがたいです。

はじめまして。最近Pythonを勉強し始めたばかりです。
JRAでなくてNAR(地方競馬)でデータ分析をやろうと思っています。
「第1回:Pythonで競馬データをスクレイピングする」のスクレイピングするURLの部分だけ変えてやってみたのですがNo table foundとエラーが出ます。
初心者すぎる質問かもしれないのですが教えていただけると助かります。

動画のコードとの変更点は
・try以下のURL部分
・#レースIDのリストを作る。NARの場合、年・競馬場・月・日・レース番号なので作り変えました。

def scrape_race_results(race_id_list, pre_race_results={}):
    race_results = pre_race_results
    for race_id in tqdm(race_id_list):
        if race_id in race_results.keys():
            continue
        try:
            time.sleep(1)
            url = "https://nar.netkeiba.com/race/result.html?race_id=" + race_id
            race_results[race_id] = pd.read_html(url)[0]
        except IndexError:
            continue
        except Exception as e:
            print(e)
            break
        except:
            break
    return race_results


#レースIDのリストを作る
race_id_list = []
place = 46
for month in range(1, 13, 1):
    for day in range(1, 32, 1):
         for r in range(1, 13, 1):
                race_id = "2020" + str(place).zfill(2) + str(month).zfill(2) +\
		str(day).zfill(2) + str(r).zfill(2)
                race_id_list.append(race_id)

Yusuke Isakaさん
ありがとうございます。
こんなにすぐに答えていただけると思わなかったので嬉しいです。
早速、試してみたらできました!

はじめまして。1年ほど前からぼちぼちPythonを勉強し始めたアラフィフ男です。
Pythonで機械学習を学び始めて、面白い題材だなと思い昨年
「競馬予想で始める機械学習〜完全版〜」を購入したのですが、長らく積読していました。
連休ですが緊急事態でどこにも出かけられないので一念発起挑戦しています。
よろしくお願い致します。

Chapter 08から始めて、Chapter 09の「第6回 lightgbm・ランダムフォレストで競馬予想」
まで来ましたが、どうしてもエラーで進めなくなってしまいました。
どなたかヘルプいただけると幸いです。

#ランダムフォレストによる予測モデル作成
from sklearn.ensemble import RandomForestClassifier

rf = RandomForestClassifier(random_state=100)
rf.fit(X_train, y_train)

のrt.fit(X_train,y_train)を実行したところでエラーが出ます。

---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-198-ac1696b17d2e> in <module>
      3 
      4 rf = RandomForestClassifier(random_state=100)
----> 5 rf.fit(X_train, y_train)

(中略)
ValueError: could not convert string to float: 'ルビーカサブランカ'

最初テキスト記載のスクリプト通りにやったのですが、
うまくいかないので、動画を何回も見て以下のようにもしてみましたがまだうまくいきません。

#上で保存したpickleファイルの読み込み
results = pd.read_pickle('results_addinfo.pickle') #第5回の結果を'results_addinfo.pickle'に保存

#前処理
results_p = preprocessing(results)

#着順を0or1にする
results_p['rank'] = results_p['着順'].map(lambda x: 1 if x<4 else 0)
results_p.drop(['着順'], axis=1, inplace=True)

#もし、動画のように着順をdropしてrankを作っている場合は
results_p['rank'] = results_p['rank'].map(lambda x: 1 if x<4 else 0)

train,test = split_data(results_p) # 動画に倣って実行

X_train = train.drop(['rank','date'],axis=1) #動画に倣って実行
y_train = train['rank'] #動画に倣って実行
X_test = test.drop(['rank','date'],axis=1) #動画に倣って実行
y_test = test['rank'] #動画に倣って実行

#ランダムフォレストによる予測モデル作成
from sklearn.ensemble import RandomForestClassifier

rf = RandomForestClassifier(random_state=100)
rf.fit(X_train, y_train)

どのように対処すればよろしいでしょうか。
miniconda環境で都度足らないライブラリをインストールしながら進めていますので、
各種ライブラリのインストールがうまくできていないのかもしれませんが、ご教示お願いいたします。

Windows10 Home 64bit
miniconda 3
conda 4.10.1
jupyter lab 3.0.14
scikit-learn 0.24.1

第6回動画の5:45あたりの馬名dropとダミー変数化はしていますか?
一応確認してみて下さい。

Nobu Sさん、早速のアドバイスありがとうございます。
おかげさまで、解決しました。

ご指摘の通り馬名dropとダミー変数化辺りに注目して
動画の通り忠実にやってみたところ無事実行できました。
これで先に進めます。

変にテキストからのコピペだけに頼りすぎていたのかもしれません。
今後気を付けます。

実は、別の方へのヘルプ
「fit_sample → fit_resample
にして解決するようです。」
にも助けられたりしています^^ゞ

解決してよかったですね。

私も初心者ですが、一応動画だけでポチポチ入力してエラーを繰り返しながら最後まで行っています。
作者さんが仕事で忙しそうなのでお役に立ててよかったです。

ここだけでなく、youtubeのコメントも見ると参考になるものもあります。

お互い頑張りましょう。

連投しましてすみません。
またまたつまずいてしまいましたので、どなたかご教示いただけると助かります。

「競馬予想で始める機械学習~完全版~」Chapter 10の
「第7回 Pythonで過去成績データをスクレイピングする」をやっているのですが、
scrape_horse_results()関数でhorse_resultsをスクレイピングしていると
途中でtqdmのカウンタが進まなくなってしまいます。

エラー表示は出ておらず、そのうち動き出すのかなと2,3時間放置しているのですが、
一旦止まると、その後カウンタは一つも進みません。
そうなると、Jupyter labの「Interrupt the kernel」も効かず、
Restart kernelをするしかありません。
そのため、pre_horse_resultsによる途中再開もできず毎回1からの読み込みです。

止まる際のtqdmカウンタはその時によって異なります。
922/11495まで進むときもあれば、98/11495、354/11495の場合もあります。
forループ内にhorse_IDを表示するようにしておいて、
止まった際にそのhorse_IDでkeiba.comさんのお馬さん情報をみてみると
正しくみられるので、存在しないページを見ているわけではないようです。

# 復旧再開
import pandas as pd
import time
from tqdm.notebook import tqdm

#前回保存したpickleファイルからデータ取得
results = pd.read_pickle('results.pickle')
horse_id_list = results['horse_id'].unique()

def scrape_horse_results(horse_id_list, pre_horse_results={}):
    
    #horse_idをkeyにしてDataFrame型を格納
    horse_results = pre_horse_results
    for horse_id in tqdm(horse_id_list):
        try:
            url = 'https://db.netkeiba.com/horse/' + horse_id
            df = pd.read_html(url)[3]
            #受賞歴がある馬の場合、3番目に受賞歴テーブルが来るため、4番目のデータを取得する
            if df.columns[0]=='受賞歴':
                df = pd.read_html(url)[4]
            
            print(horse_id, df) #監視用
            
            horse_results[horse_id] = df
            time.sleep(1)
        except IndexError:
            continue
        except Exception as e:
            print(e)
            break
        except:
            break

    return horse_results

horse_results = scrape_horse_results(horse_id_list)

race_results()やrace_info()は最後まで読み込めましたのですが、
ここにきてネットの調子が悪いのですかね。
ひたすら待っていれば、スクレイピング進んでくれるのでしょうか。
同じような現象に合われた方、アドバイスいただけると幸いです。

Windows10 Home 64bit
Python 3.8.5
miniconda 3
conda 4.10.1
jupyter lab 3.0.14
scikit-learn 0.24.1

こんにちは。
連休も最終日ですが、まだ「馬の過去成績データ」(horseResults)のスクレイピングで足踏みしたままです。
Chapter 02のクラス版で試しているのですが、
上記と同じようにエラーも出ずにtqdmのカウンタが進まなくなりフリーズしているようです。
途中にprint()文をはさんでデバッグしてみたところ、

        for horse_id in tqdm(horse_id_list):
            try:
                url = 'https://db.netkeiba.com/horse/' + horse_id
                df = pd.read_html(url)[3]

の"df = pd.read_html(url)[3]"を実行するところで止まっているようです。
(デバッガの使い方がよくわからず、print文をはさむという原始的な方法で見ています。)

実行後いきなり止まるのではなく、tqdmのカウンタ100/11702未満で止まるときもあれば、
1000/11702近くまで行くときもあります。
(残念ながら1000超えることがないです。。。)
「レース結果データ」(Results.scrape)のスクレイピングは無事終了しています。
(2020年のデータを取りました。)

try~except文を外してやってみたのですが、
やはりエラーも出してくれずただフリーズしてるっぽいので困っています。
(jupyter lab自体は生きてて、一晩たっても止まったままなのでRestart Kernel and Clear All Outputsで止めました。)

WiFiでやっているのですが、
Ethernetケーブルでルーターに直接つないでやってみようかと思いますが、
何か手立てはないものでしょうか。

私も2021年の4月までのスクレイピングをしている時に同じようなことが起きていましたが、何度かやり直してみると最後までスクレイピングしてくれました。なので、何回か試してみるといいかもしれません。
根本的な解決になっていなくて申し訳ないです。

PyCKさん、コメントありがとうございます。
PyCKさんのように、いつか最後までスクレイピングできることを願って(?)今はとりあえず、
動画とテキストで勉強進めようと思います。
動画のように2019年のデータならうまくいくのかな?!
読み込めたところまでで保存できるようにならないかなと考えているのですが、
なかなか…(^^;

僕もここでツマヅイているので参考になりました。
ありがとうございました!

相変わらず途中でエラーもなくフリーズするので、
func_timeoutなるライブラリを使って、タイムアウト処理を入れてみました。

from func_timeout import func_timeout, FunctionTimedOut

#馬の過去成績データを処理するクラス
class HorseResults:
    @staticmethod
    def scrape(horse_id_list, pre_horse_results=pd.DataFrame()):
        """
        馬の過去成績データをスクレイピングする関数

        Parameters:
        ----------
        horse_id_list : list
            馬IDのリスト

        Returns:
        ----------
        horse_results_df : pandas.DataFrame
            全馬の過去成績データをまとめてDataFrame型にしたもの
        """

        #スクレイピング済みのhorse_idを省き、
        #horse_idをkeyにしてDataFrame型を格納
        horse_id_list = set(horse_id_list) - set(pre_horse_results.index)
        horse_results = {}
        for horse_id in tqdm(horse_id_list):
            try:
                url = 'https://db.netkeiba.com/horse/' + horse_id
                df1 = func_timeout(30, pd.read_html, args=(url,))
                df = df1[3]
                #受賞歴がある馬の場合、3番目に受賞歴テーブルが来るため、4番目のデータを取得する
                if df.columns[0]=='受賞歴':
                    df = df1[4]
                df.index = [horse_id] * len(df)
                horse_results[horse_id] = df
                time.sleep(1)
            
            except IndexError:
                continue
            except FunctionTimedOut:
                print ('Time out!')
                break
            except Exception as e:
                print(e)
                break
            except:
                break
            
        #pd.DataFrame型にして一つのデータにまとめる        
        horse_results_df = pd.concat([horse_results[key] for key in horse_results])
        horse_results_df = pd.concat([pre_horse_results, horse_results_df]) # pre_horse_resultsと結合

        return horse_results_df

これで、pd.read_html()の最中にフリーズしても30秒経てば
タイムアウトで帰ってきてくれる様ですので、
それを検出していったん抜け出しています。

止まるたびに手動で再開していたのですが、面倒になって

while len(horse_results.index.unique()) < len(results['horse_id'].unique()):
    horse_results = HorseResults.scrape(horse_id_list, horse_results)

print ('Completed!')

のように、七転び八起きで全部終わるまで繰り返すようにして現在スクレイピング続行中です。
(しまいに、netkeibaさんに叱られないか少し不安です…)

まだスクレイピング最後まで完了していませんので、果たしてこれでできるのかは?です。
再開して得たデータをつなげる式も合っているのかは?です。

(うまくいってから投稿した方がよかったかも)
何かアドバイスあれば、お願いいたします。

(追記)
上記の方法でHorse_Resultsのスクレイピングが完了しました。
つぎはぎデータがちゃんと役立つかはこれから確認ですが進めてみます。
ありがとうございました。

初めまして。ほんの数週間前からpythonを学び始めた初心者です。(以前は軽くCやJavaを触った程度)
いつも動画見て勉強させてもらっています。

質問があります。(質問というより助けていただきたいというのが正確かもしれません...)

実際に予測結果を出す段階で下記のコードを実行したところエラー(実行コードの下にあるような)が発生しました。
KeyError: "['単勝'] not found in axis" と出ているのでどこかで単勝の列がないのだろうかと思って色々触ってみたのですが、エラーを解消できませんでした。
考えられる原因、エラーの解消方法についてご意見頂けませんでしょうか?
もし、この関数やクラスの中身を見たいということがあればそちらも随時載せますので、どうかよろしくお願いいたします。

実行したコード

#馬が勝つ確率を予測
pred = me.predict_proba(st.data_c.drop(['date'], axis=1))

#予測結果を表に結合
pred_table = st.data_c[['馬番']].copy()
pred_table['pred'] = pred

#確率が高い順に出力
pred_table.sort_values('pred', ascending=False)

出力されたエラー

predictpredict---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
<ipython-input-174-e2fd6271b8c1> in <module>
      1 #馬が勝つ確率を予測
----> 2 pred = me._proba(st.data_c.drop(['date'], axis=1))
      3 
      4 #予測結果を表に結合
      5 pred_table = st.data_c[['馬番']].copy()

<ipython-input-172-0bdde96d1f23> in predict_proba(self, X)
     10     #3着以内に入る確率を予測。
     11     def predict_proba(self, X):
---> 12         proba = pd.Series(self.model._proba(X.drop(['単勝'], axis=1))[:, 1], index=X.index)
     13         #proba = pd.Series(self.model.predict_proba)
     14         if self.std:

/opt/anaconda3/lib/python3.8/site-packages/pandas/core/frame.py in drop(self, labels, axis, index, columns, level, inplace, errors)
   4161                 weight  1.0     0.8
   4162         """
-> 4163         return super().drop(
   4164             labels=labels,
   4165             axis=axis,

/opt/anaconda3/lib/python3.8/site-packages/pandas/core/generic.py in drop(self, labels, axis, index, columns, level, inplace, errors)
   3885         for axis, labels in axes.items():
   3886             if labels is not None:
-> 3887                 obj = obj._drop_axis(labels, axis, level=level, errors=errors)
   3888 
   3889         if inplace:

/opt/anaconda3/lib/python3.8/site-packages/pandas/core/generic.py in _drop_axis(self, labels, axis, level, errors)
   3919                 new_axis = axis.drop(labels, level=level, errors=errors)
   3920             else:
-> 3921                 new_axis = axis.drop(labels, errors=errors)
   3922             result = self.reindex(**{axis_name: new_axis})
   3923 

/opt/anaconda3/lib/python3.8/site-packages/pandas/core/indexes/base.py in drop(self, labels, errors)
   5280         if mask.any():
   5281             if errors != "ignore":
-> 5282                 raise KeyError(f"{labels[mask]} not found in axis")
   5283             indexer = indexer[~mask]
   5284         return self.delete(indexer)

KeyError: "['単勝'] not found in axis"

最新のModelEvaluatorクラスでは

#3着以内に入る確率を予測。出馬表のデータを入れるときはdrop_tansho=Falseにする。
def predict_proba(self, X, drop_tansho=True):

となっているのでdrop_tansho=Falseを設定してみてはいかがでしょうか。
エラーメッセージの内容を見るとModelEvaluatorクラスの中身が最新ではなさそうなので、
そちらも最新にする必要がありそうです。
とりあえずModelEvaluatorクラスの.drop(['単勝'], axis=1)を
削除して試してみてもよいかもしれません。
※私、ModelEvaluatorクラスを使っていないので誤ったコメントだったらすみません。

助けになれば幸いです。

ご返信いただきありがとうございます!
ModelEvaluatorクラスの書き換えも適宜あるようですね。古い方から動画を辿っているので対応できていないところがあるかもしれないです(・・;)

ご返信にありました、
「ModelEvaluatorクラスの.drop(['単勝'], axis=1)
を削除して試してみてもよいかもしれません。」
をしたところエラーなく実行できました!

初歩的な質問にも回答いただきありがとうございました!
今後ともよろしくお願いいたします。

私も同じところでつまづいて、ModelEvaluatorクラスの
proba = pd.Series(self.model._proba(X.drop(['単勝'], axis=1))[:, 1], index=X.index)

「.drop(['単勝'], axis=1)」部分を削除したらエラーはなくなり実行できたのですが、
出馬表のデータ(st.data_c)を使って予想する時は、削除するのが正解なのでしょうか。

ModelEvaluatorクラスは最新のものを使っています。(のはずです)

ちなみに、本日のダービーを予想したら、
1着の10番シャフリヤールを2番手予想していましたよ!

そう捉えるしかないかと思っていました。
実際のところどうなのか実際にかけてみる回でお話があると助かりますよね、、

ダービーの予測ですが、いつからいつまでの過去データを使っていましたか?
自分の場合は2011〜2021年4月までの過去データでやってましたが、シャフリヤールは7番評価だったので気になりました。
1番評価はエフフォーリアでした。
さらに付け足すとサトノレイナスがビリ2評価でホンマかってちょっと疑ってまして…笑
コードは最新のものと相違ないはずです!

自分でソース見て、こうだからこう使って…とできればいいのですがね。なかなか。。。

私の環境では2015~2020年までデータ用意しました。
用意するデータがそれぞれ違うので、同じAIソフトでも予想が違ってきますね。
こちらでも1番確率が高かったのは同じくエフフォーリアだったと思います。

ようやく動画シリーズの最新回まで追いついたところで、
データ一式もやっとこさスクレイピングできたところです。
なので当方の環境でちゃんと「学習」させて予想で来てるのかも不安だったのですが、
ダービーの結果をひとまずは動いてくれているのかなと安心したところです。

まだわからないところだらけで、2周目見ないといけないなと思っています。。。

先日ここで質問して助けていただいたおかげで「第5回 BeautifulSoupで競馬データを取得する」まで進めました。ありがとうございました。
また初歩の初歩みたいな質問なのですが第5回のアンダーサンプリングのところでimblearnのインストールができませんした。色々ネットで調べたのですが皆さんどうやってインストールされたか教えていただけると助かります。

condaを使っているので
conda install -c conda-forge imbalanced-learn
conda install imbalanced-learn
conda install -c glemaitre imbalanced-learn

を試してみたのですが上手くインストールすることができませんでした。
よろしくお願いします。

私の場合(Windows10 + Miniconda)ですが、
condaのデフォルトチャンネルではimbalanced-learnがないようでしたので、
conda-forgeからインストールしたと記憶しています。

conda install -c conda-forge imbalanced-learn

ありがとうございます。
デフォルトチャンネルにはないんですね。
デフォルトのチャンネルとconda-forgeの違いもよく分かっていなかったので勉強かたがたインストールしてみます!

楽しく拝読しています。
まだまだ初心者なのですが、
払い戻し表をスクレイピングするところで
return_tablesを認識出来ません。
どうしたらよろしいでしょうか。
質問の仕方が間違っていたらすいません。

皆さんに質問なんですが、私は以下のようなエラーが出たのですが新しく更新されたReturnクラスとMEクラスは問題なく実行できましたか?

return_tables.pickle

FileNotFoundError Traceback (most recent call last)
<ipython-input-51-94c866045c8a> in <module>
1 #ModelEvaluatorクラスのオブジェクトを作成
----> 2 me = ModelEvaluator(lgb_clf, 'return_tables.pickle')

<ipython-input-38-a01eaac4745d> in init(self, model, return_tables_path)
2 def init(self, model, return_tables_path):
3 self.model = model
----> 4 self.rt = Return.read_pickle(return_tables_path)
5 self.fukusho = self.rt.fukusho
6 self.tansho = self.rt.tansho

<ipython-input-50-908912972b66> in read_pickle(cls, path_list)
6 def read_pickle(cls, path_list):
7 print(path_list)
----> 8 df = pd.read_pickle(path_list[0])
9 for path in path_list[1:]:
10 df = update_data(df, pd.read_pickle(path))

~\anaconda3\lib\site-packages\pandas\io\pickle.py in read_pickle(filepath_or_buffer, compression)
167 if not isinstance(fp_or_buf, str) and compression == "infer":
168 compression = None
--> 169 f, fh = get_handle(fp_or_buf, "rb", compression=compression, is_text=False)
170
171 # 1) try standard library Pickle

~\anaconda3\lib\site-packages\pandas\io\common.py in get_handle(path_or_buf, mode, encoding, compression, memory_map, is_text, errors)
497 else:
498 # Binary mode
--> 499 f = open(path_or_buf, mode)
500 handles.append(f)
501

FileNotFoundError: [Errno 2] No such file or directory: 'r'

回収率グラフの見方、考え方の認識が合っているかご教授お願いいたします。
2011年から2021年4月までのデータで三連複、三連単の回収率を出したところ下図のようになりました。
この場合、三連複はthresholdが0.8くらいでちょうど回収率100%だと思います。

つまり、「threshold=0.8以上の馬が3頭以上いる場合三連複boxを買い、2頭以下の場合単勝を買う」を繰り返すと回収率100%になるということでしょうか?
もう一つ例を挙げると、threshold=1.25くらいで回収率130%くらいですが
「threshold=1.25以上の馬が3頭以上いる場合三連複boxを買い、2頭以下の場合単勝を買う」を繰り返すと回収率130%になるということでしょうか?

またグラフからthresholdが1.6以上に絞って同じように買うことは期待値が低いと読み取れますか?
(もっともthresholdが1.6以上の場合はほとんど1〜2頭しかおらず三連複は買えないので単勝を買っていそうですが)

根本的に見方、考え方が間違っている場合、遠慮なく指摘くださいますと幸いです。

optunaでパラメータを最適化する際に、
同じデータを用いているのにもかかわらず、optunaするたびにハイパパラメタが変化するのはなぜなのでしょうか。
この問題を回避された方はいらっしゃいますか?
そのため、回収率グラフの評価に疑問が残ります。

Returnクラスを使用して払い戻し表のスクレイピングをしているのですが、何度やっても500件もいかずに下記エラーで止まってしまいます。(本日5/26 9:00~13:00の間で6回試行して全てNG)
<urlopen error [WinError 10060] 接続済みの呼び出し先が一定の時間を過ぎても正しく応答しなかったため、接続できませんでした。または接続済みのホストが応答しなかったため、確立された接続は失敗しました。>

race_id_listは2019年のものを使用しており下記を実行しているだけです。
return_tables = Return.scrape(race_id_list)

解決法をご存じな方いらっしゃいますか?

まずrace_id_listに手動で201901010101だけ入れたりして、return_tablesに結果が取り込めるか試してみてはどうでしょう?
コードに不備があればそれすらエラーになります。
ちゃんと取り込めているなら、race_id_listの中身は正しいか確認したり、途中まではいけてるならどこで止まっているのかreturn_tablesをみて見当をつけたいですね。
あとは、必要なパッケージは入っていますか?
今回ならfrom urllib.request import urlopenですかね。書き方はこれだけじゃないかもですが。

今回実際に動かしているクラスとエラー内容を丸々貼った方が皆さんの目にパッと入って解決が早いかもしれません。

参考までに、私は2011年から2021年4月まで全てエラーなく取り込めました。

ご回答ありがとうございます。
私の質問の最初に書いてあるReturnクラスはリンクも貼ってありますが「競馬予想で始める機械学習 〜最先端ver〜」の「chapter 02 スクレイピング」に載っているReturnクラスのソースコードをそのまま使用しています。。。(リンク先参照できないでしょうか?)

途中のレースまではスクレイピングできており、異常後にreturn_tablesを出力しても途中までの分が格納されています。return‗tablesを表示すると末尾のインデックスが「201901020612」になっているので、
次取得しようとするレースのURL https://db.netkeiba.com/race/201901020701 を確認すると存在しないレースIDだというのはわかっています。
ですが存在しないレースを無視するために下記の処理があると認識しています。

except IndexError:  
                continue

from urllib.request import urlopen もインポートしています。
手詰まり状態ですがもう少し模索してみます。。。

原因がわかりました。最初にレース結果データを作成する際に使用したrace_id_listをそのまま払い戻し表のスクレイピングでも指定していた為、存在しないレースIDのページをurlopenで指定した結果except IndexError:ではなく、except Exception:で拾ってそのままbreakしていました。
なので、すでに作成済みのレース結果データのレースIDをユニーク化したリストでrace_id_listを更新して払い戻しのスクレイピングで指定したら止まらずに動いています。まだ動作中ですが大丈夫かと思います。

レース結果データではpandasのread_htmlメソッドを使用していて、
戻り値のリストの[0]を指定しているので存在しない場合にIndexErrorになってたのでちゃんとcontinueされていたのだと気づきました。

ここにするコメントじゃなかったらすみません。
05モデル評価&回収率シュミレーションのModelEvaluatorクラスの
2つ目のdefのelse内のproba(X))となっている部分はカッコ閉じが1つ多いみたいです。

追加で05の回収率シュミレーションでmatplotlibを使ってますが、01にimportにないよいです

初心者質問で申し訳ないのですが、2021/5/21更新のModelevaluaterクラスに関して、出馬表データから単勝の数値を取り出せない現在のプログラムでは実際のレースを予測する際に

def predict_proba(self, X, std=True, minmax=False):
    proba = pd.Series(self.model.predict_proba(X.drop(['単勝'], axis=1))[:, 1], index=X.index)

この部分で['単勝']のdropでエラーが出てしまいます。

me3 = ModelEvaluator3(lgb_clf, ['return_tables.pickle'])
pred = me3.predict_proba(st.data_c.drop(['date'], axis=1))
KeyError                                  Traceback (most recent call last)
<ipython-input-71-b140a86e1687> in <module>
----> 1 pred = me3.predict_proba(st.data_c.drop(['date'], axis=1))

<ipython-input-69-8f8cbcb6e7f0> in predict_proba(self, X, drop_tansho)
     15     def predict_proba(self, X, drop_tansho=True):
     16         if drop_tansho:
---> 17             proba = pd.Series(self.model.predict_proba(X.drop(['単勝'], axis=1))[:, 1], index=X.index)
     18         else:
     19             proba = pd.Series((self.model.predict_proba(X))[:, 1], index=X.index)

~\Anaconda3\envs\Keiba\lib\site-packages\pandas\core\frame.py in drop(self, labels, axis, index, columns, level, inplace, errors)
   4313             level=level,
   4314             inplace=inplace,
-> 4315             errors=errors,
   4316         )
   4317 

~\Anaconda3\envs\Keiba\lib\site-packages\pandas\core\generic.py in drop(self, labels, axis, index, columns, level, inplace, errors)
   4151         for axis, labels in axes.items():
   4152             if labels is not None:
-> 4153                 obj = obj._drop_axis(labels, axis, level=level, errors=errors)
   4154 
   4155         if inplace:

~\Anaconda3\envs\Keiba\lib\site-packages\pandas\core\generic.py in _drop_axis(self, labels, axis, level, errors)
   4186                 new_axis = axis.drop(labels, level=level, errors=errors)
   4187             else:
-> 4188                 new_axis = axis.drop(labels, errors=errors)
   4189             result = self.reindex(**{axis_name: new_axis})
   4190 

~\Anaconda3\envs\Keiba\lib\site-packages\pandas\core\indexes\base.py in drop(self, labels, errors)
   5589         if mask.any():
   5590             if errors != "ignore":
-> 5591                 raise KeyError(f"{labels[mask]} not found in axis")
   5592             indexer = indexer[~mask]
   5593         return self.delete(indexer)

KeyError: "['単勝'] not found in axis"

動画で見逃しているだけかもしれないのですが、自分でこのようにクラスを書き替えました。

#3着以内に入る確率を予測
    def predict_proba(self, X, std=True, minmax=False, drop_tansho=True):
        if drop_tansho:
            proba = pd.Series(self.model.predict_proba(X.drop(['単勝'], axis=1))[:, 1], index=X.index)
        else:
            proba = pd.Series((self.model.predict_proba(X))[:, 1], index=X.index)

このプログラムでも、予想自体に影響はないでしょうか? 曖昧な質問申し訳ありません。回答いただけると幸いです

また初心者質問で申し訳ないのですが、HorseResultクラスに、上り3Fのデータとペースのデータ、上り3Fのタイムがペースよりも上かどうかを特徴量として採用したく、格闘しています。

スクレイピングした馬の成績データを見ると、「上り」「ペース」にそれぞれデータがあったので、取り出し、NaNデータ(出走停止やレース中止、途中退場等)のデータをfillnaで置き換えました

horse_results['ペース'].fillna('0', inplace=True)
horse_results['上り'].fillna(0, inplace=True)
horse_results['ペース']

#2018102049    34.9-38.0
#2018102049    34.7-38.5
#2018102049    35.3-38.7
#2018102049    34.8-36.5
#2018102049    35.7-37.5
#                ...    
#2017105794    34.8-37.9
#2017105794     0.0-38.9
#2017105794    34.9-35.2
#2017105794    34.9-35.2
#2017105794    34.8-36.5
#Name: ペース, Length: 87528, dtype: object

その後正規表現と文字列の分割を用いて、上り3Fのペースを取得しました。

horse_results['ペース'].str.split('-', expand=True)[1].astype(float)

#2018102049    38.0
#2018102049    38.5
#2018102049    38.7
#2018102049    36.5
#2018102049    37.5
#              ... 
#2017105794    37.9
#2017105794    38.9
#2017105794    35.2
#2017105794    35.2
#2017105794    36.5
#Name: 1, Length: 87528, dtype: float64

得られた上り3Fのペースデータと馬の上り3Fのデータを上下で比べ、Booleanで出力。その後、0,1のint型に直すことで、HorseResultsクラスのaverage関数へ導入した際に、0~1のデータで取り戻せると思い、実行しました。

sample = horse_results['ペース'].str.split('-', expand=True)[1].astype(float) > horse_results['上り']
sample.astype(int)

#2018102049    1
#2018102049    0
#2018102049    0
#2018102049    0
#2018102049    0
#            ..
#2017105794    1
#2017105794    0
#2017105794    1
#2017105794    0
#2017105794    0
#Length: 87528, dtype: int32

これをHorseResultsクラスのpreprocessing関数に組み込もうとしたのですが、pickleファイルを読み込んだ時に以下のようなエラーが出てしましました

HorseResults
class HorseResults:
    def __init__(self, horse_results):
        self.horse_results = horse_results[['日付', '着順', '上り', 'ペース', '賞金', '着差', '通過', '開催', '距離']]
        self.preprocessing()
~~~~~
    def preprocessing(self):
        df = self.horse_results.copy()
.
.
.
        df['上り'] = df['上り'].fillna(0, inplace=True)
        df['ペース'] = df['ペース'].fillna('', inplace=True)
        on_pace_bool = df['ペース'].str.split('-', expand=True)[1].astype(float) > df['上り']
        df['on_pace'] = on_pace_bool.astype(int)
        df.drop(['ペース'], axis=1, inplace=True)
        #インデックス名を与える
        df.index.name = 'horse_id'
        
        self.horse_results = df
        self.target_list = ['着順', '上り', 'on_pace', '賞金', '着差', 'first_corner', 'final_corner',
                            'first_to_rank', 'first_to_final','final_to_rank', '上り']
~~~~~
def average(self, horse_id_list, date, n_samples='all'):
.
.
.
        self.average_dict = {}
        self.average_dict['non_category'] = filtered_df.groupby(level=0)[self.target_list].mean()\
            .add_suffix('_{}R'.format(n_samples))
        for column in ['course_len', 'race_type', '開催', '上り', 'on_pace']:
~~~~~
def merge(self, results, date, n_samples='all'):
.
.
.
        merged_df = df.merge(self.average_dict['non_category'], left_on='horse_id',
                             right_index=True, how='left')
        for column in ['course_len', 'race_type', '開催', '上り', 'on_pace']:
            merged_df = merged_df.merge(self.average_dict[column], 
                                        left_on=['horse_id', column],
                                        right_index=True, how='left')

error
hr = HorseResults.read_pickle(['horse_results.pickle', 'horse_results_2021.pickle'])
hr.horse_results.head()

---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
~\Anaconda3\envs\Keiba\lib\site-packages\pandas\core\indexes\range.py in get_loc(self, key, method, tolerance)
    350                 try:
--> 351                     return self._range.index(new_key)
    352                 except ValueError as err:

ValueError: 1 is not in range

The above exception was the direct cause of the following exception:

KeyError                                  Traceback (most recent call last)
<ipython-input-239-611bca3af063> in <module>
----> 1 hr = HorseResults.read_pickle(['horse_results.pickle', 'horse_results_2021.pickle'])
      2 hr.horse_results.head() #jupyterで出力

<ipython-input-229-c5674dd92de7> in read_pickle(cls, path_list)
     15         for path in path_list[1:]:
     16             df = update_data(df, pd.read_pickle(path))
---> 17         return cls(df)
     18 
     19     @staticmethod

<ipython-input-229-c5674dd92de7> in __init__(self, horse_results)
      8     def __init__(self, horse_results):
      9         self.horse_results = horse_results[['日付', '着順', '上り', 'ペース', '賞金', '着差', '通過', '開催', '距離']]
---> 10         self.preprocessing()
     11 
     12     @classmethod

<ipython-input-229-c5674dd92de7> in preprocessing(self)
    101         df['上り'] = df['上り'].fillna(0, inplace=True)
    102         df['ペース'] = df['ペース'].fillna('', inplace=True)
--> 103         on_pace_bool = df['ペース'].str.split('-', expand=True)[1].astype(float) > df['上り']
    104         df['on_pace'] = on_pace_bool.astype(int)
    105         df.drop(['ペース'], axis=1, inplace=True)

~\Anaconda3\envs\Keiba\lib\site-packages\pandas\core\frame.py in __getitem__(self, key)
   3022             if self.columns.nlevels > 1:
   3023                 return self._getitem_multilevel(key)
-> 3024             indexer = self.columns.get_loc(key)
   3025             if is_integer(indexer):
   3026                 indexer = [indexer]

~\Anaconda3\envs\Keiba\lib\site-packages\pandas\core\indexes\range.py in get_loc(self, key, method, tolerance)
    351                     return self._range.index(new_key)
    352                 except ValueError as err:
--> 353                     raise KeyError(key) from err
    354             raise KeyError(key)
    355         return super().get_loc(key, method=method, tolerance=tolerance)

KeyError: 1

そもそもの方法が間違っているのか、リストの引数が違うのか。また、ペースや上り3Fのデータの欠損値に0を代入するのはあっているのか等、思いついたことやお気づきの点がございましたらご教授いただけると幸いです。長文失礼しました。

HorseResultsクラスを以下に修正したらいかがでしょうか?

■変更前
df['上り'] = df['上り'].fillna(0, inplace=True)
df['ペース'] = df['ペース'].fillna('', inplace=True)

■変更後
df['上り'].fillna(0, inplace=True)
df['ペース'].fillna('', inplace=True)

助けになれば幸いです。

Yusuke Isaka様、コメントありがとうございます。
あれからもう少し格闘して、上りのデータとペースのデータをHorseResults関数に組み込むことができました。
そもそも、上りとペースのデータが無いレースは成績として取得する必要が無い(着順データと同じ処理をするべき)だと判断したので、dropnaメソッドに入れて消すことにしました。

HorseResults
df.dropna(subset=['着順' ,'ペース', '上り'], inplace=True)

ペースのタイムより上かどうかの処理はあまり変わっていません。

HorseResults
on_pace_bool = df['ペース'].str.split('-', expand=True)[1].astype(float) > df['上り'].astype(float)
df['on_pace'] = on_pace_bool.astype(int)

その他target_listにもカラム名を入れ、通常の予想を行いました。
以下はデフォルトのデータ構成と、上り、on_paceデータを入れたデータ構成での単勝回収率のグラフ比較です。ただ、あまりこのあたりのクラス操作が分かっていないので、正しい結果の保証がないことは念頭に置いてご覧いただけると幸いです。
データは2020年~2021年の5/29までのを入力しています。
デフォルト:
今回:

正当性があるのか、上りやペース以上のタイムが出たかどうかのデータがリーク等禁則に当たらないかどうかが判断できないので、コメントでご指摘等いただけると幸いです。

初めまして。optunaによるハイパーパラメータの設定が実行のたびに変わる問題について、自分でも気になったので調べてみました。
機械学習どころかプログラミングすら初心者ですので的外れなことを言っていたらすみません。

この問題について、Githubで次のようなページを見つけました。

https://github.com/optuna/optuna/issues/1793

曰く、optuna.integration.lightgbm.LightGBMTunerCVにはseedオプションがあるが、optuna.integration.lightgbm.LightGBMTunerにはないそうです。

これに対し開発者さん?が、LightGBMTunerにseed parametersを作るにあたってそのデザインを決める発言の中に、

Since the original lightgbm has multiple seeds, it may be confusing if we add the seed option for Optuna. If we reuse the seed in params, users will not have to take care of Optuna's seeding. On the other hand, they may want to change the Optuna seed while keeping the seed for lightgbm to check the optimization performance/stability of Optuna.

とあります。
つまり現行の、

params = {
    'objective': 'binary', #今回は0or1の二値予測なのでbinaryを指定
    'random_state': 100
}

におけるrandam_stateは、チューニング中には無視されている?のかもしれません。

ちなみに、このseedオプションの開発は完了しているっぽいです(流し読みしただけですが…)。

https://github.com/optuna/optuna/pull/2431
optunaのマニュアルページにも、versionをlatest()とすると、optuna.integration.lightgbm.LightGBMTunerの引数に

optuna_seed=None

とあります。

https://optuna.readthedocs.io/en/latest/reference/generated/optuna.integration.lightgbm.LightGBMTuner.html
※Githubのlatest releaseはv2.7.0ですが、マニュアルページのlatestは2.8.0.dev0です。

前述の通り初心者ですので、詳しい方に確認して、間違いがあれば訂正いただきたく存じます。

情報のアップデートを忘れていました。もう結構経ってしまいましたが、すでにv2.8.0が出ていますのでoptuna_seedが使えるようになり、ハイパーパラメータが実行のたびに変わる問題を回避できるようになっています。

そのやりかたのコードやバージョン確認などの方法はどうすればよろしいでしょうか。
よろしければご教授願えませんでしょうか?

バージョン確認の方法は環境によって多少異なると思いますが、anacondaを使われているのであればAnaconda Powershell Promptで

optuna --version

と入力してエンターで出てきます。

やり方はlgb_o.train()の引数にoptuna_seed=100を追加してください。数字はなんでも大丈夫です。

optunaのバージョンが2.5.0なのですが、これでも大丈夫でしょうか?
それとも、バージョンをアップデートしなければなりませんか?
PC周りは苦手でして、何度も申し訳ありません。

バージョン2.8.0以上でないとできません。

conda uninstall optuna
conda install optuna
で解決いたしました

2021/05/15追加のModelEvaluatorクラスのsanrenpuku_box()とsanrentan_box()における、

#賭けたい馬が3頭いない時は単勝で賭ける
if len(preds)<3:
    return_list.append(np.sum([self.bet(race_id, 'tansho', umaban, 1) for umaban in preds['馬番']]))
    n_bets += 1

は、n_bets += 1 ではなく n_bets += len(preds) ではないでしょうか。
勘違いだったらすみません。

この指摘が正しければ、単勝を2枚買っているのに1枚しか買っていない計算になってしまう場合があるということですよね。成績が本来より高く見積もられてしまうのでは

おっしゃる通り、かなり高く見積もられてしまうようです。
上記の通りに修正すると、確か(threshold後半部分を除いて)回収率1を超えるところが無くなってしまったと記憶しています。
単勝を「thresholdを超える馬が2頭以下の時だけ賭ける」としても、回収率は(threshold後半部分を除いて)ほぼ0.8あたりをさまよっていたので、三連単・三連複で賭けたい馬が3頭いない時は賭けないようにするのがいいのではと思います。

語弊があったので訂正します。
× 単勝を「thresholdを超える馬が2頭以下の時だけ賭ける」としても
〇 tansho_return()を「thresholdを超える馬が2頭以下の時だけ賭ける」としても

更新ありがとうございます!

youtubeを毎回楽しみに拝見してます。
これからも応援してますので、楽しい動画おねがいします!

##予測する
#訓練データとテストデータに分ける
train, test = split_data(r.data_c)

#説明変数と目的変数に分ける。dateはこの後不要なので省く。
X_train = train.drop(['rank', 'date', '単勝'], axis=1)
y_train = train['rank']
#2021/3/12追加: テストデータの単勝オッズはシミュレーション時に使用するので残しておく
X_test = test.drop(['rank', 'date'], axis=1)
y_test = test['rank']

X_test = test.drop(['rank', 'date'], axis=1)
こちらの行に単勝がないとfeature_importanceが表示できなくて困っております。
me.feature_importance(X_test)

エラーは「ValueError: arrays must all be same length」です。

X_test = test.drop(['rank', 'date', '単勝'], axis=1)
頭捻って考えてみたのですが、追加して実行する以外に
良い案が思いつきませんでした良い解決方法はありませんか?

あと、youtubeを見ていて拾ってきた情報ですが、統計的に優位性がみられると感じた特徴量のアイディアがあります。お時間あれば、ご検討ください!

当日に取得したレースデータに、オッズとそれに付随した人気順がありますが、
人気順にソートした場合、「馬」と「次の人気の馬」との間にオッズ(倍率)差があります。
   人気 オッズ
A馬  1  2.1
B馬  2  3.5
C馬  3  9.8

A馬B馬間の差は1.4で、B馬C馬間の差は6.3あります。
この差が1.8倍を超えると、前者の馬が3着以内に入る確率が上がるようです。(20~30%)
計算式的には、
(C馬オッズ/B馬オッズ)>=1.8  3着以内に入る可能性が見受けられる?
また、3倍を超えると、極端に3着以内に入る可能性が高くなるそうです。

特徴量的には、
if (C馬オッズ/B馬オッズ - 1.7)>0
(C馬オッズ/B馬オッズ - 1.7)
elif
0

因果関係はわかりませんが、人が必死に予測する内容に「勝つであろうと予測する馬とそれ以外の馬」の
間に、熱量(想いの強さ)のゆがみが発生している気がしています(笑

拾ってきた情報なので、ネタの一つだと受け取ってください(笑)

me.feature_importance(X)の引数Xはそのカラム名を使っているだけなので、カラムの順番さえ狂っていなければどちらでもいいとは思いますが、これは「学習の際どんな特徴量がそのモデルの精度にどれだけ関わったか」を示すものなので、通常XにはX_testではなくX_trainを入れるのがいいと思います。
X_testを入れる必要があるならば、me.feature_importance(X_test.drop(['単勝'], axis=1))でOKです。

ありがとうございます!
丁寧な説明で、やっと理解できました。

初めまして。3か月前に馬券購入の強化学習についての動画を上げていたと思うのですが、その部分のソースコードはあげていただけないのでしょうか?
私が見つけれてないだけなら申し訳ないです。

いつも為になる動画やコメントありがとうございます。私は地方競馬中心ですが予想に競馬AIを試しているのですが除外馬がいるレースの出馬テーブルのスクレピングができず悩んでいます。もし解決を知っている方いましたらお知らせして頂けたら大変助かります。
エラーは:
ValueError: Length of values () does not match lenght of index ()
カッコ内は数字。出走数(除外を除いた)とインデックス数です。

ShutubaTableクラスを次のように変更します。

#変更前
jockey_td_list = soup.find_all("td", attrs={'class': 'Jockey'})
#変更後
jockey_td_list = soup.find_all("span", attrs={'class': 'Jockey'})

変更前は、「tdタグでclass属性に'Jockey'という文字列が入っているもの」を検索します。中央の出馬表を見てみると、騎手情報は<td class="Jockey">に、除外となった馬の騎手情報は<td class="Cancel_NoData Jockey">に振り分けられているようです(参考: https://race.netkeiba.com/race/shutuba.html?race_id=202109011104 )。一方、地方では除外の騎手情報は<td class="Cancel_NoData">に振り分けられます。それゆえ、変更前のコードでは除外の騎手情報がスルーされてしまいます。
ただし、地方の場合は除外か否かに関わらず、tdタグの中に<span class="Jockey">というタグが作られるようで、必要な情報はその中にあります。したがって、"td"を"span"と変えることで対応できます。

なお、すでに対処済みかもしれませんが、地方のjockey_idはアルファベットも入る場合があるようですので、その部分も修正が必要です。

#変更前
jockey_id = re.findall(r'\d+', td.find('a')['href'])[0] #数字のみを抽出
#変更後
jockey_id = re.findall(r'jockey/(\w*)', td.find('a')['href'])[0] #'jockey/'より後ろの英数字(及びアンダーバー)を抽出

Resultsクラスも同様に変更が必要になるかと思います。

改善されました。大変助かりました。ありがとうございます。しかも生データのst.dataには除外された馬がスクレピングされているのにst.data_cには入っていないのです。私の知識では全くどうなっているのか分かりませんが、これがまさしく私が望んでいたことです。地方のjockey_idにアルファベットが入っていることは私も途中で気づいて再スクレイピングしました。2週間ほど解決しようとして何もできなかったことなので本当に助かりました。

「回収率122%の競馬予想AIを実際に使うには?〜後編〜」の動画の「17:36 新しい馬の血統データの扱い方」の部分で分からないところがでてきたので質問いたします。

エラー内容:'scrape peds at horse_id_list "no_peds"'が出てきてしまった。
該当のコードは
def merge_peds(self, peds):
self.results_pe = self.results_h.merge(peds,left_on='horse_id', right_index=True, how='left')
self.no_peds = self.results_pe[self.results_pe['peds_0'].isnull()]['horse_id'].unique()
if len(self.no_peds) > 0:
print('scrape peds at horse_id_list "no_peds"')
出馬表の
st.merge_peds(p.peds_e)
st.shutuba_tables_pe.head()
でエラーが起こらなかったのに、次の動画のクラスの継承のところでエラーが起こったのです。
r.merge_peds(p.peds_e)
r.results_pe.head()でまず起こり、
r.merge_peds(p.peds_e)
r.data_pe.head()親クラスを作った後でもやはり同じように"no_peds"が出てきてしまいました。
r.no_pedsで48個のhorse_idが出てきました。
その内の一つ'2017102027'ミアキヒルゼファーに関しては一度だけ出走していてresults.loc['201904020905']で見てみるとhorse_idもありました。

スクレイピングに関しては最近始めたので19年と20年のデータは全て揃っているのにはずなのにどうしてこのようなことが起こったのかが分かりません。動画ではエラーは起こらなかったですし。
簡単な問題かもしれませんが初心者なのでどうしてこうなったのかが分かりません。
よろしくお願いします。

詳しくは分からないですが、printで意図的に表示してるコメントみたいなので、その前のコードから推測するには
スクレイピングして保存しているpedsのデータに、no_pedsででてくる48個のデータが含まれていないのではないでしょうか。
horse_idにno_pedsを指定して、48個スクレイピングして、アップデートしてみてはいかがでしょうか

npeds = Peds.scrape(r.no_peds)
newpeds = update_data(p.peds, npeds)

こんな感じだと思います。
(私の環境では再現できないので、想像で書きました)

自分ももう一度スクレイピングするしかないなと思ってました。
コードも書いていただきありがとうございました。

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

06 実際に掛けてみた・・・ページで下にスクロールをしていただくと
本番の実行コードという見出しの中に以下のコードが有るかとおもいます。

scores = me_st.predict_proba(st.data_c.drop(['date'], axis=1), train=False)
pred = st.data_c[['馬番']].copy()
pred['score'] = scores
pred.loc['202105021211'].sort_values('score', ascending=False)

上記のコードのme_stというコードはどこから現れたのかご存じの方はいらっしゃいますか?
もし、知っているがいるなら教えていただけますと助かります。

私も何が正解は知りませんが下記のように'me.predict_'でエラーなく動いているので、これが正解だと思い使わせて貰っています。

ですよね。。
ModelEvaluatorにしかpredict_proba関数はないので、
me.predict_probaとなるはずですよね!

共有していただいてありがとうございます!

初心者です。
Chapter 03のデータ前処理の所で

#5世代分の血統データの追加
st.merge_peds(p.peds_e)

#scrape peds at horse_id_list "no_peds"と表示された場合
peds_new = Peds.scrape(st.no_peds)
peds.to_pickle('peds_h.pickle') #pedsを更新する前にバックアップ
↑このコードを実行するとデータがありませんみたいなエラーが起きます。
このpeds_h.pickleはどこにあるのですか?
そもそも私の使い方が間違っているのかもしません。
もしかして#scrape peds at horse_id_list "no_peds"と表示された場合に以下のコードを使うってことですか?
peds = update_data(peds, peds_new)
peds.to_pickle('peds.pickle')
p = Peds.read_pickle(['peds.pickle'])
p.encode()
st.merge_peds(p.peds_e)

一行ずつ追っていくとわかりやすいと思います
「scrape peds at horse_id_list "no_peds"」と表示されたところは、pedsデータをResultsデータに追加しようとして、
Resultsの馬のうちpedsの中に血統データが無いから、no_pedsに保管している分を確認して!というところだと思います。
無いものは仕方ないので、peds_new = Peds.scrape(st.no_peds)でno_peds分をスクレイピングします。
保存してるPedsデータを簡単に上書きしたら、もし間違ってたら後戻りできないので、peds.to_pickle('peds_h.pickle')
で一旦バックアップ。
Peds.pickleがメインで、peds_h.pickleが何かあった時用のバックアップになってると思います。
peds = update_data(peds, peds_new)で、メインにno_pedsをスクレイプしたデータをくっ付けて、
peds.to_pickle('peds.pickle')で今後のために保存。
p = Peds.read_pickle(['peds.pickle'])で改めて読み込んで、p.encode()で使える並びに加工して
st.merge_peds(p.peds_e)で改めてResultsデータに追加。

たぶん、コードを更新続けていくうちに、調整が漏れたんだと思います。
お困りの所は、ちょっとコードの前のほうで ??? = Peds.read_pickle(['peds.pickle'])で、peds.pickleを最初に読み込んでるとおもいますが、Pedsのところは、???.pedsにするとうまくいくのではないかとおもいます。

>このpeds_h.pickleはどこにあるのですか?
peds_h.pickleが無いのではなくて、保存したいけどPeds変数には何も入ってないって言ってるのだと思います

丁寧に教えて頂きありがとうございます。助かりました。
一度実践してみます。

どうしても出馬表からオッズを取り出したいので以下のソースコードを書いたのですが、実行してみると最後の馬番のデータしか取得できませんでした。
以下のコードをどのように変更すればオッズデータを取得できるか、ご存知の方教えていただけないでしょうか?

def sss(race_id_list):
options = ChromeOptions()
driver = Chrome(options=options)
row = []
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')#全ての行を取り出す
row1 = []
for element in elements:#各行を取り出していく
tds = element.find_elements_by_tag_name('td')#各行の属性を取り出す
for td in tds:#各属性を取り出していく
row1.append(td.text)
row.append(row1[9])
time.sleep(1)
driver.close()

sss(['202109030411'])
print(row)

実行結果
['8', '13', '--', 'キセキ', '牡7', '58.0', '福永', '栗東辻野', '508(+4)', '16.6', '5', '', '']

除外の馬はどうしますか?
例えば、除外などがあるレースの場合、r.data(rはResultsクラス)やst.data(stはShutubaTableクラス)と照合するなら、これらは除外の馬の行を削除する前なので、除外の馬のオッズに欠損値か何か入れておく必要があります(行数がずれるので)。
一方r.data_cやst.data_cと照合する場合、これらは除外の馬の行は削除されているので(厳密にはdata_pの時点で除外の馬の行は削除されています)、除外の馬のオッズは無視してもいい(スクレイピングしない)ことになります。
とりあえず簡単な後者の方のコードだけ貼っておきます。race_id_listを使われているので、race_idをindexにしたDataFrameの形で出力するようにしました。

from selenium import webdriver
import chromedriver_binary

def sss(race_id_list):
    data = pd.DataFrame()
    for race_id in tqdm(race_id_list):
        time.sleep(1)
        df = pd.DataFrame()
        driver = webdriver.Chrome()
        url = 'https://race.netkeiba.com/race/shutuba.html?race_id=' + race_id        
        driver.get(url)
        html = driver.page_source
        soup = BeautifulSoup(html, "html.parser")
        
        odds_list = []
        odds_td_list = soup.find_all("td", attrs={'class': 'Txt_R Popular'})
        for td in odds_td_list:
            odds = re.findall(r'bold">(\d+\.\d+)', str(td))[0]
            odds_list.append(float(odds))

        df['odds_list'] = odds_list
        df.index = [race_id] * len(df)
        data = data.append(df)
        driver.quit()
    return data

改善案を提案していただきありがとうございます!
今回必要なのはst.data_cと照合するためでしたのでご提案いただいたコードを実装し検討してみたいと思います。
ありがとうございます!

特徴量はオリジナルのものを追加してるいるけれども基本は掲載されているコード通りなのですが、optunaを使用すると最後に追加した特徴量を最重要視するという奇妙な現象が起きるのですが、皆様はどうなんでしょうか…

「最後に追加した」というのはオリジナルの特徴量ですか?最後に追加した特徴量を最重視するなんて現象、コードのミスとは考えにくいですし、普通に考えればリークがあるとかですかね…?
自分は特徴量の追加までまだできていないので何とも言えませんが…

お返事ありがとうございます。
最初にその現象に出くわしたのが主さんが追加した"n_horses"でした。何故か頭数が重要視されていておかしいなと思い他のどうでもいいような特徴量どれを入れても最後に追加したものが重要視されてしまうようになってしまいました…
optunaのバグなのか自分の何かが間違っているのか今研究中です(笑)

n_horses追加してみましたが、重要度は上から10番目でした。「optunaを使用すると」ということは使用しない場合は問題ないのでしょうか?だとするとそのあたりに何かミスがあるかもしれませんね。

初めまして。
質問させていただきます。

netkaiba.comからスクレイピング
の部分で紹介されているコードに関して、既に保存されている部分のcontinueが機能していないのですが、同じような状況の方はいらっしゃいますでしょうか?

また、以下の部分も機能していません。
test3 = scrape_race_results(race_id_list)
for key in test3:
test3[key].index = [key] * len(test3[key])
results = pd.concat([test3[key] for key in test3], sort=False)
results.to_pickle('results.pickle')

内容は特に書き換えておらず、そのまま実行しています。

また、pickleファイルを開こうとすると
:\Users\name\results.pickle is not UTF-8 encoded
のエラーが出ます。

コメント失礼します。
Returnクラスでのスクレイピングについて質問があります。

#払い戻し表データを処理するクラス
class Return:
    @staticmethod
    def scrape(race_id_list):
        return_tables = {}
        for race_id in tqdm(race_id_list):
            try:
                url = "https://db.netkeiba.com/race/" + race_id

                #普通にスクレイピングすると複勝やワイドなどが区切られないで繋がってしまう。
                #そのため、改行コードを文字列brに変換して後でsplitする
                f = urlopen(url)
                html = f.read()
                html = html.replace(b'<br />', b'br')
                dfs = pd.read_html(html)

                #dfsの1番目に単勝〜馬連、2番目にワイド〜三連単がある
                df = pd.concat([dfs[1], dfs[2]])

                df.index = [race_id] * len(df)
                return_tables[race_id] = df
                time.sleep(1)
            except IndexError:
                continue
            except Exception as e:
                print(e)
                break
            except:
                break

        #pd.DataFrame型にして一つのデータにまとめる
        return_tables_df = pd.concat([return_tables[key] for key in return_tables])
        return return_tables_df

        return_tables_df.to_pickle("return_tables.pickle")

このように記載しているのですが、
以下のようなエラーが出ています。

Resultクラス・Hose_Resultクラス・Pedsクラスまでは順調にスクレイピングできていましたが、
払い戻しの部分のみ上手く実行できません。初歩的な質問で申し訳ございませんが、解決策をご教示いただけますと幸いです。

---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-26-d2b104e36dd5> in <module>
      5 
      6 #払い戻し表データを処理するクラス
----> 7 class Return:
      8     @staticmethod
      9     def scrape(race_id_list):

<ipython-input-26-d2b104e36dd5> in Return()
     40     return_tables = {}
     41 
---> 42     return_tables_df = pd.concat([return_tables[key] for key in return_tables])
     43 
     44     return_tables_df.to_pickle("return_tables.pickle")

~\anaconda3\lib\site-packages\pandas\core\reshape\concat.py in concat(objs, axis, join, ignore_index, keys, levels, names, verify_integrity, sort, copy)
    283     ValueError: Indexes have overlapping values: ['a']
    284     """
--> 285     op = _Concatenator(
    286         objs,
    287         axis=axis,

~\anaconda3\lib\site-packages\pandas\core\reshape\concat.py in __init__(self, objs, axis, join, keys, levels, names, ignore_index, verify_integrity, copy, sort)
    340 
    341         if len(objs) == 0:
--> 342             raise ValueError("No objects to concatenate")
    343 
    344         if keys is None:

ValueError: No objects to concatenate

最下部の書き換えで解決しました(__)

return_tables = Return.scrape(race_id_list)
return_tables.topickle('return_tables.pickle')
me = ModelEvaluator(lgb_clf, ['return_tables_2017.pickle','return_tables_2018.pickle','return_tables_2019.pickle','return_tables_2020.pickle','return_tables_2021.pickle'])

#単勝適正回収値=払い戻し金額が常に一定になるように賭けた場合の回収率
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()

としたところ

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

<ipython-input-19-562656c8c820> 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, 

ValueError: too many values to unpack (expected 3)

とエラーになってしまいました。
こちらは何が原因になりますでしょうか?
よろしくお願いいたします。

gain()を次のように修正します。

#変更前
n_bets, return_rate, n_hits = return_func(X, threshold)
#変更後
n_bets, return_rate, n_hits, std = return_func(X, threshold)

右辺の戻り値の数が左辺の変数の数と合っていないのがエラーの原因です。
return_funcにはgain()の1つ目の引数が入ります。
me.tansho_return_proper()やme.tansho_return()を見てみると、

return n_bets, return_rate, n_hits, std

となっているので、左辺にも4つの変数を設けてあげる必要があります。

ありがとうございます。こちら試したところ

g_proper = gain(me.tansho_return_proper, X_test)

  0%|          | 0/100 [00:00<?, ?it/s]
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-65-e618dcdc9ddb> in <module>
----> 1 g_proper = gain(me.tansho_return_proper, X_test)

<ipython-input-7-4b6bde1d804a> 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, std = return_func(X, threshold)
      7         if n_bets > 2:
      8             gain[n_bets] = {'return_rate': return_rate, 

<ipython-input-3-40431ea3ef86> in tansho_return_proper(self, X, threshold)
    117     #単勝適正回収値: 払い戻し金額が常に一定になるように単勝で賭けた場合の回収率
    118     def tansho_return_proper(self, X, threshold=0.5):
--> 119         pred_table = self.pred_table(X, threshold)
    120         n_bets = len(pred_table)
    121 

<ipython-input-3-40431ea3ef86> in pred_table(self, X, threshold, bet_only)
     46     def pred_table(self, X, threshold=0.5, bet_only=True):
     47         pred_table = X.copy()[['馬番', '単勝']]
---> 48         pred_table['pred'] = self.predict(X, threshold)
     49         if bet_only:
     50             return pred_table[pred_table['pred']==1][['馬番', '単勝']]

<ipython-input-3-40431ea3ef86> in predict(self, X, threshold)
     33     #0か1かを予測
     34     def predict(self, X, threshold=0.5):
---> 35         y_pred = self.predict_proba(X)
     36         return [0 if p<threshold else 1 for p in y_pred]
     37 

<ipython-input-3-40431ea3ef86> in predict_proba(self, X, train, std, minmax)
     16         if train:
     17             proba = pd.Series(
---> 18                 self.model.predict_proba(X.drop(['単勝'], axis=1))[:, 1], index=X.index
     19             )
     20         else:

~/.pyenv/versions/anaconda3-5.3.1/lib/python3.7/site-packages/lightgbm/sklearn.py in predict_proba(self, X, raw_score, start_iteration, num_iteration, pred_leaf, pred_contrib, **kwargs)
    918                       pred_leaf=False, pred_contrib=False, **kwargs):
    919         """Docstring is set after definition, using a template."""
--> 920         result = super().predict(X, raw_score, start_iteration, num_iteration, pred_leaf, pred_contrib, **kwargs)
    921         if callable(self._objective) and not (raw_score or pred_leaf or pred_contrib):
    922             _log_warning("Cannot compute class probabilities or labels "

~/.pyenv/versions/anaconda3-5.3.1/lib/python3.7/site-packages/lightgbm/sklearn.py in predict(self, X, raw_score, start_iteration, num_iteration, pred_leaf, pred_contrib, **kwargs)
    723                              "match the input. Model n_features_ is %s and "
    724                              "input n_features is %s "
--> 725                              % (self._n_features, n_features))
    726         return self._Booster.predict(X, raw_score=raw_score, start_iteration=start_iteration, num_iteration=num_iteration,
    727                                      pred_leaf=pred_leaf, pred_contrib=pred_contrib, **kwargs)

ValueError: Number of features of the model must match the input. Model n_features_ is 186 and input n_features is 185 

となりました。どうもデータが合わないっぽいのですが何が原因でしょうか?

モデル(lgb_clf)に入れたトレーニングデータ(X_train)の特徴量の数(カラム数)が、テストデータであるX_testの特徴量の数と一致していないのが原因と思われます。
トレーニングデータとテストデータは、カラム数もカラムの順番も一致させる必要があります。ただしテストデータの'単勝'に関しては、コードの設計上一時的に残しておく必要があり、実際のシミュレーションの際にはdropされるようになってるので、

pd.set_option('display.max_columns', 190) #表示する最大カラム数を190に設定。
X_train

X_test.drop(['単勝'], axis=1)

をそれぞれ別のセルで実行して比べてみてください。おそらくX_trainのほうが何か1つ多くなっていると思います。

ありがとうございました!何とか動きました。
単勝カラムのdropが違ったみたいでした。