Kaggle IEEE 上位解法の理解と自身の反省、次のアクションについて

Kaggle IEEE にソロ参加しました。結果は・・・でしたが、学びが多かったので次回のコンペで活かせるように上位解法の理解と自身の反省、今後のアクションについてまとめました。

Very short summary - 1st place

  • Main magic
    • card, D, C, V カラムを使って、ユーザを特定しだいたい 600,000 くらいのカードとユーザを見つけた
      • cardX, addrX, X_emaildomain の特徴量を使って uid を作っていた notebook はあった(下記コード参照)が、これだけでは特定
    • 上記の uid をpandas.DataFrame.agg を使って generalization し、学習データへの過学習を避ける
    • categorical features / supportive features for models
for df in [train, test]:
    df['uid1'] = df['card1'].astype(str) + '_' + df['card2'].astype(str)
    df['uid2'] = df['uid1'].astype(str) + '_' + df['card3'].astype(str) + '_' + df['card5'].astype(str)
    df['uid3'] = df['uid2'].astype(str) + '_' + df['addr1'].astype(str) + '_' + df['addr2'].astype(str)
    df['uid4'] = df['uid3'].astype(str) + '_' + df['P_emaildomain'].astype(str)
    df['uid5'] = df['uid3'].astype(str) + '_' + df['R_emaildomain'].astype(str)
  • Features validation

    • Train 2 month / skip 2 / predict 2
    • Train 4 / skip 1 / predict 1
      • スキップをはさんでいるのは、TrainとTestデータの間に間隔があるからだと思われる。Validationの分割は、TrainとTestの分割と同じ方法で行うのが定石( How to Win a Data Science Competition からの学び)
  • Models

    • Catboost (0.963915 public / 0.940826 private)
    • LGBM (0.961748 / 0.938359)
    • XGB (0.960205 / 0.932369)
  • Predictions
    • 6 folds / GroupKfold by month

1st Place Solution - Part 1

  • Time は重要ではなかった

    • Adversarial Validation で AUC ≒ 1 だったのは、fraud の性質が変化したからではなく、データセット内のユーザが変化したから
  • Magic Feature

    • コンペのホストが ここ で発言しているように、一度ユーザ(クレジットカードが)が fraud とされるとそのアカウントの fraud が 1 になる。つまり、今回のコンペは fraudulent transactions ではなく fraudulent clients (credit card) を予測するタスクだったと言える
  • Fraudulent Clients

    • ではどうやって Client を見つけるかというと、key は card1, addr1, D1
      • D1 is "days since client (credit card) began
        • ここ で言及されていた
      • よって Transaction_day - D1 は credit card の 最初のトランザクションの日が得られる
      • 同様にして、Transaction_day - D3 で前回のトランザクションの日を特定できる
        • ここ で言及されていた
