Chapter 08無料公開

動画中のソースコード(第1回〜第4回)

第2章のソースコードは常に最新のものに更新されていきます。なので競馬予想AIシリーズの動画を第1回から見ながら勉強している視聴者向けに、動画中のコードも載せておきます。また動画中で触れられていない変更点や、コメント欄の質問でよくあるエラーとその対処方法も解説します。

第1回:Pythonで競馬データをスクレイピングする

import pandas as pd
import time
from tqdm.notebook import tqdm

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://db.netkeiba.com/race/" + 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 = []
for place in range(1, 11, 1):
    for kai in range(1, 6, 1):
        for day in range(1, 13, 1):
            for r in range(1, 13, 1):
                race_id = "2019" + str(place).zfill(2) + str(kai).zfill(2) +\
		str(day).zfill(2) + str(r).zfill(2)
                race_id_list.append(race_id)

#スクレイピングしてデータを保存
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')

tqdmの仕様変更により、インポートの仕方が変わっています。

注意点

動画中では「9日目以降は無いだろう」と雑な判断をしてしまい、dayは8までしか回していませんが、実際には12日目まであるようです。 しかもタチの悪いことに、自分が実際にスクレイピングした時は12まで回していたようです。なので、動画よりデータ数が少ないと思ったら原因はこれです。

変更点

  1. 【重要】race_idが存在しないページについてもページの読み込み自体は行われているので、動画中ではtry文の最後にtime.sleep(1)を入れていますが、必ずtry文の最初に書くようにしてください。
  2. 引数のpre_race_resultsはこの後あまり使うことがなかったので、現在では削除しています。
  3. 動画ではIndexError以外のエラーが起きた場合、全てbreakをしていますが、これだとコードが間違っていた時にどんなエラーが起きているのか分かりません。 そこで現在ではExcept文の中でエラー内容を表示するようにしています。ただし、Jupyterの停止ボタンを押す操作はExceptionクラスのエラーに含まれないので、その場合は次のexceptに引っかかってbreakしてくれるようにしています
  4. 動画中では関数を実行すると辞書型が返ってきますが、どっちにしろ辞書型のままだと使えず、毎回DataFrame型に直すのでこれは関数の中に書いてしまった方が楽です。なので、Chapter03のソースコードでは関数の中にこの処理を組み込んでいます。
  5. for key in test3 だけでもtest3のキーを取り出してくれます。
  6. concat関数の引数にsort=Falseを指定していますが、pandas1.0.0からはデフォルトでsort=Falseになる仕様に変更されているので、pandas1.0.0以降のバージョンを使っている場合は指定しなくても大丈夫です。pandasのバージョンは
print(pd.__version__)

で確認することができます。

よくあるエラーと対処法

TypeError: list indices must be integers or slices, not str
(リスト型のインデックスは整数である必要があります。しかし文字列型が入力されましたよ。)

第1回の動画は画質を低くアップロードしてしまったせいもあり、scrape_race_results関数の1行目

race_results = {}

race_results = []

と、見間違ってリスト型で定義してしまうと上のエラーが起きるので注意してください。

第2回・第3回 Pythonで競馬データを加工する

def preprocessing(results):
    df = results.copy()

    # 着順に数字以外の文字列が含まれているものを取り除く
    df = df[~(df["着順"].astype(str).str.contains("\D"))]
    df["着順"] = df["着順"].astype(int)

    # 性齢を性と年齢に分ける
    df["性"] = df["性齢"].map(lambda x: str(x)[0])
    df["年齢"] = df["性齢"].map(lambda x: str(x)[1:]).astype(int)

    # 馬体重を体重と体重変化に分ける
    df["体重"] = df["馬体重"].str.split("(", expand=True)[0].astype(int)
    df["体重変化"] = df["馬体重"].str.split("(", expand=True)[1].str[:-1].astype(int)

    # データをint, floatに変換
    df["単勝"] = df["単勝"].astype(float)

    # 不要な列を削除
    df.drop(["タイム", "着差", "調教師", "性齢", "馬体重"], axis=1, inplace=True)

    return df

第4回 ロジスティック回帰で競馬予想してみた

この回では機械学習の導入として、ロジスティック回帰を使って予測モデルを作っています。ですがこの後の動画からは、より精度の高いLightGBMを使っていくことになるので、競馬予想AIの作り方だけを追いたい方はこの動画は飛ばしてもらっても大丈夫です。

#4着以下を全て4にする
clip_rank = lambda x: x if x < 4 else 4
#動画中のresultsは、preprocessing関数で前処理が行われた後のデータ
results["rank"] = results["着順"].map(clip_rank)
results.drop(["着順", "馬名"], axis=1, inplace=True)

#カテゴリ変数をダミー変数化
results_d = pd.get_dummies(results)

#訓練データとテストデータに分ける
from sklearn.model_selection import train_test_split

X = results_d.drop(["rank"], axis=1)
y = results_d["rank"]
X_train, X_test, y_train, y_test = train_test_split(
    X, y, stratify=y, test_size=0.3, random_state=0
)

#アンダーサンプリング
from imblearn.under_sampling import RandomUnderSampler

rank_1 = y_train.value_counts()[1]
rank_2 = y_train.value_counts()[2]
rank_3 = y_train.value_counts()[3]
#変更点
rus = RandomUnderSampler(
    sampling_strategy={1: rank_1, 2: rank_2, 3: rank_3, 4: rank_1}, random_state=71
)
X_train_rus, y_train_rus = rus.fit_sample(X_train.values, y_train.values)

#訓練
from sklearn.linear_model import LogisticRegression

model = LogisticRegression()
model.fit(X_train_rus, y_train_rus)

#スコアを表示
print(model.score(X_train, y_train), model.score(X_test, y_test))

#予測結果を確認
y_pred = model.predict(X_test)
pred_df = pd.DataFrame({"pred": y_pred, "actual": y_test})
pred_df[pred_df["pred"] == 1]["actual"].value_counts()

#回帰係数の確認
coefs = pd.Series(model.coef_[0], index=X.columns).sort_values()
coefs[["枠番", "馬番", "斤量", "単勝", "人気", "年齢", "体重", "体重変化"]]

動画中では、RandamUnderSamplerの引数としてratioを使っていますが、ratioの代わりにsampling_strategyを使うようにRandomUnderSamplerの仕様が変わっています

注意点

この動画中では変数resultsを、「preprocessing関数を通して前処理が行われた後のデータ」として使っています。なので、一度Jupyterを閉じるなどして最初からやるときは、preprocessing関数が書かれたセルを実行した後、

results = pd.read_pickle('results.pickle')
results = preprocessing(results)

として最初に前処理をしてください。