A1-B4の全ての列を使用したTarget Encoding

はじめに

初投稿となります。不備や分かりにくい点等あればコメントでご指摘いただけますと幸いです。
通常、Target Encodingは一列に対して行われますが、本コンペはA1-B4のプレイヤーはランダムにマッチングされます(A1は
厳密にはランダムではありませんが、簡素化のため実装含め他プレイヤーと同等に扱います)。
例えば極端な話、同じブキでも、A2に割り振られたあるブキを使用するプレイヤーの勝率はたまたま100%であるゆえに"1"に置き換えられるかも知れませんが、A3の同じブキでは"0"になる可能性もあります。A2プレイヤーとA3プレイヤーはランダムに決まるため「A2プレイヤーがあるブキを使うと強いが、A3が使うと弱い」などということはないはずなのに、該当ブキに対して本来以上/以下の評価となってしまう可能性があります。
このトピックでは、(冗長なコードかも知れませんが…)A1-B4の全ての列を使用したTarget Encodingを行います。

やることの概要

lobby-mode+ブキ毎の勝率を求めることを目標に、一度A1~B4の列を縦長にし、プレイヤーに関わらないlobby-mode+ブキ毎の勝率を一度別のDataFrameにまとめ、trainデータとtestデータに結合します。最後に、おまけとしてモデルを使わずにこの統計データだけで予測を作成し提出します。
なお、今回の実装においてtrainデータに勝率を結合することは意味がありませんが、testと同様に結合してしまうとモデルに組み込む際にはリークが発生してしまうので、参考までにご紹介します。

実装

# ライブラリのインポート
import warnings

import pandas as pd
from sklearn.model_selection import KFold

warnings.filterwarnings('ignore')
# データの読み込み
train = pd.read_csv("train_data .csv")
test = pd.read_csv("test_data .csv")
# lobby-mode毎/ブキ毎の勝率をまとめたDataFrameを返す関数の定義
def make_win_rate(df):
    # 縦に繋げる準備
    A1_df = pd.DataFrame(df[["lobby-mode", "A1-weapon", "y"]])
    A2_df = pd.DataFrame(df[["lobby-mode", "A2-weapon", "y"]])
    A3_df = pd.DataFrame(df[["lobby-mode", "A3-weapon", "y"]])
    A4_df = pd.DataFrame(df[["lobby-mode", "A4-weapon", "y"]])
    B1_df = pd.DataFrame(df[["lobby-mode", "B1-weapon", "y"]])
    B2_df = pd.DataFrame(df[["lobby-mode", "B2-weapon", "y"]])
    B3_df = pd.DataFrame(df[["lobby-mode", "B3-weapon", "y"]])
    B4_df = pd.DataFrame(df[["lobby-mode", "B4-weapon", "y"]])

    # A/Bに関わらず勝利を1、負けを0にするためにBの勝敗を入れ替える(0→1、1→0へ)
    B1_df["y"] = (B1_df["y"]+1) % 2
    B2_df["y"] = (B2_df["y"]+1) % 2
    B3_df["y"] = (B3_df["y"]+1) % 2
    B4_df["y"] = (B4_df["y"]+1) % 2

    # 列のリネーム
    A1_df.rename(columns={'A1-weapon': "weapon"}, inplace=True)
    A2_df.rename(columns={'A2-weapon': "weapon"}, inplace=True)
    A3_df.rename(columns={'A3-weapon': "weapon"}, inplace=True)
    A4_df.rename(columns={'A4-weapon': "weapon"}, inplace=True)
    B1_df.rename(columns={'B1-weapon': "weapon"}, inplace=True)
    B2_df.rename(columns={'B2-weapon': "weapon"}, inplace=True)
    B3_df.rename(columns={'B3-weapon': "weapon"}, inplace=True)
    B4_df.rename(columns={'B4-weapon': "weapon"}, inplace=True)

    # 縦に連結
    df = pd.concat([A1_df, A2_df, A3_df, A4_df, B1_df, B2_df, B3_df, B4_df]).reset_index(drop=True)

    # nanの勝率も知りたいのでfillna
    df.fillna("nan", inplace=True)

    # 後でmapするためにlobby-mode名とブキ名をまとめた列の作成
    df["lobby-mode_weapon"] = df["lobby-mode"] + "+" + df["weapon"]

    # lobby-mode名とブキ名をまとめた列の名称毎に勝率を求める
    target_mean = df.groupby(["lobby-mode_weapon"])["y"].mean()
    df["lobby-mode_weapon_win_rate"] = df["lobby-mode_weapon"].map(target_mean)

    # 必要部分だけをDataFrame化
    lobbymode_weapon_win_rate = pd.DataFrame(df.groupby(["lobby-mode_weapon"])[
                                             "lobby-mode_weapon_win_rate"].mean()).reset_index().rename(columns={"lobby-mode_weapon_win_rate": "y"})

    return lobbymode_weapon_win_rate
