PR

LightGBMで作る血統を考慮した競馬予想AI

データサイエンス
この記事は約53分で読めます。
スポンサーリンク
スポンサーリンク

はじめに¶

私は競馬予想AIの開発をしています。動画で制作過程の解説をしています。良ければ見ていってください。

また、共有するソースの一部は有料のものを使ってます。
同じように分析したい方は、以下の記事から入手ください。

競馬予想プログラムソフト 全公開!
競馬予想プログラムソフトの開発をしている者です。今回は第一弾から第四弾記事の総集編になります。 チャンネル登録で1,000円引きで入手で…

5.セカンドモデル作成

前回の話に続いて、セカンドモデル作成の本題に入る。
前回では芝とダートでモデルを分けて学習させるべきという結論に至ったが、まだ血統や前走データを考慮したモデルの分析が出来ていないので将来的には分ける予定だが決断を見送ることとした。

そのため、今回のモデルでは従来のファーストモデルの作り方のままで、血統情報を追加した場合でどうなるかを確認していく

今日やること

  1. セカンドモデルの作成計画
  2. 採用する血統情報の組合せ
  3. 結果の確認

話の流れ

  1. 下準備
  2. 血統情報を追加する関数作成
  3. セカンドモデル作成計画
  4. n個のセカンドモデル作成
  5. 性能の比較(WEBアプリ起動)
  6. 結論

 

スポンサーリンク

5-0.下準備¶

ソースの一部は有料のものを使ってます。
同じように分析したい方は、以下の記事から入手ください。

血統は後で追加するのでまずは、必要なモジュールのインポートからモデル作成用のデータの読み込みまで行う

In [1]:
import pathlib
import warnings
import lightgbm as lgbm
import pandas as pd
from typing import Literal
import tqdm
import datetime

import sys
sys.path.append(".")
sys.path.append("..")
from src.model_manager.lgbm_manager import LightGBMDataset  # noqa
from src.model_manager.lgbm_manager import LightGBMModelManager  # noqa
from src.core.meta.bet_name_meta import BetName  # noqa
from src.data_manager.preprocess_tools import DataPreProcessor  # noqa
from src.data_manager.data_loader import DataLoader  # noqa
from src.core.db.controller import getDataFrame, getTableList  # noqa

warnings.filterwarnings("ignore")

root_dir = pathlib.Path(".").absolute().parent
dbpath = root_dir / "data" / "keibadata.db"
start_year = 2000  # DBが持つ最古の年を指定
split_year = 2014  # 学習対象期間の開始年を指定
target_year = 2019  # テスト対象期間の開始年を指定
end_year = 2023  # テスト対象期間の終了年を指定 (当然DBに対象年のデータがあること)

# 各種インスタンスの作成
data_loader = DataLoader(
    start_year,
    end_year,
    dbpath=dbpath  # dbpathは各種環境に合わせてパスを指定してください。絶対パス推奨
)

dataPreP = DataPreProcessor()

df = data_loader.load_racedata()
df = dataPreP.exec_pipeline(df)
 
2024-08-23 00:23:03.387 | INFO     | src.data_manager.data_loader:load_racedata:23 - Get Year Range: 2000 -> 2023.
2024-08-23 00:23:03.387 | INFO     | src.data_manager.data_loader:load_racedata:24 - Loading Race Info ...
2024-08-23 00:23:05.389 | INFO     | src.data_manager.data_loader:load_racedata:26 - Loading Race Data ...
2024-08-23 00:23:23.323 | INFO     | src.data_manager.data_loader:load_racedata:28 - Merging Race Info and Race Data ...
 
2024-08-23 00:23:25.536 | INFO     | src.data_manager.preprocess_tools:__0_check_use_save_checkpoints:35 - Start PreProcess #0 ...
2024-08-23 00:23:25.536 | INFO     | src.data_manager.preprocess_tools:__1_exec_all_sub_prep1:38 - Start PreProcess #1 ...
2024-08-23 00:23:32.055 | INFO     | src.data_manager.preprocess_tools:__2_exec_all_sub_prep2:40 - Start PreProcess #2 ...
2024-08-23 00:23:44.971 | INFO     | src.data_manager.preprocess_tools:__3_convert_type_str_to_number:42 - Start PreProcess #3 ...
2024-08-23 00:23:48.805 | INFO     | src.data_manager.preprocess_tools:__4_drop_or_fillin_none_data:44 - Start PreProcess #4 ...
2024-08-23 00:23:52.437 | INFO     | src.data_manager.preprocess_tools:__5_exec_all_sub_prep5:46 - Start PreProcess #5 ...
2024-08-23 00:24:14.786 | INFO     | src.data_manager.preprocess_tools:__6_convert_label_to_rate_info:48 - Start PreProcess #6 ...
2024-08-23 00:24:24.937 | INFO     | src.data_manager.preprocess_tools:__7_convert_distance_to_smile:50 - Start PreProcess #7 ...
2024-08-23 00:24:25.221 | INFO     | src.data_manager.preprocess_tools:__8_category_encoding:52 - Start PreProcess #8 ...
2024-08-23 00:24:29.873 | INFO     | src.data_manager.preprocess_tools:__9_convert_raceClass_to_grade:54 - Start PreProcess #9 ...
スポンサーリンク

5-1.血統情報を追加する関数作成

血統情報の追加は、これまで行ってきた血統分析で作成した関数を流用する。
なるべく無駄な処理は省いたので、有効であればベース前処理へ追加しても良いように作成した。

血統データの読み込み

In [2]:
horseblood_list = [tbl for tbl in getTableList(dbpath) if "horseblood" in tbl]
horseblood_list.sort(key=lambda x: int(x[-4:]))
dfblood = pd.concat(
    [getDataFrame(tbl, dbpath) for tbl in horseblood_list], ignore_index=True)

