債務不履行リスクの低減

# 概要

SIGNATEの[債務不履行リスクの低減](https://signate.jp/competitions/294)の予測モデル作成を行ます。

> 借入総額や返済期間、金利、借入目的などの顧客データを使って、債務不履行リスクを予測するモデルを構築していただきます。
> 金融会社では個人や法人にお金を貸す、いわゆる融資を行い、返済額に利子を上乗せすることで利益を得ています。
> しかし、様々な理由から貸したのに返済されない、貸し倒れというケースが発生します。貸し倒れは金融会社として大きな損失であるため、できる限り避けたいですが、
> 一定確率で貸し倒れが起きることは避けられないのが現状です。したがって金融会社は、貸し倒れのリスクを可能な限り減らしたり、貸し倒れても利益がでるように適切に金利を設定したりしたいと考えています。
そこで今回は、借入総額や返済期間、金利、借入目的などの顧客データを使って、債務不履行リスクを予測するモデルの構築にチャレンジしてみましょう。

他にも「[Learn Python In Seminar](https://qiita.com/k8m/items/4952894a80c1833e5501)」のシリーズとして書いているので、よければご覧ください！

---

# 前提

- データとその説明：https://signate.jp/competitions/294/data
- ChargedOff(債務不履行)を1、FullyPaid(債務履行済み)を0として予測
- 評価基準：F1 Score

---

# インポート

```
import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
from sklearn.metrics import confusion_matrix
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import f1_score
from imblearn.under_sampling import RandomUnderSampler
```

---

# データの読み込みと整形

- トレインデータの読み込み
- 欠損値と重複の削除
- FullyPaidを0、ChargedOffを1に設定
- その他データを数値データに変換

```
# 読み込み
df = pd.read_csv('data/train.csv')

# 重複部分の削除
df.drop_duplicates(inplace=True)

# 重複部分の削除の後に新しいインデックスを取得
df.reset_index(drop=True, inplace=True)

# 欠損値を削除
df = df.dropna()

# id列を削除
del df['id']

# 「years、year」を削除 ex.「3 years」→「3」
df["term"] = df["term"].str.replace('years', '')
df["employment_length"] = df["employment_length"].str.replace(
    'years', '').str.replace(
    'year', '')

# FullyPaidを0、ChargedOffを1に設定
df["loan_status"] = df["loan_status"].str.replace(
    'FullyPaid', '0').str.replace('ChargedOff', '1')

df["term"] = df["term"].astype(int)
df["employment_length"] = df["employment_length"].astype(int)
df["loan_status"] = df["loan_status"].astype(int)

# データフレームの分離
col_categoric = ["grade", "purpose", "application_type", "loan_status"]
df_numeric = df.drop(col_categoric, axis=1)
df_categoric = df[col_categoric]

# df_categoric内の"loan_status"列と、df_numericの列を横結合する
df_tmp = pd.concat([df_categoric["loan_status"], df_numeric], axis=1)
```

---

# データの可視化

---

## FullyPaidとChargedOff

FullyPaidとChargedOffの数を棒グラフで可視化

```
# データ数を取得
counts_loan_status = df["loan_status"].value_counts()

# 棒グラフの作成
counts_loan_status.plot(kind='bar')

# グラフの表示
plt.show()
```

---

![loan-status-bar.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542018/b668d8db-d94c-17e6-db6b-ffb6df37a936.png)

- FullyPaidの数がChargedOffの数の3倍以上

---

## 量的データの可視化

loan_amnt、interested_rate、credit_scoreをヒストグラムで可視化

```
# ヒストグラムの作成(※figsizeはグラフのサイズを指定）
df_numeric.hist(figsize=(8, 6)) # df_numericは前処理で定義

# ラベルが重ならないように調整
plt.tight_layout()

# グラフの表示
plt.show()
```

---

![numeric-hist.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542018/dd6e0c14-0e3e-354e-5430-10e2783df2ee.png)

- 全体的に左寄り

---

## 目的変数ごとに可視化

目的変数ごとのヒストグラムを重ねて可視化

```
# グラフの整形
plt.figure(figsize=(12, 12))

for ncol, colname in enumerate(df_numeric.columns):
    plt.subplot(3, 3, ncol+1) # plt.subplot(縦のプロット数, 横のプロット数, プロット番号)
    sns.distplot(df_tmp.query("loan_status==0")[colname])
    sns.distplot(df_tmp.query("loan_status==1")[colname]) # 重ねる
    plt.legend(labels=["FullyPaid", "ChargedOff"], loc='upper right') # 凡例
```

---

![hist.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542018/0a9fca2e-0ca5-d67b-1a13-4fb6ae1e93b4.png)

- 金利が上がると債務不履行が増加
- 信用スコアが高いと債務不履行が若干減少

---

## 相関関係

各変数間の相関関係をヒートマップで可視化

```
# グラフの整形
plt.figure(figsize=(10, 8))

# ヒートマップの作成
sns.heatmap(df.corr(), vmin=-1.0, vmax=1.0, annot=True,
            cmap='coolwarm', linewidths=0.1) # corr関数で相関関数を算出
```

---

![heatmap.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542018/d08d7d7b-56fe-c1ca-cf5a-fe1d6564fffa.png)

- 説明変数間での強い相関がいくつか見られる

---

# 前処理

---

- grade, purpose, application_typeのダミー変数化
- 説明変数と目的変数に分離
- トレインデータとテストデータに分割

```
# ダミー変数化
df = pd.get_dummies(
    df, columns=["grade", "purpose", "application_type"])

# 目的変数のデータフレーム
y = df["loan_status"]
# 説明変数のデータフレーム
X = df.drop(["loan_status"], axis=1)

# データの分割
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.4, random_state=0)
```

---

# モデル作成

---

## 学習と予測

ロジスティック回帰分析：2値(0と1)のどちらに分類されるかを確率的に予測する手法

```
# モデルの初期化
lr = LogisticRegression()

# モデルの学習
lr.fit(X_train, y_train)

y_pred = lr.predict(X_test)
```

---

## モデルの評価

- 混同行列
- F1 Score

```
# 混同行列の作成
cm = confusion_matrix(y_true=y_test, y_pred=y_pred)

# 混同行列をデータフレーム化
df_cm = pd.DataFrame(np.rot90(cm, 2), index=["実際のChargedOff", "実際のFullyPaid"], columns=["ChargedOffの予測", "FullyPaidの予測"])
print(df_cm)

# F1 Scoreの算出
f1 = f1_score(y_test, y_pred)
print(f1)
```
↓

```
# 混同行列
               ChargedOffの予測  FullyPaidの予測
実際のChargedOff            518         16657
実際のFullyPaid             433         65422

# F1 Score
0.0571554672845636
```

---

## 混同行列の可視化

```
# heatmapによる混同行列の可視化
sns.heatmap(df_cm, annot=True, fmt="2g", cmap='Blues')
plt.yticks(va='center')
```

![result_heatmap_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542018/2f6df0ff-55ad-6a2e-d757-4fa61e5548cc.png)

---

## 結果

- F1 Scoreは0.057とかなり低い
- ChargedOffをほとんど予測できていない

---

# モデル改善

---

## ダウンサンプリング

- データの不均衡を改善

```
# 初期化
sampler = RandomUnderSampler(random_state=42)

# ダウンサンプリングの適用
X_resampled, y_resampled = sampler.fit_resample(X_train, y_train)

# 返ってきたBoolを数値に変換
df["loan_status"] = df["loan_status"].replace(True, 0).replace(False, 1)
df["loan_status"] = df["loan_status"].astype(int)
```

---

## 学習と予測

```
# 学習
lr = LogisticRegression()
lr.fit(X_resampled, y_resampled)

# 予測
y_pred = lr.predict(X_test)
```

---

## 評価

```
df_cm = pd.DataFrame(np.rot90(cm, 2), index=["実際のChargedOff", "実際のFullyPaid"], columns=["ChargedOffの予測", "FullyPaidの予測"])
print(df_cm)

f1 = f1_score(y_test, y_pred)
print(f1)
```

↓

```
# 混同行列
               ChargedOffの予測  FullyPaidの予測
実際のChargedOff          11977          5198
実際のFullyPaid           30787         35068

# F1 Score
0.3996396336275213
```

---

## 混同行列の可視化

![result_heatmap_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542018/3cdeccae-8be4-2f0e-9a80-9c8f7e954a37.png)


--- 

## 結果

- F1 Scoreが0.399に大幅改善
- ChargedOffの予測精度が改善
- FullyPaidの予測精度が低下

---

# まとめ

- 最終的にFullyPaidの予測精度が低下してしまったが、債務不履行のリスクを検知するという意味ではChargedOffの予測精度が改善できたので目的に合ったモデルになったのではないか
- 0.399というF1 ScoreはSIGNATEへの提出者順位の30位と同程度
- 金利や信用スコアの閾値を設定することでさらに改善ができそう