はじめに
会社でのデータサイエンスに関する研修の一環として、上記コンペティションに取り組みました。
上記のコンペティションは既に終了していますが、今回、Late Submissionとして投稿を行いました。
コンペティションの概要
リンク:https://www.kaggle.com/competitions/home-credit-default-risk/overview
| 項目 | 内容 |
|---|---|
| テーマ | 電話会社や小売業者などの代替データを利用し、銀行口座を持たない人々の「返済能力」を予測する |
| ビジネス上の意義 | 信用履歴が不十分なために融資を受けられない層に対し、適切な審査を行うことで金融包摂(ファイナンシャル・インクルージョン)を促進する |
| 予測対象 | 申請者がローンを返済できるか否かの二値分類(Target: 0 = 返済成功、1 = 困難) |
| (参考)コンペ本番期間 | 2018年5月〜2018年8月 |
-
提供されるデータ
本コンペでは、メインの申請情報のほかに、過去の履歴データを含む計7つのテーブルが提供されています。ファイル名 日本語名 概要(データの性質) 含まれる項目のサンプル application_train/test 主申請データ 本ローンの申請情報。静的な属性データ。 収入、家族構成、住居状況、EXT_SOURCE (外部スコア) bureau 他社借入履歴 信用情報機関から取得した、他社での過去のローン履歴。 他社借入額、返済状況、クレジットの種類、延滞回数 bureau_balance 他社借入残高推移 bureauデータの月次残高・ステータス履歴。月ごとの返済ステータス(延滞なし/1ヶ月延滞など) previous_application 自社過去申請履歴 Home Credit社での過去のローン申請情報。 過去の融資承認/拒否結果、希望額、金利タイプ POS_CASH_balance POS/現金ローン残高 過去の自社ローンの月次残高履歴(分割払い)。 残り支払い回数、現在の割賦状況、延滞フラグ installments_payments 分割払いの支払実績 過去の自社ローンにおける「実際の支払い」記録。 予定日 vs 実際の支払日、支払額の過不足 credit_card_balance クレジットカード残高 過去の自社クレジットカードの月次利用履歴。 限度額、利用額、キャッシング回数、最小支払額
実施前に考えていたこと
- AutoMLの活用
もともとAutoMLツールとしてpycaretをよく使用していました。ただし上記の制約として、依存関係が厳しく、もともとの環境での競合が起こりやすいことや、新しいバージョンのpythonやnumpyへの対応が比較的遅いこと、総じて、近年の開発スピードが遅い(と感じている)ことから、そろそろ別のツールを利用してみたいと思っていました。近年、上記に代わるツールとしてAutoGluonを耳にすることが多くなり、以前から活用したいと思っていたので、今回のAutoGluon(v1.5.0)を活用することとしていました。
(pycaretはcontributorとして、一部、githubでのcommitも実施していましたので、愛着のあるツールではあるので少々寂しいですが…)
- 生成AIの活用
AutoMLを活用する前提とはいえ、特徴量の生成においては、比較的自力で行う必要があると感じます。過去のコンペティションでは自力で仮説立案していましたが、今回、geminiを活用して、その立案やコードの生成をサポートしてもらいました。ちなみに、過去の入賞者の解法は閲覧しない旨をプロンプトに含めています。
これらを踏まえた上で、まあ真ん中より上のスコアが取れればよいかな?と考えていました。
AutoGluonとは
AutoGluonはAWSの研究者チームが中心となって開発しているオープンソース(Apache 2.0ライセンス)のAutoML(Automated Machine Learning)ライブラリです。表形式データ・画像・テキスト・時系列など幅広いデータに対応していますが、本記事では表形式データの予測を担う AutoGluon Tabular を使用しています。
AutoGluon Tabularの最大の特徴は、fit() の1回の呼び出しだけで以下のプロセスを自動的に実行する点です。
| プロセス | 内容 |
|---|---|
| 前処理 | 欠損値補完・カテゴリ変数のエンコーディング・データ型推論を自動適用 |
| 複数モデルの並列学習 | LightGBM・CatBoost・XGBoost・RandomForest・ニューラルネットワーク等を同時に学習 |
| ハイパーパラメータの選択 | 事前定義された候補セットから多様な設定のモデルを構築 |
| バギング(k-fold CV) | 各モデルをk-fold交差検証で学習し、OOF予測確率を算出 |
| アンサンブル | OOF予測をもとに各モデルへの最適な重みを決定し、WeightedEnsembleを構築 |
単一モデルを選んでハイパーパラメータを探索する従来のAutoMLとは異なり、多様なモデルをk-fold Baggingで学習して多層スタックアンサンブルを構築する設計が精度面での強みです。論文(AutoGluon-Tabular: Robust and Accurate AutoML for Structured Data)では、生データのみで4時間学習した結果、Kaggleの人気コンペで参加者の上位1%に相当する精度を記録したことが報告されています。
詳細は公式ドキュメントを参照してください。
最終スコア
| 評価環境 | スコア (AUC) |
|---|---|
| Private Score | 0.79342 |
| Public Score | 0.79936 |
| (参考)Local CV | 0.79596 |
コンペティションに参加していた場合、上記のスコアは915位(7,176人中)に該当します。ですので、まあ一定の目標は達成できたのかな、という感じです。
ちなみに、あと約0.002スコアを挙げていればbronzeラインに到達できたので、もう少し頑張ればよかったかなとも感じます。
実施内容
取り組みの流れは以下の通りです。
- 特徴量エンジニアリング — 複数のサブテーブルからドメイン知識に基づく特徴量を生成
- 前処理 — カラム名のクレンジングとメモリ削減
- AutoGluonによる学習 — アンサンブルモデルの構築
- 学習結果の確認 — 各モデルのスコアやアンサンブルウェイトの比較
- 予測・submission生成 — テストデータへの推論
- (参考)特徴量重要度 — アンサンブル全体に対するPermutation Importance
- (参考)アンサンブル構成モデルのパラメータ — 各モデルのハイパーパラメータ
1.特徴量エンジニアリング
まず分析の前提として、どのような特徴量を作成したかを整理します。
データ読み込み
Home Creditのコンペデータは申込テーブル(application_train/test)を中心に、7つのサブテーブルが提供されていますので、pandasに読み込みます。
| テーブル | 内容 |
|---|---|
application_train/test |
メイン申込情報(収入・職業・家族構成など) |
bureau |
他金融機関への過去ローン履歴 |
bureau_balance |
bureau の月次残高・返済状況 |
previous_application |
過去の申込履歴(承認・拒否・種別) |
installments_payments |
分割払いの実績(予定日・実支払日・金額) |
credit_card_balance |
クレジットカードの月次残高履歴 |
POS_CASH_balance |
POS/現金ローンの月次残高履歴 |
INPUT_DIR = './input/competitions/home-credit-default-risk/'
train = pd.read_csv(f'{INPUT_DIR}application_train.csv')
test = pd.read_csv(f'{INPUT_DIR}application_test.csv')
bureau = pd.read_csv(f'{INPUT_DIR}bureau.csv')
bb = pd.read_csv(f'{INPUT_DIR}bureau_balance.csv')
cc = pd.read_csv(f'{INPUT_DIR}credit_card_balance.csv')
inst = pd.read_csv(f'{INPUT_DIR}installments_payments.csv')
prev = pd.read_csv(f'{INPUT_DIR}previous_application.csv')
pos = pd.read_csv(f'{INPUT_DIR}POS_CASH_balance.csv')
集計方法の設計
サブテーブルは顧客1人に対して複数行存在するため、SK_ID_CURR 単位で集約する必要があります。全数値カラムに対して mean / max / min / sum / std / var の6統計量を一括生成する共通関数を用意しました。
また、集計前に各行の欠損値数を {PREFIX}_ROW_MISSING_COUNT として追加しています。これは「その顧客の履歴レコードの中でどれだけ情報が欠けているか」を示す特徴量で、欠損の多い顧客は信用情報が薄く、デフォルトリスクと相関する可能性があります。この欠損数自体も6統計量で集約され、例えば INST_ROW_MISSING_COUNT_MEAN(分割払いレコードの平均欠損数)として最終的な特徴量になります。
def aggregate_all_numerics(df, group_var, prefix):
df[f'{prefix}_ROW_MISSING_COUNT'] = df.isnull().sum(axis=1)
cols = [c for c in df.columns if df[c].dtype in ['int64', 'float64']
and c not in [group_var, 'SK_ID_PREV', 'SK_ID_BUREAU']]
agg_funcs = ['mean', 'max', 'min', 'sum', 'std', 'var']
agg_dict = {c: agg_funcs for c in cols}
agg_df = df.groupby(group_var).agg(agg_dict)
agg_df.columns = [f"{prefix}_{c.upper()}_{f.upper()}" for c, f in agg_df.columns.values]
return agg_df
時間減衰(Time Decay)集計
また、分割払いの支払実績(installments_payments)は、直近ほど現在の信用力を反映するという考えから、時間減衰(Time Decay)による加重平均も追加しています。365日を基準とした指数減衰で、過去の記録ほど重みを小さくします。
def aggregate_weighted_numerics(df, group_var, prefix, time_col):
df = df.copy()
# 直近を1とし、過去に行くほど指数関数的に減少(365日で減衰)
df['TIME_WEIGHT'] = np.exp(df[time_col] / 365)
cols = [c for c in df.columns if df[c].dtype in ['int64', 'float64']
and c not in [group_var, 'SK_ID_PREV', 'SK_ID_BUREAU', 'TIME_WEIGHT']]
for col in cols:
df[f'{col}_WEIGHTED'] = df[col] * df['TIME_WEIGHT']
weighted_cols = [f'{c}_WEIGHTED' for c in cols]
agg_df = df.groupby(group_var)[weighted_cols].mean()
agg_df.columns = [f"{prefix}_W_{c.replace('_WEIGHTED', '').upper()}_MEAN"
for c in agg_df.columns]
return agg_df
なぜ「指数減少」なのか?
aggregate_weighted_numericsでは、時間の経過に対して指数関数的な減衰(Time Decay)を導入しています。これは、指数関数は、現在の価値の $x$% が常に失われていくという変化を表します。これにより、データがどれほど古くなっても重みがマイナスにならない(0に漸近する)、かつ単なる期間平均では埋もれてしまう「最近の行動の変化」を強調できると考えて採用しています。
サブテーブルごとの特徴量
各サブテーブルに対して、統計量集計に加えてドメイン知識に基づく特徴量を個別に追加しています。統計量集計(mean / max / min / sum / std / var)で生成される特徴量は {PREFIX}_{元カラム名}_{統計量} の命名規則に従います。
bureau_balance
返済状況(STATUS)の文字列を数値スコアに変換してから集計しています。
| 特徴量名 | 内容 |
|---|---|
BB_STATUS_SCORE_MEAN |
返済ステータススコアの平均(C/X=0、1〜5は延滞月数) |
BB_STATUS_SCORE_MAX |
返済ステータススコアの最大値(最悪の延滞状況) |
BB_STATUS_SCORE_MIN |
返済ステータススコアの最小値 |
BB_STATUS_SCORE_SUM |
返済ステータススコアの合計 |
BB_STATUS_SCORE_STD |
返済ステータススコアの標準偏差(ばらつき) |
BB_STATUS_SCORE_VAR |
返済ステータススコアの分散 |
BB_ROW_MISSING_COUNT_* |
各行の欠損値数の統計量 |
bureau(bureau_balance 集計を結合後)
他金融機関ローン履歴全体と、ローン種別(コンシューマー・クレジットカード)ごとの別集計を作成しています。
| 特徴量名 | 内容 |
|---|---|
BUREAU_{数値カラム名}_{統計量} |
全数値カラムの mean/max/min/sum/std/var |
BUREAU_CREDIT_UTILIZATION_MEAN/MAX/... |
与信枠活用率(残高 ÷ 限度額)の統計量 |
B_TYPE_CONSUMER_CREDIT_{カラム名}_{統計量} |
コンシューマーローンのみの統計量 |
B_TYPE_CREDIT_CARD_{カラム名}_{統計量} |
クレジットカードのみの統計量 |
BUREAU_ROW_MISSING_COUNT_* |
各行の欠損値数の統計量 |
previous_application
過去申込の数値集計に加えて、カテゴリ変数3種をダミー変数化して申込件数を集計しています。
| 特徴量名 | 内容 |
|---|---|
PREV_{数値カラム名}_{統計量} |
全数値カラムの mean/max/min/sum/std/var |
PREV_NAME_CONTRACT_STATUS_Approved |
承認された申込件数 |
PREV_NAME_CONTRACT_STATUS_Refused |
拒否された申込件数 |
PREV_NAME_CONTRACT_STATUS_Canceled |
キャンセルされた申込件数 |
PREV_NAME_CONTRACT_STATUS_Unused_offer |
未使用オファーの申込件数 |
PREV_NAME_CONTRACT_TYPE_Cash_loans |
現金ローンの申込件数 |
PREV_NAME_CONTRACT_TYPE_Consumer_loans |
コンシューマーローンの申込件数 |
PREV_NAME_CONTRACT_TYPE_Revolving_loans |
リボルビングローンの申込件数 |
PREV_CODE_REJECT_REASON_{理由} |
拒否理由別の申込件数(HC/LIMIT/SCO/VERIF/XNA 等) |
LAST_LOAN_IS_REFUSED |
直近の申込が拒否だったか(0/1フラグ) |
PREV_ROW_MISSING_COUNT_* |
各行の欠損値数の統計量 |
installments_payments
集計前に3つの派生特徴量を追加したうえで、通常集計・Time Decay集計・直近1年集計の3種を作成しています。
- DPD(Days Past Due): 実際の支払日が予定日より何日遅れたかを示す延滞日数です。マイナス(早払い)はゼロに丸めます。
- DBD(Days Before Due): 予定日より何日早く支払ったかを示す早期返済日数です。延滞した場合はゼロに丸めます。
- PAYMENT_RATIO: 実際の支払額が予定支払額の何倍かを示す比率です。1未満は過少支払い、1超は過剰支払いを意味します。
inst['DPD'] = (inst['DAYS_ENTRY_PAYMENT'] - inst['DAYS_INSTALMENT']).clip(lower=0)
inst['DBD'] = (inst['DAYS_INSTALMENT'] - inst['DAYS_ENTRY_PAYMENT']).clip(lower=0)
inst['PAYMENT_RATIO'] = inst['AMT_PAYMENT'] / (inst['AMT_INSTALMENT'] + 1e-5)
| 特徴量名 | 集計種別 | 内容 |
|---|---|---|
INST_{数値カラム名}_{統計量} |
通常集計 | 全数値カラムの mean/max/min/sum/std/var |
INST_DPD_{統計量} |
通常集計 | 延滞日数(支払日 − 予定日、0以上)の統計量 |
INST_DBD_{統計量} |
通常集計 | 早期返済日数(予定日 − 支払日、0以上)の統計量 |
INST_PAYMENT_RATIO_{統計量} |
通常集計 | 実支払額 ÷ 予定支払額の統計量 |
INST_W_{数値カラム名}_MEAN |
Time Decay集計 |
DAYS_INSTALMENT を時間軸とした指数減衰加重平均(直近ほど重みが大きい) |
INST_RECENT_{数値カラム名}_{統計量} |
直近1年集計 |
DAYS_INSTALMENT > -365 の行のみを対象とした通常集計 |
INST_ROW_MISSING_COUNT_{統計量} |
通常集計 | 各行の欠損値数の統計量 |
credit_card_balance
| 特徴量名 | 内容 |
|---|---|
CC_{数値カラム名}_{統計量} |
全数値カラムの mean/max/min/sum/std/var |
CC_ROW_MISSING_COUNT_* |
各行の欠損値数の統計量 |
POS_CASH_balance
| 特徴量名 | 内容 |
|---|---|
POS_IS_DPD_MEAN/SUM/... |
延滞フラグ(SK_DPD > 0)の統計量 |
POS_COMPLETED_RATIO_MEAN/MAX/... |
残回数進捗率(残回数 ÷ 総回数)の統計量 |
POS_{数値カラム名}_{統計量} |
全数値カラムの mean/max/min/sum/std/var |
POS_ROW_MISSING_COUNT_* |
各行の欠損値数の統計量 |
メインテーブルの派生特徴量
申込テーブル自体のカラムからも、ドメイン知識に基づく派生特徴量を作成しています。大きく4グループに分けられます。
(1)外部スコア(EXT_SOURCE)の多層分析
EDAやベースラインモデルの作成から、予測には、外部スコア(EXT_SOURCE)の影響が高い
捉えていました。そのため、3つの外部信用スコアをそのまま使うだけでなく、スコア間の一致度・乖離・組み合わせを特徴量化しています。複数の信用機関のスコアが乖離している場合、何らかのリスク要因が潜んでいる可能性を捉えることが狙いです。
| 特徴量名 | 内容 |
|---|---|
EXT_SOURCES_MEAN |
EXT_SOURCE_1〜3 の平均 |
EXT_SOURCES_SUM |
EXT_SOURCE_1〜3 の合計 |
EXT_SOURCES_STD |
EXT_SOURCE_1〜3 の標準偏差(スコアのばらつき) |
EXT_SOURCES_PROD |
EXT_SOURCE_1 × 2 × 3 の積(全て高い場合のみ大きくなる) |
EXT_SOURCES_VAR |
EXT_SOURCE_1〜3 の分散 |
EXT_DIFF_1_2 |
|EXT_SOURCE_1 − EXT_SOURCE_2|(スコア1と2の乖離) |
EXT_DIFF_2_3 |
|EXT_SOURCE_2 − EXT_SOURCE_3|(スコア2と3の乖離) |
EXT_DIFF_1_3 |
|EXT_SOURCE_1 − EXT_SOURCE_3|(スコア1と3の乖離) |
EXT_RATIO_1_2 |
EXT_SOURCE_1 ÷ EXT_SOURCE_2(スコア1と2の比率) |
(2)家計の余力・支払い能力
ローン返済能力を収入・支出・家族構成の観点から多角的に数値化しています。
| 特徴量名 | 内容 |
|---|---|
LEFT_CASH |
AMT_INCOME_TOTAL − AMT_ANNUITY(返済後の手取り) |
LEFT_CASH_PER_PERSON |
LEFT_CASH ÷ CNT_FAM_MEMBERS(一人当たり余裕) |
ANNUITY_INCOME_PERC |
AMT_ANNUITY ÷ AMT_INCOME_TOTAL(収入に占める返済割合) |
PAYMENT_RATE |
AMT_ANNUITY ÷ AMT_CREDIT(返済額 ÷ ローン総額) |
CREDIT_TO_INCOME |
AMT_CREDIT ÷ AMT_INCOME_TOTAL(返済負担率) |
INCOME_PER_PERSON |
AMT_INCOME_TOTAL ÷ CNT_FAM_MEMBERS(一人当たり収入) |
(3)期間・安定性
雇用期間や各種登録日の関係から、生活の安定性を表す特徴量を作成しています。
| 特徴量名 | 内容 |
|---|---|
EXPECTED_LOAN_TERM |
AMT_CREDIT ÷ AMT_ANNUITY(予想返済期間) |
DAYS_EMPLOYED_REG_RATIO |
DAYS_EMPLOYED ÷ DAYS_REGISTRATION(雇用期間 ÷ 住所登録期間) |
EMPLOYED_TO_BIRTH_RATIO |
DAYS_EMPLOYED ÷ DAYS_BIRTH(人生に占める就業割合) |
ID_REG_DIFF |
DAYS_ID_PUBLISH − DAYS_REGISTRATION(身分証発行日と住所登録日の差) |
REGION_RATING_PROD |
REGION_RATING_CLIENT × REGION_RATING_CLIENT_W_CITY(地域評価スコアの積) |
(4)セグメント別相対評価・支払いトレンド
同じ職種・学歴グループ内での収入の相対的な位置づけを特徴量化することで、絶対値では見えないリスクを捉えます。また、直近1年の支払い状況と全期間平均を比較したトレンド特徴量も追加しています。
| 特徴量名 | 内容 |
|---|---|
INCOME_REL_OCCUPATION_TYPE |
AMT_INCOME_TOTAL ÷ 同職種の平均収入(職種内での相対収入) |
INCOME_REL_NAME_EDUCATION_TYPE |
AMT_INCOME_TOTAL ÷ 同学歴の平均収入(学歴内での相対収入) |
PAYMENT_TREND |
INST_RECENT_AMT_PAYMENT_MEAN ÷ INST_AMT_PAYMENT_MEAN(直近1年 vs 全期間の支払い額比) |
DPD_TREND |
INST_RECENT_DPD_MEAN − INST_DPD_MEAN(直近1年 vs 全期間の延滞日数差、正なら悪化) |
コード全量
import pandas as pd
import numpy as np
# --- 0. 共通関数:数値カラム集計 ---
def aggregate_all_numerics(df, group_var, prefix):
df[f'{prefix}_ROW_MISSING_COUNT'] = df.isnull().sum(axis=1)
cols = [c for c in df.columns if df[c].dtype in ['int64', 'float64'] and c not in [group_var, 'SK_ID_PREV', 'SK_ID_BUREAU']]
agg_funcs = ['mean', 'max', 'min', 'sum', 'std', 'var']
agg_dict = {c: agg_funcs for c in cols}
agg_df = df.groupby(group_var).agg(agg_dict)
agg_df.columns = [f"{prefix}_{c.upper()}_{f.upper()}" for c, f in agg_df.columns.values]
return agg_df
# --- 0.1 時間の重み付け集計関数 (Time Decay) ---
def aggregate_weighted_numerics(df, group_var, prefix, time_col):
df = df.copy()
# 直近を1とし、過去に行くほど指数関数的に減少(365日で減衰)
df['TIME_WEIGHT'] = np.exp(df[time_col] / 365)
cols = [c for c in df.columns if df[c].dtype in ['int64', 'float64'] and c not in [group_var, 'SK_ID_PREV', 'SK_ID_BUREAU', 'TIME_WEIGHT']]
for col in cols:
df[f'{col}_WEIGHTED'] = df[col] * df['TIME_WEIGHT']
weighted_cols = [f'{c}_WEIGHTED' for c in cols]
agg_df = df.groupby(group_var)[weighted_cols].mean()
agg_df.columns = [f"{prefix}_W_{c.replace('_WEIGHTED', '').upper()}_MEAN" for c in agg_df.columns]
return agg_df
# --- 1. Bureau & Bureau Balance (与信枠活用率 + 種別集計) ---
status_map = {'C': 0, 'X': 0, '0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5}
bb['STATUS_SCORE'] = bb['STATUS'].map(status_map)
bb_agg = aggregate_all_numerics(bb, 'SK_ID_BUREAU', 'BB')
bureau_full = bureau.merge(bb_agg, on='SK_ID_BUREAU', how='left')
bureau_full['CREDIT_UTILIZATION'] = bureau_full['AMT_CREDIT_SUM'] / (bureau_full['AMT_CREDIT_SUM_LIMIT'] + 1e-5)
b_agg = aggregate_all_numerics(bureau_full, 'SK_ID_CURR', 'BUREAU')
for loan_type in ['Consumer credit', 'Credit card']:
type_df = bureau_full[bureau_full['CREDIT_TYPE'] == loan_type]
if not type_df.empty:
b_agg = b_agg.join(aggregate_all_numerics(type_df, 'SK_ID_CURR', f'B_TYPE_{loan_type.upper().replace(" ", "_")}'), how='left')
# --- 2. Previous Application (拒否理由・フラグ・統計量) ---
p_agg = aggregate_all_numerics(prev, 'SK_ID_CURR', 'PREV')
for col in ['NAME_CONTRACT_STATUS', 'NAME_CONTRACT_TYPE', 'CODE_REJECT_REASON']:
dummies = pd.get_dummies(prev[['SK_ID_CURR', col]], columns=[col])
p_agg = p_agg.join(dummies.groupby('SK_ID_CURR').sum().add_prefix('PREV_'))
last_prev = prev.sort_values('DAYS_DECISION').groupby('SK_ID_CURR').last()
last_prev['LAST_LOAN_IS_REFUSED'] = (last_prev['NAME_CONTRACT_STATUS'] == 'Refused').astype(int)
p_agg = p_agg.join(last_prev[['LAST_LOAN_IS_REFUSED']])
# --- 3. Installments Payments (DPD/DBD/比率 + Time Decay) ---
inst['DPD'] = (inst['DAYS_ENTRY_PAYMENT'] - inst['DAYS_INSTALMENT']).clip(lower=0)
inst['DBD'] = (inst['DAYS_INSTALMENT'] - inst['DAYS_ENTRY_PAYMENT']).clip(lower=0)
inst['PAYMENT_RATIO'] = inst['AMT_PAYMENT'] / (inst['AMT_INSTALMENT'] + 1e-5)
i_agg = aggregate_all_numerics(inst, 'SK_ID_CURR', 'INST')
i_agg = i_agg.join(aggregate_weighted_numerics(inst, 'SK_ID_CURR', 'INST', 'DAYS_INSTALMENT'), how='left')
inst_recent = inst[inst['DAYS_INSTALMENT'] > -365]
if not inst_recent.empty:
i_agg = i_agg.join(aggregate_all_numerics(inst_recent, 'SK_ID_CURR', 'INST_RECENT'), how='left')
# --- 4. Credit Card & POS CASH (進捗率・延滞フラグ) ---
c_agg = aggregate_all_numerics(cc, 'SK_ID_CURR', 'CC')
pos['IS_DPD'] = (pos['SK_DPD'] > 0).astype(int)
pos['COMPLETED_RATIO'] = pos['CNT_INSTALMENT_FUTURE'] / (pos['CNT_INSTALMENT'] + 1e-5)
pos_agg = aggregate_all_numerics(pos, 'SK_ID_CURR', 'POS')
# --- 5. メインテーブルへの結合 ---
for df_agg in [b_agg, p_agg, i_agg, c_agg, pos_agg]:
train = train.merge(df_agg, on='SK_ID_CURR', how='left')
test = test.merge(df_agg, on='SK_ID_CURR', how='left')
# --- 6. ドメイン知識 + 外部スコア多層分析 + 全期間トレンド ---
for df in [train, test]:
# 欠損値と外部スコアの「不一致・乖離」
df['TOTAL_MISSING_COUNT'] = df.isnull().sum(axis=1)
ext_srcs = ['EXT_SOURCE_1', 'EXT_SOURCE_2', 'EXT_SOURCE_3']
df['EXT_SOURCES_MEAN'] = df[ext_srcs].mean(axis=1)
df['EXT_SOURCES_SUM'] = df[ext_srcs].sum(axis=1)
df['EXT_SOURCES_STD'] = df[ext_srcs].std(axis=1)
df['EXT_SOURCES_PROD'] = df['EXT_SOURCE_1'] * df['EXT_SOURCE_2'] * df['EXT_SOURCE_3']
df['EXT_SOURCES_VAR'] = df[ext_srcs].var(axis=1)
df['EXT_DIFF_1_2'] = (df['EXT_SOURCE_1'] - df['EXT_SOURCE_2']).abs()
df['EXT_DIFF_2_3'] = (df['EXT_SOURCE_2'] - df['EXT_SOURCE_3']).abs()
df['EXT_DIFF_1_3'] = (df['EXT_SOURCE_1'] - df['EXT_SOURCE_3']).abs()
df['EXT_RATIO_1_2'] = df['EXT_SOURCE_1'] / (df['EXT_SOURCE_2'] + 1e-5)
# 家計の余力・支払い能力
df['LEFT_CASH'] = df['AMT_INCOME_TOTAL'] - df['AMT_ANNUITY']
df['LEFT_CASH_PER_PERSON'] = df['LEFT_CASH'] / (df['CNT_FAM_MEMBERS'] + 1e-5)
df['ANNUITY_INCOME_PERC'] = df['AMT_ANNUITY'] / (df['AMT_INCOME_TOTAL'] + 1e-5)
df['PAYMENT_RATE'] = df['AMT_ANNUITY'] / (df['AMT_CREDIT'] + 1e-5)
df['CREDIT_TO_INCOME'] = df['AMT_CREDIT'] / (df['AMT_INCOME_TOTAL'] + 1e-5)
df['INCOME_PER_PERSON'] = df['AMT_INCOME_TOTAL'] / (df['CNT_FAM_MEMBERS'] + 1e-5)
# 期間・安定性・トレンド
df['EXPECTED_LOAN_TERM'] = df['AMT_CREDIT'] / (df['AMT_ANNUITY'] + 1e-5)
df['DAYS_EMPLOYED_REG_RATIO'] = df['DAYS_EMPLOYED'] / (df['DAYS_REGISTRATION'] + 1e-5)
df['EMPLOYED_TO_BIRTH_RATIO'] = df['DAYS_EMPLOYED'] / (df['DAYS_BIRTH'] + 1e-5)
df['ID_REG_DIFF'] = df['DAYS_ID_PUBLISH'] - df['DAYS_REGISTRATION']
df['REGION_RATING_PROD'] = df['REGION_RATING_CLIENT'] * df['REGION_RATING_CLIENT_W_CITY']
# セグメント別相対評価
for g in ['OCCUPATION_TYPE', 'NAME_EDUCATION_TYPE']:
df[f'INCOME_REL_{g}'] = df['AMT_INCOME_TOTAL'] / (df.groupby(g)['AMT_INCOME_TOTAL'].transform('mean') + 1e-5)
if 'INST_RECENT_AMT_PAYMENT_MEAN' in df.columns:
df['PAYMENT_TREND'] = df['INST_RECENT_AMT_PAYMENT_MEAN'] / (df['INST_AMT_PAYMENT_MEAN'] + 1e-5)
df['DPD_TREND'] = df['INST_RECENT_DPD_MEAN'] - df['INST_DPD_MEAN']
2.前処理
カラム名のクレンジング
特徴量結合後のDataFrameには、カテゴリ変数のダミー展開や集計によって特殊文字(<, >, {, } など)を含むカラム名が生成される恐れがあります。LightGBMを含む多くのライブラリはこれらの文字を受け付けないため、アンダースコアに統一するクレンジング関数を適用しています。
import re
def clean_column_names(df):
# 特殊文字(<, >, ,, :, ", {, })をアンダースコアに置換
df.columns = [re.sub(r'[<>,.:\"{}]', '_', col) for col in df.columns]
# 空白をアンダースコアに置換
df.columns = [col.replace(' ', '_') for col in df.columns]
# 重複したアンダースコアを1つにまとめる
df.columns = [re.sub(r'__+', '_', col) for col in df.columns]
return df
train = clean_column_names(train)
test = clean_column_names(test)
メモリ削減
サブテーブルの結合後は特徴量数が1,000列超になり、デフォルトの int64 / float64 のままではメモリを大量に消費します。各カラムの値域を確認して最小精度の型にダウンキャストすることでメモリ使用量を削減しています。
import numpy as np
import gc
def reduce_mem_usage(df, verbose=True):
numerics = ['int16', 'int32', 'int64', 'float16', 'float32', 'float64']
start_mem = df.memory_usage().sum() / 1024**2
for col in df.columns:
col_type = df[col].dtypes
if col_type in numerics:
c_min = df[col].min()
c_max = df[col].max()
if str(col_type)[:3] == 'int':
if c_min > np.iinfo(np.int8).min and c_max < np.iinfo(np.int8).max:
df[col] = df[col].astype(np.int8)
elif c_min > np.iinfo(np.int16).min and c_max < np.iinfo(np.int16).max:
df[col] = df[col].astype(np.int16)
elif c_min > np.iinfo(np.int32).min and c_max < np.iinfo(np.int32).max:
df[col] = df[col].astype(np.int32)
else:
if c_min > np.finfo(np.float16).min and c_max < np.finfo(np.float16).max:
df[col] = df[col].astype(np.float16)
elif c_min > np.finfo(np.float32).min and c_max < np.finfo(np.float32).max:
df[col] = df[col].astype(np.float32)
end_mem = df.memory_usage().sum() / 1024**2
if verbose:
print(f'Mem. usage decreased to {end_mem:5.2f} Mb '
f'({100 * (start_mem - end_mem) / start_mem:.1f}% reduction)')
return df
train = reduce_mem_usage(train)
test = reduce_mem_usage(test)
# 不要になった中間テーブルをメモリから解放
del bb, bb_agg, bureau, bureau_full, b_agg
del prev, p_agg, last_prev
del inst, i_agg, inst_recent
del cc, c_agg
del pos, pos_agg
gc.collect()
3.AutoGluonによる学習
AutoGluonのTablarPredictorでモデルを構築します。
コード
from autogluon.tabular import TabularDataset, TabularPredictor
train_data = TabularDataset(train)
predictor = TabularPredictor(
label='TARGET',
eval_metric='roc_auc'
).fit(
train_data,
presets='high_quality_v150',
num_stack_levels=0,
time_limit=3600 * 6,
excluded_model_types=['NN_TORCH', 'FASTAI', 'TabPFN'],
ag_args_ensemble={'fold_fitting_strategy': 'sequential_local'},
ag_args_fit={'num_gpus': 1}
)
パラメータと意図
| パラメータ | 値 | 意図 |
|---|---|---|
eval_metric |
roc_auc |
本コンペの精度指標であるAUCで最適化 |
presets |
high_quality_v150 |
精度重視のプリセット(後述) |
num_stack_levels |
0 |
スタッキングなし(学習時間の節約・後述) |
time_limit |
3600 * 6 |
最大6時間の時間(学習時間の節約) |
excluded_model_types |
NN_TORCH, FASTAI, TabPFN
|
GPU使用量・安定性の観点で除外 |
fold_fitting_strategy |
sequential_local |
foldを逐次学習(並列より安定) |
num_gpus |
1 |
GPU(RTX 3090)を使用 |
high_quality プリセットの内部設定
high_quality プリセットを指定すると、AutoGluonは内部で以下の設定を自動的に有効にします。
# high_quality プリセットの実態(presets_configs.py より)
high_quality = {
'auto_stack': True, # バギングを自動有効化
'refit_full': True, # バギング後に全データで再学習
'set_best_to_refit_full': True, # ベストモデルをFULLモデルに差し替え
'_save_bag_folds': False, # foldモデルは推論後にディスクから破棄
}
auto_stack=True により、データ量に応じて num_bag_folds が自動決定されます。今回のデータ(約30万行)では 8-fold が選択されており、モデル名の S1F1〜S1F8 がその8つのfoldに対応しています。
分割方法は Stratified KFold が採用されており、二値分類問題では各foldのTARGET=1の比率が元のデータ(約8%)と揃うよう層化抽出されます。これにより不均衡データでもfoldごとにクラス比率が安定し、OOFスコアの信頼性が高まります。(ドキュメントでは分割方法が明確でないですが、メソッドのデフォルトは "auto" となっており、ソースコードから二値分類・多クラス分類では StratifiedKFoldであることが確認できます。)
学習の全体フロー
AutoGluonの学習は フェーズ1(BAG学習) と フェーズ2(FULL再学習) の2段階で進みます。
【フェーズ1:8-fold BAG学習】
全データ(約30万行)を8分割
┌──────────────────────────────────────────────────────┐
│ S1F1: fold2〜8で学習 → fold1でOOF予測(検証) │
│ S1F2: fold1,3〜8で学習 → fold2でOOF予測 │
│ ... │
│ S1F8: fold1〜7で学習 → fold8でOOF予測 │
└──────────────────────────────────────────────────────┘
↓
全行のOOF予測確率が揃う
(各行は自分を含まないfoldモデルで予測 → 過学習のない公正な予測確率)
↓
OOFスコア(AUC)でモデルを評価・比較
【フェーズ2:FULL再学習】
各モデルを全データ(8/8)で1モデルとして再学習
→ LightGBM_r177_BAG_L1_FULL、CatBoost_c1_BAG_L1_FULL ... が生成される
※ num_boost_round等の終了条件はBAG時の各foldの平均値を使用
【フェーズ3:WeightedEnsemble構築】
OOF予測確率を使って各モデルへの最適な重みを決定
→ WeightedEnsemble_L2(実際の推論はFULLモデルの加重平均で行われる)
BAGを行ってからFULLを作る理由
一見すると「foldで学習してからまた全データで学習し直すのは二度手間」に見えますが、これには重要な理由が2つあると考えられます。
① early stoppingの終了条件を決めるため
LightGBMやCatBoostはearly stoppingで学習を止めますが、そのためには検証データが必要です。BAGの各foldの検証データを使って「何ラウンドで最適か(num_boost_round)」を各foldで算出し、FULL再学習ではその平均値を終了条件として使います。検証データなしに全データで学習しても、適切な終了条件が決められないと考えられます。
② OOF予測がアンサンブルの重み決定に不可欠
WeightedEnsemble_L2 の重みを決めるには、各モデルの「過学習していない予測確率」が必要です。BAGのOOF予測は各行がホールドアウトデータへの予測であるため、汎化性能を反映した公正な予測確率が得られます。これがなければ正しいアンサンブル重みの最適化ができないと考えられます。
BAG(_BAG_L1) |
FULL(_BAG_L1_FULL) |
|
|---|---|---|
| 学習データ | 7/8 のfold | 全データ(8/8) |
| 主な用途 | OOF予測・アンサンブル重み決定 | テストデータへの実際の推論 |
| バリデーションスコア | あり(OOFで計算) | なし(NaN) |
num_stack_levels=0 について
num_stack_levels=0 にするとスタッキングが無効になります。num_stack_levels=1 以上にするとL2モデルがL1モデルのOOF予測を特徴量として利用することでさらに精度が上がりますが、有効にするには num_bag_folds >= 2 の明示指定が必要です(未指定のままでは ValueError になります)。また学習時間がおよそ num_stack_levels+1 倍になるため、今回はtime_limitとのバランスを考慮して無効にしています。
num_stack_levels=0(今回):
L1モデル群(LightGBM, CatBoost...)→ WeightedEnsemble_L2
num_stack_levels=1(有効にした場合):
L1モデル群 → L2モデル群(L1のOOF予測を特徴量に追加)→ WeightedEnsemble_L3
4.学習結果の確認
学習後にleaderboardを確認して各モデルのOOFスコアを比較します。
lb = predictor.leaderboard(train_data, silent=False)
leaderboard() は各モデルのOOFスコア(score_val)・学習時間(fit_time)・推論時間(pred_time_val)等を一覧表示します。
実行結果
【フェーズ1:8-fold BAG学習】_BAG_L1 系(OOFスコアあり)
| モデル | score_val(OOF AUC) | fit_time(s) |
|---|---|---|
| WeightedEnsemble_L2 | 0.7959 | 14,465 |
| LightGBMPrep_r21_BAG_L1 | 0.7953 | 2,996 |
| LightGBM_r163_BAG_L1 | 0.7933 | 3,225 |
| LightGBM_r120_BAG_L1 | 0.7930 | 3,325 |
| LightGBM_r177_BAG_L1 | 0.7928 | 2,241 |
| LightGBM_r6_BAG_L1 | 0.7923 | 3,542 |
| LightGBM_r72_BAG_L1 | 0.7912 | 1,875 |
| CatBoost_c1_BAG_L1 | 0.7904 | 797 |
score_val はすべてOOFスコア(AUC)です。WeightedEnsemble_L2 のスコアがすべての単体モデルを上回っており、アンサンブル効果が確認できます。なお WeightedEnsemble_L2 の fit_time が約4時間と長いのは、アンサンブル重みの最適化だけでなく構成モデルの学習時間を合算して計上しているためです。
メモリ制約とtime_limitによりプリセットが想定する全ハイパーパラメータ候補を学習しきれておらず、一部のモデルのみが完成しています。
【フェーズ2:FULL再学習】_BAG_L1_FULL 系(全データ再学習済み・テストスコアあり)
| モデル | score_test | fit_time(s) |
|---|---|---|
| LightGBM_r163_BAG_L1_FULL | 0.9499 | 355 |
| LightGBM_r72_BAG_L1_FULL | 0.9471 | 205 |
| WeightedEnsemble_L2_FULL | 0.9220 | 1,721 |
| LightGBMPrep_r21_BAG_L1_FULL | 0.9208 | 332 |
| LightGBM_r177_BAG_L1_FULL | 0.9143 | 270 |
| LightGBM_r6_BAG_L1_FULL | 0.8448 | 516 |
| CatBoost_c1_BAG_L1_FULL | 0.8273 | 94 |
| LightGBM_r120_BAG_L1_FULL | 0.8267 | 461 |
score_test は leaderboard(train_data) に訓練データを渡した際に算出される訓練データへの適合スコアであり、汎化性能を示すOOFスコアとは異なります。_FULL モデルは全データで学習しているためこの値が高くなるのは当然で、過学習の指標ではありません。汎化性能の比較には必ず _BAG_L1 系の score_val を参照してください。
① LightGBM_r163 が単体トップ(OOF: 0.7933)
7種類のモデルのOOFスコアは0.7904〜0.7953と僅差ですが、LightGBMPrep_r21(前処理あり)が最上位でした。AutoGluonが探索した多様なハイパーパラメータの中でも、前処理の有無がスコアに影響していることが分かります。
② CatBoostが単体では最下位(OOF: 0.7904)
CatBoostはAUCを直接最適化せず eval_metric=Logloss で確率のキャリブレーションも同時に最適化するため、AUC単体では他のLightGBMより若干劣ることがあります。しかし他モデルとの予測の相関が低く、アンサンブルの多様性に貢献するためアンサンブルウェイトには相応の重みが付きます(詳細は後述のアンサンブル構成モデルの分析を参照)。
③ LightGBMPrep について
LightGBMPrep はAutoGluonが内部でカテゴリ変数にOne-hotエンコーディング等の追加前処理を施したうえで学習するLightGBMバリアントです。通常の LightGBM と並列して試すことで、前処理の有無による差異をアンサンブルの多様性に活かせます。
【フェーズ3:WeightedEnsemble構築】アンサンブルウェイト
WeightedEnsemble_L2 が各モデルに割り当てた重みは以下の通りです。
| モデル | Weight | OOF AUC |
|---|---|---|
| LightGBMPrep_r21_BAG_L1 | 0.529 | 0.7953 |
| LightGBM_r163_BAG_L1 | 0.176 | 0.7933 |
| CatBoost_c1_BAG_L1 | 0.118 | 0.7904 |
| LightGBM_r177_BAG_L1 | 0.059 | 0.7928 |
| LightGBM_r72_BAG_L1 | 0.059 | 0.7912 |
| LightGBM_r120_BAG_L1 | 0.059 | 0.7930 |
| LightGBM_r6_BAG_L1 | 0(採用なし) | 0.7923 |
LightGBMPrep_r21 がウェイト0.529と突出して高く、アンサンブルの中核を担っています。OOFスコアでも単体トップであったため、スコアとウェイトが一致した形です。
CatBoost_c1 はOOFスコアが最下位(0.7904)にもかかわらずウェイト0.118が付いています。他のLightGBM系モデルと予測の相関が低く、アンサンブルの多様性に貢献しているためと考えられます。
LightGBM_r6 はOOFスコアが0.7923と中位にもかかわらずウェイト0で採用されていません。AutoGluonのウェイト最適化は「このモデルを加えてもアンサンブル全体のOOFスコアが改善しない」と判断した場合にウェイト0を割り当てます。つまり LightGBM_r6 の予測は他モデルの線形結合で再現できてしまうため、追加の貢献がなかったと解釈できます。
5.予測・submission生成
テストデータに対して確率を予測し、submission用CSVを生成します。predict_proba() は各クラスの予測確率を返すため、デフォルト確率(列インデックス1)を TARGET として使用しています。
from datetime import datetime
y_pred_proba = predictor.predict_proba(test)
submission = pd.DataFrame({
'SK_ID_CURR': test['SK_ID_CURR'],
'TARGET': y_pred_proba.iloc[:, 1] # デフォルト(TARGET=1)の確率
})
now = datetime.now().strftime('%Y%m%d_%H%M')
file_name = f'submission_{now}.csv'
submission.to_csv(file_name, index=False)
print(f"✨ {file_name} を作成しました!")
6.(参考)特徴量重要度(Permutation Importance)
predictor.feature_importance() はアンサンブル全体に対するPermutation Importanceを算出します。各特徴量を1つずつランダムにシャッフルしてスコアの低下量を測定するため、予測への実際の貢献度を反映しています。
import matplotlib.pyplot as plt
import seaborn as sns
# アンサンブル全体のPermutation Importanceを算出
importance = predictor.feature_importance(train_data)
# 上位50件を可視化
top_50 = importance.head(50)
plt.figure(figsize=(10, 15))
sns.barplot(x='importance', y=top_50.index, data=top_50)
plt.title('Top 50 Feature Importance')
plt.xlabel('Importance Score')
plt.ylabel('Feature Name')
plt.show()
# 全特徴量をCSVに保存
importance.to_csv(f'feature_importance_{now}.csv')
print("✅ 全特徴量の重要度をCSVに保存しました。")
Permutation Importanceの特性と注意点
Permutation Importanceはモデルに依存しない汎用的な指標ですが、以下の点に注意が必要です。
| 注意点 | 内容 |
|---|---|
| 相関特徴量の過小評価 | 相関の高い特徴量が複数ある場合、片方をシャッフルしても残りで補完されるためスコアが下がりにくい |
| 計算コスト | シャッフル×評価を特徴量数分繰り返すため時間がかかる |
| 訓練データ vs テストデータ | 訓練データで算出すると過学習した特徴量を過大評価する可能性がある |
上位の特徴量
全1,049件の特徴量に対するPermutation Importanceの上位30件を以下に示します。importanceは最上位の ORGANIZATION_TYPE(0.01895)を1とした相対値で表示しています。
① ORGANIZATION_TYPE がトップ
2位の AMT_ANNUITY(0.455)の2倍以上という突出した重要度です。勤務先の業種がデフォルトリスクに強く影響することを示しています。
② メイン特徴量が依然として上位を占める
上位30件のうち17件がapplication_trainのオリジナル特徴量または派生特徴量です。丁寧な特徴量エンジニアリングを行いましたが、Permutation Importanceの観点ではサブテーブルの集計特徴量より申込者の基本属性情報の方が予測への寄与が大きい結果となりました。
③ サブテーブル由来で最上位は INST_W_DPD_MEAN(8位・相対0.153)
Time Decay集計で作成した支払い延滞日数の加重平均がトップ10入りしました。直近の支払い遅延ほど重みを大きくした集計設計が効いており、単純集計より信用リスクを鋭く捉えていることが示唆されます。
④ EXT_SOURCE 系の見かけ上の問題
EXT_SOURCE 系の特徴量は、Permutation Importanceで見ると数値が不均一に見えます。全件を整理すると以下の通りです。
| 特徴量 | 相対importance | p値 |
|---|---|---|
EXT_SOURCES_MEAN |
0.233 | 0.0046 |
EXT_SOURCE_3 |
0.089 | 0.0003 |
EXT_DIFF_2_3 |
0.076 | 0.0001 |
EXT_DIFF_1_2 |
0.065 | 0.0002 |
EXT_SOURCE_1 |
0.060 | 0.0037 |
EXT_RATIO_1_2 |
0.049 | 0.0003 |
EXT_SOURCES_VAR |
0.046 | 0.0018 |
EXT_SOURCES_STD |
0.043 | 0.0009 |
EXT_DIFF_1_3 |
0.028 | 0.0029 |
EXT_SOURCE_2 |
-0.016 | 0.843 |
EXT_SOURCES_SUM |
-0.023 | 0.871 |
EXT_SOURCES_PROD |
-0.027 | 0.911 |
注目すべきは下3件(EXT_SOURCE_2・EXT_SOURCES_SUM・EXT_SOURCES_PROD)のマイナス値と、EXT_SOURCE_1/3 が EXT_SOURCES_MEAN より大幅に低く見える点です。これらはPermutation Importanceの相関過小評価によって生じる見かけ上の問題の可能性があります。
EXT_SOURCE_1・EXT_SOURCE_2・EXT_SOURCE_3 の3変数は相互に相関しており、EXT_SOURCES_MEAN(平均)・EXT_SOURCES_SUM(合計)・EXT_SOURCES_PROD(積)もそれらから作られた派生特徴量です。Permutation Importanceはある特徴量をシャッフルしたときのスコア低下量を重要度とするため、相関する特徴量が複数存在するとシャッフルの影響が分散します。たとえば EXT_SOURCE_1 をシャッフルしても EXT_SOURCE_2・EXT_SOURCE_3・EXT_SOURCES_MEAN が生きているため、モデルはそちらで補完してスコアをほぼ維持できます。その結果、個々の EXT_SOURCE は「それほど重要でない」と判定されがちです。
マイナス重要度の解釈も同様です。EXT_SOURCES_SUM や EXT_SOURCES_PROD をシャッフルしても他の相関特徴量で補完されるためスコアが落ちず、ランダム誤差でわずかにプラスになることもあります。p値が0.843〜0.911と非常に高く(統計的に有意でない)、これらのマイナス値はノイズの範囲と解釈するのが妥当かと思います。EXT_SOURCE 系は実質的に重要な特徴量群ですが、Permutation Importanceという指標の性質上、相関構造のある特徴量群に対しては過小評価が生じていると考えられます。
⑤ データソース別の貢献度
相対importance > 0.06(上位30件水準)の特徴量を集計すると、貢献度の内訳は以下の通りです。
| データソース | 件数 | 相対importance合計 |
|---|---|---|
| application_train(メイン・派生含む) | 22件 | 3.76 |
| EXT_SOURCE系(正値のみ) | 9件 | 0.52 |
| bureau(B_TYPE種別集計) | 4件 | 0.35 |
| installments(Time Decay) | 2件 | 0.21 |
| bureau(通常集計) | 3件 | 0.18 |
| installments(通常・直近含む) | 2件 | 0.14 |
| POS_CASH | 1件 | 0.07 |
メインテーブルの圧倒的な貢献度が際立ちますが、Time Decay集計(INST_W_*)がinstallments系の中で最上位(8位)に入っており、特徴量エンジニアリングの成果として評価できます。
7.アンサンブル構成モデルのパラメータ
WeightedEnsemble_L2 は6モデル(CatBoost×1・LightGBM系×5)のOOF予測確率を加重平均して最終予測を出力します。ここでは各モデルのハイパーパラメータを横断的に比較し、なぜアンサンブルが単体モデルを上回るのかを考えます。
ハイパーパラメータと重みの一覧
AutoGluonが事前定義した多様なハイパーパラメータ候補セットから選ばれた各モデルのパラメータと、OOFスコアおよびアンサンブルウェイトを合わせて示します。モデル名の r177 r163 r72 などの数字はこの候補リストのインデックスに対応しています。
| パラメータ | CatBoost_c1 W=0.118 |
LGBMPrep_r21 W=0.529 |
LGBM_r163 W=0.176 |
LGBM_r177 W=0.059 |
LGBM_r72 W=0.059 |
LGBM_r120 W=0.059 |
|---|---|---|---|---|---|---|
| OOF AUC | 0.7904 | 0.7953 | 0.7933 | 0.7928 | 0.7912 | 0.7930 |
| eval_metric | Logloss | — | — | — | — | — |
| num_leaves | — | 20 | 84 | 39 | 180 | 5 |
| num_boost_round | 1522 | 5155 | 1770 | 2615 | 957 | 9992 |
| learning_rate | 0.050 | 0.013 | 0.011 | 0.018 | 0.018 | 0.013 |
| feature_fraction | — | 0.425 | 0.528 | 0.462 | 0.404 | 0.733 |
| bagging_fraction | — | 0.711 | 0.978 | 0.877 | 0.950 | 0.854 |
| lambda_l1 | — | 0.514 | 0.027 | 0.238 | 0.766 | 0.241 |
| lambda_l2 | — | 0.513 | 0.838 | 0.355 | 1.604 | 0.298 |
| min_data_in_leaf | — | 2 | 3 | 3 | 12 | 12 |
| extra_trees | — | False | False | True | True | True |
| $\hspace{50em}$ | ||||||
各モデルの特性
CatBoost_c1(W=0.118)
唯一のCatBoostモデルで、eval_metric=Logloss による確率キャリブレーションを最適化します。OOFスコアは最下位(0.7904)ながらウェイト0.118が付いているのは、LightGBM系5モデルとは根本的に異なるアルゴリズムで学習しており、予測の誤差パターンが他と重ならないためです。カテゴリ変数をネイティブに処理できる点も差別化要因で、LightGBMではlabel encodingに変換して扱う ORGANIZATION_TYPE などの高カーディナリティ変数を直接扱えます。
LightGBMPrep_r21(W=0.529)
アンサンブルの中核を担うモデルです。LightGBMPrep はカテゴリ変数にOne-hotエンコーディングを適用する前処理付きバリアントで、通常の LightGBM と異なる特徴量表現を持ちます。num_leaves=20 とシンプルな木構造・bagging_fraction=0.711 と積極的なサブサンプリングにより汎化性能が高く、OOFスコアでも単体トップ(0.7953)でした。
LightGBM_r163(W=0.176)
num_leaves=84・lambda_l1=0.027(弱い正則化)の設計で、複雑なパターンも拾いに行くモデルです。extra_trees=False でランダムな分割閾値を使わない通常のLightGBMとして動作します。num_boost_round=1770 と比較的少ないラウンドで収束しています。
LightGBM_r177(W=0.059)
num_leaves=39・extra_trees=True(ランダム分割閾値)の組み合わせで、RandomForest的な挙動をLightGBMの枠組みで実現します。feature_fraction=0.462 と半数以下の特徴量しか使わないため、他モデルとは見ている特徴量が大きく異なり多様性に貢献します。
LightGBM_r72(W=0.059)
num_leaves=180(採用モデル最大)と最も複雑な木を持ちながら、lambda_l1=0.766, lambda_l2=1.604 と最も強い正則化で過学習を抑制しています。複雑さと正則化を両立した設計です。
LightGBM_r120(W=0.059)
feature_fraction=0.733 と最も多くの特徴量を使用し、num_boost_round=9992 と最多のラウンドで学習します。num_leaves=5 という非常にシンプルな木で多数のラウンドを重ねる設計で、他モデルとは対照的なアプローチです。
なぜアンサンブルが有効なのか
各モデルのOOFスコアは0.7904〜0.7953と0.005以内の僅差ですが、アンサンブル(WeightedEnsemble_L2)は0.7959とすべての単体モデルを上回りました。その理由は、各モデルが意図的に異なる軸で多様性を持つ設計になっているためと考えられます。
| 多様性の軸 | 具体例 |
|---|---|
| アルゴリズム | CatBoost(Logloss最適化)vs LightGBM(AUC最適化) |
| 木の複雑さ | num_leaves: 5(単純)〜 180(複雑) |
| 正則化の強さ | lambda_l2: 0.298〜1.604 |
| 特徴量サブセット | feature_fraction: 0.404〜0.733 |
| 分割方法 | extra_trees: True(ランダム閾値)vs False(最適閾値) |
| 特徴量エンコーディング | One-hot(LightGBMPrep)vs label encoding(LightGBM) |
単体モデルが苦手なサンプルをそれぞれ異なる角度からカバーし合うことで、個々の精度の合計以上の予測力が生まれます。AutoGluonのランダムサーチはこの多様性を半自動的に実現しており、アンサンブルが最終的に最も高い汎化性能を示したことがleaderboardのスコアからも確認できます。
おわりに
本記事ではHome Credit Default Riskコンペに対して、特徴量エンジニアリング・AutoGluonによる学習・モデル分析の流れを記録しました。最終スコアはPublic 0.79936・Private 0.79342(参加時換算で7,176人中915位相当)となり、当初目標の「中央値より上」は達成できました。
今回の取り組みの振り返り
特徴量エンジニアリングでは、サブテーブルへの通常集計に加えてTime Decay加重集計・直近1年集計・ドメイン知識ベースの派生特徴量を組み合わせ、計1,049件の特徴量を生成しました。Permutation Importanceの結果から、Time Decay集計の INST_W_DPD_MEAN がサブテーブル由来の特徴量の中で最上位(全体8位)に入り、集計設計の工夫が一定の効果を示しました。
AutoGluonのアンサンブル分析では、アルゴリズム・木の複雑さ・正則化・特徴量サブセット・分割方法・エンコーディングの6軸で多様性を持つモデル群が、OOFスコアの僅差(0.005以内)にもかかわらず加重平均によりすべての単体モデルを上回ることを確認できました。
試したけど、うまくいかなかったこと
試行錯誤の中で効果が得られなかった取り組みも記録しておきます。
アンダーサンプリング
本コンペのTARGETはデフォルト(1)が約8%と不均衡データです。少数クラスの学習を強化する目的および処理速度の軽量化を目的として、多数クラス(TARGET=0)をダウンサンプリングしてクラス比率を均等に近づけてから学習を試みました。しかし結果はAUC 0.78台にとどまり、フルデータで学習した場合(0.795)を下回りました。
AutoGluonのような勾配ブースティングベースのアンサンブルはクラス不均衡に比較的ロバストです。今回のケースでは、データを間引くことで失われる多数クラスの情報量の損失の方が大きかったと考えられます。eval_metric='roc_auc' でAUCを直接最適化している場合、クラス比率の補正よりもデータ量を確保する方が有利に働くようです。
特徴量の事前削減
1,049件という特徴量数は処理が重く、学習時間の短縮を目的として特徴量を約200件に絞ったうえで学習を試みました。こちらもAUC 0.77〜0.78台にとどまり、フルの特徴量セットには及びませんでした。
個々の重要度が低い特徴量も、アンサンブルの中では他の特徴量と組み合わさることで間接的に貢献している可能性があります。単純な重要度の足切りではなく、モデルに使わせたうえでAutoGluon自身がアンサンブルの重みで取捨選択させる方が、結果的に精度が高くなりました。次節の④で特徴量の絞り込みを改善案として挙げていますが、閾値の設定には注意が必要です。
さらなるスコア改善に向けて(時間が足りずできなかったこと)
ブロンズラインまで約0.002という結果を踏まえると、以下の工夫でさらなる改善が見込たかな、と感じています。
① スタッキングの有効化(num_stack_levels=1)
今回は学習時間との兼ね合いで num_stack_levels=0 としましたが、L2モデルがL1モデルのOOF予測を特徴量として利用するスタッキングを有効にすることで、さらなる精度向上につながる余地があります。学習時間は約2倍になりますが、time_limit を延ばすか、特徴量数を事前に削減して対応する形が現実的だったと思います。
② hyperparameter_tune_kwargs によるハイパーパラメータ探索の追加
今回はAutoGluonの事前定義済み候補セットから選ばれたモデルのみでアンサンブルを構成しました。hyperparameter_tune_kwargs='auto' を指定してベイズ最適化による探索を有効にすることで、候補セットに含まれないパラメータ空間のモデルが加わり、アンサンブルの多様性がさらに高まる可能性があります。ただし探索分だけ学習時間が増えるため、time_limit の延長とのセットで検討する形になります。
③ サブテーブルへのTime Decay集計の横展開
今回はTime Decay集計を installments_payments のみに適用しました。bureau_balance(月次残高推移)や POS_CASH_balance(月次残高)にも同様の時間減衰集計を適用することで、これらのサブテーブルからより信号の強い特徴量が得られる可能性があります。
④ 特徴量の絞り込み(importance-based pruning)
今回の1,049件のうち、Permutation Importanceがマイナスまたはp値が有意でない特徴量が相当数含まれています。これらを除去することでモデルのノイズを減らし、学習時間の短縮とスコア改善を同時に狙える可能性があります。ただし前述の通り、単純な足切りはスコア低下を招く可能性もあるため、閾値の設定には慎重に検証を行った上で実施するのがよさそうです。
⑤ Target Encodingの活用
ORGANIZATION_TYPE(全体1位)・OCCUPATION_TYPE(全体6位)は高カーディナリティのカテゴリ変数ですが、今回は特別な前処理なしにAutoGluonに渡しています。これらにTarget Encoding(デフォルト率の平均値で置換)を施すことで、カテゴリ変数の信号をより直接的に学習させられる可能性があります。(リーケージを防ぐためにOOF形式でのエンコーディングは必須となりますが…)