血統データ追加用関数の作成
泥臭すぎるので無理に理解しなくていいです。
スマートなやり方をすると逆にややこしくなるので、一旦以下のようにする

In [3]:
def add_blood_info_to_df(
    df: pd.DataFrame,
    mode: Literal[
        "s", "ss", "sb", "b", "bs", "bb",
        "sss", "ssb", "sbs", "sbb", "bss", "bsb", "bbs", "bbb",
        "ssss", "sssb", "ssbs", "ssbb", "sbss", "sbsb", "sbbs", "sbbb",
        "bsss", "bssb", "bsbs", "bsbb", "bbss", "bbsb", "bbbs", "bbbb",
        "sssss", "ssssb", "sssbs", "sssbb", "ssbss", "ssbsb", "ssbbs", "ssbbb",
        "sbsss", "sbssb", "sbsbs", "sbsbb", "sbbss", "sbbsb", "sbbbs", "sbbbb",
        "bssss", "bsssb", "bssbs", "bssbb", "bsbss", "bsbsb", "bsbbs", "bsbbb",
        "bbsss", "bbssb", "bbsbs", "bbsbb", "bbbss", "bbbsb", "bbbbs", "bbbbb",
    ]
):
    index_map = {
        # region Stallion 1gen, 2gen  3gen
        "s": (0, "stallion", "gen1"),
        "ss": (0, "sStallion", "gen2"), "sb": (8, "sBreed", "gen2"),
        "sss": (0, "s2Stallion", "gen3"), "ssb": (4, "s2Breed", "gen3"),
        "sbs": (8, "sbStallion", "gen3"), "sbb": (12, "sbBreed", "gen3"),
        # endregion
        # region Stallion 4gen
        "ssss": (0, "s3Stallion", "gen4"), "sssb": (2, "s3Breed", "gen4"),
        "ssbs": (4, "s2bStallion", "gen4"), "ssbb": (6, "s2bBreed", "gen4"),
        "sbss": (8, "sbsStallion", "gen4"), "sbsb": (10, "sbsBreed", "gen4"),
        "sbbs": (12, "sb2Stallion", "gen4"), "sbbb": (14, "sb2Breed", "gen4"),
        # endregion
        # region Stallion 5gen
        "sssss": (0, "s4Stallion", "gen5"), "ssssb": (1, "s4Breed", "gen5"),
        "sssbs": (2, "s3bStallion", "gen5"), "sssbb": (3, "s3bBreed", "gen5"),
        "ssbss": (4, "s2bsStallion", "gen5"), "ssbsb": (5, "s2bsBreed", "gen5"),
        "ssbbs": (6, "s2b2Stallion", "gen5"), "ssbbb": (7, "s2b2Breed", "gen5"),
        "sbsss": (8, "sbs2Stallion", "gen5"), "sbssb": (9, "sbs2Breed", "gen5"),
        "sbsbs": (10, "sbsbStallion", "gen5"), "sbsbb": (11, "sbsbBreed", "gen5"),
        "sbbss": (12, "sb2sStallion", "gen5"), "sbbsb": (13, "sb2sBreed", "gen5"),
        "sbbbs": (14, "sb3Stallion", "gen5"), "sbbbb": (15, "sb3Breed", "gen5"),
        # endregion
        # region Breed 1gen, 2gen 3gen
        "b": (16, "breed", "gen1"),
        "bs": (16, "bStallion", "gen2"), "bb": (24, "bBreed", "gen2"),
        "bss": (16, "bsStallion", "gen3"), "bsb": (20, "bsBreed", "gen3"),
        "bbs": (24, "b2Stallion", "gen3"), "bbb": (28, "b2Breed", "gen3"),
        # endregion
        # region Breed 4gen
        "bsss": (16, "bs2Stallion", "gen4"), "bssb": (18, "bs2Breed", "gen4"),
        "bsbs": (20, "bsbStallion", "gen4"), "bsbb": (22, "bsbBreed", "gen4"),
        "bbss": (24, "b2sStallion", "gen4"), "bbsb": (26, "b2sBreed", "gen4"),
        "bbbs": (28, "b3Stallion", "gen4"), "bbbb": (30, "b3Breed", "gen4"),
        # endregion
        # region Breed 5gen
        "bssss": (16, "bs3Stallion", "gen5"), "bsssb": (17, "bs3Breed", "gen5"),
        "bssbs": (18, "bs2bStallion", "gen5"), "bssbb": (19, "bs2bBreed", "gen5"),
        "bsbss": (20, "bsbsStallion", "gen5"), "bsbsb": (21, "bsbsBreed", "gen5"),
        "bsbbs": (22, "bsb2Stallion", "gen5"), "bsbbb": (23, "bsb2Breed", "gen5"),
        "bbsss": (24, "b2s2Stallion", "gen5"), "bbssb": (25, "b2s2Breed", "gen5"),
        "bbsbs": (26, "b2sbStallion", "gen5"), "bbsbb": (27, "b2sbBreed", "gen5"),
        "bbbss": (28, "b3sStallion", "gen5"), "bbbsb": (29, "b3sBreed", "gen5"),
        "bbbbs": (30, "b4Stallion", "gen5"), "bbbbb": (31, "b4Breed", "gen5"),
        # endregion
    }
    idx, prefix, genCol = index_map[mode]
    idf = dfblood.iloc[idx::32].reset_index(drop=True)
    idf[genCol] = idf[genCol].str.split("\n", expand=True)[
        0].str.replace("\n", "")
    df[f"{prefix}Id"] = df["horseId"].map(
        idf.set_index("horseId")[f"{genCol}ID"])
    df[f"{prefix}Name"] = df["horseId"].map(idf.set_index("horseId")[genCol])
    return df, idf[["horseId", genCol, f"{genCol}ID"]].rename(columns={genCol: "GenName", f"{genCol}ID": "GenID"})

