まだタイトルない

アウトプット用です

【Kaggle挑戦記】バスケコンペでメダル拾えるかと思ったらそんな甘い話なかった【#7】

こんにちは!役立たずてよです!
自分で役立たずとは言いつつも、「まぁそこまででもないっしょ!」と思っていました。 でも、今回のバスケコンペで...................

こんにちは、kaggleのバスケコンペに参加してきました。

NCAAはNational Collegiate Athletic Associationの略で全米大学体育協会という意味です。そのなかのバスケットボール男子/女子の大会の結果を予測するというコンペです。

毎年有るイベントのためバスケコンペは近年は毎年の定番になっているみたいで、去年は情勢的に中止になってしまったものの2019や2018のソリューションを参考にすることができました。 また、大きな違いとして今年からメダル対象になっています。下表のようなチーム数がメダルを手にすることが出来ます。運要素が強いとはいえ他のコンペと比べたら銀メダル狙いやすそう?

下記は参加チーム数とメダルのボーダーになります

参加チーム数 金圏 銀圏 銅圏
707 11 50 100
451 10 50 100

今回は過去ソリューションを参考にサブを作ったのでオリジナル要素をアウトプットすることは出来ないです。そのかわり、来年の自分やこれから始める人の学びになりそうなことを書いていこうと思います。

サブミット形式

ID Pred
2021_3104_3112 0.5
2021_3104_3116 0.5

64チームのシングルエリミネーションに対して、全部の組み合わせについて予測をしていきます。ID列は[年チームID1チームID2]という構成で、若いチームIDが先にくる。若いチームIDのチームが勝つ確率を0~1でPredに設定します。

行数は64*63/2=2016で2016行です。

この中から実際に行われた試合64試合分の予測を使って評価が行われます。今回NCAAMの方はCOVID19の影響で1試合中止になり、63試合で集計となりました。

評価値

log loss

極端な間違いに対するペナルティが大きいことが特徴の評価値になります。

from sklearn.metrics import log_loss
log_loss([1],[0],labels=[0,1])

>>34.538776394910684

絶対勝つからpredに1を入れて結果が外れると34.5になるため、64試合で割るとscoreが0.54ほど上昇してしまいます。
そして、過去のコンペの優勝スコア1,2が0.4~0.5程度のため、大きな間違いは詰みです。

余談ですが、シードが1-16されていてこの数字は1から順に強いチームに割り当てられ、シード1の初戦はシード16のようにトーナメント表も決まっているみたいです。そしてシードが大きいチームが小さいチームに勝つことをupset(番狂わせ)と言い、発生率は男子が多く女子だと珍しいことから、女子のソリューションではシード1と16の組み合わせは1が100%勝つような予測を出すのが選択肢に入ってきます。

データと方針とpandasの勉強

都市や大会のマスタなども配られていますが、基本的にレギュラーシーズンの試合データとNCAAトーナメント(過去分)の試合データを使ってtrainingデータを作っていきます。

具体的にはトーナメントの結果をラベルとし、特徴量にその都市のレギュラーシーズンの成績を肉付けていきます

注意点としては、勝チーム情報と負けチーム情報というふうに列が分かれています。これだとどちらかのチームIDが勝率100%のデータになってしまうため、勝チーム情報と負けチーム情報を入れ替えたテーブルを結合して特徴として成り立つようにします。 ※正直これで大丈夫な根拠を理解できていないです。同じ試合のレコードが2個あるということになってないのかなぁ

チーム情報を入れ替えて一つのdataframeにするコード

