Philarium

Keiba AI Prediction

機械学習ベースの競馬予測システム

学生を対象としたAIコンテストのために開発したプロジェクトです。 参加者は各自のモデルを用いて実際のレースに仮想ポイントを賭け、その増減を競い合いました。 1シーズン全約3,500レース・延べ約4.8万頭の出走データに加え、各出走馬のキャリアを数年遡る約25万件の過去戦績を自動収集し、 馬場状態・斤量・馬体重・直近成績といった多面的な要因から各レースの勝ち馬を予測するAIシステムを構築しました。

システム構成

データ収集 (Data Collection)

urllib & BeautifulSoupによる堅牢なスクレイピングに加え、動的ページにはSelenium(headless Chrome)を併用。 サーバー負荷に配慮した待機制御に加え、取得したHTMLをバイナリ保存して再解析できる、再現性の高いデータ基盤を構築。

前処理 (Preprocessing)

Pandasを用いた大規模データ処理。欠損値の補完、カテゴリ変数の数値化、正規表現によるテキスト情報の構造化。 生データ→前処理→特徴量→学習の番号付きステージ設計でパイプラインを管理。

モデル構築 (Modeling)

LightGBM(勾配ブースティング決定木)による勝ち馬の二値分類。正例率約7%の不均衡データに対し、 early stoppingとパラメータ探索で汎化性能を最適化。

モデル設計と検証

「当てる」だけでなく「正しく検証する」ことを重視し、実運用を見据えた設計を行いました。

時系列バリデーション

学習・評価データを日付で分割(2023年10月を境界)し、「過去から未来を予測する」本番と同じ条件で汎化性能を測定。 ランダム分割による楽観的評価を排除。

リーク防止の二重設計

特徴量集計の段階でも予測対象レースより過去の戦績のみを使用(date > date_horse)。 時系列分割と合わせ、未来情報の混入を二重に遮断。

学習・推論の特徴量共通化

バッチ学習と、Seleniumで出馬表をリアルタイム取得するライブ推論の双方で特徴量生成ロジックを共通化し、 train/serving skew(学習時と本番の食い違い)を回避。

マルチスケール特徴量

直近3/5/10走とキャリア通算の4ウィンドウで着順・賞金をローリング集計し、「調子」と「地力」を分離して表現。 計26特徴量をconfig駆動で管理。

コード例

特徴量エンジニアリングの核心部分です。過去N走の成績を集計し、馬ごとの「実力」を表す特徴量を生成しています。

class FeatureCreator:
    """レース×出走馬の母集団に、各馬の過去戦績から特徴量を付与するクラス"""

    def __init__(self, results_filepath, race_info_filepath, horse_results_filepath):
        self.results = pd.read_csv(results_filepath, sep="\t")
        self.race_info = pd.read_csv(race_info_filepath, sep="\t")
        self.horse_results = pd.read_csv(horse_results_filepath, sep="\t")

        # レース×馬の組み合わせをベースとする母集団を構築
        self.population = self.results[["race_id", "horse_id"]].merge(
            self.race_info[["race_id", "date"]], on="race_id"
        )

    def agg_horse_n_races(self, n_races: list[int] = [3, 5, 10, 1000]):
        """直近N走の着順・賞金平均を集計し、馬の能力指標を算出"""
        grouped_df = (
            self.population.merge(
                self.horse_results, on=["horse_id"], suffixes=("", "_horse")
            )
            # リーク防止: 予測対象レースより過去の戦績のみを使用
            .query("date > date_horse")
            .sort_values("date_horse", ascending=False)
            .groupby(["race_id", "horse_id"])
        )
        merged_df = self.population.copy()

        # 直近3走(=調子)からキャリア通算(=地力)まで、複数スケールで集計
        for n_race in n_races:
            df = (
                grouped_df.head(n_race)
                .groupby(["race_id", "horse_id"])[["rank", "prize"]]
                .mean()
            ).add_suffix(f"_{n_race}races")

            merged_df = merged_df.merge(df, on=["race_id", "horse_id"])

        self.agg_horse_n_races_df = merged_df

特徴量の重要度

モデルが予測時にどの特徴量を重視しているかを可視化しました。 以下のプロットはLightGBMによって算出されたFeature Importance(Gain)の上位特徴量です。

Feature Importance Plot

「近3走賞金」や「近3走着順」が上位に来ていることから、 直近のレース成績が非常に強力な予測因子であることがわかります。 また、「馬固有能力」や「調教師」、「騎手」なども重要な要素として寄与しており、 これらを組み合わせることで回収率の向上を図りました。

結果と成果

テスト期間(時系列分割による未来のレース)に対し、実際の払戻データ2.7万件を用いた単勝購入シミュレーションで検証しました。

購入戦略的中率回収率
単勝人気1位を購入(市場のベースライン)32.9%79.4%
モデル予測1位を購入26.1%86.8%
予測確率の高いレースに限定して購入33.0%108.0%

回収率ベースでモデル予測(86.8%)がオッズという市場の集合知(79.4%)を上回り、市場に対するエッジを持つことを確認。 さらにモデルの予測確率でベットを選別する戦略により、控除率約20%の単勝市場でプラス収支となる回収率108%を達成しました。 データ収集基盤から特徴量設計・時系列検証・払戻シミュレーションまでの一連のパイプラインを独力で構築し、 「予測精度」ではなく「期待値」で意思決定する、実践的な機械学習の運用を体得したプロジェクトです。