追加するデータは以下の10種類

  1. 父、母
  2. 父母
  3. 父父
  4. 母父
  5. 母母父
  6. 母母母父
  7. 母母母母父
  8. 父母父
  9. 父母母父
  10. 父母母母父
In [4]:
df, _ = add_blood_info_to_df(df, "s")  # 父
df, _ = add_blood_info_to_df(df, "b")  # 母
df, _ = add_blood_info_to_df(df, "sb")  # 父母
df, _ = add_blood_info_to_df(df, "ss")  # 父父
df, _ = add_blood_info_to_df(df, "bs")  # 母父
df, _ = add_blood_info_to_df(df, "bbs")  # 母母父
df, _ = add_blood_info_to_df(df, "bbbs")  # 母母母父
df, _ = add_blood_info_to_df(df, "bbbbs")  # 母母母母父
df, _ = add_blood_info_to_df(df, "sbs")  # 父母父
df, _ = add_blood_info_to_df(df, "sbbs")  # 父母母父
df, _ = add_blood_info_to_df(df, "sbbbs")  # 父母母母父
In [5]:
import gc
del dfblood
gc.collect()
Out[5]:
74
スポンサーリンク

5-2.セカンドモデル作成計画

血統分析パート3で行った血統と回収率の関係でみた血統の組合せで勝率を見る方法が幾分結果が良かったのでそれを採用したい。
しかし、計算方法が特殊なため再現するにはこれまた既存ソースに手を加える必要がある。

まずは、血統を入れるだけでも効果があるのかを確認したい
今回の場合追加する血統情報はhorseId形式になっているので、LightGBMでは不要だがベース前処理でカテゴリエンコードしているためエンコードしておきたい

よって、ここでは血統情報の効果を見るために、以下の7個のモデルを作成する

モデル1
父と母のhorseIdのみ追加
モデル2
モデル1+父父のhorseId追加
モデル3
モデル1+母父のhorseId追加
モデル4
モデル3+母母父のhorseId追加
モデル5
モデル4+母母母父と母母母母父のhorseId追加
モデル6
モデル5+父父のhorseId追加
モデル7
モデル6+父母父と父母母父と父母母母父のhorseId追加

また、モデル設定はファーストモデルと同じとする

スポンサーリンク

5-3.n個のセカンドモデル作成

5-3-0.効率化のための準備

モデル作成用関数

In [6]:
def create_model(df: pd.DataFrame, lgbm_model_manager: LightGBMModelManager):
    dataset_mapping = lgbm_model_manager.make_dataset_mapping(df)
    dataset_mapping = lgbm_model_manager.setup_dataset(dataset_mapping)
    params = {
        'boosting_type': 'gbdt',
        # 二値分類
        'objective': 'binary',
        'metric': 'auc',
        'verbose': 0,
        'seed': 77777,
        'learning_rate': 0.01,
        "n_estimators": 10000
    }

    lgbm_model_manager.train_all(
        params,
        dataset_mapping,
        stopping_rounds=500,  # ここで指定した値を超えるまでは、early stopさせない
        val_num=250  # ログを出力するスパン
    )
    for dataset_dict in dataset_mapping.values():
        lgbm_model_manager.load_model(dataset_dict.name)
        lgbm_model_manager.predict(dataset_dict)

    bet_mode = BetName.tan
    for dataset_dict in dataset_mapping.values():
        lgbm_model_manager.set_bet_column(dataset_dict, bet_mode)
    _, dfbetva, dfbette = lgbm_model_manager.merge_dataframe_data(
        dataset_mapping, mode=True)

    dfbetva, dfbette = lgbm_model_manager.generate_profit_loss(
        dfbetva, dfbette, bet_mode)

    lgbm_model_manager.basic_analyze(dataset_mapping)

    dftrain, dfvalid, dftest = lgbm_model_manager.merge_dataframe_data(
        dataset_mapping,
        mode=True
    )
    summary_dict = lgbm_model_manager.gegnerate_odds_graph(
        dftrain, dfvalid, dftest, bet_mode)

    lgbm_model_manager.export_model_info()

5-3-1.モデル1: 父と母のhorseId追加

父と母のhorseIdをエンコード

In [7]:
stllIdEnc = {hId: idx for idx, hId in enumerate(df["stallionId"].unique())}
df["stallionId_en"] = df["stallionId"].map(stllIdEnc).astype("category")
brdIdEnc = {hId: idx for idx, hId in enumerate(df["breedId"].unique())}
df["breedId_en"] = df["breedId"].map(brdIdEnc).astype("category")
del brdIdEnc, stllIdEnc

モデル作成準備

In [ ]:
lgbm_model_manager = LightGBMModelManager(
    # modelsディレクトリ配下に作成したいモデル名のフォルダパスを指定。
    # フォルダパスは絶対パスにすると安全です。
    root_dir / "models" / "blood_model_part1",
    split_year,
    target_year,
    end_year
)
# 説明変数にするカラム
feature_columns = [
    'distance',
    'number',
    'boxNum',
    'odds',
    'favorite',
    'age',
    'jweight',
    'weight',
    'gl',
    'race_span',
    "raceGrade",  # グレード情報を追加
] + dataPreP.encoding_columns

# 血統情報を追加
feature_columns += ["stallionId_en", "breedId_en"]

# 目的変数用のカラム
objective_column = "label_in1"

# 説明変数と目的変数をモデル作成用のインスタンスへセット
lgbm_model_manager.set_feature_and_objective_columns(
    feature_columns, objective_column)