# 定義した関数で作成されるDataFrameの確認
# ブキ140種(nan含む)×lobby-mode2種=280行のDataFrame
lobbymode_weapon_win_rate = make_win_rate(train)
print(lobbymode_weapon_win_rate.shape)
lobbymode_weapon_win_rate.head()
(280, 2)
lobby-mode_weapon y
0 gachi+52gal 0.486050
1 gachi+52gal_becchu 0.475782
2 gachi+52gal_deco 0.501385
3 gachi+96gal 0.509632
4 gachi+96gal_deco 0.494369
# trainとtestにもlobby-mode名とブキ名をまとめた列を作成しておく
players = ["A1-", "A2-", "A3-", "A4-", "B1-", "B2-", "B3-", "B4-"]

for player in players:
    train["lobby-mode_" + player + "weapon"] = train["lobby-mode"] + "+" + train[player + "weapon"]
    test["lobby-mode_" + player + "weapon"] = test["lobby-mode"] + "+" + test[player + "weapon"]
# testにmap
# testは全trainデータから算出した勝率を使用
lobbymode_weapon_win_rate = make_win_rate(train)

lobbymode_weapon_cols = ['lobby-mode_A1-weapon', 'lobby-mode_A2-weapon', 'lobby-mode_A3-weapon', 'lobby-mode_A4-weapon',
                         'lobby-mode_B1-weapon', 'lobby-mode_B2-weapon', 'lobby-mode_B3-weapon', 'lobby-mode_B4-weapon']

for c in lobbymode_weapon_cols:
    target_mean = lobbymode_weapon_win_rate.groupby(["lobby-mode_weapon"])["y"].mean()
    test[c + "_win_rate"] = test[c].map(target_mean)
# trainにmap
# trainは全データを使用した勝率を結合してしまうとモデルに組み込んだ際にリークしてしまうのでFoldで区切る
# この実装上は必要のない処理

# Foldで区切りながら都度勝率のDataFrameを作成しFoldのindex毎に値を書き換えたいので、先に結果を格納する列を作成しておく
rate_cols = ['lobby-mode_A1-weapon_win_rate', 'lobby-mode_A2-weapon_win_rate', 'lobby-mode_A3-weapon_win_rate', 'lobby-mode_A4-weapon_win_rate',
             'lobby-mode_B1-weapon_win_rate', 'lobby-mode_B2-weapon_win_rate', 'lobby-mode_B3-weapon_win_rate', 'lobby-mode_B4-weapon_win_rate']

for c in rate_cols:
    train[c] = np.nan

# Foldで区切りながら都度勝率のDaraFrameを作成し、リークしないようにmap
# モデルを学習する際の分割数、seedも同じものにする
kf = KFold(n_splits=5, shuffle=True, random_state=0)
for idx_1, idx_2 in kf.split(train):
    lobbymode_weapon_win_rate = make_win_rate(train.iloc[idx_1])
    for c in lobbymode_weapon_cols:
        target_mean = lobbymode_weapon_win_rate.groupby(["lobby-mode_weapon"])["y"].mean()
        train[c + "_win_rate"][idx_2] = train[c].iloc[idx_2].map(target_mean)
# testに結合した勝率を使用して予測の作成

# A1-A4の勝率とB1-B4の勝率の逆数をすべて掛けてAチームの勝率を求める

test["pred"] = test[rate_cols[0]] * test[rate_cols[1]] * test[rate_cols[2]] * test[rate_cols[3]]\
    * (1-test[rate_cols[4]]) * (1-test[rate_cols[5]]) * (1-test[rate_cols[6]]) * (1-test[rate_cols[7]])

# predが基準となる0.5**8以上であれば勝ち予測、そうで無ければ負け予測
test["y"] = 0
test["y"][test["pred"] >= 0.5**8] = 1
# 提出用ファイルの作成
# LB:0.534792
pd.DataFrame({"id": range(len(test)), "y": test["y"]}).to_csv("submission.csv", index=False)

最後に

  • 当然、コードを少し変えれば、lobby-mode+weapon以外の勝率を求めることもできます。
  • 色々な組み合わせの勝率を求めてlgbmの特徴量に加えたのですが、どれもそこまで重要度は高くありません…
    • 一定以上の結果を出すにはAチーム、Bチーム、AB両チームのブキの組み合わせを加味した特徴量を作成する必要があるのでしょうか…色々試しているのですが全然結果が出ません。。
  • A1プレイヤーは他のトピックでも指摘がある通りランダム選択ではありません。A1プレイヤーを除いて処理をする等の対応に効果があるかも知れません。

添付データ

  • win_rate.ipynb?X-Amz-Expires=10800&X-Amz-Date=20241121T132450Z&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIP7GCBGMWPMZ42PQ
  • Favicon
    new user
    コメントするには 新規登録 もしくは ログイン が必要です。