SIGNATE 【練習問題】債務不履行リスクの低減 で20位にランクインした解法

  • 2024年12月4日
  • 2024年12月4日
  • SGINATE
  • 33回
  • 0件

この記事では、SIGNATEの練習問題「債務不履行リスクの低減」では20位にランクイン(2024年11月時点)した解法を解説します。実際のコードを用いて具体的に説明しますので、この問題にチャレンジしようと考えている人はぜひ参考にしてください。

コンペの概要

このコンペの目的は、貸付データを用いて債務不履行のリスクを予測するモデルを構築することです。与えられた過去のローン情報を基に、各ローン申請者が「FullyPaid(完済)」または「ChargedOff(不履行)」になる確率を算出します。

債務不履行のリスクは、借入期間や目的、申請者の収入や雇用状態、申請方法など、多岐にわたる要因に影響される可能性があります。データからこれらの要因を分析し、適切な予測モデルを作成することが課題です。

入力データ

このコンペで提供されるデータは以下の通りです。詳細についてはSiganateのサイトをご参照ください。

  • 学習用データ: 過去のローン情報が記録されており、以下の特徴が含まれます。
    • 借入期間(term)、ローンのグレード(grade)、収入、雇用期間(employment_length)など。
    • ローンの目的(purpose)、申請方法(application_type)などのカテゴリカルデータ。
    • 貸付額や利率といった数値型の特徴量。
    • 対象変数 loan_status(完済か不履行かを示すラベル)。
  • 評価用データ: 学習用データと同様の特徴量を持つが、loan_status は欠落しており、これを予測する必要があります。

モデル構築と実装

以下は、実際に使用したコード全体です。このコードでは、データの前処理、特徴量エンジニアリング、LightGBMを用いたモデルのトレーニングと評価を行っています。

コード全体

import pandas as pd
from sklearn.preprocessing import OneHotEncoder, StandardScaler
from sklearn.impute import SimpleImputer
from sklearn.metrics import f1_score, accuracy_score
import lightgbm as lgb

# トレーニング時の前処理と特徴量名の保存
def preprocess_data(data, encoder=None, scaler=None, fit=False):
    # 欠損値の補完
    imputer = SimpleImputer(strategy='most_frequent')
    data['employment_length'] = imputer.fit_transform(data[['employment_length']])
    
    # 'term'を数値に変換
    data['term_numeric'] = data['term'].str.extract('(\d+)').astype(int)

    # 新しい特徴量の作成
    data['loan_to_interest_ratio'] = data['loan_amnt'] / data['interest_rate']

    # グレードを数値に変換
    grades = ['A1', 'A2', 'A3', 'A4', 'A5', 'B1', 'B2', 'B3', 'B4', 'B5', 'C1', 'C2', 'C3', 'C4', 'C5',
              'D1', 'D2', 'D3', 'D4', 'D5', 'E1', 'E2', 'E3', 'E4', 'E5', 'F1', 'F2', 'F3', 'F4', 'F5',
              'G1', 'G2', 'G3', 'G4', 'G5']
    grade_values = {grade: len(grades) - i for i, grade in enumerate(grades)}
    data['grade_numeric'] = data['grade'].map(grade_values)

    # 'grade'列を削除
    data = data.drop('grade', axis=1)

    # employment_lengthも数値に変換
    employment_mapping = {
        '10+ years': 10,
        '9 years': 9,
        '8 years': 8,
        '7 years': 7,
        '6 years': 6,
        '5 years': 5,
        '4 years': 4,
        '3 years': 3,
        '2 years': 2,
        '1 year': 1,
        '< 1 year': 0,
        'n/a': 0
    }
    data['employment_length'] = data['employment_length'].map(employment_mapping).fillna(0).astype(int)

    # カテゴリ変数の加工
    categorical_features = ['term', 'purpose', 'application_type']
    if fit:
        encoder = OneHotEncoder(sparse=False, handle_unknown='ignore')
        encoded_features = encoder.fit_transform(data[categorical_features])
        # エンコーダーが生成した特徴量名を保存
        encoded_feature_names = encoder.get_feature_names_out(categorical_features)
    else:
        encoded_features = encoder.transform(data[categorical_features])
        encoded_feature_names = encoder.get_feature_names_out(categorical_features)
    
    encoded_df = pd.DataFrame(encoded_features, columns=encoded_feature_names)
    data = data.drop(categorical_features, axis=1)
    data = pd.concat([data, encoded_df], axis=1)

    # 数値変数のスケーリング
    numerical_features = ['loan_amnt', 'interest_rate', 'credit_score', 'grade_numeric', 'loan_to_interest_ratio']
    if fit:
        scaler = StandardScaler()
        data[numerical_features] = scaler.fit_transform(data[numerical_features])
    else:
        data[numerical_features] = scaler.transform(data[numerical_features])

    # トレーニング時の特徴量名を保存
    if fit:
        feature_names = data.columns.drop('loan_status')
    else:
        feature_names = None

    return data, encoder, scaler, feature_names

# トレーニングデータの読み込み
train_data = pd.read_csv('train.csv')

# トレーニングデータの前処理と特徴量名の保存
train_data, encoder, scaler, feature_names = preprocess_data(train_data, fit=True)

# モデルのトレーニング
X_train = train_data.drop('loan_status', axis=1)
y_train = train_data['loan_status'].map({'FullyPaid': 0, 'ChargedOff': 1})