# 目的変数の作成: 1着のデータに正解フラグを立てる処理を実行
df = lgbm_model_manager.add_objective_column_to_df(df, "label", 1)

モデル作成実行

In [ ]:
create_model(df, lgbm_model_manager)

5-3-2.モデル2: モデル1+父父のhorseId追加

父父のhorseIdをエンコード

In [10]:
sStllIdEnc = {hId: idx for idx, hId in enumerate(df["sStallionId"].unique())}
df["sStallionId_en"] = df["sStallionId"].map(sStllIdEnc).astype("category")
del sStllIdEnc

モデル作成準備

In [ ]:
lgbm_model_manager = LightGBMModelManager(
    # modelsディレクトリ配下に作成したいモデル名のフォルダパスを指定。
    # フォルダパスは絶対パスにすると安全です。
    root_dir / "models" / "blood_model_part2",
    split_year,
    target_year,
    end_year
)
# 説明変数にするカラム
feature_columns = [
    'distance',
    'number',
    'boxNum',
    'odds',
    'favorite',
    'age',
    'jweight',
    'weight',
    'gl',
    'race_span',
    "raceGrade",  # グレード情報を追加
] + dataPreP.encoding_columns

# 血統情報を追加
feature_columns += ["stallionId_en", "breedId_en", "sStallionId_en"]

# 目的変数用のカラム
objective_column = "label_in1"

# 説明変数と目的変数をモデル作成用のインスタンスへセット
lgbm_model_manager.set_feature_and_objective_columns(
    feature_columns, objective_column)

# 目的変数の作成: 1着のデータに正解フラグを立てる処理を実行
df = lgbm_model_manager.add_objective_column_to_df(df, "label", 1)

モデル作成実行

In [ ]:
create_model(df, lgbm_model_manager)
 

5-3-3.モデル3: モデル1+母父のhorseId追加

母父のhorseIdをエンコード

In [13]:
sStllIdEnc = {hId: idx for idx, hId in enumerate(df["bStallionId"].unique())}
df["bStallionId_en"] = df["bStallionId"].map(sStllIdEnc).astype("category")
del sStllIdEnc

モデル作成準備

In [ ]:
lgbm_model_manager = LightGBMModelManager(
    # modelsディレクトリ配下に作成したいモデル名のフォルダパスを指定。
    # フォルダパスは絶対パスにすると安全です。
    root_dir / "models" / "blood_model_part3",
    split_year,
    target_year,
    end_year
)
# 説明変数にするカラム
feature_columns = [
    'distance',
    'number',
    'boxNum',
    'odds',
    'favorite',
    'age',
    'jweight',
    'weight',
    'gl',
    'race_span',
    "raceGrade",  # グレード情報を追加
] + dataPreP.encoding_columns

# 血統情報を追加
feature_columns += ["stallionId_en", "breedId_en", "bStallionId_en"]

# 目的変数用のカラム
objective_column = "label_in1"

# 説明変数と目的変数をモデル作成用のインスタンスへセット
lgbm_model_manager.set_feature_and_objective_columns(
    feature_columns, objective_column)

# 目的変数の作成: 1着のデータに正解フラグを立てる処理を実行
df = lgbm_model_manager.add_objective_column_to_df(df, "label", 1)

モデル作成実行

In [ ]:
create_model(df, lgbm_model_manager)

5-3-4.モデル4: モデル3+母母父のhorseId追加

母母父のhorseIdをエンコード

In [16]:
sStllIdEnc = {hId: idx for idx, hId in enumerate(df["b2StallionId"].unique())}
df["b2StallionId_en"] = df["b2StallionId"].map(sStllIdEnc).astype("category")
del sStllIdEnc

モデル作成準備

In [ ]:
lgbm_model_manager = LightGBMModelManager(
    # modelsディレクトリ配下に作成したいモデル名のフォルダパスを指定。
    # フォルダパスは絶対パスにすると安全です。
    root_dir / "models" / "blood_model_part4",
    split_year,
    target_year,
    end_year
)
# 説明変数にするカラム
feature_columns = [
    'distance',
    'number',
    'boxNum',
    'odds',
    'favorite',
    'age',
    'jweight',
    'weight',
    'gl',
    'race_span',
    "raceGrade",  # グレード情報を追加
] + dataPreP.encoding_columns

# 血統情報を追加
feature_columns += ["stallionId_en", "breedId_en",
                    "bStallionId_en", "b2StallionId_en"]

# 目的変数用のカラム
objective_column = "label_in1"

# 説明変数と目的変数をモデル作成用のインスタンスへセット
lgbm_model_manager.set_feature_and_objective_columns(
    feature_columns, objective_column)

# 目的変数の作成: 1着のデータに正解フラグを立てる処理を実行
df = lgbm_model_manager.add_objective_column_to_df(df, "label", 1)

モデル作成実行

In [ ]:
create_model(df, lgbm_model_manager)

5-3-5.モデル5: モデル4+母母母父+母母母母父のhorseId追加

母母父のhorseIdをエンコード

In [19]:
sStllIdEnc = {hId: idx for idx, hId in enumerate(df["b3StallionId"].unique())}
df["b3StallionId_en"] = df["b3StallionId"].map(sStllIdEnc).astype("category")
sStllIdEnc = {hId: idx for idx, hId in enumerate(df["b4StallionId"].unique())}
df["b4StallionId_en"] = df["b4StallionId"].map(sStllIdEnc).astype("category")
del sStllIdEnc

モデル作成準備

