プログラミング

【Python】機械学習を用いた競馬予想【前処理・特徴量生成編】

悩んでいる人

人手で競馬予想を行うのは限界があるため、機械学習を利用したい。

収集したデータの前処理の方法と特徴量の生成方法を教えて欲しい。

こんなお悩みを解決します。

前回は、データの収集方法について解説しました。

前回の記事を確認していない方は、以下の記事を参考にデータ収集を行っておいてください。

あわせて読みたい
【Python】機械学習を用いた競馬予想【データ収集編】

続きを見る

今回は、前処理の方法と特徴量の生成方法について解説します。

実際にPythonの実装結果もあわせて記載していくので、興味がある方はぜひご覧ください。

効率良く技術習得したい方へ

短期間でプログラミング技術を習得したい場合は、経験者からフォローしてもらえる環境下で勉強することをおすすめします。

詳細は、以下の記事をご覧ください。

【比較】プログラミングスクールおすすめランキング6選【初心者向け】

続きを見る

今回の実装結果

今回の実装結果は、GitHubに掲載しています。

一部省略している箇所もあるため、全体像を把握したい方は、以下のリンクにアクセスしてください。

https://github.com/yuruto-free/machine-learning-keiba/tree/v0.3.0

また、Preprocess.pyだけは、以下を参照してください。

https://github.com/yuruto-free/machine-learning-keiba/blob/v0.4.1/workspace/keiba/modules/Preprocess.py

注意点・お詫び

今回は、前処理だけを記載しますが、先行してlightGBMという機械学習アルゴリズムを用いて学習・評価もしています。

しかしながら、結果は散々なもので、期待した性能は出ていません。

以降の内容は、実装時の備忘録として残しておきますが、期待する効果を出すためには、前処理の方法を考える必要があることが分かりました。

具体的な評価内容・評価結果を以下に掲載しておきます。

項目内容
機械学習手法で解く問題1着から3着の馬を当てる
学習時の条件各レースのうち、1着から6着までの馬の情報(当日のコースや過去の成績など)を用いる。
※学習時のデータのバランスを考慮し、7着以降は参照しないようにしています。
※動作確認のため、テストデータに対しても、候補を絞った上で予測させています。
機械学習手法lightGBMによる2値分類
学習用データ、評価用データ・学習用データ:1884レース分(総レコード数:11306件≒1884[レース]*6[頭/レース])
・評価用データ:1010レース分(総レコード数:6058件≒1010[レース]*6[頭/レース])
評価結果再現率(正解ラベルが1着から3着となっている馬のうち、実際に1着から3着に入ると予測できた数の割合):50%
→勘で当てている状態(6頭のうち3頭を適当に当てているのと同じであるため)
手元での評価結果(簡易版)

このため、今回の内容は、実装時の参考程度の情報となります。

前処理の概要

今回は、以下のステップで前処理を行っていきます。

  1. レース結果、レース情報、払い戻し結果、馬の過去成績、血統情報に対し、情報抽出や無効値の処理などの整形処理を行う。
  2. レース結果・レース情報・血統情報をマージ後、各馬に対し、過去成績の集計結果を付与する。
  3. カテゴリ変数をラベルエンコーディングし、機械学習の入力に用いる特徴量を生成する。

各ステップの実施結果(参考)

各ステップにより得られた結果を以下に示します。

また、前回のスクレイピング結果と比較できるように、処理前後の画像を掲載します。

【整形処理】レース結果

レース結果(スクレイピング結果)
レース結果(整形処理後)

【整形処理】レース情報

レース情報(スクレイピング結果)
レース情報(整形処理後)

【整形処理】払い戻し結果

払い戻し結果(スクレイピング結果)
払い戻し結果(整形処理後)

【整形処理】馬の過去成績

馬の過去成績(スクレイピング結果)
馬の過去成績(整形処理後)

【整形処理】血統情報

血統情報(スクレイピング結果)
血統情報(整形処理後)

【マージ処理】レース結果・レース情報・血統情報の結合と馬の過去成績の集計結果のマージ

こちらは、新たに作成したデータとなるため、マージ処理後の結果のみ掲載します。

マージ処理結果

また、columnの情報は以下のようになります。

マージ処理後のcolumnの情報

【特徴量生成】カテゴリ変数のラベルエンコーディング実施

今回は、lightGBMという決定木ベースのアルゴリズムを利用するため、カテゴリ変数の処理のみ行い、正規化等のスケーリング値処理は行いません。

実際に入力として利用する特徴量は以下のようになります。

機械学習の入力となる特徴量

また、columnの情報は以下のようになります。

特徴量のcolumnの情報

ディレクトリ構成

以降では、Pythonを用いて前処理を行うためのプログラムについて解説します。

まず、該当するプログラムを追加した後のディレクトリ構造は、以下のようになります。


