Kaggle Home Credit - Credit Risk Model Stability 金メダル(13位)解法

# 0. はじめに
2024年5月に終了したKaggleのコンペティション、Home Credit - Credit Risk Model Stabilityに弊社DXイノベーションセンターのデータサイエンティスト3人で参加しました。 結果、金メダルを獲得し、3人のうち2人はKaggle Competition Masterに昇格しました！ 
そこで今回は本コンペの振り返りと解法の共有をしていきたいと思います。

# 1. Kaggleとは
企業や研究機関がデータサイエンスに関するコンペティションを提供し、参加者がその課題を解決するための最適なモデルを開発して精度を競うプラットフォームです。

# 2. コンペ概要
**[Home Credit - Credit Risk Model Stability](https://www.kaggle.com/competitions/home-credit-credit-risk-model-stability)**
![Home Credit.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3683909/8c28d3d1-acf9-cf85-d335-fa41dd54b659.png)


ローン申請をした顧客のうち、どの顧客がデフォルト(貸し倒れ)になる可能性が高いかを予測する二値分類タスクのテーブルデータコンペです。単に予測精度が高ければいいわけではなく、長期にわたる予測精度の安定性が求められました。この安定性を評価するオリジナル評価指標が厄介で本コンペを複雑にしていました。詳しくは後述します。

- データセット
    trainとtestでそれぞれ複数のテーブルが与えられ、合計で465個の特徴量がありました。データセットには以下のような情報が含まれています。
    - 申請者に関する情報(生年月日、性別、家族の人数、住所、資産額、年収、雇用主等)
      ※機微な情報は変換されています
    - 過去の融資情報
    - 口座残高履歴
    - 負債残高履歴
    - カード残高履歴
 
    複数テーブルで構成されるため、各テーブルはcase_idをキーとして結合します。テーブルの中には履歴データが格納されているテーブルもあり、そのようなテーブルは同じcase_idが複数回記録されているのでcase_idごとに集約してからbaseテーブルに結合する必要があります。履歴データの種類によってdepthという値でテーブルが分類されていて、depth >= 1のテーブルが履歴データに相当します。

    depth = 0
    - base  
        - case_id
          各ケースに対する一意の識別子。
        - date_decision
          各ケースの承認に関する決定が下された日付。
        - WEEK_NUM
          集計に使用される週番号。date_decisionから計算される。
        - target
          各ケースに対する申請者の債務不履行の有無。0 or 1の二値で、0と1の割合が97:3の不均衡データ。
    - static_0
    - static_cb_0

    depth = 1
    - applprev_1
    - other_1
    - tax_registry_a/b/c_1
    - credit_bureau_a/b_1
    - deposit_1
    - person_1
    - debitcard_1

    depth = 2
    - applprev_2
    - person_2
    - credit_bureau_a/b_2

    <br/>
    今回のデータセットの特徴としてnullが多かったことが挙げられます。全データの約30%がnullで、70%以上がnullのテーブルもありました。

- データセット数(≒case_id数)
train: 1526659  
test: trainの約90%

- 評価指標
    gini stabilityというオリジナル指標で評価されました。AUC[^1]から計算される通常のgini係数に加えて予測値の安定性が求められます。
    まず、WEEK_NUMごとにgini係数が計算されます。
    $$ \mathrm{gini} = 2 \times \mathrm{AUC} - 1 $$
    次に、$y$を上記のgini係数、$x$をWEEK_NUMとし、$y = ax + b$と線形回帰して求めた回帰係数$a$および残差residualsを使用して、以下のように計算されます。
    $$ \mathrm{gini \;stability} = mean(gini) + 88.0\times min(0, a) - 0.5 \times std(\mathrm{residuals}) $$

    <div style="width: 40%; margin: 0 auto;">

    ![Home Credit Fitting.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3683909/d57be730-69ef-e226-d48c-dbaf2677c945.png)

    </div>

    この評価式は以下のように解釈できます。
    1. gini係数が高いほどスコアは良くなる(第1項)
    2. 直近より将来の予測結果が悪いと大きなペナルティが科される(第2項)
    3. WEEK_NUMごとにgini係数にばらつきがあるとペナルティが科される(第3項)

    しかし、この指標には問題があって、gini係数の数値を意図的に悪化させることでスコアを向上させることができました。具体的には下図のようにWEEK_NUMの前半のgini係数を意図的に下げ、$a$をできるだけ大きい値にすることでペナルティを避けるという方法です。この方法はコンペ内でmetric hackと呼ばれていました。
    <div style="width: 60%; margin: 0 auto;">

    ![Home Credit metric hack.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3683909/be6b2b2f-9077-2e48-912e-4ee2bead2d31.png)

    </div>

    metric hackが可能であることはコンペ初期から指摘されていて、2月から3月にかけて約3週間コンペが中断しました。その間にコンペのホスト側で評価指標の再検討が行われました。結果的に指標は変更されなかったものの、metric hack対策として以下のようなtestデータの編集が行われました。
    1. WEEK_NUMの固定値化
    2. date_decisionおよび日付特徴量の変換
    ※どのような変換を行ったのかは明らかにされていない

    testデータの編集により参加者はtestデータのあるレコードがどのWEEK_NUMに対応しているか分からなくなるので、metric hackは出来ないとホストは主張していました。しかし、実は抜け道があることがコンペ終盤に判明し、各チームはmetric hackを最終提出に組み込むことになります。

- コンペ結果
3人でチームを組み、13位/3856チーム中で金メダル獲得！
![Home Credit Leader Board.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3683909/ea8f40cf-5fcc-cc3b-6faa-12cb85389f0e.png)

# 3. 基本解法
ファイルサイズが大きいため、pandasで前処理・特徴量エンジニアリングをするとメモリが足りない&時間がかかりました。そのため、polarsでの前処理・特徴量エンジニアリングが必須でした。

特徴量エンジニアリングはテーブルごとに特徴量作成をしたのち、depth=1 or 2のテーブルに関してはcase_idごとに集約し、様々な統計量を特徴量とするのが基本となります。特徴量は四則演算で作成することが主でした。

モデルはLightGBMが中心で、たまにCatBoostが使われていることもありました。どうやら欠損値にも意味があったようで、欠損値補完が必須となるNNは精度が悪くほとんど使いものにならなかった印象です。

コンペ終盤に[公開されたmetric hackの手法](https://www.kaggle.com/code/tritionalval/this-is-the-way)はAdverarial Validationに基づく手法でした。trainデータかtestデータかを予測するモデルを作成し、trainデータに近しいと予測されたtestデータの予測値を悪化させるという手法です。

今回のデータはWEEK_NUMにしたがって特徴量の分布が変わっていく特徴がありました。逆に言うと、WEEK_NUMが近いと特徴量の分布も似ていると予想されるわけです。下図のとおり、trainデータとtestデータはWEEK_NUMについて連続で分布しています。trainデータに似ているということはtestデータの前半に該当すると考えられので、このデータの予測値を悪化させればmetric hackが可能ということになります。
<div style="width: 50%; margin: 0 auto;">

![Home Credit Adversarial Validation.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3683909/e2d9c7be-3126-c85a-afe8-97442cf414a7.png)

</div>
この手法が公開されるとどのチームも解法に組み込んでいました。Public LBに有効なのは確実だが、Private LBにも有効かという議論がコンペの終盤に盛り上がっていました。

# 4. チーム解法
上記基本解法をベースにいくつかの有効な特徴量エンジニアリング、モデルのアンサンブル、オリジナルのmetric hackで後処理をしたものがチーム解法となります。[Kaggle disucussion](https://www.kaggle.com/competitions/home-credit-credit-risk-model-stability/discussion/508113)にも解法を投稿しているのでこちらも参照してください。

<div style="text-align: center;">
    チーム解法の概観図
</div>

<div style="width: 60%; margin: 0 auto;">

![Home Credit figure.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3683909/c1145aa4-6adf-dd8a-07e7-b22ada1785bf.png)

</div>

- 特徴量作成
    特にnullが多いテーブル(credit_bureau_b1, other_1, deposit_1, debitcard_1)は使用せず、それ以外のテーブルから特徴量を作成しました。
    スコア向上に寄与したと確実に言える特徴量をピックアップして紹介します。
    - 申請者の年代
    - 申請者の雇用開始時の年齢
    - 申請者の雇用開始から現在までの期間
    - tax_registryテーブルの統合
    tax_registryテーブルはデータ提供元が異なる3種類のテーブルがありました。各テーブルの列名は異なっていましたが、対応関係を推測することで1つのテーブルとして扱いました。
    - カテゴリ値の集約統計量にmodeとn_uniqueを追加
    ※polarsを使用したmodeは下記のように使わないと再現性が確保できないです
    ```
    pl.col(col).mode().sort().first()
    ```

    カテゴリ値のエンコーディングはCount Encodingを使用しました。その他のエンコーディング方法ではLBが下がりました。

- 特徴量採用/選択
    新たな特徴量を追加した際、LBが改善したときの特徴量のみを採用しました。いい感じの特徴量を追加してCV[^2]は上がったけどLB[^3]は下がった…ということばかりでしたが、結果的にはこの採用方法が正解だったようです。

    特徴量選択に関しては様々な手法を試しましたが、[この公開ノートブック](https://www.kaggle.com/code/zivanwan/home-credit-lgb-more-robust/notebook)の手法を採用することで落ち着きました。

    1. 欠損値の処理
    特徴量ごとに欠損値の数をカウントし、欠損値の数に基づいて列をグループ分けします。

    2. 相関に基づく列のグループ化
    1で作成したグループ内で、相関係数が一定の閾値以上の特徴量を同じグループにまとめます。これにより、欠損値数が同じかつ高い相関がある特徴量は同じグループにまとめられます。

    3. グループ内の特徴量選択
    各グループ内で、最もユニークな値を持つ特徴量を選択します。以上の操作でグループ内で最も情報を保持している特徴量を残すことになります。

    この特徴量選択に加えて明らかにデータシフトがある特徴量を除くことで、特徴量を772個から411個まで減らしました。

- バリデーション
n=5のStratified Group KFold(shuffle=True, groups=WEEK_NUM)

- モデル
n=5のseed avaragingをした以下のモデルをRidgeClassifierでスタッキングし、その予測値をCalibratedClassifierCVで補正しました。

<div style="text-align: center;">

|Model|Main Parameters  |
|--|--|
|XGBoost  |  |
|CatBoost  |  |
|LightGBM  | boosting=”gbdt”, extra_tree=True |
|LightGBM  | boosting=”gbdt” |
|LightGBM  | boosting=”rf” |
|LightGBM  | boosting=”rf”, extra_tree=True |
|LightGBM  | boosting=”dart” |
|LightGBM  | boosting=”dart”, extra_tree=True |
|HistGradientBoostingClassifier  |  |
|LightGBM  | boosting=”gbdt”, extra_tree=True, data_sample_strategy=”goss” |

</div>

- metric hack
公開されていたmetric hackの方法がPrivate LBのtestデータにも有効かは不透明だったので他の方法も模索していました。データを眺めていたところ、pmts_year_1139Tというクレジットカードの契約年を表す列がWEEK_NUMの代替として使えるのではないかと気づきました。
pmts_year_1139Tの最新年はdate_decision年に相当する可能性が高く、testデータの編集において日付の変換がされていないことも確認できました。これらの結果に基づき、pmts_year_1139Tの最新年を利用したmetric hackを実装しました。

    ```
    submission = pd.read_csv("submission.csv")
    pmts_year = ... # max pmts_year_1139T group by case_id
        submission.loc[pmts_year == 2020, "score"] = (submission.loc[pmts_year == 2020, "score"] - 0.07).clip(0)
        submission.loc[pmts_year == 2021, "score"] = (submission.loc[pmts_year == 2021, "score"] - 0.06).clip(0)
        submission.loc[pmts_year == 2022, "score"] = (submission.loc[pmts_year == 2022, "score"] - 0.02).clip(0)
        submission.to_csv("submission.csv", index=False)
    ```

    結果的には公開されていたmetric hackの方法はPrivate LBには効かず、このオリジナルmetric hackはPrivate LBに有効でした。

- 試したが上手くいかなかったこと
  - Pseudo Labeling
  - Meta Features
  - Clustering  
  - Target Encoding
  - 特徴量選択
    - Adversarial Validationを用いた方法
    - 統計検定を用いた方法
    - 特徴量重要度を用いた方法
    - Boruta-Shap
  - 前回のHome Credit コンペで有効とされた特徴量エンジニアリング全般

# 5. さいごに
チームメートの協力もあって念願の金メダルを獲得し、Kaggle Masterに昇格することができました！
今まで学んだ知識を総動員してコンペに挑戦し、費やした時間も今まで一番多かったと思います。polarsはほとんど使用したことがなかったので、polarsを使えるようになったのが今回の一番の学びかもしれません。
今後は画像系のコンペにも挑戦していきたいです！

[^1]: 分類モデルの性能を評価する指標です。AUCが1に近いほどモデルの性能が高く、0.5に近いとランダム分類と同等です。
[^2]: Cross Validationの略で、LBと並列して使われるときはtrainデータを使用したスコアを意味することが多いです。 
[^3]: Leader Boardの略で、CVと並列して使われるときはLeader Boardにおけるスコアを意味することが多いです。