Signate 第2回 金融データ活用チャレンジ ベースラインサマリー
第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は製造業等、同じ意味の数字がいくつかあるため、一部数字は変換を行う
... SectorとFranchiseCode: カテゴリカル変数へ変換
- 今回の借り入れに関する変数(RevLineCr, LowDoc)
... 公式ページには値の候補が2つ(YesとNoのYN)と記載があるが、実際の値の種類は2より多い。YN以外はNaNへ置換
- 日付系の変数(DisbursementDate, ApprovalDate)
... 日付型へ変更 → 年を抽出(借りた月や日にはあまり意味はないと思われるため)
- 金額系の変数(DisbursementGross, GrAppv, SBA_Appv)
... 数値型へ変更
その他特徴量エンジニアリング
適当に思いついたものを記載しています。背景にありそうな仮定もカッコで記載しています。
-
金額の割合を見てみる
- (SBAが保証する金額に対して借りる金額が小さければリスクは低そう)
-
借り手と貸し手が同じ州か見てみる
- (違う州まで借りに行ってるのは財政が厳しい可能性?)
-
SBAの承認年と借りた年の差を見てみる
- (ここの承認年が長い企業は設立年数が長く、リスクが低いかも)
-
NaNの列数のカウント
- 一般的に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
というパラメーターを指定することでカテゴリカル変数を扱うことができます。
- LightGBMのハイパラは以下(自分の書いた記事を自分で参考にした)
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に書いていないので、おまけとして。
- 特徴量エンジニアリング
- 自分が昔参加したコンペだと、HomeCreditやIEEEなどが参考になりそうです。
- 利率なんかはHomeCreditコンペでも聞いていた記憶があります。
- 次元削減なんかもありかもしれません。
- カテゴリカル変数はTarget Encodingしてもよいかもです。
- cutoffの変更
- やり方が全く分からなかった。。全通りだけでなく、Youden indexなどを使う方法もあるらしいです。
- 学習アルゴリズムの変更
- データの重複っぽいものを上手く扱う
-
df_train.query("ApprovalDate == '22-Sep-06' and State == 'AZ'")
とすると、ほぼ重複してそうなデータ(生成してる感あるデータ)が見えたりします。このあたりを上手く扱うのもよいかも。
-
Discussion