In [ ]:
lgbm_model_manager = LightGBMModelManager(
    # modelsディレクトリ配下に作成したいモデル名のフォルダパスを指定。
    # フォルダパスは絶対パスにすると安全です。
    root_dir / "models" / "blood_model_part5",
    split_year,
    target_year,
    end_year
)
# 説明変数にするカラム
feature_columns = [
    'distance',
    'number',
    'boxNum',
    'odds',
    'favorite',
    'age',
    'jweight',
    'weight',
    'gl',
    'race_span',
    "raceGrade",  # グレード情報を追加
] + dataPreP.encoding_columns

# 血統情報を追加
feature_columns += ["stallionId_en", "breedId_en", "bStallionId_en",
                    "b2StallionId_en", "b3StallionId_en", "b4StallionId_en"]

# 目的変数用のカラム
objective_column = "label_in1"

# 説明変数と目的変数をモデル作成用のインスタンスへセット
lgbm_model_manager.set_feature_and_objective_columns(
    feature_columns, objective_column)

# 目的変数の作成: 1着のデータに正解フラグを立てる処理を実行
df = lgbm_model_manager.add_objective_column_to_df(df, "label", 1)

モデル作成実行

In [ ]:
create_model(df, lgbm_model_manager)

5-3-6.モデル6: モデル5+父父のhorseId追加¶

In [ ]:
lgbm_model_manager = LightGBMModelManager(
    # modelsディレクトリ配下に作成したいモデル名のフォルダパスを指定。
    # フォルダパスは絶対パスにすると安全です。
    root_dir / "models" / "blood_model_part6",
    split_year,
    target_year,
    end_year
)
# 説明変数にするカラム
feature_columns = [
    'distance',
    'number',
    'boxNum',
    'odds',
    'favorite',
    'age',
    'jweight',
    'weight',
    'gl',
    'race_span',
    "raceGrade",  # グレード情報を追加
] + dataPreP.encoding_columns

# 血統情報を追加
feature_columns += ["stallionId_en", "breedId_en", "bStallionId_en",
                    "b2StallionId_en", "b3StallionId_en", "b4StallionId_en", "sStallionId_en"]

# 目的変数用のカラム
objective_column = "label_in1"

# 説明変数と目的変数をモデル作成用のインスタンスへセット
lgbm_model_manager.set_feature_and_objective_columns(
    feature_columns, objective_column)

# 目的変数の作成: 1着のデータに正解フラグを立てる処理を実行
df = lgbm_model_manager.add_objective_column_to_df(df, "label", 1)

モデル作成実行

In [ ]:
create_model(df, lgbm_model_manager)

5-3-7.モデル7: モデル6+父母父+父母母父+父母母母父のhorseId追加

父母父と父母母父と父母母母父のhorseIdをエンコード

In [24]:
sStllIdEnc = {hId: idx for idx, hId in enumerate(df["sbStallionId"].unique())}
df["sbStallionId_en"] = df["sbStallionId"].map(sStllIdEnc).astype("category")
sStllIdEnc = {hId: idx for idx, hId in enumerate(df["sb2StallionId"].unique())}
df["sb2StallionId_en"] = df["sb2StallionId"].map(sStllIdEnc).astype("category")
sStllIdEnc = {hId: idx for idx, hId in enumerate(df["sb3StallionId"].unique())}
df["sb3StallionId_en"] = df["sb3StallionId"].map(sStllIdEnc).astype("category")
del sStllIdEnc
gc.collect()
Out[24]:
33
In [ ]:
lgbm_model_manager = LightGBMModelManager(
    # modelsディレクトリ配下に作成したいモデル名のフォルダパスを指定。
    # フォルダパスは絶対パスにすると安全です。
    root_dir / "models" / "blood_model_part7",
    split_year,
    target_year,
    end_year
)
# 説明変数にするカラム
feature_columns = [
    'distance',
    'number',
    'boxNum',
    'odds',
    'favorite',
    'age',
    'jweight',
    'weight',
    'gl',
    'race_span',
    "raceGrade",  # グレード情報を追加
] + dataPreP.encoding_columns

# 血統情報を追加
feature_columns += ["stallionId_en", "breedId_en", "bStallionId_en", "b2StallionId_en", "b3StallionId_en",
                    "b4StallionId_en", "sStallionId_en", "sbStallionId_en", "sb2StallionId_en", "sb3StallionId_en"]

# 目的変数用のカラム
objective_column = "label_in1"

# 説明変数と目的変数をモデル作成用のインスタンスへセット
lgbm_model_manager.set_feature_and_objective_columns(
    feature_columns, objective_column)

# 目的変数の作成: 1着のデータに正解フラグを立てる処理を実行
df = lgbm_model_manager.add_objective_column_to_df(df, "label", 1)

モデル作成実行

In [ ]:
create_model(df, lgbm_model_manager)
スポンサーリンク

5-4.性能の比較(WEBアプリ起動)

以下のコードを実行するとWEBアプリが起動します

このセルを実行しても起動できますが、コマンドプロンプトで実行することを推奨します。
このセルで起動させる場合は、最後の行の「#」を削除して実行してください。

In [27]:
! python ../app_keiba/manage.py makemigrations
! python ../app_keiba/manage.py migrate 
! echo server launch OK
# ! python ../app_keiba/manage.py runserver 12345
 
No changes detected
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, model_analyzer, sessions
Running migrations:
  No migrations to apply.
server launch OK

「server launch OK」の表示がでたら以下のリンクをクリックしてWEBアプリへアクセス

http://localhost:12345/index.html
  モデルID 支持率OGS 回収率OGS AonBOGS