for df in [train, test]:
    df['day'] = df['TransactionDT'] / (24*60*60)
    df['card1_addr1'] = df['card1'] + '_' + df['addr1']
    df['uid'] = df[card1_addr1.astype(str) + '_' + np.floor(df['day'] - df['D1']).astype(str)
  • Preventing Overfitting
    • uid をそのまま使うともちろん過学習してしまうため、aggregated feature を作成
      • 例えば、Cx, Mx カラムを使って new_features = df.groupby('uid')[CM_columns].agg(['mean'])
        • なぜ aggregated feature を作ると判別できるようになるのかについては、How the Magic Works の説明がわかりやすい
      • 最後に過学習を避けるため uid を削除
      • これでモデルは未知のユーザかどうかを分類できるようになる
        • aggregated feature はこんなイメージ
agg_types = ['max', 'min', 'sum', 'mean', 'std', 'count']
for agg_type in agg_types:
    new_col_name = cat_col + '_' + agg_col + '_' + agg_type
    temp = pd.concat([train[[cat_col, agg_col]], test[[cat_col, agg_col]]])
    temp = temp.groupby([cat_col])[agg_col].agg([agg_type]).reset_index().rename(columns={agg_type: new_col_name})
    temp.index = list(temp[cat_col])
    temp = temp[new_col_name].to_dict()
    train[new_col_name] = train[cat_col].map(temp)
    test[new_col_name]  = test[cat_col].map(temp)

1st Place Solution - Part 2

  • Final Model
    • CatBoost (Public/Private LB of 0.9639/0.9408), LGBM (0.9617/0.9384), and XGB (0.9602/0.9324)
      • それぞれ別々の特徴量エンジニアリングをしていたので、多様性があった
      • final submission は Catboost の予測結果を利用した LightGBMのスタッキングと他のモデルの アンサンブル (equal weight)
      • それらの結果は、post process として同じ client は Client ごとのスコアの平均に書き換えることでスコアを伸ばせた
  • How to FInd UIDs

    • 具体的な方法については ここ
    • 430 columns の中から顧客を特定するための特徴量はどれか
      • trainとtestデータに含まれる顧客の多くは異なっている
      • Transaction_day - D系カラム をしてから、最初の53の特徴量を利用して Adversarial Validation をすると 0.999 になった。つまりこれらの特徴量は顧客を識別するのに重要だとわかる
      • D10n, D1n, D15n, C13, D4n, card1, D2n, card2, addr1, TransactionAmt, dist1 が最も重要だと判断した
        • Adversarial Validation で feature importance を上から見ていけばこれらが出てくるはず
      • V系のカラムについて
        • これも同じくV系特徴量だけで Adversarial Validation を行ったところ、AUC = 0.999 となった
        • 最終的に encode_AG(['V127','V136','V307','V309','V314,'V320`] ,['uid'], ['nunique']) で特徴量を作成
  • EDA

    • IEEE Transaction columns Reference を参考に
    • V系カラムの次元削減を行った
      • NANの傾向からグループを作成
      • グループごとに PCA を行った
      • グループから 最も相関が少ない特徴を選択
      • それぞれのグループを全カラムの平均で置き換える
  • 特徴選択
    • 今回はたくさんカラムがあるので特徴選択はとても重要
    • XGBのモデルは 250 カラム
    • 使ったトリックは下記
      • forward feature selection (using single or groups of features)
      • recursive feature elimination (using single or groups of features)
      • permutation importance
      • adversarial validation
      • correlation analysis
      • time consistency
        • 1つまたはグループの特徴量を使って、学習データの初めの月だけを利用してシングルモデルを学習させ、学習データの最後の月を使って予測をする。これである特徴量が時間を超えて一貫性があるかを判断することができる。
        • 5%の特徴量がモデルの精度を下げた。これはつまりモデルはあるパターンを見つけたが、未来にはそれは存在していなかったといえる。
        • 精度を下げた特徴量については取り除いたという理解
      • client consistency
      • train/test distribution analysis
  • バリデーションの戦略
    • 特定のバリデーションを信頼してなかったので、たくさんの方法を試した
      • Trainの最初の2ヶ月分を学習、1ヶ月をスキップ、最後の1ヶ月を予測
      • Trainの最初の2ヶ月分を学習、2ヶ月をスキップ、最後の2ヶ月を予測
      • Trainの最初の1ヶ月分を学習、4ヶ月をスキップ、最後の1ヶ月を予測

2nd Solution, CPMP View

  • Data Cleaning

    • 各特徴量ごとに train と test の distribution をチェック
    • raw feature と frequency encoding feature をプロット
      • raw feature の場合、train と test の分布に差があるが、frequency encoding feature の分布には差がないので raw feature は削除して、frequency encoding feature を利用した
  • Cross Validation

    • train と test の間には時間的ギャップが存在すること、test が train から数ヶ月離れていることをCVで再現しようとした
    • 結果的に下記の folds にした
      • 0 | 2 3 4 5 6
      • 0 1 | 3 4 5 6
      • 0 1 2 | 4 5 6
      • 0 1 2 3 | 5 6
    • 上記の CV で AUC の平均をとった
    • この CV で特徴量の評価、特徴選択とハイパーパラメータの最適化を行った
  • Feature Engineering

    • まずは全特徴量の frequency encodin を行って LightGBM で上記の CV を行い、permutation importance から特徴量選択を行った
    • permutation importance の概要については Permutation Importanceを使ってモデルがどの特徴量から学習したかを定量化する がわかりやすい
    • permutation 後にモデルが改善しなければその特徴量を保持した
      • これによっていくつかの特徴量を削除した
    • いくつかの uid 候補があったが最終的に下記を使った
      • 過学習を避けるために uid はそのまま使わないようにした_
        • しかし結果的には catboost のモデルで uid を使った結果が最もスコアが高かった
      • aggregate feature は下記。これで public LB で0.952
data['uid1'] =  (data.day - data.D1).astype(str) +'_' + data.P_emaildomain.astype(str)
data['uid2'] =  (data.card1.astype(str) +'_' + data.addr1.astype(str) +'_' + (data.day - data.D1).astype(str) +'_' + data.P_emaildomain.astype(str))
def add_gr(data, col):
    cols = data.columns
    gr = data.groupby(col)
    data[col+'_count'] = gr.TransactionID.transform('count').astype('int32')
    data[col+'_next_dt'] = gr.TransactionDT.shift(-1)
    data[col+'_next_dt'] -= data.TransactionDT
    data[col+'_mean_dt'] = gr[col+'_next_dt'].transform('mean').astype('float32')
    data[col+'_std_dt'] = gr[col+'_next_dt'].transform('std').astype('float32')
    data[col+'_median_dt'] = gr[col+'_next_dt'].transform('median').astype('float32')

    data[col+'_next_amt'] = gr.TransactionAmt.shift(-1)
    data[col+'_mean_amt'] = gr.TransactionAmt.transform('mean').astype('float32')
    data[col+'_std_amt'] = gr.TransactionAmt.transform('std').astype('float32')
    data[col+'_median_amt'] = gr.TransactionAmt.transform('median').astype('float32')
    new_cols = list(set(data.columns) - set(cols) -set([col+'_next_dt']))
    return new_cols
  • Final Blend
    • 基本的に LightGBM を利用し、Catboost, XGBoost も試した
      • XGBoost はほとんど LightGBM と同じ
    • チームのモデルを blend
    • 一度 fraud と判定されるとそれ以降のそのユーザのトランザクションは全て fraud となるデータセットであることを知って post processing を行いスコアを上げた

9th place solution notes

だいたい1位のソリューションと同じ。

  • 同じユーザに属するトランザクションを特定することに重きを置いていた(他のトップチームも同じはず)
  • ユーザを特定するのには、"2017-11-30" + TransactionDT - D1 が効いた
    • 同様に別のD系カラムにも適用した
  • またユーザの特定には V95, V97, V96, V126, V128, V127 を利用した
    • これらをどう特定したのかは書かれてはいない
  • ユーザ特定をした結果を特徴量ではなく、post processing、pseudo labeling に利用した
  • 最初の3ヵ月分を学習、1ヵ月分をスキップ、2ヵ月をバリデーションに使った。
    • これらは public LB には相関していたが、private LB ではうまくいかなかった
  • モデルは LightGBM, Catboost and XGBoost を利用

学び・反省

  • ホストの発言やD1カラムについては Discussion で公開されていて、uid系特徴量を作成している notebook もあったので、Discussion をちゃんと読んでいれば顧客 (クレジットカード)を特定するタスクととらえることは難しくなかったかもしれない
    • 大きなヒントが隠れているので当たり前だが Discussion をちゃんと読むことは大事
    • 特に今回については、Data Description (Details and Discussion) をきちんと読み込むことが重要だった
    • コンペのホストの発言 には upvote がたくさんついていたので気づくことは難しくなかった
  • EDAから仮説を立てて特徴量を作ることがあまり得意ではないことを理解した
    • upvoteの多いEDA カーネルをフォークして使うのは楽だが、時間があるときは最初はゼロから自分でEDAするのがいいと思った。そうしないと思考が停止する傾向がある
    • 特徴量エンジニアリング時に思考停止して、適当に特徴量生成をして全くスコアが上がらない時期があった
    • 当たり前だがちゃんとタスクを理解すること
    • 以前のコンペの経験があまり活きなかった
      • 特殊めなコンペ(Instant Gratificationなど)ばかり出ていた気がするからかも
  • 今回のコンペで EDA や 特徴量エンジニアリングにおけるスニペットを作成できたので、次回から使いまわせるはず
  • Discussion での英語は全く抵抗がなくなって、以前よりも辞書で意味を調べることも少なくなったので余計な時間が取られなくなった
    • 分からない単語は Quizlet という単語帳サービスを使ってまとめて、暇なときに見返すようにしていたのでそれがうまくいっているのかも
  • スプレッドシートで追加した特徴量と Local CV、LBスコア、カーネルのURL などをまとめるようにしてみた
    • 特徴量エンジニアリングの試行錯誤で同じことをすることが避けられた
    • もっとスマートな管理ができたら良い f:id:taigok:20191013111824p:plain
  • Feature Engineering Techniques は他のコンペでも使える知識
  • テーブルデータコンペでもNNを使えるようになりたい
    • アンサンブルに効いてくるし、得意な人もあまりいないと思うのでできるようになれば、チームマージ力が上がる(笑)
    • LB9564 NN solution overview は参考になる

次のアクション

  • 引き続き本コンペの上位解法を読む
    • discussion を読むだけで終わらず、実際に自分で実装してスコアが上がるところまでやりきる
    • EDAから仮説構築 -> 特徴量エンジニアリング ができるように、終わったコンペでもいいので取り組んでみる
  • Kaggleで勝つデータ分析の技術 を買ったので全部読み切る
  • NFL Big Data Bowl に参加してソロでメダルを取る!