AIの作り方②データは宝の山!競馬AI予測に必要なデータ前処理・特徴エンジニアリング
前回の記事「AIの作り方①AIの基本とデータ収集:競馬予測の第一歩」では、競馬予測AIを作るための基本とデータ収集についてお話ししました。今回はその続きを深掘りして、「データ前処理」と「特徴エンジニアリング」について説明します。これらのステップをマスターすることで、予測精度を大幅に向上させることができます。
データの前処理を知ろう
データ前処理とは?
まずはデータ前処理についてです。データ前処理とは、収集したデータをAIモデルが使いやすい形に整える作業のことです。具体的には、データのクリーニング、不足データの補完、データの正規化や標準化などが含まれます。これらの作業をきちんと行うことで、AIモデルのパフォーマンスを最大化できます。
例えば機械学習は数字を扱うのは得意ですが、文字を扱うのは苦手です。前回作成した”all_result.csv”の「馬体重」を見てみると450(-6)、 460(+6)などとあります。これは人間が見たら「体重(体重増減)」と一見して意味が分かりますが、機械学習では意味が分かりません。したがって「馬体重」から「体重」と「体重増減」の2つに分解してあげないといけません。
ほかにも「着 順」には失、取、除などがあります。これらもデータがとれなかったということですから、データから削除しなければいけません。
実際のコード
これらのことを踏まえて前処理をするコードが以下の通りです。以下のコードをcolabにコピペして実行してみましょう。
import numpy as np
import pandas as pd
import os
import warnings
warnings.simplefilter('ignore')
BASE_DIR = "."
all_result = pd.read_csv(os.path.join(BASE_DIR, "all_result.csv"))
def preprocess_result(df):
df = df[~df["着 順"].isin(['中', '取', '失', '除', np.nan])]
df["着 順"] = df["着 順"].replace("\(降\)", "", regex = True).replace("\(再\)", "", regex = True).astype(int)
df['タイム'] = df['タイム'].str.replace(".", ":")
df[['分', '秒', "ミリ秒"]] = df['タイム'].str.split(':', expand=True).astype(float)
df['タイム'] = df['分'] * 60 + df['秒'] + 0.1 * df['ミリ秒']
df[["体重", "体重増減"]] = df["馬体重"].str.replace(")", "").str.split("(", regex = False, expand = True).astype(float)
df[['性', '年齢']] = df['性齢'].str.extract(r'(牡|牝|セ)(\d+)', expand=True)
df["年齢"] = df["年齢"].astype(int)
df["単勝オッズ"] = df["単勝"].str.replace(",", "").replace("---", None).astype(float)
df["賞金"] = df["prize"].str.replace(",", "").replace("nan", "0").astype(float).fillna(0)
df = df.drop(columns = ['性齢', '着差', '単勝', '馬体重', 'horse id', 'jockey id', 'trainer id', 'owner id', 'prize', '通過', '分', '秒', 'ミリ秒'])
return df
df = preprocess_result(all_result)
次にこれらのコードの解説をします。今回も「プログラミングに興味ない」という人は実行するだけで次の記事にいってもらってかまいません!
df = df[~df["着 順"].isin(['中', '取', '失', '除', np.nan])]
df["着 順"] = df["着 順"].replace("\(降\)", "", regex=True).replace("\(再\)", "", regex=True).astype(int)
この部分は、「中止」「取止め」「失格」「除外」などの値を持つレコードをデータフレームから除外しています。次に、着順データをクリーニングしています。着順データには「降」や「再」といった特別な注釈が付いていることがあります。これらを取り除いて数値に変換します。
df['タイム'] = df['タイム'].str.replace(".", ":")
df[['分', '秒', "ミリ秒"]] = df['タイム'].str.split(':', expand=True).astype(float)
df['タイム'] = df['分'] * 60 + df['秒'] + 0.1 * df['ミリ秒']
タイムデータは「分:秒.ミリ秒」の形式で提供されているため、これを秒単位に変換します。この部分のコードは、まずタイムデータの「.」を「:」に置換し、その後、分・秒・ミリ秒に分割してから、それらを合計して総秒数を計算しています。
df[["体重", "体重増減"]] = df["馬体重"].str.replace(")", "").str.split("(", regex=False, expand=True).astype(float)
df[['性', '年齢']] = df['性齢'].str.extract(r'(牡|牝|セ)(\d+)', expand=True)
df["年齢"] = df["年齢"].astype(int)
馬体重データには、体重と体重の増減が含まれています。これを個別の列に分けます。このコードは、馬体重データを括弧で分割し、それぞれ体重と体重増減の列として保存します。
また、性齢データには、性別と年齢が含まれています。これを個別の列に分けます。性別(牡・牝・セ)と年齢を正規表現を使って抽出し、それぞれの列に保存します。
df["単勝オッズ"] = df["単勝"].str.replace(",", "").replace("---", None).astype(float)
df["賞金"] = df["prize"].str.replace(",", "").replace("nan", "0").astype(float).fillna(0)
単勝オッズや賞金データには、カンマ区切り(カンマ区切りはプログラミング上では「数字」ではなく「文字」として認識されてしまう)や不完全なデータが含まれていることがあります。これを数値に変換します。
df = df.drop(columns=['性齢', '着差', '単勝', '馬体重', 'horse id', 'jockey id', 'trainer id', 'owner id', 'prize', '通過', '分', '秒', 'ミリ秒'])
このコードは、予測に直接必要ない列をデータフレームから削除します。
特徴エンジニアリングについて解説
特徴エンジニアリングとは?
次に、特徴エンジニアリングについて説明します。特徴エンジニアリングは、予測に必要な特徴量を選び出し、必要に応じて新しい特徴量を作り出す作業です。これにより、モデルがより有用な情報を学習できるようになります。
最初のステップは、既存のデータから重要な特徴量、使えない特徴量を選び出すことです。競馬予測の場合、以下のような特徴量が考えられます:
- 馬の情報:性別や年齢、単勝オッズ、体重など。
- レース情報:日付など。
- 賞金:レース後にしか分からないため特徴量に使ってはいけない。
次に、新しい特徴量を作成する方法について考えます。例えば、過去のレース結果から「最近のパフォーマンススコア」を作成することができます。これは、馬の直近の成績を評価した指標です。また、騎手の名前や性別などのカテゴリーデータを数値データに変換する「エンコーディング」という手法も有効です。
過去のレース結果を用いる方法:
競馬予測において、馬や騎手の過去のパフォーマンスデータは非常に重要です。以下に、過去のレース結果を用いた新しい特徴量の例をいくつか紹介します。
- 平均タイム: 直近の数レース(例: 3~5レース)の平均タイムを計算し、馬のコンディションやスピードの指標として使用します。
- 勝率: 過去のレースでの1位の回数を総レース数で割って勝率を算出します。これにより、どの馬が安定して良い成績を収めているかが分かります。
エンコーディングの方法:
- ラベルエンコーディング:カテゴリーデータを数値に変換します。例えば、「武豊」を1、「横山武史」を2のようにします。
- ワンホットエンコーディング:カテゴリーデータをバイナリ変数に変換します。例えば、騎手が3人いる場合、それぞれの騎手に対してバイナリ変数を作成し、該当する騎手の変数を1、それ以外を0にします。
実際のコード
これらのことを踏まえて前処理をするコードが以下の通りです。以下のコードをcolabにコピペして実行してみましょう。
def engineer_result(df):
df["1走前着 順"] = df.groupby(["馬名"])["着 順"].apply(lambda x: x.shift(1)).reset_index(drop=True)
df["平均着 順"] = df.groupby(["騎手"])["着 順"].apply(lambda x: x.shift().rolling(window=20, min_periods=1).mean()).reset_index(drop=True)
categorical_columns = ["性", "騎手"] # エンコーディングする列を指定
df = pd.get_dummies(df, columns=categorical_columns, drop_first=True)
df['日付'] = pd.to_datetime(df['日付'], format='%Y年%m月%d日')
df['月'] = df['日付'].dt.month
df['日'] = df['日付'].dt.day
df['月日の数値'] = (df['月'] - 1) * 31 + df['日']
df['月日の数値'] = df['月日の数値'] / 372
df['sin日付'] = np.sin(2 * np.pi * df['月日の数値'])
df['cos日付'] = np.cos(2 * np.pi * df['月日の数値'])
df = df.drop(columns = ['馬名', 'タイム', '調教師', '上がり','レースID', '日付', '賞金', '月', '日', '月日の数値'])
return df
df = engineer_result(df)
df.to_csv(os.path.join(BASE_DIR, "processed_all_result.csv"), index = False)
次にこれらのコードの解説をします。相も変わらず「プログラミングに興味ない」という人は実行するだけで次の記事にいってもらってかまいません!
df["1走前着 順"] = df.groupby(["馬名"])["着 順"].apply(lambda x: x.shift(1)).reset_index(drop=True)
このコードは、各馬(「馬名」でグループ化)ごとに前回のレースの着順を取得し、新しい列「1走前着 順」に保存しています。shift(1)
を使うことで、各馬の直前の着順データを参照しています。
df["平均着 順"] = df.groupby(["騎手"])["着 順"].apply(lambda x: x.shift().rolling(window=20, min_periods=1).mean()).reset_index(drop=True)
この部分は、各騎手の平均着順を計算しています。騎手ごとに「着 順」列を20レースのローリングウィンドウで平均化し、過去のパフォーマンスを指標として残しています。min_periods=1
により、データが少ない場合でも計算できるようにしています。
categorical_columns = ["性", "騎手"]
df = pd.get_dummies(df, columns=categorical_columns, drop_first=True)
「性」(牡・牝・セなど)や「騎手」などのカテゴリ変数は、機械学習で直接扱えないため、ダミー変数に変換します。pd.get_dummies
を使い、これらの列を数値に変換し、drop_first=True
により一つのカテゴリを基準として削除することで、データの冗長性を減らします。今回はワンホットエンコーディングを用いましたが、ラベルエンコーディングを行うことも可能です。
df['日付'] = pd.to_datetime(df['日付'], format='%Y年%m月%d日')
df['月'] = df['日付'].dt.month
df['日'] = df['日付'].dt.day
df['月日の数値'] = (df['月'] - 1) * 31 + df['日']
df['月日の数値'] = df['月日の数値'] / 372
df['sin日付'] = np.sin(2 * np.pi * df['月日の数値'])
df['cos日付'] = np.cos(2 * np.pi * df['月日の数値'])
まず、pd.to_datetime
を使って日付データを標準的な日付形式に変換しています。その後、「月」と「日」のデータを取り出し、月日を数値化した新しい列「月日の数値」を作成しています。この値を0~1の範囲に収め、さらにサイン波(sin)とコサイン波(cos)のトランスフォームを行うことで、周期的な性質を持つ日付データをモデルで扱いやすくしています。
df = df.drop(columns = ['馬名', 'タイム', '調教師', '上がり', 'レースID', '日付', '賞金', '月', '日', '月日の数値'])
予測に直接必要でない、あるいは他の列で表現されている重複データとなる列を削除し、データの冗長性を取り除きます。例えば、「馬名」や「レースID」などはモデルの予測には不必要と判断されています。
まとめ
この記事では、競馬予測AIを作成するためのデータ前処理とクリーニングの重要性について詳しく解説しました。具体的には、レース結果データから必要な情報を抽出し、不要なデータを除去するプロセスを紹介しました。processed_all_result.csv
をダウンロードしてエクセルで観察することで得られたデータが良くわかります。
次回の記事では、モデルの選定と構築について解説します。一緒に競馬予測AIをさらに進化させていきましょう。