.
|-- Dockerfile
|-- docker-compose.yml
|-- entrypoint.sh
`-- workspace
    `-- keiba
        |-- data
        |   |-- html
        |   |   |-- horse
        |   |   |-- ped
        |   |   `-- race
        |   |-- master
        |   `-- raw
        |       |-- results.pkl
        |       |-- race_info.pkl
        |       |-- payback.pkl
        |       |-- horse_results.pkl
        |       `-- ped_results.pkl
        |-- models
        |-- modules
        |   |-- __init__.py # [変更]モジュールロード用
        |   |-- Constants.py # [変更]定数用
        |   |-- Collection.py
        |   |-- Preprocess.py # [追加]前処理用
        |   |-- DataManager.py # [追加]マージ用
        |   `-- FeatureEngineering.py # [追加]特徴量生成用
        |-- scrape.ipynb
        `-- preprocess_learning.ipynb # [追加]前処理・特徴量生成用(学習部分は次回)

以降では、処理の全体の流れを解説後、実装例と共に処理内容を解説したいと思います。

処理全体の流れ

まず、処理の全体の流れについて説明します。

別ファイルに分けている内容もあるため、参照する段階であわせて紹介します。

%load_ext autoreload
%reload_ext autoreload
%autoreload 2

import pandas as pd
from modules import Preprocess
from UserParams import Params
from datetime import datetime
pd.set_option('display.max_columns', 100)
pd.set_option('display.max_rows', 30)

# ==================
# 前処理&データ結合
# ==================
# 集計開始年月日
_year = 2007
# インスタンス生成
preprocess = Preprocess()
# 各テーブルの前処理
manager = preprocess.preprocess_rawdata(since_=datetime(_year, 1, 1))
columns_info = manager.get_columns()

# データ内容の確認(Jupyter用)
#_results = manager.__dict__['_DataManager__results']
#_raceinfo = manager.__dict__['_DataManager__raceinfo']
#_horseresults = manager.__dict__['_DataManager__horseresults']
#_paybacks = manager.__dict__['_DataManager__paybacks']
#_peds = manager.__dict__['_DataManager__peds']
#_results.head()
#_raceinfo.head()
#_paybacks.head(11)
#_horseresults.head()
#_peds.head()
#columns_info.horseresults.T.set_axis(['対応するcolumn名の意味'], axis='columns')

# データの結合
# 「馬の過去成績」を対象としたデータ結合
horseresults = columns_info.horseresults
target_columns = [horseresults[key][0] for key in Params.TARGET_COLUMNS]
group_columns =  [horseresults[key][0] for key in Params.GROUP_COLUMNS]
merged_data = manager.merge(target_columns, group_columns)

# データ内容の確認(Jupyter用)
#merged_data.df.head()
#merged_data.df.columns
#merged_data.df.describe()

# ==========
# 特徴量生成
# ==========
table_df = merged_data.table

# ラベルエンコーディング&ダミー変数化の処理による特徴量生成
functions = {
    'dumminize':        {'columns': []},
    'label_encoding':   {'columns': [table_df[key][0] for key in Params.CATEGORY_COLUMNS]},
    'encode_jockey_id': {},
    'encode_horse_id':  {},
    'drop':             {'columns': table_df['DATE'].to_list()},
}
inputs = manager.get_features(merged_data.df, functions)

# データ内容の確認(Jupyter用)
#inputs.features
#inputs.features.columns

最初の7行目でインポートしているUserParams.pyは、以下のような内容であり、個別に設定する情報を定義しています。

from dataclasses import dataclass

@dataclass(frozen=True)
class Params:
    # 馬の過去成績を取得する際の対象
    TARGET_COLUMNS = ('LAST_TIME', 'RANK', 'RANK_DIFF', 'PRIZE', 'first_corner', 'final_corner', 'mean_corner')
    # 馬の過去成績を取得する際のグループ化して取得する対象
    GROUP_COLUMNS = ('race_type', 'course_len', 'PLACE')
    # カテゴリー変数化する対象
    CATEGORY_COLUMNS = ('PLACE', 'WEATHER', 'GROUND_STATE', 'sex', 'race_type', 'around', 'race_class')

また、Preprocess.pyDataManger.pyFeatureEngineering.pyの構成は、それぞれ以下のようになります。

Preprocess.py

import numpy as np
import pandas as pd
import re
from .Constants import LocalPaths, HorseResultsCols, ResultsCols, Master
from .DataManager import DataManager

class _Extractor:
    def __init__(self, name, pattern, callback=None):
        # 詳細は以降で解説
    def extract(self, series):
        # 詳細は以降で解説

class Preprocess:
    def __init__(self):
        """
        初期化処理
        """
        # クラスメソッド用の変数
        self.__class_name = self.__class__.__name__
        self.__date_format = '%Y-%m-%d'
        self.__course_len_scaler = 100.0  # course_lenの正規化
        self.__max_relatives = 2          # 対象とする親等の最大値(例:2親等 -> 2)

    def __read_pickle(self, filepath, err_msg='function'):
        """
        pickleファイルの読み込み
        
        Parameters
        ----------
        filepath : str
            ファイルパス

        Returns
        -------
        df : pd.DataFrame
            読み込んだDataFrame        
        """
        try:
            df = pd.read_pickle(filepath)
        except Exception as e:
            raise Exception(f'Error({self.__class_name}::{err_msg}) {e}')
        
        return df

    def preprocess_rawdata(self, 
                           results_filepath=LocalPaths.RAW_RESULTS_PATH,
                           raceinfo_filepath=LocalPaths.RAW_RACEINFO_PATH,
                           payback_filepath=LocalPaths.RAW_PAYBACK_PATH,
                           horseresults_filepath=LocalPaths.RAW_HORSERESULTS_PATH,
                           peds_filepath=LocalPaths.RAW_PEDS_PATH,
                           since_=None):
        """
        生データの前処理
        
        Parameters
        ----------
        results_filepath : str
            レース結果テーブルのファイルパス
        raceinfo_filepath : str
            レース情報テーブルのファイルパス
        payback_filepath : str
            払い戻し結果テーブルのファイルパス
        horseresults_filepath : str
            馬の過去成績テーブルのファイルパス
        peds_filepath : str
            血統情報テーブルのファイルパス
        since_ : datetime or None
            集計開始年月日
            
        Returns
        -------
        manager : _DataManager
            データ管理用インスタンス
        """
        # 前処理実行
        results = self.__preprocess_results(results_filepath)
        raceinfo = self.__preprocess_raceinfo(raceinfo_filepath)
        paybacks = self.__preprocess_payback(payback_filepath)
        horseresults = self.__preprocess_horseresults(horseresults_filepath)
        peds = self.__preprocess_peds(peds_filepath)
        # 集計期間の更新
        if since_ is not None:
            # 集計開始年月日以降に開催されたレースを抽出
            valid_indices = raceinfo[raceinfo[HorseResultsCols.DATE] >= since_].index
            # フィルタリング
            _func = lambda _df: _df[_df.index.isin(valid_indices)]
            results = _func(results)
            raceinfo = _func(raceinfo)
            paybacks = _func(paybacks)
        
        # データ管理用のインスタンス作成
        manager = DataManager(results, raceinfo, paybacks, horseresults, peds)
        
        return manager

    def __preprocess_results(self, filepath):
        # 詳細は以降で解説
    def __preprocess_raceinfo(self, filepath):
        # 詳細は以降で解説
    def __preprocess_payback(self, filepath):
        # 詳細は以降で解説
    def __preprocess_horseresults(self, filepath):
        # 詳細は以降で解説
    def __preprocess_peds(self, filepath):
        # 詳細は以降で解説

DataManager.py

import numpy as np
import pandas as pd
import re
from dataclasses import dataclass
from typing import Any
from .Constants import HorseResultsCols, ResultsCols
from .FeatureEngineering import FeatureEngineering

@dataclass
class _ValidColumns:
    """
    DataManagerが持っている各データcolumn一覧
    """
    results: pd.DataFrame
    raceinfo: pd.DataFrame
    horseresults: pd.DataFrame

@dataclass
class _MergedData:
    """
    結合後のデータとcolumn一覧
    """
    df: pd.DataFrame
    table: pd.DataFrame

class DataManager:
    """
    DataManager : データ管理用クラス
    
    Attributes
    ----------
    __results : pd.DataFrame
        レース結果テーブル
    __raceinfo : pd.DataFrame
        レース情報テーブル
    __paybacks : pd.DataFrame
        払い戻し結果テーブル
    __horseresults : pd.DataFrame
        過去成績テーブル
    __peds : pd.DataFrame
        血統情報テーブル
    """
    def __init__(self, results, raceinfo, paybacks, horseresults, peds):
        """
        初期化処理
        """
        self.__results = results
        self.__raceinfo = raceinfo
        self.__paybacks = paybacks
        self.__horseresults = horseresults
        self.__peds = peds
        
    def __create_column_df(self, _cls, columns, description):
        """
        定数定義されたcolumn名とPythonでの変数定義のペアを生成する
        
        Parameters
        ----------
        _cls : object
            集計対象のクラス
        columns : list-like object
            該当するcolumn
        description : str
            概要説明
        """
        pattern = re.compile(r'^__')
        pairs = dict(map(lambda key: (_cls.__dict__[key], key),
                         filter(lambda val: not bool(pattern.match(val)), dir(_cls))))
        data = {'description': description}
        data.update({pairs.get(name, name): [name] for name in columns})
        df = pd.DataFrame(data)
        
        return df

    def get_columns(self):
        """
        有効なcolumn一覧の取得
        
        Returns
        -------
        instance : _ValidColumns
            _ValidColumnsのインスタンス
        """
        columns = {
            'results': self.__create_column_df(ResultsCols, self.__results.columns, 'レース結果テーブル'),
            'raceinfo': self.__create_column_df(HorseResultsCols, self.__raceinfo.columns, 'レース情報テーブル'),
            'horseresults': self.__create_column_df(HorseResultsCols, self.__horseresults.columns, '馬の過去成績テーブル'),
        }
        instance = _ValidColumns(**columns)
        
        return instance

    @property
    def paybacks(self):
        return self.__paybacks

    def __custom_aggregation(self, _df, target_columns, group_columns):
        # 詳細は以降で解説
    def merge(self, target_columns, group_columns):
        # 詳細は以降で解説
    def get_features(self, df, functions=None):
        """
        特徴量の取得
        
        Parameters
        ----------
        df : pd.DataFrame
            処理対象のDataFrame
        functions : dict or None
            呼び出す関数一覧
                key:   関数名(str)
                value: 関数の引数(dict)
            
        Returns
        -------
        instance : FeatureEngineering
            FeatureEngineeringのインスタンス
        """
        if functions is None:
            functions = {}

        # 初期化
        instance = FeatureEngineering(df)
        # 指定された関数を順に呼び出す
        for name, kwargs in functions.items():
            _func = getattr(instance, name, None)
            # 関数が存在するかつ呼び出し可能
            if hasattr(instance, name) and callable(_func):
                _func(**kwargs)

        return instance

FeatureEngineering.py

import pandas as pd
from sklearn.preprocessing import LabelEncoder
from .Constants import SystemPaths

class FeatureEngineering:
    """
    FeatureEngineering : 特徴量エンジニアリング用クラス
    
    Attributes
    ----------
    features : pd.DataFrame
        特徴量
    """
    def __init__(self, df):
        """
        初期化
        
        Parameters
        ----------
        df : pd.DataFrame
            入力データ
        """
        self.features = df

    def drop(self, columns):
        # 詳細は以降で解説
    def dumminize(self, columns):
        # 詳細は以降で解説
    def encode_jockey_id(self):
        # 詳細は以降で解説
    def encode_horse_id(self):
        # 詳細は以降で解説
    def label_encoding(self, columns):
        # 詳細は以降で解説

整形処理

次に、整形処理について解説します。

レース結果に対する整形処理

レース結果に対しては、以下の4点の処理を行います。

  • 着順の無効値の削除と降着判定された場合も扱えるようにする。
  • 性齢を性別と年齢に分割する。
  • 体重を馬体重と体重変化に分割する。
  • 処理に必要なcolumnを抽出する。

ここで、体重のcolumnは、以下のような形式となっているため、正規表現を利用することでデータを抽出できます。

馬体重
486(-4)
460(+4)
504(+8)
体重の構成例

pandasには、extractというメソッドが用意されており、対象のcolumnを正規表現のパターンに従って分割することができます。

具体例としては、以下のようになります。

# 体重を馬体重と体重変化に分割
# ResultsCols.HORSE_WEIGHTは、Constants.pyで「'馬体重'」として定義されている
weight_diff_df = df[ResultsCols.HORSE_WEIGHT].str.extract('(?P\d+)\((?P[-+\d]+)\)')

先程例示した「体重の構成例」に対し、上記を実行すると、以下のようなDataFrameが生成されます。

weightdiff
486-4
460+4
504+8
extractメソッドの実行により得られるDataFrame

同様に、正規表現を用いることで、着順のうち、不適切なものを削除したり、降着判定されたものも正しく扱えるようになります。

また、1レースに出場する馬の数も重要な情報になると考えられるため、整形処理時に情報を追加します。

これは、race_idの数を数えることで実現でき、実装例は以下のようになります。

# 頭数の設定
# resultsにはrace_idをIndexとするレース結果が格納されている前提
index_counts = results.index.value_counts(sort=False)
results['n_horse'] = 0
results.loc[index_counts.index, 'n_horse'] = index_counts

以上を踏まえてレース結果の整形処理を行った結果は以下のようになります。

    def __preprocess_results(self, filepath):
        """
        レース結果テーブルに対する前処理を行う
        
        Parameters
        ----------
        filepath : str
            ファイルパス
        Returns
        -------
        results : pd.DataFrame
            前処理後のレース結果テーブル
        """
        # データ読み込み
        df = self.__read_pickle(filepath, err_msg='preprocess_results')
        # columnのスペースを削除
        df = df.set_axis(df.columns.map(lambda x: x.replace(' ', '')), axis='columns', copy=False)
        # RANKが数字以外のものは削除・降着判定されたものも正しく扱う
        df = df[df[ResultsCols.RANK].str.contains('^\d+', na=True)].dropna(subset=[ResultsCols.RANK], axis=0)
        df[ResultsCols.RANK] = df[ResultsCols.RANK].replace('\D+', '', regex=True).map(int)
        # 抽出する列を指定
        same_cols = [
            ResultsCols.FRAME_NUMBER,
            ResultsCols.HORSE_NUMBER,
            ResultsCols.WEIGHT,
            'horse_id',
            'jockey_id',
        ]
        # 性齢を性別と年齢に分割
        sex_age_df = df[ResultsCols.SEX_AGE].str.extract('(?P\D+)(?P\d+)')
        sex_age_df['age'] = sex_age_df['age'].map(int)
        # 体重を馬体重と体重変化に分割
        weight_diff_df = df[ResultsCols.HORSE_WEIGHT].str.extract(
            '(?P\d+)\((?P[-+\d]+)\)'
        )
        weight_diff_df = weight_diff_df.fillna(weight_diff_df.mode().iloc[0]).applymap(float)
        # データ結合
        results = pd.concat([df[same_cols].copy(), sex_age_df, weight_diff_df], axis=1)
        # 頭数の設定
        index_counts = results.index.value_counts(sort=False)
        results['n_horse'] = 0
        results.loc[index_counts.index, 'n_horse'] = index_counts
        # 着順処理
        results['rank'] = df[ResultsCols.RANK]
        return results

【23/3/4追記】上記の処理の一部に問題が合ったため、修正いたしました。

修正内容は以下のようになります。

        # columnのスペースを削除
        df = df.set_axis(df.columns.map(lambda x: x.replace(' ', '')), axis='columns', copy=False)
        # RANKが数字以外のものは削除・降着判定されたものも正しく扱う
-       df = df[df[ResultsCols.RANK].str.contains('^\d+', na=False)]
-       df[ResultsCols.RANK] = df[ResultsCols.RANK].str.replace('\D+', '', regex=True).map(int)
+       df = df[df[ResultsCols.RANK].str.contains('^\d+', na=True)].dropna(subset=[ResultsCols.RANK], axis=0)
+       df[ResultsCols.RANK] = df[ResultsCols.RANK].replace('\D+', '', regex=True).map(int)
        # 抽出する列を指定
        same_cols = [
            ResultsCols.FRAME_NUMBER,

レース情報に対する整形処理

レース情報は、「-」区切りの1行の文字列として保存しているため、文字列から該当する情報を抽出する必要があります。

ここでは、_Extractorというクラスを作成し、このクラス内で抽出処理を行うような構成としました。

まず、_Extractorクラスの実装を以下に示します。

class _Extractor:
    """
    _Extractor : データ抽出器
    
    Attributes
    ----------
    __name : str
        column name of DataFrame
    __pattern : re.compile
        正規表現パターン
    __callback : callable or None
        callback function
    """
    def __init__(self, name, pattern, callback=None):
        """
        初期化処理
        
        Parameters
        ----------
        name : str
            column name of DataFrame
        pattern : re.compile
            正規表現パターン
        callback : callable or None
            callback function
        """
        self.__name = name
        self.__pattern = pattern
        self.__callback = callback
    def extract(self, series):
        """
        抽出器
        
        Parameters
        ----------
        series : pd.Series
            変換対象
                    
        Returns
        -------
        result : pd.DataFrame
            変換結果
        """
        result = series.str.extract(f'(?P<{self.__name}>{self.__pattern})')
        if self.__callback is not None:
            result = result[self.__name].map(self.__callback)
        return result

このクラスは、以下のような構成となっています。

項目内容
__nameextractメソッドで抽出した際のDataFrameのcolumn名
__pattern抽出時に用いる正規表現のパターン
__callback抽出後にDataFrameに対して実行する処理
extract与えられた文字列に対し、抽出処理を行うメソッド
_Extractorクラスの構成

次に、_Extractorクラスを用いて、レース情報のDataFrameを構築していきます。

スクレイピング結果のレース情報は、以下のようになっているため、これに対して正規表現を割り当てていきます。

'ダ右1200m-天候-曇-ダート-稍重-発走-10-25-2020年4月12日-3回中山6日目-3歳未勝利-馬齢'

上記の例を用いて、抽出したい情報、該当箇所、正規表現パターンの例、callbackの例を以下に示します。

抽出したい情報該当箇所正規表現パターンの例callbackの例
開催場所中山'札幌|函館|福島|高知|佐賀|荒尾|'-
天気'晴|曇|小雨|雨|小雪|雪'-
馬場状態稍重'良|稍重|重|不良'-
日付2020年4月12日'\d+年\d+月\d+'年or月を「-」に置換
レースタイプダート'芝|ダ|障'「ダ」を「ダート」に置換
コースの回り方'右|左|直線|障害'-
レースクラス未勝利'新馬|未勝利|1勝クラス|2勝クラス|3勝クラス|オープン|障害'-
コースの長さ1200m'\d+m'末尾の「m」を削除・スケーリング値
文字列から該当する情報を抽出する際の例

正規表現パターンに示した情報は、Constants.pyに定義しているため、この情報を用いることで必要な情報を抽出できます。

以上を踏まえた実装例は以下のようになります。

    def __preprocess_raceinfo(self, filepath):
        """
        レース情報テーブルに対する前処理を行う
        
        Parameters
        ----------
        filepath : str
            ファイルパス

        Returns
        -------
        results : pd.DataFrame
            前処理後のレース情報テーブル
        """
        df = self.__read_pickle(filepath, err_msg='preprocess_raceinfo')
        
        extractors = [
            # PLACEの抽出
            _Extractor(HorseResultsCols.PLACE, '|'.join(Master.PLACES.keys()).replace('(', '\(').replace(')', '\)')),
            # WEATHERの抽出
            _Extractor(HorseResultsCols.WEATHER, '|'.join(Master.WEATHER)),
            # GROUND_STATEの抽出
            _Extractor(HorseResultsCols.GROUND_STATE, '|'.join(Master.GROUND_STATES.values())),
            # dateの抽出
            _Extractor(HorseResultsCols.DATE, '\d+年\d+月\d+', 
                       callback=lambda x: re.sub(r'年|月', '-', x)),
            # RACE_TYPEの抽出
            _Extractor('race_type', '|'.join(Master.RACE_TYPES.keys()), 
                       callback=lambda x: Master.RACE_TYPES[x]),
            # AROUNDの抽出
            _Extractor('around', '|'.join(Master.AROUND)),
            # RACE_CLASSの抽出
            _Extractor('race_class', '|'.join(Master.RACE_CLASSES)),
            # course_lenの抽出
            _Extractor('course_len', '\d+m', 
                       callback=lambda x: float(x[:-1]) / self.__course_len_scaler),
        ]
        texts = df['texts']
        results = pd.concat([instance.extract(texts) for instance in extractors], axis=1)
        results.fillna({'race_class': 'unknown'}, inplace=True)
        results[HorseResultsCols.DATE] = pd.to_datetime(results[HorseResultsCols.DATE], format=self.__date_format)

        return results

払い戻し結果に対する整形処理

払い戻し結果は、ワイドと複勝が文字列結合された状態で保存されているため、分解して扱えるように整形する必要があります。

スクレイピング結果と整形後のイメージは、以下のようになります。(race_idは省略しています)

0123
単勝52401
複勝5 3 2120 220 6301 4 9
枠連2 - 34301
馬連3 - 58202
ワイド3 - 5 2 - 5 2 - 3410 1,230 2,8603 18 30
馬単5 → 313302
三連複2 - 3 - 5707022
三連単5 → 3 → 22692080
スクレイピング結果
typematchedprice
単勝5240
複勝5120
複勝3220
複勝2630
枠連2-3430
馬連3-5820
ワイド3-5410
ワイド2-51230
ワイド2-32860
馬単5→31330
三連複2-3-57070
三連単5→3→226920
整形後のイメージ

上記の処理は、「複勝・ワイド」だけ個別に処理し、最後に結合すれば実現できます。

実装例は以下のようになります。

    def __preprocess_payback(self, filepath):
        """
        払い戻しテーブルに対する前処理を行う
        
        Parameters
        ----------
        filepath : str
            ファイルパス

        Returns
        -------
        results : pd.DataFrame
            前処理後の払い戻しテーブル
        """
        df = self.__read_pickle(filepath, err_msg='preprocess_payback')
        # column名変更
        df = df.set_axis(['type', 'matched', 'price', 'popular'], axis='columns', copy=False)
        # popularの列を削除
        df.drop(columns='popular', inplace=True)
        # ワイドと複勝を分割
        judge = df['type'].isin(['ワイド', '複勝'])
        filtered_df = df.loc[~judge, :].apply(lambda x: x.str.replace(' ', ''))
        matched = df.loc[judge, 'matched'].replace(r'\s-\s', '-', regex=True).str.split()
        price = df.loc[judge, ['type', 'price']].apply(lambda x: x.str.split() if x.name != 'type' else x)
        modified_df = pd.concat([matched.explode(), price.explode('price')], axis=1)
        # バラバラにしたデータを結合・整形
        results = pd.concat([filtered_df, modified_df]).sort_index().fillna(0)
        results['price'] = results['price'].map(lambda x: int(str(x).replace(',', '')))
        
        return results

【23/3/4追記】上記の処理の一部に問題が合ったため、修正いたしました。

修正内容は以下のようになります。

        # ワイドと複勝を分割
        judge = df['type'].isin(['ワイド', '複勝'])
        filtered_df = df.loc[~judge, :].apply(lambda x: x.str.replace(' ', ''))
-       matched = df.loc[judge, 'matched'].str.replace(r'\s-\s', '-', regex=True).str.split()
+       matched = df.loc[judge, 'matched'].replace(r'\s-\s', '-', regex=True).str.split()
        price = df.loc[judge, ['type', 'price']].apply(lambda x: x.str.split() if x.name != 'type' else x)
        modified_df = pd.concat([matched.explode(), price.explode('price')], axis=1)
        # バラバラにしたデータを結合・整形

馬の過去成績に対する整形処理

馬の過去成績は、以下に示すように、レース結果と似たような処理を行います。

  • 着順の無効値の削除と降着判定された場合も扱えるようにする。
  • 性齢を性別と年齢に分割する。
  • 体重を馬体重と体重変化に分割する。
  • 無効値を削除する。
  • 「通過」情報からコーナーを算出する。
  • 処理に必要なcolumnを抽出する。

ここでは、新規に登場した「通過」情報について解説します。

「通過」情報は、以下のようなフォーマットになっています。(horse_idは省略しています。)

通過
13-14
14-14-12
10-10-9-8
「通過」情報のフォーマット

馬の過去成績によって、フォーマットが異なるため、ここでは「最初の通過順位」、「最後の通過順位」、「通過順位の平均」の3つを算出します。

上記の例で集計した結果を以下に示します。

最初の通過順位最後の通過順位通過順位の平均
131413.5
141213.3
1089.25
算出結果

上記を踏まえた実装例は以下のようになります。

    def __preprocess_horseresults(self, filepath):
        """
        馬の過去成績テーブルに対する前処理を行う
        
        Parameters
        ----------
        filepath : str
            ファイルパス
        Returns
        -------
        results : pd.DataFrame
            前処理後の馬の過去成績テーブル
        """
        df = self.__read_pickle(filepath, err_msg='preprocess_horseresults')
        # columnのスペースを削除
        df = df.set_axis(df.columns.map(lambda x: x.replace(' ', '')), axis='columns', copy=False)
        # RANKが数字以外のものは削除・降着判定されたものも正しく扱う
        df = df[df[HorseResultsCols.RANK].str.contains('^\d+', na=True)].dropna(subset=[
            HorseResultsCols.RANK, HorseResultsCols.R
        ], how='any', axis=0)
        df[HorseResultsCols.RANK] = df[HorseResultsCols.RANK].replace('\D+', '', regex=True).map(int)
        # GROUND_STATEがNaNとなっているものを削除
        df = df.dropna(subset=[HorseResultsCols.GROUND_STATE], how='any', axis=0)
        # PRIZEの値の補完
        df.fillna({HorseResultsCols.PRIZE: 0.0}, inplace=True)
        # 実数を整数に変換
        targets = [
            HorseResultsCols.R, 
            HorseResultsCols.N_HORSES,
            HorseResultsCols.FRAME_NUMBER,
        ]
        df = df[df[targets].isna().sum(axis=1).map(lambda x: not bool(x))]
        df[targets] = df[targets].applymap(int)
        # 日付のフォーマット変更
        df[HorseResultsCols.DATE] = pd.to_datetime(df[HorseResultsCols.DATE], format=self.__date_format)
        # GROUND_STATEのフォーマット変更
        df[HorseResultsCols.GROUND_STATE] = df[HorseResultsCols.GROUND_STATE].map(
            lambda x: Master.GROUND_STATES[x]
        )
        # PLACEのフォーマット変更
        df[HorseResultsCols.PLACE] = df[HorseResultsCols.PLACE].replace('\d', '', regex=True)
        # 抽出する列を指定
        same_cols = [
            HorseResultsCols.DATE,
            HorseResultsCols.PLACE,
            HorseResultsCols.WEATHER,
            HorseResultsCols.R,
            HorseResultsCols.N_HORSES,
            HorseResultsCols.FRAME_NUMBER,
            HorseResultsCols.HORSE_NUMBER,
            HorseResultsCols.WEIGHT,
            HorseResultsCols.GROUND_STATE,
            HorseResultsCols.RANK,
            HorseResultsCols.RANK_DIFF,
            HorseResultsCols.LAST_TIME,
            HorseResultsCols.PRIZE,
        ]
        # 体重を馬体重と体重変化に分割
        weight_diff_df = df[HorseResultsCols.HORSE_WEIGHT].str.extract(
            '(?P\d+)\((?P[-+\d]+)\)'
        )
        weight_diff_df = weight_diff_df.fillna(weight_diff_df.mode().iloc[0]).applymap(float)
        # 距離をレースタイプと全長に分割
        race_type_distance_df = df[HorseResultsCols.COURSE_LEN].str.extract(
            '(?P\w)(?P\d+)'
        )
        race_type_distance_df['race_type'] = race_type_distance_df['race_type'].map(
            lambda x: Master.RACE_TYPES[x]
        )
        # コーナー算出
        corners = df[HorseResultsCols.CORNER].str.split('-').fillna('0')
        several_corner_df = pd.DataFrame({
            'first_corner': corners.map(lambda xs: int(xs[0])),
            'final_corner': corners.map(lambda xs: int(xs[-1])),
            'mean_corner':  corners.map(lambda xs: np.mean(list(map(int, xs))) if isinstance(xs, list) else float(xs)),
        })
        # データ結合
        results = pd.concat([
            df[same_cols].copy(), weight_diff_df, race_type_distance_df, several_corner_df
        ], axis=1)
        results['course_len'] = results['course_len'].map(lambda x: float(x) / self.__course_len_scaler)
        return results

【23/3/4追記】上記の処理の一部に問題が合ったため、修正いたしました。

修正内容は以下のようになります。

        # columnのスペースを削除
        df = df.set_axis(df.columns.map(lambda x: x.replace(' ', '')), axis='columns', copy=False)
        # RANKが数字以外のものは削除・降着判定されたものも正しく扱う
-       df = df[df[HorseResultsCols.RANK].str.contains('^\d+', na=False)]
-       df[HorseResultsCols.RANK] = df[HorseResultsCols.RANK].str.replace('\D+', '', regex=True).map(int)
-       # HORSE_WEIGHTが不適切なものは削除
-       df = df[df[HorseResultsCols.HORSE_WEIGHT].str.contains('^\d+', na=False)]
+       df = df[df[HorseResultsCols.RANK].str.contains('^\d+', na=True)].dropna(subset=[
+           HorseResultsCols.RANK, HorseResultsCols.R
+       ], how='any', axis=0)
+       df[HorseResultsCols.RANK] = df[HorseResultsCols.RANK].replace('\D+', '', regex=True).map(int)
        # GROUND_STATEがNaNとなっているものを削除
        df = df.dropna(subset=[HorseResultsCols.GROUND_STATE], how='any', axis=0)
        # PRIZEの値の補完
            lambda x: Master.GROUND_STATES[x]
        )
        # PLACEのフォーマット変更
-       df[HorseResultsCols.PLACE] = df[HorseResultsCols.PLACE].str.replace('\d', '', regex=True)
+       df[HorseResultsCols.PLACE] = df[HorseResultsCols.PLACE].replace('\d', '', regex=True)
        # 抽出する列を指定
        same_cols = [
            HorseResultsCols.DATE,

血統情報に対する整形処理

血統情報に関しては、遠い祖先の遺伝子情報は現世に大きな影響を与えないと仮定して処理を行います。

具体的には、何親等まで対象とするかを決めておき、該当する箇所のみ残す、という処理を行いました。

今回は、ヒューリスティックに定義し、2親等まで残すことにしております。

何親等になるかは、スクレイピング結果を格納する際に合わせて定義しているため、その定義結果を参照する構成となっています。

上記を踏まえた実装例は以下のようになります。

    def __preprocess_peds(self, filepath):
        """
        血統テーブルに対する前処理を行う
        
        Parameters
        ----------
        filepath : str
            ファイルパス

        Returns
        -------
        results : pd.DataFrame
            前処理後の血統テーブル
        """
        df = self.__read_pickle(filepath, err_msg='preprocess_peds')
        # 対象とする「n親等」までの情報を抽出
        judge = lambda xs: np.nan if (isinstance(xs, float) and np.isnan(xs)) or xs[1] > self.__max_relatives else xs[0]
        results = df.applymap(judge).dropna(axis=1)
        
        return results

マージ処理

次に、マージ処理について解説します。

マージする際のステップとしては、以下のようになります。

  1. レース結果とレース情報を結合する。
  2. horse_idを基準に1.の結果と血統情報を結合する。
  3. horse_idとレース開催日ごとに、該当する馬の過去成績を集計し、集計結果を結合する。

1.と2.は、pandasmergeメソッドを用いることで実現できますが、3.に関しては、自前で実装する必要があります。

このため、ここでは、馬の過去成績の集計方法について解説します。

馬の過去成績に対する集計処理

集計は、以下の手順で実施します。

  1. horse_idとレース開催日をグループ化する。
  2. 馬の過去成績から、該当するhorse_idとレース開催日以前の情報を取得する。
  3. 事前に与えた条件(下記参照)に従い、集計する。
    条件:UserParams.pyTARGET_COLUMNSGROUP_COLUMNS

例えば、2023年1月28日の東京で開催されたレース結果(race_id: 202305010111)を参照すると以下のようになっています。(一部省略、一部説明のため情報を追加)

開催race_typecourse_lenhorse_idjockey_idrank
東京2000m2019105207053391
東京2000m2019104896006662
東京2000m2019100596011153
東京2000m2018104579053864
東京2000m2016104319011795
東京2000m2018104788052126
東京2000m2015104724011227
東京2000m2016103730010928
東京2000m2016100550010439
2023年1月28日の東京で開催されたレース結果

今回は、このうち、7着目の「2015104724」に着目して解説します。

horse_idが2015104724の馬に対し、過去の成績を取得すると、以下のようになります。(一部省略、一部説明のため情報を追加)

日付開催馬場着順着差上り賞金race_typecourse_lenfirst_cornerfinal_cornermean_corner
2023-01-28東京70.834.10.02000m1099.33
2022-10-30阪神131.033.80.01800m151113.00
2022-08-14小倉153.137.60.02000m4147.50
2022-07-24小倉90.534.20.01800m111011.75
2022-06-18阪神141.534.90.01600m151515.00
2022-05-07新潟50.532.6250.01600m111111.00
2022-03-06阪神71.135.10.01800m666.00
2022-02-20小倉稍重80.836.30.01800m655.75
2021-12-05阪神1-0.334.01838.21800m666.00
2021-11-13阪神81.036.50.02000m655.75
過去の成績(対象:2015104724)

過去の成績を集計する際は、2023年1月28日以前の結果を取得する必要があります。

今回の場合、上記のテーブルの2行目以降が集計対象となります。

集計対象が定まったら、後は、以下の表にある情報を対象に集計(平均値の算出)を行います。

対象・区分Pythonスクリプトでの変数名DataFrame上での名称
集計対象のcolumnLAST_TIME上り
RANK着順
RANK_DIFF着差
PRIZE賞金
first_cornerfirst_corner
final_cornerfinal_corner
mean_cornermean_corner
グループ化して集計する対象race_typerace_type
course_lencourse_len
PLACE開催

また、今回は、以下の2パターンで集計しています。

  1. 「集計対象のcolumn」で指定したcolumnのすべての範囲に対し、平均値を算出する。
  2. 「グループ化して集計する対象」で指定したそれぞれのcolumnに対し、今回と同じケースに限定して平均値を算出する。

1つ目の集計方法

このケースでは、「集計対象のcolumn」に挙がっている項目をそれぞれ集計していきます。

上記のケースでは、以下のように平均値を算出できます。

集計対象のcolumn(DataFrame上での名称)平均値
上り35.00
着順8.889
着差1.022
賞金232.022
first_corner8.889
final_corner9.222
mean_corner9.083
「集計対象のcolumn」に対する集計結果

2つ目の集計方法

2つ目の集計方法は、馬の過去成績に対して集計範囲がさらに限定された状態で集計することになります。

まず、2023年1月28日の情報から、「グループ化して集計する対象」は以下のように対応付けられます。

グループ化して集計する対象内容
race_type
course_len2000m
PLACE東京
2023年1月28日のレースに対する該当例

上記に対して、フィルタリングした上で集計することで、地形や距離に応じた得意・不得意を特徴として表現できると考えました。

例えば、race_typeであれば、「芝」となっているものだけをフィルタリングした後、1.と同様にすべての範囲に対して平均値を算出します。

提示したデータの都合上、ここではcourse_lenに対して、集計する例を示します。

馬の過去成績のうち、該当する箇所は以下の2か所となります。

日付開催馬場着順着差上り賞金race_typecourse_lenfirst_cornerfinal_cornermean_corner
2022-08-14小倉153.137.60.02000m4147.50
2021-11-13阪神81.036.50.02000m655.75
過去の成績(対象:2015104724かつcourse_lenが2000mとなっているもの)

上記の表に対して、「集計対象のcolumn」に対する集計を行うと、以下のようになります。

集計対象のcolumn(DataFrame上での名称)平均値
上り37.050
着順11.500
着差2.050
賞金0.000
first_corner5.000
final_corner9.500
mean_corner6.625
「集計対象のcolumn」に対する集計結果(course_lenが2000mのもの限定)

少し長くなりましたが、これらを踏まえた集計処理の実装例は以下のようになります。

    def __custom_aggregation(self, _df, target_columns, group_columns):
        """
        対象のhorse_idとdateに対する集計処理
        Parameters
        ----------
        _df : pd.Series
            集計時の評価データが含まれるDataFrame
        target_columns : list
            結合時のcolumn
        group_columns : list
            グループ化して集計する際に対象とするcolumn
        Returns
        -------
        results : pd.DataFrame
            集計結果のDataFrame
        """
        def _calc_mean_mode(base_series, target_df, columns):
            """
            平均値と最頻値を算出
            Parameters
            ----------
            base_series : pd.Series or None
                集計対象のSeries
            target_df : pd.DataFrame
                集計対象のDataFrame
            columns : list
                集計対象の列名のリスト
            Returns
            -------
            output : pd.Series
                出力結果
            """
            @dataclass
            class _CondParams:
                name: str
                condition: Any
            if base_series is None:
                # 集計時の制約条件なしの場合
                _params = _CondParams(
                    name='',
                    condition=map(bool, np.ones(len(target_df), dtype=np.int64)),
                )
            else:
                # 集計時の制約条件ありの場合
                _params = _CondParams(
                    name=base_series.name,
                    condition=target_df[base_series.name].isin(base_series.values),
                )
            # 集計方法の決定(True: 何もしない, False: mean)
            # pd.to_numericで無効値がない(すべて数値である)場合、平均値を算出
            agg_table = target_df[columns].apply(
                lambda _series: pd.to_numeric(_series, errors='coerce').isna().sum() > 0
            ).to_dict()
            # 集計対象の分類
            mean_info = {key: f'{_params.name}{key}_mean' for key, value in agg_table.items() if not value}
            mean_cols = mean_info.keys()
            # 集計処理
            ret_mean = target_df.loc[_params.condition, mean_cols].agg('mean').rename(mean_info)
            # 出力結果の生成
            output = ret_mean.dropna()
            if output.empty:
                output = pd.Series(dtype=object)
            return output
        # 集計対象の情報を取得
        name = HorseResultsCols.DATE
        date = _df[name].values[0]
        horse_id = _df['horse_id'].values
        # 過去成績テーブルのフィルタリング
        target_df = self.__horseresults[
            self.__horseresults.index.isin(horse_id) & (self.__horseresults[name] < date)
        ]
        # ========
        # 集計処理
        # ========
        if target_df.empty:
            results = pd.DataFrame({'dummy': [0]})
        else:
            outputs = {'dummy': 0}
            _kwargs = {'target_df': target_df, 'columns': target_columns}
            # 制約条件なしの場合の処理
            outputs.update(_calc_mean_mode(None, **_kwargs).to_dict())
            # group_columnsの各要素に対する処理
            _agg_series = _df[group_columns].agg(lambda _series: _calc_mean_mode(_series, **_kwargs))
            outputs.update(_agg_series.stack().reset_index(level=1, drop=True).to_dict())
            # 結果の集計
            results = pd.DataFrame({str(key): [value] for key, value in outputs.items()})
        return results.set_index('dummy')
    def merge(self, target_columns, group_columns):
        """
        データ結合
        Parameters
        ----------
        target_columns : list
            結合時のcolumn
        group_columns : list
            グループ化して集計する際に対象とするcolumn
        Returns
        -------
        result : _MergedData
            _MergedDataのインスタンス
                df: 結合後のデータ
                table: 列名とPythonコードの変数名との対応関係
        """
        # レース結果テーブルとレース情報テーブルの結合
        df = self.__results.merge(self.__raceinfo, left_index=True, right_index=True).reset_index()
        # 馬IDを基準に血統情報テーブルを結合(欠損値は最頻値で置換)
        df = df.merge(self.__peds.reset_index(drop=False), on='horse_id').fillna('mode')
        # 集計対象に存在する馬IDのみ抽出
        valid_horse_ids = self.__horseresults.index.unique()
        filtered_df = df[df['horse_id'].isin(valid_horse_ids)]
        # 集計
        _groupby_target = ['horse_id', HorseResultsCols.DATE]
        _extraction_cols = _groupby_target + group_columns
        aggregated_df = filtered_df.groupby(by=_groupby_target, group_keys=True)[_extraction_cols].apply(
            self.__custom_aggregation, target_columns=target_columns, group_columns=group_columns
        ).reset_index().drop(columns=['dummy'])
        # 集計結果を結合
        merged_df = df.merge(aggregated_df, on=_groupby_target, how='outer')
        filled_agg_df = pd.concat([
            merged_df[df.columns],
            merged_df.loc[:, merged_df.columns.str.match('.*mean$')].fillna(0),
        ], axis=1).set_index('race_id')
        # インスタンス生成
        result = _MergedData(**{
            'df': filled_agg_df,
            'table': self.__create_column_df(HorseResultsCols, filled_agg_df.columns, '結合後テーブル'),
        })
        return result

特徴量生成

最後に、特徴量生成について解説します。

特徴量生成では、カテゴリ変数(性別、開催場所、天気など)をラベルエンコーディングという方法を用いて数値情報に変換します。

また、horse_idをラベルエンコーディングする場合、血統情報も考慮する必要があるため、別途処理を分けています。

今回実装したメソッドの一覧を以下に示します。

メソッド名内容備考
drop指定したcolumnを削除する。DataFrameのdropのラッパー
dumminize指定したcolumnをダミー変数化する。今回は利用しない
encode_jockey_idjockey_id をラベルエンコーディングする。対応関係をmaster以下に保存
encode_horse_idhorse_id をラベルエンコーディングする。対応関係をmaster以下に保存
label_encoding指定したcolumnをラベルエンコーディングする。-
実装済みのメソッド一覧

対応する実装結果は、以下のようになります。

    def drop(self, columns):
        """
        指定されたcolumnを削除

        Parameters
        ----------
        columns : list
            削除対象のcolumn

        Returns
        -------
        self : FeatureEngineering
            FeatureEngineeringのインスタンス
        """
        self.features.drop(columns=columns, inplace=True)

        return self

    def dumminize(self, columns):
        """
        指定されたcolumnをダミー変数化

        Parameters
        ----------
        columns : list
            ダミー変数化の対象となるcolumn

        Returns
        -------
        self : FeatureEngineering
            FeatureEngineeringのインスタンス
        """
        if len(columns) > 0:
            dummy_df = pd.get_dummies(self.features[columns])
            extracted_df = self.features.loc[:, ~self.features.columns.isin(columns)].copy()
            self.features = pd.concat([extracted_df, dummy_df], axis=1)

        return self

    def encode_jockey_id(self):
        """
        jockey id をラベルエンコーディング

        Returns
        -------
        self : FeatureEngineering
            FeatureEngineeringのインスタンス
        """
        # jockey idを変換
        name = 'jockey_id'
        encoder = LabelEncoder()
        original = self.features[name].values
        transformed = encoder.fit_transform(original)
        df = pd.DataFrame({
            name: original,
            'encoded_id': transformed,
        })
        # 対応関係を保存
        df.to_pickle(SystemPaths.ENCODED_JOCKEYID_PATH)
        # 変換結果を格納
        self.features[name] = transformed
        self.features[name] = self.features[name].astype('category')

        return self

    def encode_horse_id(self):
        """
        horse id をラベルエンコーディング

        Returns
        -------
        self : FeatureEngineering
            FeatureEngineeringのインスタンス
        """
        # 集計対象のcolumnの定義
        columns = self.features.columns
        _peds = columns[columns.str.match('^peds_.*')].to_list()
        horse_ids = ['horse_id'] + _peds
        # 対象データの取得
        data = self.features[horse_ids].stack().reset_index(drop=True).unique()
        # ラベルエンコーディングの実施
        encoder = LabelEncoder()
        transformed = encoder.fit_transform(data)
        # 対応関係を保存
        df = pd.DataFrame({
            'horse_id': data,
            'encoded_id': transformed,
        })
        df.to_pickle(SystemPaths.ENCODED_HORSEID_PATH)
        # 変換結果を格納
        self.features[horse_ids] = self.features[horse_ids].apply(lambda series: encoder.transform(series))
        self.features[horse_ids] = self.features[horse_ids].astype('category')

        return self

    def label_encoding(self, columns):
        """
        指定されたcolumnをラベルエンコーディング

        Parameters
        ----------
        columns : list
            ラベルエンコーディングの対象となるcolumn

        Returns
        -------
        self : FeatureEngineering
            FeatureEngineeringのインスタンス
        """
        encoder = LabelEncoder()

        for name in columns:
            self.features[name] = encoder.fit_transform(self.features[name])
            self.features[name] = self.features[name].astype('category')

        return self

上記の処理を呼び出すためには、以下のように呼び出し関係のテーブル(functions)を作成した上で、get_featuresメソッドを呼び出します。

# インスタンス生成
preprocess = Preprocess()
# 各テーブルの前処理(整形処理)
manager = preprocess.preprocess_rawdata(since_=datetime(_year, 1, 1))
columns_info = manager.get_columns()
# 「馬の過去成績」を対象としたデータ結合
horseresults = columns_info.horseresults
target_columns = [horseresults[key][0] for key in Params.TARGET_COLUMNS]
group_columns =  [horseresults[key][0] for key in Params.GROUP_COLUMNS]
merged_data = manager.merge(target_columns, group_columns)

# ===========
# 特徴量生成
# ===========
table_df = merged_data.table
# ラベルエンコーディング&ダミー変数化の処理による特徴量生成
functions = {
    'dumminize':        {'columns': []},
    'label_encoding':   {'columns': [table_df[key][0] for key in Params.CATEGORY_COLUMNS]},
    'encode_jockey_id': {},
    'encode_horse_id':  {},
    'drop':             {'columns': table_df['DATE'].to_list()},
}
inputs = manager.get_features(merged_data.df, functions)

functionsのフォーマットは、以下のようになります。

# functionsのフォーマット
functions = {
    function_name: {argument_name: argument_data},
}
# function_name: 呼び出すメソッドの名称
#   例: 'drop'
# argument_name: メソッドの引数名
#   例: 'columns'
# argument_data: 引数の内容
#   例: table_df['DATE'].to_list()

今回は、以上の操作を行って得られた結果を特徴量としました。

まとめ

今回は、機械学習を用いた競馬予想を行うための前処理の方法について解説しました。

機械学習に入力する情報となるため、特徴量の良し悪しが予測結果に大きく影響します。

このため、試行錯誤を繰り返して、有効な特徴量を定義していく必要があります。

以降では、機械学習手法まで一通り解説した後に、特徴量の見直しを行っていきたいと思います。

効率良く技術習得したい方へ

今回の話の中で、プログラミングについてよく分からなかった方もいると思います。

このような場合、エラーが発生した際に対応できなくなってしまうため、経験者からフォローしてもらえる環境下で勉強することをおすすめします。

詳細は、以下の記事をご覧ください。

【比較】プログラミングスクールおすすめランキング6選【初心者向け】

続きを見る

続きの記事を執筆しました。

あわせて読みたい
【Python】機械学習を用いた競馬予想【モデル構築・評価編】

続きを見る

Remaining:
サポートしていただけるとブログ運営の励みになります!

スポンサードリンク



-プログラミング
-, , ,

S