# https://www.kaggle.com/raddar/paris-madness?scriptVersionId=9740989
def prepare_data(df):
    df_swap = df[['Season', 'DayNum', 'LTeamID', 'LScore', 'WTeamID', 'WScore', 'WLoc', 'NumOT', 
    'LFGM', 'LFGA', 'LFGM3', 'LFGA3', 'LFTM', 'LFTA', 'LOR', 'LDR', 'LAst', 'LTO', 'LStl', 'LBlk', 'LPF', 
    'WFGM', 'WFGA', 'WFGM3', 'WFGA3', 'WFTM', 'WFTA', 'WOR', 'WDR', 'WAst', 'WTO', 'WStl', 'WBlk', 'WPF']]
    
    # home awayの入れ替え
    # df.loc[行の条件, 列の条件]
    df_swap.loc[df['WLoc'] == 'H', 'WLoc'] = 'A'
    df_swap.loc[df['WLoc'] == 'A', 'WLoc'] = 'H'
    df.columns.values[6] = 'location'
    df_swap.columns.values[6] = 'location'
    
    # win loseでなくチーム1,チーム2にカラム名を変えておく
    # df.columnsにlistで設定ができる
    df.columns = [x.replace('W', 'T1_').replace('L', 'T2_') for x in list(df.columns)]
    df_swap.columns = [x.replace('L', 'T1_').replace('W', 'T2_') for x in list(df_swap.columns)]
    
    # 結合
    # pd.concat デフォルトでaxis=0(縦方向結合)
    output = pd.concat([df, df_swap]).reset_index(drop=True)
    output.loc[output.location=='N', 'location'] = '0'
    output.loc[output.location=='H', 'location'] = '1'
    output.loc[output.location=='A', 'location'] = '-1'
    output.location = output.location.astype(int)
    
    # PointDiff列を作成し、得点差を代入
    output['PointDiff'] = output['T1_Score'] - output['T2_Score']
    
    return output

レギュラーシーズンについて、各年のチーム毎に列ごとの平均値を計算する.
T2側の特徴量は複数のチームの成績がごちゃまぜになるが、これは対戦相手の成績という情報になる。こういう捉え方はソリューションを読むまで気づきませんでした。

# groupby(["Season", 'T1_TeamID']) 同じSeason T1_TeamIDでまとめる
# [boxscore_cols]使用する列をリストで指定
# agg(funcs) まとめ方を指定、今回は np.mean
funcs = [np.mean]
season_statistics = regular_data.groupby(["Season", 'T1_TeamID'])[boxscore_cols].agg(funcs).reset_index()

集計したデータについて、チーム1の特徴用とチーム2の特徴用に加工してくっつける作業をします。

# copy()をしないと参照渡しになってしまう
season_statistics_T1 = season_statistics.copy()
season_statistics_T2 = season_statistics.copy()

# 列名の加工
season_statistics_T1.columns = ["T1_" + x.replace("T1_","").replace("T2_","opponent_") for x in list(season_statistics_T1.columns)]
season_statistics_T2.columns = ["T2_" + x.replace("T1_","").replace("T2_","opponent_") for x in list(season_statistics_T2.columns)]
# 後処理
season_statistics_T1.columns.values[0] = "Season"
season_statistics_T2.columns.values[0] = "Season"

# 結合
tourney_data = pd.merge(tourney_data, season_statistics_T1, on = ['Season', 'T1_TeamID'], how = 'left')
tourney_data = pd.merge(tourney_data, season_statistics_T2, on = ['Season', 'T2_TeamID'], how = 'left')

同様にseasonとteamIDをキーに特徴量を作って、team1用とteam2用に加工して肉付けしていく流れになります。

結果

基本的にはraddarさんのノート3を写経して、少し特徴量を加えたり削ったりして、lgbmでも学習してアンサンブルを行いました。
女子の方はupsetが起きにくいということで、1回戦は第1~4シードが勝利だけでなく、2回戦も第1,2シードが勝利するというおみくじを仕込みましたが下記マッチアップでupsetが発生して爆死しました。もう一つのsubも特に振るうことはなかったです。

  • Arkansas[4]-Wright St.[13]

男子の方はmodelの出力そのままのものと、ゴンザガ大を全部勝率1にしたものをおみくじsubしました。決勝開始前時点ではおみくじsubが65位まで上がっていたのですが、決勝で負けてしまいmodelの出力そのままの方がスコアがいいということになりましたが、こちらもメダル圏外でした。

f:id:teyoblog:20210406204337p:plain
男子決勝前までの図
f:id:teyoblog:20210406204415p:plain
決勝前までのsubの様子
f:id:teyoblog:20210406204442p:plain
終結

感想

今回、スポーツ大会開催前にモデルで予測してそれを元に大会を楽しむという新しいものでした。日本からは無料で見る手段がなかったため、公式サイトのスコア速報を追ったりすることしか出来ませんでしたがゴンザガ大の準決勝はとてもハラハラしました。

スコア的には、基本的にメダルの圏外にいて、ゴンザガが勝てば少し上がるようなものだったので予測精度はほか参加者より劣っていたと思います。また力をつけていきたいです。