import numpy as np
import pandas as pd
pd.set_option('display.max_columns', None)
pd.set_option('display.max_rows', 99)
import matplotlib.pyplot as plt
import seaborn as sns
sns.set()
data_raw = pd.read_csv('loan_data_2007_2014.csv', index_col=0)
C:\Users\User\AppData\Local\Temp\ipykernel_1224\2969852544.py:1: DtypeWarning: Columns (20) have mixed types. Specify dtype option on import or set low_memory=False.
data_raw = pd.read_csv('loan_data_2007_2014.csv', index_col=0)
data_raw.shape
(466285, 74)
data_raw.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 466285 entries, 0 to 466284
Data columns (total 74 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 id 466285 non-null int64
1 member_id 466285 non-null int64
2 loan_amnt 466285 non-null int64
3 funded_amnt 466285 non-null int64
4 funded_amnt_inv 466285 non-null float64
5 term 466285 non-null object
6 int_rate 466285 non-null float64
7 installment 466285 non-null float64
8 grade 466285 non-null object
9 sub_grade 466285 non-null object
10 emp_title 438697 non-null object
11 emp_length 445277 non-null object
12 home_ownership 466285 non-null object
13 annual_inc 466281 non-null float64
14 verification_status 466285 non-null object
15 issue_d 466285 non-null object
16 loan_status 466285 non-null object
17 pymnt_plan 466285 non-null object
18 url 466285 non-null object
19 desc 125983 non-null object
20 purpose 466285 non-null object
21 title 466265 non-null object
22 zip_code 466285 non-null object
23 addr_state 466285 non-null object
24 dti 466285 non-null float64
25 delinq_2yrs 466256 non-null float64
26 earliest_cr_line 466256 non-null object
27 inq_last_6mths 466256 non-null float64
28 mths_since_last_delinq 215934 non-null float64
29 mths_since_last_record 62638 non-null float64
30 open_acc 466256 non-null float64
31 pub_rec 466256 non-null float64
32 revol_bal 466285 non-null int64
33 revol_util 465945 non-null float64
34 total_acc 466256 non-null float64
35 initial_list_status 466285 non-null object
36 out_prncp 466285 non-null float64
37 out_prncp_inv 466285 non-null float64
38 total_pymnt 466285 non-null float64
39 total_pymnt_inv 466285 non-null float64
40 total_rec_prncp 466285 non-null float64
41 total_rec_int 466285 non-null float64
42 total_rec_late_fee 466285 non-null float64
43 recoveries 466285 non-null float64
44 collection_recovery_fee 466285 non-null float64
45 last_pymnt_d 465909 non-null object
46 last_pymnt_amnt 466285 non-null float64
47 next_pymnt_d 239071 non-null object
48 last_credit_pull_d 466243 non-null object
49 collections_12_mths_ex_med 466140 non-null float64
50 mths_since_last_major_derog 98974 non-null float64
51 policy_code 466285 non-null int64
52 application_type 466285 non-null object
53 annual_inc_joint 0 non-null float64
54 dti_joint 0 non-null float64
55 verification_status_joint 0 non-null float64
56 acc_now_delinq 466256 non-null float64
57 tot_coll_amt 396009 non-null float64
58 tot_cur_bal 396009 non-null float64
59 open_acc_6m 0 non-null float64
60 open_il_6m 0 non-null float64
61 open_il_12m 0 non-null float64
62 open_il_24m 0 non-null float64
63 mths_since_rcnt_il 0 non-null float64
64 total_bal_il 0 non-null float64
65 il_util 0 non-null float64
66 open_rv_12m 0 non-null float64
67 open_rv_24m 0 non-null float64
68 max_bal_bc 0 non-null float64
69 all_util 0 non-null float64
70 total_rev_hi_lim 396009 non-null float64
71 inq_fi 0 non-null float64
72 total_cu_tl 0 non-null float64
73 inq_last_12m 0 non-null float64
dtypes: float64(46), int64(6), object(22)
memory usage: 266.8+ MB
data_raw.sample()
| id | member_id | loan_amnt | funded_amnt | funded_amnt_inv | term | int_rate | installment | grade | sub_grade | emp_title | emp_length | home_ownership | annual_inc | verification_status | issue_d | loan_status | pymnt_plan | url | desc | purpose | title | zip_code | addr_state | dti | delinq_2yrs | earliest_cr_line | inq_last_6mths | mths_since_last_delinq | mths_since_last_record | open_acc | pub_rec | revol_bal | revol_util | total_acc | initial_list_status | out_prncp | out_prncp_inv | total_pymnt | total_pymnt_inv | total_rec_prncp | total_rec_int | total_rec_late_fee | recoveries | collection_recovery_fee | last_pymnt_d | last_pymnt_amnt | next_pymnt_d | last_credit_pull_d | collections_12_mths_ex_med | mths_since_last_major_derog | policy_code | application_type | annual_inc_joint | dti_joint | verification_status_joint | acc_now_delinq | tot_coll_amt | tot_cur_bal | open_acc_6m | open_il_6m | open_il_12m | open_il_24m | mths_since_rcnt_il | total_bal_il | il_util | open_rv_12m | open_rv_24m | max_bal_bc | all_util | total_rev_hi_lim | inq_fi | total_cu_tl | inq_last_12m | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 379495 | 17633586 | 19766112 | 7000 | 7000 | 7000.0 | 36 months | 13.98 | 239.18 | C | C3 | Associate | 1 year | RENT | 20000.0 | Not Verified | Jun-14 | Fully Paid | n | https://www.lendingclub.com/browse/loanDetail…. | NaN | debt_consolidation | Debt consolidation | 480xx | MI | 5.88 | 0.0 | Jan-98 | 0.0 | NaN | NaN | 9.0 | 0.0 | 3581 | 89.5 | 21.0 | w | 0.0 | 0.0 | 7923.67 | 7923.67 | 7000.0 | 923.67 | 0.0 | 0.0 | 0.0 | Jul-15 | 5053.51 | NaN | Jan-16 | 0.0 | NaN | 1 | INDIVIDUAL | NaN | NaN | NaN | 0.0 | 832.0 | 28121.0 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 4000.0 | NaN | NaN | NaN |
data_raw.id.nunique()
466285
data_raw.member_id.nunique()
466285
Terlihat bahwa tidak ada id atau member_id
yang duplikat, artinya setiap baris sudah mewakili satu individu.
Selanjutnya, pembuangan fitur-fitur yang tidak berguna dilakukan. Contohnya seperti fitur yang merupakan id unik, berupa free text, nilainya kosong semua (NULL), dsb.
cols_to_drop = [
# unique id
'id'
, 'member_id'
# free text
, 'url'
, 'desc'
# all null / constant / others
, 'zip_code'
, 'annual_inc_joint'
, 'dti_joint'
, 'verification_status_joint'
, 'open_acc_6m'
, 'open_il_6m'
, 'open_il_12m'
, 'open_il_24m'
, 'mths_since_rcnt_il'
, 'total_bal_il'
, 'il_util'
, 'open_rv_12m'
, 'open_rv_24m'
, 'max_bal_bc'
, 'all_util'
, 'inq_fi'
, 'total_cu_tl'
, 'inq_last_12m'
# expert judgment
, 'sub_grade'
]
data = data_raw.drop(cols_to_drop, axis=1)
data.sample(5)
| loan_amnt | funded_amnt | funded_amnt_inv | term | int_rate | installment | grade | emp_title | emp_length | home_ownership | annual_inc | verification_status | issue_d | loan_status | pymnt_plan | purpose | title | addr_state | dti | delinq_2yrs | earliest_cr_line | inq_last_6mths | mths_since_last_delinq | mths_since_last_record | open_acc | pub_rec | revol_bal | revol_util | total_acc | initial_list_status | out_prncp | out_prncp_inv | total_pymnt | total_pymnt_inv | total_rec_prncp | total_rec_int | total_rec_late_fee | recoveries | collection_recovery_fee | last_pymnt_d | last_pymnt_amnt | next_pymnt_d | last_credit_pull_d | collections_12_mths_ex_med | mths_since_last_major_derog | policy_code | application_type | acc_now_delinq | tot_coll_amt | tot_cur_bal | total_rev_hi_lim | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 372611 | 19200 | 19200 | 19200.0 | 36 months | 12.99 | 646.84 | C | Sr Mgr. WW Field Services | 6 years | MORTGAGE | 169000.0 | Source Verified | Jun-14 | Current | n | debt_consolidation | Debt consolidation | TX | 14.69 | 0.0 | Sep-82 | 0.0 | NaN | NaN | 16.0 | 0.0 | 50835 | 73.4 | 21.0 | w | 9994.30 | 9994.30 | 12289.96000 | 12289.96 | 9205.70 | 3084.26 | 0.0 | 0.0 | 0.0 | Jan-16 | 646.84 | Feb-16 | Jan-16 | 0.0 | NaN | 1 | INDIVIDUAL | 0.0 | 0.0 | 324434.0 | 69300.0 |
| 59649 | 5000 | 5000 | 5000.0 | 36 months | 15.61 | 174.83 | C | Admin Assist II | 10+ years | OWN | 33000.0 | Not Verified | Nov-13 | Current | n | debt_consolidation | Debt consolidation | TX | 26.80 | 0.0 | Dec-03 | 2.0 | 44.0 | NaN | 8.0 | 0.0 | 5667 | 48.9 | 19.0 | f | 1781.36 | 1781.36 | 4369.99000 | 4369.99 | 3218.64 | 1151.35 | 0.0 | 0.0 | 0.0 | Jan-16 | 174.83 | Feb-16 | Jan-16 | 0.0 | 47.0 | 1 | INDIVIDUAL | 0.0 | 75.0 | 24965.0 | 11600.0 |
| 292727 | 5000 | 5000 | 5000.0 | 36 months | 10.15 | 161.69 | B | employment program representative | 10+ years | MORTGAGE | 90000.0 | Verified | Oct-14 | Late (16-30 days) | n | credit_card | Credit card refinancing | CA | 7.87 | 2.0 | Oct-87 | 1.0 | 5.0 | NaN | 11.0 | 0.0 | 4408 | 23.4 | 53.0 | w | 3233.34 | 3233.34 | 2263.66000 | 2263.66 | 1766.66 | 497.00 | 0.0 | 0.0 | 0.0 | Jan-16 | 161.69 | Feb-16 | Jan-16 | 0.0 | 5.0 | 1 | INDIVIDUAL | 0.0 | 0.0 | 159715.0 | 18800.0 |
| 291676 | 10000 | 10000 | 10000.0 | 36 months | 8.39 | 315.17 | A | Professor | 8 years | RENT | 120000.0 | Not Verified | Oct-14 | Current | n | debt_consolidation | Debt consolidation | OH | 7.22 | 0.0 | Jul-04 | 1.0 | NaN | NaN | 5.0 | 0.0 | 6054 | 36.0 | 12.0 | w | 6135.58 | 6135.58 | 4727.55000 | 4727.55 | 3864.42 | 863.13 | 0.0 | 0.0 | 0.0 | Jan-16 | 315.17 | Feb-16 | Jan-16 | 0.0 | NaN | 1 | INDIVIDUAL | 0.0 | 0.0 | 36663.0 | 16800.0 |
| 164566 | 12000 | 12000 | 12000.0 | 36 months | 13.11 | 404.97 | B | Costco | 5 years | MORTGAGE | 61000.0 | Not Verified | Feb-13 | Fully Paid | n | debt_consolidation | refi lend.club loan +1 Credit Card | CO | 8.99 | 0.0 | Jan-00 | 0.0 | 41.0 | NaN | 6.0 | 0.0 | 5234 | 61.6 | 14.0 | f | 0.00 | 0.00 | 14472.28613 | 14472.29 | 12000.00 | 2472.29 | 0.0 | 0.0 | 0.0 | Jul-15 | 3141.17 | NaN | Jan-16 | 0.0 | NaN | 1 | INDIVIDUAL | 0.0 | 360.0 | 196803.0 | 8500.0 |
Dalam project credit risk modeling, tujuan utama adalah untuk melakukan prediksi terhadap suatu individu akan kemampuan mereka untuk melakukan pembayaran terhadap pinjaman/kredit yang diberikan. Oleh karena itu, variabel target yang digunakan harus mencerminkan kemampuan individu dalam hal tersebut.
Dalam dataset ini, variabel loan_status adalah variabel
yang dapat dijadikan variabel target karena mencerminkan performa
masing-masing individu dalam melakukan pembayaran terhadap
pinjaman/kredit selama ini.
data.loan_status.value_counts(normalize=True)*100
Current 48.087757
Fully Paid 39.619332
Charged Off 9.109236
Late (31-120 days) 1.479782
In Grace Period 0.674695
Does not meet the credit policy. Status:Fully Paid 0.426349
Late (16-30 days) 0.261214
Default 0.178432
Does not meet the credit policy. Status:Charged Off 0.163205
Name: loan_status, dtype: float64
Dapat dilihat bahwa variabel loan_status memiliki
beberapa nilai:
Current artinya pembayaran lancar;
Charged Off artinya pembayaran macet sehingga
dihapusbukukan; Late artinya pembayaran telat dilakukan;
In Grace Period artinya dalam masa tenggang;
Fully Paid artinya pembayaran lunas; Default
artinya pembayaran macet
Dari definisi-definisi tersebut, masing-masing individu dapat
ditandai apakah mereka merupakan bad loan (peminjam yang
buruk) atau good loan (peminjam yang baik)
Definisi bad dan good loan terkadang bisa berbeda tergantung dari kebutuhan bisnis. Pada contoh ini, saya menggunakan keterlambatan pembayaran di atas 30 hari dan yang lebih buruk dari itu sebagai penanda bad loan.
bad_status = [
'Charged Off'
, 'Default'
, 'Does not meet the credit policy. Status:Charged Off'
, 'Late (31-120 days)'
]
data['bad_flag'] = np.where(data['loan_status'].isin(bad_status), 1, 0)
data['bad_flag'].value_counts(normalize=True)*100
0 89.069346
1 10.930654
Name: bad_flag, dtype: float64
Setelah melakukan flagging terhadap bad/good loan, dapat dilihat bahwa jumlah individu yang ditandai sebagai bad loan jauh lebih sedikit daripada good loan. Hal ini menyebabkan problem ini menjadi problem imbalanced dataset.
Jangan lupa untuk membuang kolom asal loan_status
data.drop('loan_status', axis=1, inplace=True)
Pada step ini, dilakukan pembersihan/modifikasi beberapa fitur ke dalam format yang dapat digunakan untuk modeling.
Memodifikasi emp_length. Contoh: 4 years -> 4
data['emp_length'].unique()
array(['10+ years', '< 1 year', '1 year', '3 years', '8 years', '9 years',
'4 years', '5 years', '6 years', '2 years', '7 years', nan],
dtype=object)
data['emp_length_int'] = data['emp_length'].str.replace('\+ years', '')
data['emp_length_int'] = data['emp_length_int'].str.replace('< 1 year', str(0))
data['emp_length_int'] = data['emp_length_int'].str.replace(' years', '')
data['emp_length_int'] = data['emp_length_int'].str.replace(' year', '')
C:\Users\User\AppData\Local\Temp\ipykernel_1224\1742887160.py:1: FutureWarning: The default value of regex will change from True to False in a future version.
data['emp_length_int'] = data['emp_length'].str.replace('\+ years', '')
data['emp_length_int'] = data['emp_length_int'].astype(float)
data.drop('emp_length', axis=1, inplace=True)
Memodifikasi term. Contoh: 36 months -> 36
data['term'].unique()
data['term_int'] = data['term'].str.replace(' months', '')
data['term_int'] = data['term_int'].astype(float)
data.drop('term', axis=1, inplace=True)
Memodifikasi earliest_cr_line dari format bulan-tahun
menjadi perhitungan berapa lama waktu berlalu sejak waktu tersebut.
Untuk melakukan hal ini, umumnya digunakan reference date = hari ini.
Namun, karena dataset ini merupakan dataset tahun 2007-2014, maka akan
lebih relevan jika menggunakan reference date di sekitar tahun 2017.
Dalam contoh ini, saya menggunakan tanggal 2017-12-01 sebagai reference
date.
data['earliest_cr_line'].head(3)
data['earliest_cr_line_date'] = pd.to_datetime(data['earliest_cr_line'], format='%b-%y')
data['earliest_cr_line_date'].head(3)
data['mths_since_earliest_cr_line'] = round(pd.to_numeric((pd.to_datetime('2017-12-01') - data['earliest_cr_line_date']) / np.timedelta64(1, 'M')))
data['mths_since_earliest_cr_line'].head(3)
data['mths_since_earliest_cr_line'].describe()
Terlihat ada nilai yang aneh, yaitu negatif.
data[data['mths_since_earliest_cr_line']<0][['earliest_cr_line', 'earliest_cr_line_date', 'mths_since_earliest_cr_line']].head(3)
Ternyata nilai negatif muncul karena fungsi Python salah menginterpretasikan tahun 62 menjadi tahun 2062, padahal seharusnya merupakan tahun 1962.
Untuk mengatasi hal ini, dapat dilakukan preprocessing lebih jauh jika ingin membenarkan tahun 2062 menjadi 1962. Namun, kali ini saya hanya mengubah nilai yang negatif menjadi nilai maximum dari fitur tersebut. Karena di sini saya mengetahui bahwa nilai-nilai yang negatif artinya adalah data yang sudah tua (tahun 1900an), maka masih masuk akal jika saya mengganti nilai-nilai tersebut menjadi nilai terbesar.
data.loc[data['mths_since_earliest_cr_line']<0, 'mths_since_earliest_cr_line'] = data['mths_since_earliest_cr_line'].max()
data.drop(['earliest_cr_line', 'earliest_cr_line_date'], axis=1, inplace=True)
Konsep preprocessing yang dilakukan sama dengan yang dilakukan
terhadap variabel earliest_cr_line
data['issue_d_date'] = pd.to_datetime(data['issue_d'], format='%b-%y')
data['mths_since_issue_d'] = round(pd.to_numeric((pd.to_datetime('2017-12-01') - data['issue_d_date']) / np.timedelta64(1, 'M')))
data['mths_since_issue_d'].describe()
data.drop(['issue_d', 'issue_d_date'], axis=1, inplace=True)
Konsep preprocessing yang dilakukan sama dengan yang dilakukan
terhadap variabel earliest_cr_line
data['last_pymnt_d_date'] = pd.to_datetime(data['last_pymnt_d'], format='%b-%y')
data['mths_since_last_pymnt_d'] = round(pd.to_numeric((pd.to_datetime('2017-12-01') - data['last_pymnt_d_date']) / np.timedelta64(1, 'M')))
data['mths_since_last_pymnt_d'].describe()
data.drop(['last_pymnt_d', 'last_pymnt_d_date'], axis=1, inplace=True)
Konsep preprocessing yang dilakukan sama dengan yang dilakukan
terhadap variabel earliest_cr_line
data['next_pymnt_d_date'] = pd.to_datetime(data['next_pymnt_d'], format='%b-%y')
data['mths_since_next_pymnt_d'] = round(pd.to_numeric((pd.to_datetime('2017-12-01') - data['next_pymnt_d_date']) / np.timedelta64(1, 'M')))
data['mths_since_next_pymnt_d'].describe()
data.drop(['next_pymnt_d', 'next_pymnt_d_date'], axis=1, inplace=True)
Konsep preprocessing yang dilakukan sama dengan yang dilakukan
terhadap variabel earliest_cr_line
data['last_credit_pull_d_date'] = pd.to_datetime(data['last_credit_pull_d'], format='%b-%y')
data['mths_since_last_credit_pull_d'] = round(pd.to_numeric((pd.to_datetime('2017-12-01') - data['last_credit_pull_d_date']) / np.timedelta64(1, 'M')))
data['mths_since_last_credit_pull_d'].describe()
data.drop(['last_credit_pull_d', 'last_credit_pull_d_date'], axis=1, inplace=True)
plt.figure(figsize=(20,20))
sns.heatmap(data.corr())
Di sini, jika ada pasangan fitur-fitur yang memiliki korelasi tinggi maka akan diambil salah satu saja. Nilai korelasi yang dijadikan patokan sebagai korelasi tinggi tidak pasti, umumnya digunakan angka 0.7.
corr_matrix = data.corr().abs()
upper = corr_matrix.where(np.triu(np.ones(corr_matrix.shape), k=1).astype(np.bool))
to_drop_hicorr = [column for column in upper.columns if any(upper[column] > 0.7)]
to_drop_hicorr
data.drop(to_drop_hicorr, axis=1, inplace=True)
data.select_dtypes(include='object').nunique()
Pada tahap ini dilakukan pembuangan fitur yang memiliki nilai unik yang sangat tinggi (high cardinality) dan fitur yang hanya memiliki satu nilai unik saja.
data.drop(['emp_title', 'title', 'application_type'], axis=1, inplace=True)
data.select_dtypes(exclude='object').nunique()
Ternyata, pada tipe data selain object juga terdapat
fitur yang hanya memiliki satu nilai unik saja, maka akan ikut dibuang
juga.
data.drop(['policy_code'], axis=1, inplace=True)
for col in data.select_dtypes(include='object').columns.tolist():
print(data[col].value_counts(normalize=True)*100)
print('\n')
Fitur yang sangat didominasi oleh salah satu nilai saja akan dibuang pada tahap ini.
data.drop('pymnt_plan', axis=1, inplace=True)
check_missing = data.isnull().sum() * 100 / data.shape[0]
check_missing[check_missing > 0].sort_values(ascending=False)
Di sini, kolom-kolom dengan missing values di atas 75% dibuang
data.drop('mths_since_last_record', axis=1, inplace=True)
data['annual_inc'].fillna(data['annual_inc'].mean(), inplace=True)
data['mths_since_earliest_cr_line'].fillna(0, inplace=True)
data['acc_now_delinq'].fillna(0, inplace=True)
data['total_acc'].fillna(0, inplace=True)
data['pub_rec'].fillna(0, inplace=True)
data['open_acc'].fillna(0, inplace=True)
data['inq_last_6mths'].fillna(0, inplace=True)
data['delinq_2yrs'].fillna(0, inplace=True)
data['collections_12_mths_ex_med'].fillna(0, inplace=True)
data['revol_util'].fillna(0, inplace=True)
data['emp_length_int'].fillna(0, inplace=True)
data['tot_cur_bal'].fillna(0, inplace=True)
data['tot_coll_amt'].fillna(0, inplace=True)
data['mths_since_last_delinq'].fillna(-1, inplace=True)
Semua kolom kategorikal dilakukan One Hot Encoding.
categorical_cols = [col for col in data.select_dtypes(include='object').columns.tolist()]
onehot = pd.get_dummies(data[categorical_cols], drop_first=True)
onehot.head()
Semua kolom numerikal dilakukan proses standarisasi dengan StandardScaler.
numerical_cols = [col for col in data.columns.tolist() if col not in categorical_cols + ['bad_flag']]
from sklearn.preprocessing import StandardScaler
ss = StandardScaler()
std = pd.DataFrame(ss.fit_transform(data[numerical_cols]), columns=numerical_cols)
std.head()
Menggabungkan kembali kolom-kolom hasil transformasi
data_model = pd.concat([onehot, std, data[['bad_flag']]], axis=1)
from sklearn.model_selection import train_test_split
X = data_model.drop('bad_flag', axis=1)
y = data_model['bad_flag']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
X_train.shape, X_test.shape
Pada contoh ini digunakan algoritma Random Forest untuk pemodelan.
from sklearn.ensemble import RandomForestClassifier
rfc = RandomForestClassifier(max_depth=4)
rfc.fit(X_train, y_train)
Feature Importance dapat ditampilkan.
arr_feature_importances = rfc.feature_importances_
arr_feature_names = X_train.columns.values
df_feature_importance = pd.DataFrame(index=range(len(arr_feature_importances)), columns=['feature', 'importance'])
df_feature_importance['feature'] = arr_feature_names
df_feature_importance['importance'] = arr_feature_importances
df_all_features = df_feature_importance.sort_values(by='importance', ascending=False)
df_all_features
Untuk mengukur performa model, dua metrik yang umum dipakai dalam dunia credit risk adalah AUC dan KS.
y_pred_proba = rfc.predict_proba(X_test)[:][:,1]
df_actual_predicted = pd.concat([pd.DataFrame(np.array(y_test), columns=['y_actual']), pd.DataFrame(y_pred_proba, columns=['y_pred_proba'])], axis=1)
df_actual_predicted.index = y_test.index
from sklearn.metrics import roc_curve, roc_auc_score
fpr, tpr, tr = roc_curve(df_actual_predicted['y_actual'], df_actual_predicted['y_pred_proba'])
auc = roc_auc_score(df_actual_predicted['y_actual'], df_actual_predicted['y_pred_proba'])
plt.plot(fpr, tpr, label='AUC = %0.4f' %auc)
plt.plot(fpr, fpr, linestyle = '--', color='k')
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('ROC Curve')
plt.legend()
df_actual_predicted = df_actual_predicted.sort_values('y_pred_proba')
df_actual_predicted = df_actual_predicted.reset_index()
df_actual_predicted['Cumulative N Population'] = df_actual_predicted.index + 1
df_actual_predicted['Cumulative N Bad'] = df_actual_predicted['y_actual'].cumsum()
df_actual_predicted['Cumulative N Good'] = df_actual_predicted['Cumulative N Population'] - df_actual_predicted['Cumulative N Bad']
df_actual_predicted['Cumulative Perc Population'] = df_actual_predicted['Cumulative N Population'] / df_actual_predicted.shape[0]
df_actual_predicted['Cumulative Perc Bad'] = df_actual_predicted['Cumulative N Bad'] / df_actual_predicted['y_actual'].sum()
df_actual_predicted['Cumulative Perc Good'] = df_actual_predicted['Cumulative N Good'] / (df_actual_predicted.shape[0] - df_actual_predicted['y_actual'].sum())
df_actual_predicted.head()
KS = max(df_actual_predicted['Cumulative Perc Good'] - df_actual_predicted['Cumulative Perc Bad'])
plt.plot(df_actual_predicted['y_pred_proba'], df_actual_predicted['Cumulative Perc Bad'], color='r')
plt.plot(df_actual_predicted['y_pred_proba'], df_actual_predicted['Cumulative Perc Good'], color='b')
plt.xlabel('Estimated Probability for Being Bad')
plt.ylabel('Cumulative %')
plt.title('Kolmogorov-Smirnov: %0.4f' %KS)
Model yang dibangun menghasilkan performa AUC = 0.857
dan KS = 0.56. Pada dunia credit risk modeling, umumnya AUC
di atas 0.7 dan KS di atas 0.3 sudah termasuk performa yang baik.
Beberapa hal lain yang dapat dilakukan untuk project ini:
Jika menginginkan interpretabilitas yang lebih tinggi, dapat mempertimbangkan untuk membuat Credit Scorecard dengan menggunakan algoritma Logistic Regression dengan pendekatan-pendekatannya seperti Feature Selection menggunakan Information Value dan Feature Engineering menggunakan Weight of Evidence.
Jika interpretabilitas tidak terlalu dibutuhkan, dapat mempertimbangkan untuk mencoba algoritma-algoritma Machine Learning lainnya seperti Boosting.
Melakukan hyperparameter tuning.
Melakukan pemeriksaan atau memastikan bahwa model yang telah dibuat tidak overfitting. Hal ini dapat dilakukan dengan mencoba membandingkan hasil performa model ketika diprediksi terhadap data training dan ketika diprediksi terhadap data testing.
Umumnya, langkah yang lebih tepat adalah melakukan Train-Test Split terlebih dahulu sebelum melakukan transformasi fitur seperti encoding atau scaling. Namun, karena alasan simplisitas contoh ini melakukan sebaliknya karena umumnya perbedaan performa juga tidak terlalu berbeda.