1 blood_model_part4 0.49716 -7.56706 0.08280
2 blood_model_part5 0.49100 -7.57026 0.07960
3 blood_model_part6 0.49100 -7.57026 0.07960
4 blood_model_part3 0.44011 -7.63086 0.01900
5 blood_model_part1 0.41878 -7.66353 -0.01367
6 blood_model_part7 0.38123 -7.67139 -0.02153
7 blood_model_part2 0.40725 -7.67494 -0.02509
8 first_model (baseline) 0.41924 -7.64492  

あまりにも微々たる差過ぎて最早差がないレベル

任意のモデルの特徴量重要度をみる

スポンサーリンク

5-5.特徴量重要度

5-5-1.関数の作成¶

In [28]:
# 指定したモデルIDから最新の2023年下期(2023second)モデルを読込
# 特徴量重要度を取得
def get_model_feature_important(
    model_id: Literal[
        "blood_model_part1", "blood_model_part2",
        "blood_model_part3", "blood_model_part4", "blood_model_part5",
        "blood_model_part6", "blood_model_part8"
    ],
    model_attr="2023second",
    mode: Literal["gain", "split"] = "gain"
):
    lgbm_model_manager = LightGBMModelManager(
        # modelsディレクトリ配下に作成したいモデル名のフォルダパスを指定。
        # フォルダパスは絶対パスにすると安全です。
        root_dir / "models" / model_id,
        split_year,
        target_year,
        end_year
    )
    lgbm_model_manager.load_model(model_attr)

    dfimp = pd.DataFrame(
        lgbm_model_manager.model.feature_importance(
            importance_type=mode),
        index=lgbm_model_manager.feature_columns,
        columns=[model_id]
    ).sort_values(model_id)

    # 重要度が0の特徴量を削除
    return dfimp[dfimp > 0]

5-5-2.重要度の確認

全期間のモデルの2023secondの内容を確認する

In [ ]:
dfimp: pd.DataFrame = pd.concat(
    [
        get_model_feature_important(f"blood_model_part{n}")
        for n in range(1, 8)
    ],
    axis=1
).dropna(
    axis=0, how="all")
In [30]:
(dfimp/1000).round(1).loc[
    dfimp.mean(axis=1).sort_values().index].fillna("")
Out[30]:
  blood_model_part1 blood_model_part2 blood_model_part3 blood_model_part4 blood_model_part5 blood_model_part6 blood_model_part7
jockeyId_en 4.5 4.5 1.8 0.6 0.4 0.4 0.4
stallionId_en 12.0 12.0 7.9 6.4 3.6 3.6 3.6
teacherId_en 21.2 21.2 15.7 13.1 8.9 8.9 8.9
b2StallionId_en       20.9 13.2 13.2 13.2
horseId_en 19.0 19.0 13.5 15.7 14.2 14.2 14.2
bStallionId_en     22.2 19.2 13.4 13.4 13.4
b3StallionId_en         19.0 19.0 19.0
b4StallionId_en         23.1 23.1 23.1
breedId_en 109.8 109.8 88.8 83.7 69.6 69.6 69.6
odds 1739.8 1739.8 1648.1 1667.4 1646.2 1646.2 1646.2
スポンサーリンク

5-6.ここまでの考察

特徴量重要度の結果からオッズが高いのはファーストモデルと同様の結果であるので、学習はできてそうである
驚くことに、0でない重要度の一覧に父父や父母父以降の特徴量がいない
一方で母や母父系の特徴量が軒並み重要度で出ており、母系の種牡馬が競走馬の能力に関係しているのだろうかと考えられる

しかし、この結果はやや微妙にも思える
結論から言うと、母や母父のhorseIdを特徴量にするには表現力が足りないのではないかと考える。
LightGBMに限った話ではないが、特に決定木ベースのモデルではこのようなカテゴリ特徴量の扱いがやや特殊になっている。
というのも、決定木は各特徴量に境界線を引いて場合分けをしていき、タスクを解いていくものになっている。
そのため、カテゴリ特徴量をグループA, グループB, …, グループNのように分類していくことになる。
つまり、今回では1着となる競走馬を知りたいというタスクであるため、LightGBMは学習する際に母や母父などのhorseIdをみてこのhorseIdを持っていると1着になる、1着にならないという判断でグループ分けを行うようになっている。
考え方によってはそのような学習の仕方でも問題ないと判断することもあるが、今回の競馬というドメインでみると不適とみるべきと考える

なぜならば、話が少し発散するが現在の競馬予想モデルでは1頭ずつ勝利するかの確信度を推論したのち、別の処理でレース情報単位に最も確信度の高い競走馬の馬券を購入するといった運用になっている
つまり、モデルは1頭の情報をみて確信度を出しているため、同じレースに出走する他の競走馬の影響を考慮できていない状態である。
このような問題はランキング学習などを行えばLightGBMでも解決できるものであるが、少なくとも私の想定する競馬予想モデルは10数頭出走するレース情報から互いの競走馬の相性やレース条件との相性など、競走馬の外的/内的な要素から総合的に判断して勝利する競走馬を予測できるものだと考えている。
しかし、LightGBMで血統のhorseIdだけを特徴量として採用してしまうと、LightGBMは他の競走馬関係なく特定のhorseIdを持っているからこの馬が勝つと学習しかねない状態であると言わざるを得ない。
これは競走馬とその血統だけに限った話ではなく、騎手や調教師でも今はjockeyIdやteacherIdというカテゴリ特徴量として学習させている。
そうなってくると、LightGBMは各カテゴリの値に対して、確信度として情報を集約して判断してしまうことになると考えられる。
そのため、このhorseIdやjockeyIdの情報を持つ競走馬はレース条件やら何やらの外的情報も合算して、平均してこのぐらいの確信で勝てるだろうという情報しかLightGBMは学習できないと考える。
これは前回の芝とダートでモデルを分けるかの分析で分けた方が精度が良かったことからも支持される主張ではないかと考える
(芝だけダートだけとデータを分けて学習することによって、LightGBMは芝とダートを合算した確信度ではなく、芝だけダートだけの確信度に集中して学習できたからだという意味である)

