🦔

Signate 第2回 金融データ活用チャレンジ ベースラインサマリー

2024/01/27に公開

第2回 金融データ活用チャレンジ のベースラインを作成してみます。このZenn記事では全体的な方針をさっくり書いています。早くコードを動かしたい人はColabへのリンクからColabへ移行してください

基本情報

コンペURL https://signate.jp/competitions/1325
コンペ課題概要 企業向けローンの返済可否予測
全コード Colabへのリンク(Public score: 0.6738)

EDA

  • あんまりNotebookでは頑張っていません。
    • 最低限、各列の基礎的な統計情報(列の型、NaNがある行数、値の種類)を見ました。
    • データ全体的には、カテゴリカル変数多めで、NaNは少ないデータという印象でした。

前処理

まず最初にソースコードを示します。下に変換の方針も記載しています。

ソースコード

def preprocess(df, replace_dict=None, ce_dict=None):
    # 貸借手の所在地系の変数
    # City: Cityは汎用性が低いと考えられるためDrop
    df.drop("City", axis=1, inplace=True)

    # 借り手の会社に関する変数(Sector, FranchiseCode)
    # 31-33, 44-45, 48-49 は同じらしい => 32,33を31に, 45を44に, 49を48に変換
    code_dict = {
        32: 31,
        33: 31,
        45: 44,
        49: 48
    }
    df["Sector"] = df["Sector"].replace(code_dict)

    # 今回の借り入れに関する変数(RevLineCr, LowDoc)
    # 公式ページには値の候補が2つ(YesとNoのYN)と記載があるが、実際の値の種類は2より多い。YN以外はNaNへ置換
    revline_dict = {'0': np.nan, 'T': np.nan}
    df["RevLineCr"] = df["RevLineCr"].replace(revline_dict)

    lowdoc_dict = {'C': np.nan, '0': np.nan, 'S': np.nan, 'A': np.nan}
    df["LowDoc"] = df["LowDoc"].replace(lowdoc_dict)

    # 日付系の変数(DisbursementDate, ApprovalDate)
    # 日付型へ変更 → 年を抽出(借りた月や日にはあまり意味はないと思われるため)
    df['DisbursementDate'] = pd.to_datetime(df['DisbursementDate'], format='%d-%b-%y')
    df["DisbursementYear"] = df["DisbursementDate"].dt.year
    df.drop(["DisbursementDate", "ApprovalDate"], axis=1, inplace=True)

    # 本来数値型のものを変換する
    cols = ["DisbursementGross", "GrAppv", "SBA_Appv"]
    df[cols] = df[cols].applymap(lambda x: x.strip().replace('$', '').replace(',', '')).astype(float).astype(int)

    # 特徴量エンジニアリング
    df["FY_Diff"] = df["ApprovalFY"] - df["DisbursementYear"]
    df["State_is_BankState"] = (df["State"] == df["BankState"])
    df["State_is_BankState"] = df["State_is_BankState"].replace({True: 1, False: 0})

    df['SBA_Portion'] = df['SBA_Appv'] / df['GrAppv']
    df["DisbursementGrossRatio"] = df["DisbursementGross"] / df["GrAppv"]
    df["MonthlyRepayment"] = df["GrAppv"] / df["Term"]
    df["NullCount"] = df.isnull().sum(axis=1)

    # カテゴリカル変数の設定
    df[cols_category] = df[cols_category].fillna(-1)

    # train
    if replace_dict is None:
        # countencode, labelencode
        # ce_dict: 列名を入れるとそのカテゴリのデータがどのくらいあるかを返してくれます
        # replace_dict: 列名を入れるとlabelencodeのための数字を返してくれます
        ce_dict = {}
        replace_dict = {}
        for col in cols_category:
            replace_dict[col] = {}
            vc = df[col].value_counts()
            ce_dict[col] = vc
            replace_dict_in_dict = {}
            for i, k in enumerate(vc.keys()):
                replace_dict_in_dict[k] = i
            replace_dict[col] = replace_dict_in_dict
            df[f"{col}_CountEncode"] = df[col].replace(vc).astype(int)
            df[col] = df[col].replace(replace_dict_in_dict).astype(int)
        return df, replace_dict, ce_dict

    # test
    else:
        for col in cols_category:
            # カウントエンコード
            test_vals_uniq = df[col].unique()
            ce_dict_in_dict = ce_dict[col]
            for test_val in test_vals_uniq:
                if test_val not in ce_dict_in_dict.keys():
                    ce_dict_in_dict[test_val] = -1
            df[f"{col}_CountEncode"] = df[col].replace(ce_dict_in_dict).astype(int)

            # LabelEncode
            test_vals_uniq = df[col].unique()
            replace_dict_in_dict = replace_dict[col]
            for test_val in test_vals_uniq:
                if test_val not in replace_dict_in_dict.keys():
                    replace_dict_in_dict[test_val] = -1
            df[col] = df[col].replace(replace_dict_in_dict).astype(int)
        return df

