【競馬予想AI】新馬戦予想に掲示板データを使ってみた話

## はじめに

競馬の新馬戦予想は、出走馬に関する過去のデータが乏しく難しいものです。そこで、netkeibaの馬毎の掲示板データを用いて、機械学習を活用した新馬戦の予測に挑戦しました。この記事では、データ収集から学習、予測までのプロセスと、その結果について詳しく紹介します。

## データ収集

netkeibaから新馬戦に関するデータをスクレイピングで収集しました。対象としたのは2022年にJRAが開催した新馬戦301件、出走した馬3948頭分の掲示板データです。スクレイピング後は、データの前処理として、Unicode正規化や特殊文字の除去などを行いました。

```python:テキストの前処理
def preprocessing_text(text):
    
    text = unicodedata.normalize('NFC', text)
    
    # ()で囲まれた部分を削除
    emoji= r'\([^)]*\)'
    text = re.sub(emoji, "", text)
    
    text = re.sub(r'[^\w\s]', "", text)
    
    text = text.replace("\n", "")
    
    
    return text
```

## 学習データの作成

学習データは、`doc2vec`を使用してベクトル化しました。目的変数としては、各馬の掲示板コメントからその馬が3着以内に入るかどうかを予測する二値分類です。最終的に得られた学習データは26,920件でした。

```python:doc2vecを使ったベクトル化
# 文書をトークン化して、TaggedDocument形式に変換
tagged_data = [TaggedDocument(words=word_tokenize(_d.lower()), 
            tags=[str(i)]) for (i, (_d, _l)) in df_train.iterrows()]

# Doc2Vecモデルの構築とトレーニング
model = Doc2Vec(vector_size=20, min_count=1, epochs=100)
model.build_vocab(tagged_data)
model.train(tagged_data, total_examples=model.corpus_count, epochs=model.epochs)

# 文書ベクトルを取得
vectors = [model.dv[i] for i in range(len(tagged_data))]

# ターゲット（ラベル）の準備
labels = [label for _, label in df_train.values]

# 簡単なモデルで動作確認
# データをトレーニングセットとテストセットに分割
X_train, X_test, y_train, y_test = train_test_split(vectors, labels, test_size=0.25, random_state=42)
# ロジスティック回帰モデルのトレーニング
lr_model = LogisticRegression()
lr_model.fit(X_train, y_train)

# モデルの精度をテスト
print("モデルの精度: ", lr_model.score(X_test, y_test))

# モデルの精度:  0.6462179868969625
```


## 学習と予測

機械学習モデルにはLightGBMを採用し、Optunaでハイパーパラメータのチューニングを行いました。KFoldを使用して汎化性能を測定し、性能評価にはPrecision、Recall、F-score、Accuracy、AUCを用いました。

```python
kf = KFold(n_splits=5, shuffle=True)
vectors = np.array(vectors)
labels = np.array(labels)

scores = []
models = []
# KFoldで精度を求める
for i, (train_index, test_index) in enumerate(kf.split(vectors,labels)):
    print(f"Fold {i}:")
    X_train = vectors[train_index]
    y_train = labels[train_index]
    X_test = vectors[test_index]
    y_test = labels[test_index]
    
    dtrain = lgb.Dataset(X_train, label=y_train)
    deval = lgb.Dataset(X_test, label=y_test)
    
    params = {
        'objective': 'binary',
        'verbose': 0,
        'random_state': 1,
        'metrics': 'auc'
    }
    
    model = lgb.train(params, train_set=dtrain, valid_sets=deval)
    
    y_pred_proba = model.predict(X_test)
    y_pred = np.where(y_pred_proba>=0.5, 1, 0)
    confu = confusion_matrix(y_test, y_pred) # 混同行列
    # print(confu)
    p = precision_score(y_test, y_pred, average='macro') # 精度
    r = recall_score(y_test, y_pred, average='macro') # 再現率
    f1 = f1_score(y_test, y_pred, average='macro') # F値
    acc = accuracy_score(y_test, y_pred) # 正解率
    auc = roc_auc_score(y_test, y_pred_proba)
    print('精度 {:.2f}、再現率 {:.2f}、F値 {:.2f}、正解率 {:.2f}、AUC {:.2f}'.format(p, r, f1, acc, auc))
    scores.append((p, r, f1, acc, auc))
```

## 結果

残念ながら、どの予測精度も期待ほど高くなかったことが判明しました。特にAUCはほぼ0.5と、ランダム予測と同等の精度でした。この結果から、掲示板データだけを使用した予測は困難であると結論付けられます。

```python:結果表示
pd.DataFrame(scores, columns=["精度", "再現率", "F値", "正解率", "AUC"])
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/456646/e4ed97c8-41e5-45d5-cb1b-8aa9fbc47fff.png)

## 考察

予測精度の向上には、他の機械学習モデルとのアンサンブル学習や、血統情報などの追加特徴量の活用が有効と考えています。ただし、厩舎コメントなど一部のデータは有料コンテンツのため、活用には制限があります。

## まとめ

netkeibaの掲示板データを用いた新馬戦の予測は難しいものの、機械学習による競馬予想の可能性を探る第一歩となりました。今後はさらなるデータの探求と、予測モデルの改善に挑みたいと思います。