対戦ゲームデータ分析甲子園

目指せ"Another" バトル優勝!

賞金: 100,000 参加ユーザー数: 606 3年以上前に終了

7th Place Solution (Pub: ~60th Pri: 7th)

■ 7th Place Solution (Pub: >60th Pri: 7th)

まずはじめにコンペを開催してくださった運営の皆様と参加者の皆様に感謝申し上げます。

Splatoonは全くやったことがありませんが、有名なゲームで開始当初Twitterなどでも盛り上がっており面白そうだったので参加させていただきました。 ランダム性も高く、時間かけた分スコアが伸びるようなタスクでもあまりなかったため少し辛かったです。。 全体的な解法はより上位のソリューションに期待して、大きくShakeUpしたこともあり一部個人的なポイントを共有させていただきます。

Summary

基本的なアプローチは通常のテーブルデータ系のアプローチです。

  • 集計特徴量大量作成
  • Null Importanceで特徴量選択
  • GBDT系+NN + Linear系のスタッキング

Point

個人的に思う自分の解法の中でのポイントは

  1. A1プレイヤー以外のチーム内レベル順ソート

    ドメイン知識が全くないため今でもなぜ効いていたかはよくわかっていませんが、A1以外のプレイヤーの順番(A2~A4のプレイヤー)はチーム内でレベル順にソートし直しました。ゲームを知りませんので、この順番に意味があるかわからなく、A1以外はランダムであると仮定するとランダムに混ざっているよりかは、強さ順に並んでいる方が綺麗で学習もうまくいきそうという推測です。 もし順番にゲーム内で理由があるなら、たまたまかもしれません。 初期段階で結果的にCVが一番よかったため、ソートしたデータをベースにしていました。

     def sort_player_by_level_except_A1(df):
         df_copy = df.copy()
         df_copy.drop(columns=[c for c in df_copy if ('-level' in c) & ('A1' not in c)], inplace=True)
         df_copy.drop(columns=[c for c in df_copy if ('-rank' in c) & ('A1' not in c)], inplace=True)
         df_copy.drop(columns=[c for c in df_copy if ('-weapon' in c) & ('A1' not in c)], inplace=True)
    
         for team in ['A', 'B']:
             if team == 'A':
                 d_level = df[[f'{team}2-level', f'{team}3-level', f'{team}4-level']].T.to_dict(orient='list')
                 d_rank = df[[f'{team}2-rank', f'{team}3-rank', f'{team}4-rank']].T.to_dict(orient='list')
                 d_weapon = df[[f'{team}2-weapon', f'{team}3-weapon', f'{team}4-weapon']].T.to_dict(orient='list')
    
                 team_df_columns = [
                     #f'{team}1-level', f'{team}1-rank', f'{team}1-weapon',
                     f'{team}2-level', f'{team}2-rank', f'{team}2-weapon',
                     f'{team}3-level', f'{team}3-rank', f'{team}3-weapon',
                     f'{team}4-level', f'{team}4-rank', f'{team}4-weapon',
                 ]
             else:
                 d_level = df[[f'{team}1-level', f'{team}2-level', f'{team}3-level', f'{team}4-level']].T.to_dict(orient='list')
                 d_rank = df[[f'{team}1-rank', f'{team}2-rank', f'{team}3-rank', f'{team}4-rank']].T.to_dict(orient='list')
                 d_weapon = df[[f'{team}1-weapon', f'{team}2-weapon', f'{team}3-weapon', f'{team}4-weapon']].T.to_dict(orient='list')
                 team_df_columns = [
                     f'{team}1-level', f'{team}1-rank', f'{team}1-weapon',
                     f'{team}2-level', f'{team}2-rank', f'{team}2-weapon',
                     f'{team}3-level', f'{team}3-rank', f'{team}3-weapon',
                     f'{team}4-level', f'{team}4-rank', f'{team}4-weapon',
                 ]
    
             d1 = {}
             for k, v in d_rank.items():
                 l = []
                 for a1, a2, a3 in zip(v, d_level[k], d_weapon[k]):
                     l.append([a1, a2, a3])
                 d1[k] = l
    
             d2 = {}
             for k, v in d1.items():
                 d2[k] = sum(sorted(d1[k], key=lambda x: (x[0], x[1])), [])
    
             team_df = pd.DataFrame.from_dict(d2, orient='index')
             team_df.columns = team_df_columns
             df_copy = df_copy.merge(team_df, left_index=True, right_index=True, how='left')
    
         return df_copy
  1. A1プレイヤーでのStratifiedKFold

    日をまたいだA1プレイヤーの特定はノイズも多くなかなか難しかったものの(masato8823さんのトピック→https://prob.space/competitions/game_winner/discussions/masato8823-Postb93d4bc4fde321a8215b ) 、同時間の投稿における同一A1プレイヤーの推定は簡易的に行えましたのでこちらの連番を対象にStratifiedKFoldを行い学習しました。

    パプリックスコアは低かった(0.558)ですが、privateスコアが結果的に一番高くなりました。分割をこちらでやられてるんでしょうか? もしくは、ある程度各プレイヤーの勝率が収束しており、結果的にうまくバランスよく分割できたのかもしれません

     def create_A1_seqnum_features(df):
    
         # A1_seqnum 同時刻に同条件で投稿しているA1プレイヤーの連番
         seq_df = df.drop_duplicates(['period', 'A1-weapon', 'A1-rank', 'A1-level']).reset_index(drop=True)
         seq_df = seq_df.reset_index()[['period', 'A1-weapon', 'A1-rank', 'A1-level', 'index']].rename(columns={'index': 'A1_seqnum'})
         df = df.merge(seq_df, on=['period', 'A1-weapon', 'A1-rank', 'A1-level'], how='left')
    
         return df
  1. 集計特徴量

    基本的な集計系の特徴量使用です。参考までにTop20FeatureImportanceも貼っておきます。

     def create_base_feature(df):
    
         # --- Period ---
         df['month'] = df['period'].dt.month
         df['dayofmonth'] = df['period'].dt.day
         df['dayofweek'] = df['period'].dt.dayofweek
         df['hour'] = df['period'].dt.hour
    
         # --- Level ---
         for level in ['A1-level', 'A2-level', 'A3-level', 'A4-level', 'B1-level', 'B2-level', 'B3-level', 'B4-level']:
             df[f'{level}_99div'] = df[level] // 99
             df[f'{level}_99rem'] = df[level] % 99
    
         df['Alevel_sum'] = df[['A1-level', 'A2-level', 'A3-level', 'A4-level']].sum(axis=1)
         df['Alevel_std'] = df[['A1-level', 'A2-level', 'A3-level', 'A4-level']].std(axis=1)
         df['Alevel_skew'] = df[['A1-level', 'A2-level', 'A3-level', 'A4-level']].skew(axis=1)
         df['Blevel_sum'] = df[['B1-level', 'B2-level', 'B3-level', 'B4-level']].sum(axis=1)
         df['Blevel_std'] = df[['B1-level', 'B2-level', 'B3-level', 'B4-level']].std(axis=1)
         df['Blevel_skew'] = df[['B1-level', 'B2-level', 'B3-level', 'B4-level']].skew(axis=1)
    
         df['level_diff'] = df['Alevel_sum'] - df['Blevel_sum']
         df['level_ratio'] = df['Alevel_sum'] / df['Blevel_sum']
         df.loc[np.isfinite(df.level_ratio), 'level_ratio_cut'] = pd.cut(df.loc[np.isfinite(df.level_ratio), 'level_ratio'], 20, labels=False)
         #df['level_ratio_qcut'] = pd.qcut(df['level_ratio'], 20, labels=[i for i in range(20)], duplicates='drop') 
    
         # --- Rank ---
         df['Arank_sum'] = df[['A1-rank', 'A2-rank', 'A3-rank', 'A4-rank']].sum(axis=1)
         df['Arank_std'] = df[['A1-rank', 'A2-rank', 'A3-rank', 'A4-rank']].std(axis=1)
         df['Arank_skew'] = df[['A1-rank', 'A2-rank', 'A3-rank', 'A4-rank']].skew(axis=1)
         df['Brank_sum'] = df[['B1-rank', 'B2-rank', 'B3-rank', 'B4-rank']].sum(axis=1)
         df['Brank_std'] = df[['B1-rank', 'B2-rank', 'B3-rank', 'B4-rank']].std(axis=1)
         df['Brank_skew'] = df[['B1-rank', 'B2-rank', 'B3-rank', 'B4-rank']].skew(axis=1)
    
         df['rank_diff'] = df['Arank_sum'] - df['Brank_sum']
         df['rank_ratio'] = df['Arank_sum'] / df['Brank_sum']
         df.loc[np.isfinite(df.level_ratio_cut), 'rank_ratio_cut'] = pd.cut(df.loc[np.isfinite(df.rank_ratio), 'rank_ratio'], 20, labels=False)
         #df['rank_ratio_qcut'] = pd.qcut(df['rank_ratio'], 20, labels=[i for i in range(20)], duplicates='drop')
    
         # --- splatnet ---
         df['Asplatnet_sum'] = df[['A1_splatnet', 'A2_splatnet', 'A3_splatnet', 'A4_splatnet']].sum(axis=1)
         df['Asplatnet_std'] = df[['A1_splatnet', 'A2_splatnet', 'A3_splatnet', 'A4_splatnet']].std(axis=1)
         df['Asplatnet_skew'] = df[['A1_splatnet', 'A2_splatnet', 'A3_splatnet', 'A4_splatnet']].skew(axis=1)
         df['Bsplatnet_sum'] = df[['B1_splatnet', 'B2_splatnet', 'B3_splatnet', 'B4_splatnet']].sum(axis=1)
         df['Bsplatnet_std'] = df[['B1_splatnet', 'B2_splatnet', 'B3_splatnet', 'B4_splatnet']].std(axis=1)
         df['Bsplatnet_skew'] = df[['B1_splatnet', 'B2_splatnet', 'B3_splatnet', 'B4_splatnet']].skew(axis=1)
    
         df['splatnet_diff'] = df['Asplatnet_sum'] - df['Bsplatnet_sum']
         df['splatnet_ratio'] = df['Asplatnet_sum'] / df['Bsplatnet_sum']
         df.loc[np.isfinite(df.splatnet_ratio), 'splatnet_ratio_cut'] = pd.cut(df.loc[np.isfinite(df.splatnet_ratio), 'splatnet_ratio'], 20, labels=False)
         #df['splatnet_ratio_qcut'] = pd.qcut(df['splatnet_ratio'], 20, labels=[i for i in range(20)], duplicates='drop')
    
         return df
     Feature importance 0: ('A1-rank_min_ratio_rank_ratio', 1774.846758365631)
     Feature importance 1: ('A1_seqnum_mean_diff_level_diff', 910.2062306404114)
     Feature importance 2: ('B1_category2', 516.2507209777832)
     Feature importance 3: ('B3-weapon_min_diff_rank_diff', 426.2479922771454)
     Feature importance 4: ('A1_seqnum_mean_diff_level_ratio', 410.01326155662537)
     Feature importance 5: ('A4_category1', 387.31628799438477)
     Feature importance 6: ('A3_category1', 382.49681973457336)
     Feature importance 7: ('Asplatnet_std', 280.09175205230713)
     Feature importance 8: ('A1-rank_max_diff_rank_ratio', 262.1223609447479)
     Feature importance 9: ('B2-rank_min_diff_level_diff', 251.01503992080688)
     Feature importance 10: ('A1_playernum_mean_diff_Brank_sum', 239.02090072631836)
     Feature importance 11: ('B2_category1', 233.08357858657837)
     Feature importance 12: ('A1-rank_max_ratio_rank_ratio', 221.75276899337769)
     Feature importance 13: ('A2-weapon_min_Arank_sum', 216.08291101455688)
     Feature importance 14: ('A1_seqnum_max_Brank_sum', 197.79781126976013)
     Feature importance 15: ('A1_seqnum_mean_diff_Brank_sum', 196.66624999046326)
     Feature importance 16: ('A3_category2', 193.10358953475952)
     Feature importance 17: ('B2-weapon_min_diff_rank_diff', 184.9084596633911)
     Feature importance 18: ('A2-weapon_skew_rank_diff', 182.678528547287)
     Feature importance 19: ('A1-weapon_max_min_diff_Asplatnet_sum', 181.3388111591339)
     Feature importance 20: ('A1_playernum_max_Bsplatnet_skew', 176.16425037384033)
  2. objectiveはregression GBDT系のobjectiveはbinaryよりもregressionで学習した方が良かったです。 自身の経験的には二値分類でもregressionの方が若干ながら精度が良いことの方が多く、いまだにどういうケースで使い分けすべきかがよくわかっていません。

Null Importance, Tuning, Stacking

lgb, xgb, catboostそれぞれNullImportanceを行い、上位特徴量でOptunaでハイパラチューニングを行いました。
それらとNNとSklearnのlinear系のモデルをいくつか作り、2層目でRidgeでStackingを行いました。

Score

LocalCV: 0.563 (5Fold A1_seqnum StaratifiedKFold)
Public: 0.558
Private: 0.565

以上です。 また次コンペや別コンペでもお会いした時は、対戦orチームマージなどもよろしくお願いします。(プロフにtwitter貼ってますので、もしよろしければフォローしてください(あまり投稿してないですが><))

Favicon
new user
コメントするには 新規登録 もしくは ログイン が必要です。