カテゴリカル変数の変換

勾配ブースティングでは数字型はそのまま取り扱うことができますが、カテゴリカルな変数は何らか数値に変換して扱う必要があります。

ここではEDAでの解析を見ながら、変換が必要そうな列を変換する方針を決めていきます。まず、カテゴリカルな変数の説明を大まかな分類に分けた上で書き出してみます。


  • 貸借手の所在地系の変数

    • City: 借り手の会社の所在地(市)
    • State: 借り手の会社の所在地(州)
    • BankState: 貸し手の所在地(州)
  • 借り手の会社に関する変数(数値として読み込まれているが、本来カテゴリカルな数字)

    • Sector: 産業分類コード
    • FranchiseCode: どのブランドのフランチャイズであるかを識別する一意の5桁のコード
  • 今回の借り入れに関する変数

    • RevLineCr: リボルビング信用枠か
    • LowDoc: 15 万ドル未満のローンを1ページの短い申請で処理できるプログラムか
  • 日付系の変数

    • DisbursementDate: 銀行によって支払われた日
    • ApprovalDate: 米国中小企業庁の承認日
  • 金額系の変数

    • DisbursementGross: 銀行によって支払われた金額
    • GrAppv: 銀行によって承認されたローンの総額
    • SBA_Appv: SBAが保証する承認されたローンの金額

今回のチュートリアルでは、以下のように変換してみたいと思います。

  • 全体方針

... カテゴリカル変数は基本的にはLabelEncodingとCountEncodingを行う

  • 貸借手の所在地系の変数

... City: Cityは汎用性が低いと考えられるためDrop

  • 借り手の会社に関する変数(Sector, FranchiseCode)

... Sector: 公式ページに、31~33は製造業等、同じ意味の数字がいくつかあるため、一部数字は変換を行う

... SectorFranchiseCode: カテゴリカル変数へ変換

  • 今回の借り入れに関する変数(RevLineCr, LowDoc)

... 公式ページには値の候補が2つ(YesとNoのYN)と記載があるが、実際の値の種類は2より多い。YN以外はNaNへ置換

  • 日付系の変数(DisbursementDate, ApprovalDate)

... 日付型へ変更 → 年を抽出(借りた月や日にはあまり意味はないと思われるため)

  • 金額系の変数(DisbursementGross, GrAppv, SBA_Appv)

... 数値型へ変更

その他特徴量エンジニアリング

適当に思いついたものを記載しています。背景にありそうな仮定もカッコで記載しています。

  • 金額の割合を見てみる

    • (SBAが保証する金額に対して借りる金額が小さければリスクは低そう)
  • 借り手と貸し手が同じ州か見てみる

    • (違う州まで借りに行ってるのは財政が厳しい可能性?)
  • SBAの承認年と借りた年の差を見てみる

    • (ここの承認年が長い企業は設立年数が長く、リスクが低いかも)
  • NaNの列数のカウント

    • 一般的にNaNが多いとデフォルトリスクが高いことが多い
      (データがない = リスクが高いという可能性がある)

前処理 - 後確認 - 相関係数の確認

特徴量エンジニアリングの簡易的な成果確認のために、target(MIS_Status)との相関を見てみした。

NullCountやRevLineCrのCountEncodeが結構働いてそうです。

s_per = df_train.corr("pearson")[target].sort_values()
s_spr = df_train.corr("spearman")[target].sort_values()
df_corr = pd.concat([s_per, s_spr], axis=1)
df_corr.columns = ["Pearson", "Spearman"]

# 平均値でソート
df_corr.loc[df_corr.mean(axis=1).sort_values(ascending=False).keys(), :].drop(target)

学習・評価・予測

勾配ブースティング(LightGBM)による学習を行いました。LightGBMは学習時にcategorical_featureというパラメーターを指定することでカテゴリカル変数を扱うことができます。

params_lgb = {
    "n_estimators": 3000,
    "learning_rate": 0.01,
    "colsample_bytree": 0.8,
    "subsample_freq": 1,
    "subsample": 0.8,
    "random_seed": 0,
}
  • CV: Stratified K-Fold(01の割合が同じになるように分割)

  • 01のcutoff: 全通り見てみて、最もf1スコアが高くなるものを算出


以降の改善について

ここからはNotebookに書いていないので、おまけとして。

  • 特徴量エンジニアリング
  • cutoffの変更
    • やり方が全く分からなかった。。全通りだけでなく、Youden indexなどを使う方法もあるらしいです。
  • 学習アルゴリズムの変更
  • データの重複っぽいものを上手く扱う
    • df_train.query("ApprovalDate == '22-Sep-06' and State == 'AZ'")とすると、ほぼ重複してそうなデータ(生成してる感あるデータ)が見えたりします。このあたりを上手く扱うのもよいかも。

Discussion