# LightGBMモデルの構築と訓練
lgb_model = lgb.LGBMClassifier(class_weight='balanced', random_state=42)
lgb_model.fit(X_train, y_train)

# トレーニングデータに対する予測と評価
y_train_pred = lgb_model.predict(X_train)
train_f1 = f1_score(y_train, y_train_pred)
train_accuracy = accuracy_score(y_train, y_train_pred)

print(f"Training F1 Score: {train_f1}")
print(f"Training Accuracy: {train_accuracy}")

# テストデータの読み込み
test_data = pd.read_csv('test.csv')

# 元のID列を保存
test_ids = test_data['id']

# テストデータの前処理
test_data, _, _, _ = preprocess_data(test_data, encoder=encoder, scaler=scaler, fit=False)

# トレーニング時と同じ特徴量セットに揃える
X_submit = test_data[feature_names]

# 予測
y_submit_pred = lgb_model.predict(X_submit)

# submit.csvの生成
submit_df = pd.DataFrame({
    'id': test_ids,
    'loan_status': y_submit_pred
})

# 結果を保存(ヘッダーなし)
submit_df.to_csv('submit.csv', index=False, header=False)

# 結果の確認
print(submit_df.head())

解説

データの前処理と特徴量エンジニアリング

preprocess_data 関数では、データセットに対して以下の処理を行います。

欠損値の補完
employment_length 列の欠損値を最頻値で補完しました。この補完方法は、該当列がカテゴリカルな意味合いを持つ場合に適しています。

imputer = SimpleImputer(strategy='most_frequent')
data['employment_length'] = imputer.fit_transform(data[['employment_length']])

特徴量の作成
loan_to_interest_ratio を新たに作成しました。この変数は、貸付金額(loan_amnt)を利率(interest_rate)で割ったもので、返済可否に影響を与えると考えられるため特徴量として追加しました。この変数により、利率の高い貸付金額のリスクが反映されることを期待しました。

data['loan_to_interest_ratio'] = data['loan_amnt'] / data['interest_rate']

カテゴリカルデータのエンコード
OneHotEncoder を用いてカテゴリカル変数をダミー変数に変換しています。エンコードされた列名も記録されるため、後続の操作が簡便になります。

encoder = OneHotEncoder(sparse=False, handle_unknown='ignore')
encoded_features = encoder.fit_transform(data[categorical_features])

数値データのスケーリング
StandardScaler を用いてスケーリングすることで、モデル学習に適すると思われるデータ範囲に整えました。

scaler = StandardScaler()
data[numerical_features] = scaler.fit_transform(data[numerical_features])

LightGBMモデルの構築

LightGBMを用いてモデルを構築し、トレーニングデータを使用して学習を行いました。特に、class_weight='balanced' を指定することで、クラスの不均衡(FullyPaidとChargedOffの分布差)に対応したことがポイントです。

lgb_model = lgb.LGBMClassifier(class_weight='balanced', random_state=42)
lgb_model.fit(X_train, y_train)

class_weight='balanced': クラスの不均衡(FullyPaidChargedOffの分布差)を自動で調整くれます。

モデルの評価

訓練データを用いて予測を行い、F1スコアとAccuracyを計算しました。

y_train_pred = lgb_model.predict(X_train)
train_f1 = f1_score(y_train, y_train_pred)
train_accuracy = accuracy_score(y_train, y_train_pred)

print(f"Training F1 Score: {train_f1}")
print(f"Training Accuracy: {train_accuracy}")
Training F1 Score: 0.4319254874427532
Training Accuracy: 0.6358008886833281

テストデータの予測と提出

学習済みモデルを用いてテストデータの予測を行い、提出用のファイルを生成します。

submit_df = pd.DataFrame({
    'id': test_ids,
    'loan_status': y_submit_pred
})
submit_df.to_csv('submit.csv', index=False, header=False)
生成されたsubmit.csvを、SIGNATEの該当ページで投稿すると結果を確認できます。

考察

上位入賞できたポイントについて考えてみます。

重要度

LightGBMのplot_importanceを用いて、特徴量の重要度を可視化しました。作成したloan_to_interest_ratioが上位にいることがわかります。

import matplotlib.pyplot as plt
lgb.plot_importance(lgb_model, max_num_features=10, figsize=(10, 6))
plt.show()

精度改善のポイント

この課題では、class_weight='balanced'を加えたことで、クラス不均衡の影響を緩和し、不履行のクラスを正しく識別する能力(F1スコア)が大幅に向上しました。これにより、モデルのバランスが改善され、総合的な性能が高まったことが分かります。

ちなみにこれを外して学習を行った場合のスコアは以下の通りでした。

Training F1 Score: 0.08913018455281432
Training Accuracy: 0.8039280463833232

同じモデルとは思えないほどの差です。

今回の練習問題では、下図のように予測対象となる変数の値(FullyPaidChargedOff)の件数に大きな差がありました。このようなケースでは、クラスの不均衡に対処することが予測精度を向上させる重要な鍵となることがわかりました。


以上、SIGNATE 【練習問題】債務不履行リスクの低減 についての解説でした。この記事のコードをそのまま実行するだけでも上位入賞は可能です。ただし、最適なハイパーパラメータや追加の特徴量を試行錯誤することで、さらに上位を目指せるでしょう。ぜひ独自の工夫を加えて、1桁順位を狙ってみてください。そして上位入賞を果たした際にはこの記事にコメントいただける嬉しいです。

最新情報をチェックしよう!