つまり、血統のhorseIdなど個を割り出せる情報をカテゴリ特徴量として扱うには、今回で言えば1着を当てるための特徴量としての表現力が足りないのではないかと考える。

スポンサーリンク

5-7.勝率の情報を追加したモデルの作成

やりたくなかったが、上記のような考察をサポートするためにも、血統分析パート3で実施したレース条件(馬場×距離カテゴリ)ごとに血統(父,母,母父,母母父など)の勝率の情報を追加してモデルの学習を行う

血統分析パート3↓

血統だけで回収率130%超えた話
Pythonを学び始めた人の多くは血統情報を機械学習モデルに入れて稼げるモデルを作りたいと思ってるわけなんですけど、意外と血統情報だけを使った簡単なルールベースのモデルだけでも回収率100%超えられるみたいなので、そのやり方とPythonのソースを共有します。

5-7-1.勝率情報の追加版のモデルインスタンス作成

性能比較の結果からモデル4の父,母,母父,母母父の四種の血統を追加する代わりに、父, 母, 母父, 母母父の勝率情報を追加する

まずはモデル作成用のインスタンスを作成

In [ ]:
lgbm_model_manager = LightGBMModelManager(
    # modelsディレクトリ配下に作成したいモデル名のフォルダパスを指定。
    # フォルダパスは絶対パスにすると安全です。
    root_dir / "models" / "blood_model_part4-ext",
    split_year,
    target_year,
    end_year
)
# 説明変数にするカラム
feature_columns = [
    'distance',
    'number',
    'boxNum',
    'odds',
    'favorite',
    'age',
    'jweight',
    'weight',
    'gl',
    'race_span',
    "raceGrade",  # グレード情報を追加
] + dataPreP.encoding_columns

# 血統情報を追加
feature_columns += ["stallionId_en", "breedId_en",
                    "bStallionId_en", "b2StallionId_en"]
# 血統の勝率情報の追加
feature_columns += ["winR_stallion", "winR_breed",
                    "winR_bStallion", "winR_b2Stallion"]


# 目的変数用のカラム
objective_column = "label_in1"

# 説明変数と目的変数をモデル作成用のインスタンスへセット
lgbm_model_manager.set_feature_and_objective_columns(
    feature_columns, objective_column)

# 目的変数の作成: 1着のデータに正解フラグを立てる処理を実行
df = lgbm_model_manager.add_objective_column_to_df(df, "label", 1)

5-7-2.勝率追加用処理

勝率情報追加用に先にdataset_mappingを作成しておく

In [32]:
dataset_mapping = lgbm_model_manager.make_dataset_mapping(df)
 
2024-08-23 01:40:08.142 | INFO     | src.data_manager.dataset_tools:make_dataset_mapping:103 - Generate dataset mapping. Year Range: 2019 -> 2023

一旦は動くことを目的に実装

In [33]:
blood_set = [["stallionId"], ["breedId"],
             ["bStallionId"], ["b2StallionId"]]

now = datetime.datetime.now().strftime('%Y/%m/%d %H:%M:%S')

with tqdm.tqdm(
    total=len(dataset_mapping),
    desc=f"Add blood win rate in 2019first ({now}) ..."
) as pbar:

    for key, dataset in dataset_mapping.items():
        now = datetime.datetime.now().strftime('%Y/%m/%d %H:%M:%S')
        pbar.set_description(desc=f"Add blood win rate in {key} ({now}) ...")
        winR_map = {}

        for target_list in [["train", ["train", "valid"]], ["train", "valid", ["test"]]]:
            concat_list = target_list[:-1]
            target_set = target_list[-1]
            idf2 = pd.concat([dataset.__dict__[target]
                             for target in concat_list])
            set_columns_dict = {}
            idf = df[df["raceDate"] <= idf2["raceDate"].max()]

            for blood_cross in blood_set:
                dffilter = idf[blood_cross[0]].isin(
                    idf2[blood_cross[0]].unique())
                for bc in blood_cross[1:]:
                    dffilter &= idf[bc].isin(idf2[bc].unique())
                idf3 = idf[dffilter]
                group_columns = ["field", "dist_cat"]+blood_cross
                idfagg = idf3.groupby(group_columns)[objective_column].mean()
                set_column = "winR_"+"_".join([b[:-2] for b in blood_cross])
                set_columns_dict[set_column] = idfagg

            for t in target_set:
                winR_map[t] = set_columns_dict.copy()

        for mode, winR in winR_map.items():
            for kcol, idfagg in winR.items():
                group_columns = idfagg.reset_index().columns.tolist()[:-1]
                idf = dataset.__dict__[mode].set_index(group_columns)
                idf[kcol] = idfagg
                idf[kcol].fillna(0, inplace=True)
                dataset.__dict__[mode] = idf.reset_index()
        pbar.update()
 
Add blood win rate in 2023second (2024/08/23 01:44:51) ...: 100%|██████████| 10/10 [05:16<00:00, 31.68s/it]
In [34]:
def create_model_winR(
    dataset_mapping: dict[str, LightGBMDataset],
    lgbm_model_manager: LightGBMModelManager
):
    dataset_mapping = lgbm_model_manager.setup_dataset(dataset_mapping)
    params = {
        'boosting_type': 'gbdt',
        # 二値分類
        'objective': 'binary',
        'metric': 'auc',
        'verbose': 0,
        'seed': 77777,
        'learning_rate': 0.01,
        "n_estimators": 10000
    }

    lgbm_model_manager.train_all(
        params,
        dataset_mapping,
        stopping_rounds=500,  # ここで指定した値を超えるまでは、early stopさせない
        val_num=250  # ログを出力するスパン
    )
    for dataset_dict in dataset_mapping.values():
        lgbm_model_manager.load_model(dataset_dict.name)
        lgbm_model_manager.predict(dataset_dict)

    bet_mode = BetName.tan
    for dataset_dict in dataset_mapping.values():
        lgbm_model_manager.set_bet_column(dataset_dict, bet_mode)
    _, dfbetva, dfbette = lgbm_model_manager.merge_dataframe_data(
        dataset_mapping, mode=True)

    dfbetva, dfbette = lgbm_model_manager.generate_profit_loss(
        dfbetva, dfbette, bet_mode)

    lgbm_model_manager.basic_analyze(dataset_mapping)

    dftrain, dfvalid, dftest = lgbm_model_manager.merge_dataframe_data(
        dataset_mapping,
        mode=True
    )
    summary_dict = lgbm_model_manager.gegnerate_odds_graph(
        dftrain, dfvalid, dftest, bet_mode)

    lgbm_model_manager.export_model_info()

5-7-3.モデル作成実行¶

In [ ]:
create_model_winR(dataset_mapping, lgbm_model_manager)

5-7-4.性能の比較(WEBアプリ起動)

以下のコードを実行するとWEBアプリが起動します

このセルを実行しても起動できますが、コマンドプロンプトで実行することを推奨します。
このセルで起動させる場合は、最後の行の「#」を削除して実行してください。

In [36]:
! python ../app_keiba/manage.py makemigrations
! python ../app_keiba/manage.py migrate 
! echo server launch OK
# ! python ../app_keiba/manage.py runserver 12345
 
No changes detected
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, model_analyzer, sessions
Running migrations:
  No migrations to apply.
server launch OK

「server launch OK」の表示がでたら以下のリンクをクリックしてWEBアプリへアクセス

http://localhost:12345/index.html
  モデルID 支持率OGS 回収率OGS AonBOGS
1 blood_model_part4-ext 1.02802 -4.55082 3.09904
2 blood_model_part4 0.49716 -7.56706 0.08280
3 first_model
(baseline)
0.41924 -7.64492  

5-7-5.血統モデル4と重要度の比較¶

In [ ]:
dfimp: pd.DataFrame = pd.concat(
    [
        get_model_feature_important(f"first_model", mode="gain"),
        get_model_feature_important(f"blood_model_part4", mode="gain"),
        get_model_feature_important(f"blood_model_part4-ext", mode="gain")
    ],
    axis=1
).dropna(
    axis=0, how="all")

dfimp.round(2).loc[
    dfimp.mean(axis=1).sort_values().index].fillna("")

5-7-6.考察

重要度を見るとオッズがもっとも高いことに変わりはないが、extモデルの方ではwinR_breedである繫殖牝馬の産駒の勝率が強く関係している様子。
しかも、オッズの重要度に対して約半分ほどモデルに影響していることが分かる。
他には、繫殖牝馬の勝率の次にレースグレードの特徴量も高くでているのも少し気になる結果。

結果を深堀すると、血統モデル4でも繁殖牝馬のカテゴリ特徴量がオッズの次に重要と出ていたが、今回のように過去成績の情報を特徴量として追加すると、そちらの方が重要であると判断されていることが分かる。
これは5-6節で展開した考察通りの結果になっていると解釈できる。
つまりLightGBMなどの決定木モデル特有の問題かは定かではないが、カテゴリ情報をそのままモデルの学習として使うには表現力が足りないことが分かった。

しかし、今回血統の勝率を情報として加えたが、繫殖牝馬以外の血統に対する勝率の情報が重要度として出てきていないのも一つ気がかりである。
カテゴリは重要度で出現しているが、勝率になると出てきていないということは、カテゴリの方が本タスクを解く上で重要であると判断されていることになる。
この結果について、原因は2つあると考える。

原因1
父系の情報はその勝率では表現力が足りない。
またはカテゴリとしてしか意味を持たない
原因2
繫殖牝馬の産駒はそもそも数が少ないので、兄弟が強いとそれだけ能力が共通するのかもしれない
つまり、母の父系や父の血統で勝率をみるとそれだけ兄弟、孫が沢山いるため能力が平均的または分散が大きくなり、特定の特徴の判断が出来ないのではと考えられる

上記の原因の対応にはさらなる血統に関する特徴量の工夫が必要になると考えられるが、まだそれを実施するのは尚早なため他の特徴量の分析を進めることとする

スポンサーリンク

5-8.結論

5-8-1.分かったこと

  1. 血統のhorseIdだけを特徴量として入れるのは、1着を当てる上では表現力が足りないことが分かった
  2. 母の兄弟馬の影響がオッズの次に関係することが分かった
  3. 母父や母母父などの勝率は影響がなく、カテゴリとして意味があることが分かった
  4. 血統の勝率を特徴量として追加すると、汎化能力が向上することが分かった
    ⇒ カテゴリ特徴量ではなく、ステータス情報を追加すると有効な可能性

5-8-2.決定事項

セカンドモデルの決定
血統モデル4 Extraをセカンドモデルとする
ファーストモデルに父,母,母父,母母父のhorseIdと馬場×距離カテゴリごとの勝率を追加したモデル
ベース前処理追加
5代血統のうち父,母,母父,母…父の情報を追加する処理の追加
追加する血統の種別は暫定なので、追加する血統種別を任意に選択できるようにしておく

5-8-3.次回やること

裏でやること
指定したカテゴリに対する勝率の情報追加処理の開発
次回の内容
サードモデル開発に向けた過去成績の分析

コメント

タイトルとURLをコピーしました