Credit scoring menjadi salah satu cara untuk melakukan prediksi apakah pengajuan kredit dari seseorang akan memberikan hasil profit dan loss pada lembaga yang memberikan pinjaman. Credit scoring atau penilaian kredit adalah suatu sistem yang diterapkan oleh suatu lembaga pembiayaan seperti bank, fintech, p2p lending atau pihak pemberi pinjaman lainnya untuk menilai profil risiko peminjam terkait kelayakannya mendapat pendanaan atau tidak. Informasi nasabah yang mengajukan pinjaman akan dilengkapi dengan sejumlah persyaratan dokumen yang wajib dilampirkan oleh calon peminjam untuk kemudian diverifikasi keabsahannya oleh lembaga jasa keuangan. Informasi inilah yang kemudian akan menjadi data input untuk dihitung nilai score-nya dan menjadi dasar untuk menentukan pemberian kreditnya.
Hal-hal yang akan dilakukan pada project analisis ini adalah:
Melakukan preprocessing data sebelum dilakukan cross validation dan juga tahapan initial characteristic analysis
Membuat data scaling dengan WOE, melakukan variable selection dengan IV, serta melakukan coarse classing
Mengimplementasikan pemodelan machine learning untuk prediksi scorecard
Membuat penilaian dalam bentuk score point untuk setiap bin yang diakumulasikan menjadi scorecard
Melakukan evaluasi pembentukan scorecard dan prediksi pada data baru.
Pada proyek kali ini, data Source diambil dai kaggle dengan link berikut: https://www.kaggle.com/datasets/atulmittal199174/credit-risk-analysis-for-extending-bank-loans. Dimana jenis Scoring yang akan dilakukan adalah Credit Application, yakni: untuk modeling credit granting decision untuk kasus-kasus pengajuan aplikasi pertama kali.
#import dataset
<- read.csv("data_project/bankloans.csv")
kredit head(kredit,5)
## age ed employ address income debtinc creddebt othdebt default
## 1 41 3 17 12 176 9.3 11.359392 5.008608 1
## 2 27 1 10 6 31 17.3 1.362202 4.000798 0
## 3 40 1 15 14 55 5.5 0.856075 2.168925 0
## 4 41 1 15 14 120 2.9 2.658720 0.821280 0
## 5 24 2 2 0 28 17.3 1.787436 3.056564 1
#cek tipe data
glimpse(kredit)
## Rows: 1,150
## Columns: 9
## $ age <int> 41, 27, 40, 41, 24, 41, 39, 43, 24, 36, 27, 25, 52, 37, 48, 3…
## $ ed <int> 3, 1, 1, 1, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 1, 1, 3, 1…
## $ employ <int> 17, 10, 15, 15, 2, 5, 20, 12, 3, 0, 0, 4, 24, 6, 22, 9, 13, 2…
## $ address <int> 12, 6, 14, 14, 0, 5, 9, 11, 4, 13, 1, 0, 14, 9, 15, 6, 6, 19,…
## $ income <int> 176, 31, 55, 120, 28, 25, 67, 38, 19, 25, 16, 23, 64, 29, 100…
## $ debtinc <dbl> 9.3, 17.3, 5.5, 2.9, 17.3, 10.2, 30.6, 3.6, 24.4, 19.7, 1.7, …
## $ creddebt <dbl> 11.359392, 1.362202, 0.856075, 2.658720, 1.787436, 0.392700, …
## $ othdebt <dbl> 5.008608, 4.000798, 2.168925, 0.821280, 3.056564, 2.157300, 1…
## $ default <int> 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0…
Keterangan kolom data adalah sebagai berikut:
Variabel Prediktor
age: Age of the Customers
ed: Education Level
employ: Work Experience
addres: Address of the Customer
income: Yearly Income of the customer
debtinc: Debt to Income ratio
creddebt: Credit to Debt ratio
othdebt: Other debts
Variabel Target:
#cek missing value pada variabel target
sum(is.na(kredit$default))
## [1] 450
Dari hasil di atas diketahui bahwa sebanyak 450 data pada variabel
target memiliki missing value sehingga terpaksa harus dibuang. Setelah
itu, proses dilanjutkan dengan mengubah kelas data untuk variabel
ed
dan address
menjadi factor.
#membuang row dengan missing value pada variabel target
#perubahan tipe data
<- kredit %>%
kredit drop_na() %>%
mutate(ed = as.factor(ed),
address = as.factor(address))
glimpse(kredit)
## Rows: 700
## Columns: 9
## $ age <int> 41, 27, 40, 41, 24, 41, 39, 43, 24, 36, 27, 25, 52, 37, 48, 3…
## $ ed <fct> 3, 1, 1, 1, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 1, 1, 3, 1…
## $ employ <int> 17, 10, 15, 15, 2, 5, 20, 12, 3, 0, 0, 4, 24, 6, 22, 9, 13, 2…
## $ address <fct> 12, 6, 14, 14, 0, 5, 9, 11, 4, 13, 1, 0, 14, 9, 15, 6, 6, 19,…
## $ income <int> 176, 31, 55, 120, 28, 25, 67, 38, 19, 25, 16, 23, 64, 29, 100…
## $ debtinc <dbl> 9.3, 17.3, 5.5, 2.9, 17.3, 10.2, 30.6, 3.6, 24.4, 19.7, 1.7, …
## $ creddebt <dbl> 11.359392, 1.362202, 0.856075, 2.658720, 1.787436, 0.392700, …
## $ othdebt <dbl> 5.008608, 4.000798, 2.168925, 0.821280, 3.056564, 2.157300, 1…
## $ default <int> 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0…
sum(is.na(kredit$default))
## [1] 0
Dari proses di atas terlihat data yang tersisa seluruhnya sudah memiliki variable target. Proses dilanjutkan dengan mengecek distribusi data pada variabel target.
#Mengecek Distribusi Variabel Target
table(kredit$default) %>%
prop.table()
##
## 0 1
## 0.7385714 0.2614286
Walaupun proporsi dari variabel target agak tidak seimbang, namun untuk tahapan ini akan dibiarkan terlebih dahulu karena akan diproses saat sudah menjadi data train.
#pengecekan distribusi data
describe(kredit)
## variable class count missing_rate unique_count identical_rate min
## <char> <char> <int> <num> <int> <num> <num>
## 1: age integer 700 0 37 0.0629 20.000000
## 2: ed factor 700 0 5 0.5314 NA
## 3: employ integer 700 0 32 0.0886 0.000000
## 4: address factor 700 0 31 0.0843 NA
## 5: income integer 700 0 114 0.0343 14.000000
## 6: debtinc numeric 700 0 231 0.0143 0.400000
## 7: creddebt numeric 700 0 695 0.0029 0.011696
## 8: othdebt numeric 700 0 699 0.0029 0.045584
## 9: default integer 700 0 2 0.7386 0.000000
## p25 p50 p75 max mean sd cv
## <num> <num> <num> <num> <num> <num> <num>
## 1: 29.0000000 34.0000000 40.000000 56.00000 34.8600 7.9973 0.2294
## 2: NA NA NA NA NA NA NA
## 3: 3.0000000 7.0000000 12.000000 31.00000 8.3886 6.6580 0.7937
## 4: NA NA NA NA NA NA NA
## 5: 24.0000000 34.0000000 55.000000 446.00000 45.6014 36.8142 0.8073
## 6: 5.0000000 8.6000000 14.125000 41.30000 10.2606 6.8272 0.6654
## 7: 0.3690593 0.8548695 1.901955 20.56131 1.5536 2.1172 1.3628
## 8: 1.0441782 1.9875675 3.923065 27.03360 3.0582 3.2876 1.0750
## 9: 0.0000000 0.0000000 1.000000 1.00000 0.2614 0.4397 1.6820
Dataset keseluruhan akan dibagi menjadi data Train dan Data Test. Hal ini dilakukan dengan menggunakan fungsi Splitter.
RNGkind(sample.kind= "Rounding")
set.seed(123) # mengunci kerandoman data
#membuat binary split data menjadi set data training dan testing dengan proporsi 80:20
<- initial_split(data = kredit,
splitter prop = 0.8) # data train
# splitting
<- training(splitter)
train <- testing(splitter) test
Data Train dicek keseimbangan data dari variabel targetnya.
table(train$default) %>%
prop.table()
##
## 0 1
## 0.7375 0.2625
Jika datanya dirasa belum seimbang, maka dapat dilakukan adjustment data train dengan teknik SMOTE agar proporsi variabel targetnya menjadi lebih baik
# train <- SmoteClassif(form = default ~ .,
# dat = train,
# C.perc = "balance",
# dist = "HVDM")
Melakukan pengecekan keseimbangan data variabel target dari data train yang baru
# table(train$default) %>%
# prop.table()
Namun kali ini data yang digunakan tetap data awal, dimana proposi 73:26 pada variabel target dianggap masih masuk dalam kategori seimbang.
Membuat Data Binning
<- woebin(dt = train,
binning y = "default",
positive = 0)
## ℹ Creating woe binning ...
## ✔ Binning on 560 rows and 9 columns in 00:00:03
Mengubah Dataframe ke dalam WOE
# data train
<- woebin_ply(dt = train,
train_woe bins = binning)
## ℹ Converting into woe values ...
## ✔ Woe transformating on 560 rows and 8 columns in 00:00:01
# data test
<- woebin_ply(dt = test,
test_woe bins = binning)
## ℹ Converting into woe values ...
## ✔ Woe transformating on 140 rows and 8 columns in 00:00:01
Information Value (IV)
iv(dt = train_woe,
y ='default',
positive = 0)
## variable info_value
## <char> <num>
## 1: debtinc_woe 0.81304157
## 2: employ_woe 0.71084053
## 3: address_woe 0.35987867
## 4: creddebt_woe 0.35331456
## 5: age_woe 0.29780930
## 6: othdebt_woe 0.29680119
## 7: income_woe 0.19543587
## 8: ed_woe 0.06896655
Menurut (Siddiqi, Naeem), skor IV dapat dikategorikan menjadi nilai berikut:
Berdasarkan value IV yang dihasilkan diperoleh kesimpulan bahwa semua variabel dapat digunakan karena memiliki IV > 0.02
Logical Trend and Business Consideration / Coarse Classing
<- woebin_plot(bins = binning)
plot plot
## $age
##
## $ed
##
## $employ
##
## $address
##
## $income
##
## $debtinc
##
## $creddebt
##
## $othdebt
Model akan dibuat dengan memasukkan keseluruhan variabel predictor
<- glm(formula = default ~ .,
model data = train_woe,
family = "binomial")
summary(model)
##
## Call:
## glm(formula = default ~ ., family = "binomial", data = train_woe)
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) -1.0671 0.1283 -8.314 < 2e-16 ***
## age_woe -0.3365 0.2675 -1.258 0.208319
## ed_woe -0.4813 0.5040 -0.955 0.339633
## employ_woe -1.1533 0.2012 -5.731 0.00000000996 ***
## address_woe -1.0684 0.2330 -4.586 0.00000451558 ***
## income_woe -0.3806 0.3430 -1.110 0.267142
## debtinc_woe -0.5873 0.1605 -3.660 0.000252 ***
## creddebt_woe -1.3889 0.2681 -5.181 0.00000022070 ***
## othdebt_woe -0.9225 0.2822 -3.269 0.001078 **
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for binomial family taken to be 1)
##
## Null deviance: 644.73 on 559 degrees of freedom
## Residual deviance: 421.27 on 551 degrees of freedom
## AIC: 439.27
##
## Number of Fisher Scoring iterations: 6
Model kedua akan dibuat hanya menggunakan prediktor dengan signifikasi
<- glm(formula = default ~ employ_woe + address_woe + debtinc_woe + debtinc_woe + creddebt_woe + othdebt_woe,
model_2 data = train_woe,
family = "binomial")
summary(model_2)
##
## Call:
## glm(formula = default ~ employ_woe + address_woe + debtinc_woe +
## debtinc_woe + creddebt_woe + othdebt_woe, family = "binomial",
## data = train_woe)
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) -1.0743 0.1280 -8.393 < 2e-16 ***
## employ_woe -1.3492 0.1802 -7.486 0.0000000000000708 ***
## address_woe -1.1753 0.2175 -5.403 0.0000000656086478 ***
## debtinc_woe -0.6050 0.1558 -3.883 0.000103 ***
## creddebt_woe -1.3604 0.2533 -5.372 0.0000000779960706 ***
## othdebt_woe -0.8587 0.2736 -3.139 0.001698 **
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for binomial family taken to be 1)
##
## Null deviance: 644.73 on 559 degrees of freedom
## Residual deviance: 425.46 on 554 degrees of freedom
## AIC: 437.46
##
## Number of Fisher Scoring iterations: 6
Model Asumsi - Multikolinearitas
Model yang akan digunakan akan dilakukan uji asumsi dengan menggunakan Uji VIF
vif(model_2)
## employ_woe address_woe debtinc_woe creddebt_woe othdebt_woe
## 1.275469 1.098485 1.252619 1.435451 1.209899
Dari hasil uji VIF ditemukan nilai dari setiap variabel < 10 sehingga dapat dikatakan variabel prediktor tidak memiliki multikolinearitas.
Prediction
# Melakukan prediksi pada data test
$pred_risk <- predict(object = model_2,
test_woenewdata = test_woe,
type = "response")
test_woe
## default age_woe ed_woe employ_woe address_woe income_woe
## <int> <num> <num> <num> <num> <num>
## 1: 0 -0.297903627 0.2691962 0.605271061 -0.4650310 0.45336281
## 2: 0 0.562034169 0.2691962 -0.958907034 0.4186621 0.47106239
## 3: 0 0.562034169 0.2691962 0.001881468 0.4186621 0.07456595
## 4: 0 0.562034169 -0.2979036 -0.958907034 1.0082053 -0.36396538
## 5: 0 0.562034169 0.2691962 2.704654612 -0.4650310 0.07456595
## ---
## 136: 0 0.562034169 0.2691962 0.605271061 0.4186621 -0.36396538
## 137: 0 -1.033015006 -0.2265391 -0.958907034 -0.4650310 0.47106239
## 138: 0 0.003076926 0.2691962 2.704654612 0.4186621 0.65646561
## 139: 0 0.562034169 0.2691962 0.605271061 0.4186621 0.07456595
## 140: 0 -1.033015006 0.2691962 -0.958907034 0.2066759 -0.55344193
## debtinc_woe creddebt_woe othdebt_woe pred_risk
## <num> <num> <num> <num>
## 1: -0.8932531 -0.3070780 -0.30259643 0.4684547929
## 2: -0.8932531 -0.3070780 0.82328298 0.4946529678
## 3: 1.0157896 0.3757522 -0.30259643 0.0805516049
## 4: 1.0157896 1.1154194 0.39410135 0.0311890696
## 5: 1.0157896 0.3757522 2.40097220 0.0006330753
## ---
## 136: 1.0157896 -0.1857171 0.54252135 0.0387608309
## 137: 1.0157896 0.3757522 -0.02621027 0.4164655579
## 138: 1.0157896 -0.3070780 -0.30259643 0.0057518111
## 139: -0.1350734 0.3757522 -0.30259643 0.0722494001
## 140: 1.0157896 1.1154194 -0.02621027 0.1059277000
Evaluation Model
<- list(test = test_woe$pred_risk) list_pred
<- list(test = test_woe$default) list_label
perf_eva
untuk melakukan
evaluasiperf_eva(pred = list_pred,
label = list_label,
confusion_matrix = TRUE,
threshold = 0.5,
show_plot = c("ks", "roc"))
## ℹ The threshold of confusion matrix is 0.5000.
## $binomial_metric
## $binomial_metric$test
## MSE RMSE LogLoss R2 KS AUC Gini
## <num> <num> <num> <num> <num> <num> <num>
## 1: 0.1716665 0.4143265 0.520948 0.1138592 0.4590209 0.7724147 0.5448295
##
##
## $confusion_matrix
## $confusion_matrix$test
## label pred_0 pred_1 error
## <char> <num> <num> <num>
## 1: 0 92 9 0.08910891
## 2: 1 26 10 0.72222222
## 3: total 118 19 0.25547445
##
##
## $pic
## TableGrob (1 x 2) "arrange": 2 grobs
## z cells name grob
## 1 1 (1-1,1-1) arrange gtable[layout]
## 2 2 (1-1,2-2) arrange gtable[layout]
KS Statistics
KS (Kolmogorov-Smirnov) Statistics adalah metode statistik yang digunakan untuk mengukur perbedaan antara dua distribusi kumulatif empiris, sering digunakan untuk membandingkan distribusi skor antara dua kelompok, misalnya dalam analisis kinerja credit model (membedakan antara good dan bad customers).
Nilai KS yang tinggi menunjukkan model yang baik dalam membedakan antara kedua kelompok (misalnya, good vs bad customers). Biasanya, nilai KS > 0.4 dianggap baik dalam kredit scoring.
Pada model_2
terlihat dari hasil analisis nilai KS berada
pada angka 0.459 sehingga dapat disimpulkan bahwa model diaanggap baik
dalam kredit scoring.
# membentuk scorecard
<- scorecard(bins = binning,
score_card model = model_2,
odds0 = 1/19,
points0 = 600,
pdo = 20)
Mengubah Karakteristik menjadi Score
<- scorecard_ply(dt = train,
score_train card = score_card,
only_total_score = F)
%>% head() score_train
## employ_points address_points debtinc_points creddebt_points othdebt_points
## <num> <num> <num> <num> <num>
## 1: 24 14 18 -7 -7
## 2: -37 7 -2 15 20
## 3: 24 34 18 15 -8
## 4: 105 14 18 15 20
## 5: 24 -16 -39 -12 -7
## 6: -37 14 -2 15 13
## score
## <num>
## 1: 588
## 2: 549
## 3: 629
## 4: 718
## 5: 496
## 6: 549
<- scorecard_ply(dt = test,
score_test card = score_card,
only_total_score = F)
%>% head() score_test
## employ_points address_points debtinc_points creddebt_points othdebt_points
## <num> <num> <num> <num> <num>
## 1: 24 -16 -16 -12 -7
## 2: -37 14 -16 -12 20
## 3: 0 14 18 15 -7
## 4: -37 34 18 44 10
## 5: 105 -16 18 15 59
## 6: 24 -16 -2 15 -7
## score
## <num>
## 1: 519
## 2: 515
## 3: 586
## 4: 615
## 5: 727
## 6: 560
Dapat dilihat dari setiap karakteristik asli debitur diubah menjadi
bentuk points dan setiap points tersebut dijumlahkan ke dalam kolom
score
yang menyatakan total points dari debitur
tersebut.
Population Stability Index (PSI) adalah suatu metrik yang digunakan dalam analisis kredit dan risiko kredit untuk mengevaluasi stabilitas atau perubahan dalam distribusi poin atau skor kredit dari suatu populasi dari waktu ke waktu.
Langkah-langkah:
Membuat list dengan function list()
<- list(train = score_train$score,
score_list test = score_test$score)
<- list(train = train_woe$default,
label_list test = test_woe$default)
perf_psi()
Parameter:
score
: list berisikan score antara data train dan
testlabel
: list berisikan label antara data train dan test
(tidak wajib)positive
: kelas positive<- perf_psi(score = score_list,
psi label = label_list,
positive = 0)
$psi # psi data frame psi
## variable dataset psi
## <char> <char> <num>
## 1: pred train_test Inf
Menurut Siddqi Naeem, index PSI memiliki rentang:
PSI < 0.10: Biasanya dianggap sebagai tanda bahwa tidak ada perubahan yang signifikan dalam distribusi skor kredit, dan populasi skor cenderung stabil.
PSI 0.10 s.d 0.25: Menunjukkan perubahan yang relatif kecil yang mungkin memerlukan penyelidikan lebih lanjut. Meskipun tidak menciptakan alarm besar, tetapi tetap penting untuk memahami faktor-faktor apa yang mungkin menyebabkan perubahan tersebut.
PSI > 0.25: Menandakan pergeseran yang signifikan dalam distribusi scorecard dan dapat dianggap sebagai sinyal bahwa ada perubahan yang patut diperhatikan dalam profil risiko kredit. Ini mungkin memerlukan analisis mendalam dan tindakan lebih lanjut.
Penentuan seorang debitur termasuk dalam “GOOD” atau “BAD” debitur, harus ditetapkan nilai cutoff (batas) dari total score debitur. Untuk menentukan nilai cutoff dibutuhkan informasi approval rate dan juga bad rate sehingga dapat menentukan persentase debitur yang dianggap GOOD dan juga tingkat risiko dari debitur yang dianggap GOOD.
Untuk mendapatkan nilai approval rate dan bad rate kita membutuhkan
function approval_rate()
.
approval_rate()
akan menampilkan tabel berupa pilihan
nilai cutoff, approval rate, negative rate (bad rate) beserta detail
lainnya. Adapun parameter approval_rate()
antara lain:
score
: Kolom score total dari data debitur.label
: Kolom label yang menyatakan apakah debitur
default/not default.positive
: Kelas positif dari kolom
label
.# using score test
approval_rate(score = score_test$score,
label = test_woe$default,
positive = 0)
## Key: <datset>
## bin approval_rate neg_rate count_approved neg_approved count neg
## <fctr> <num> <num> <int> <int> <int> <int>
## 1: [-Inf,503) 0.9051 0.2258 124 28 13 8
## 2: [503,529) 0.8029 0.2091 110 23 14 5
## 3: [529,549) 0.7153 0.1633 98 16 12 7
## 4: [549,562) 0.6131 0.1429 84 12 14 4
## 5: [562,580) 0.5036 0.1014 69 7 15 5
## 6: [580,586) 0.4234 0.0690 58 4 11 3
## 7: [586,600) 0.3285 0.0444 45 2 13 2
## 8: [600,626) 0.2044 0.0357 28 1 17 1
## 9: [626,650) 0.1168 0.0625 16 1 12 0
## 10: [650, Inf) 0.0000 0.0000 0 0 16 1
## pos
## <int>
## 1: 5
## 2: 9
## 3: 5
## 4: 10
## 5: 10
## 6: 8
## 7: 11
## 8: 16
## 9: 12
## 10: 15
Penentuan nilai cutoff dapat digunakan sebagai acuan apakah seorang debitur dianggap “GOOD” behaviour ketika total points scorecard di atas nilai cutoff, begitupun akan mendapatkan “BAD” behaviour jika total points di bawah nilai cutoff.
Kasus: semisal kita mempunyai debitur dengan karakteristik nilai asli sebagai berikut. Bagaimana keputusan scorenya?
<- data.frame(list(
new_data employ = 40,
address = 4,
debtinc = 12,
creddebt = 0.856075,
othdebt = 3.277652))
new_data
## employ address debtinc creddebt othdebt
## 1 40 4 12 0.856075 3.277652
Untuk mengubah karakteristik nilai asli hingga menjadi total points dan penentuan behaviour, maka perlu dilakukan tahapan sebagai berikut:
scorecard_ply()
Beberapa tahapan ini akan sering digunakan, maka dari itu sebaiknya dibuatkan sebuah fungsi sehingga dapat digunakan berulang kali dengan mudah.
Fungsi yang dibuat bernama predict_behaviour()
dengan
parameter berikut:
data
: Data karakteristik nilai asli debitur yang akan
diprediksi skor dan behaviour.score_card
: Objek pembentukan score_card hasil dari
fungsi scorecard()
cutoff
: Nilai cutoff ditentukan userSelanjutnya tinggal memanggil fungsi predict_behaviour()
dengan memasukkan data debitur beserta objek
score_card
.
# predict behaviour
<- predict_behaviour(data = new_data,
result score_card = score_card,
cutoff = 539)
result
## score recommendation
## <num> <char>
## 1: 641 GOOD
Untuk menggabungkan data asli / karakteristik debitur dengan hasil
score dan rekomendasi menggunakan fuction cbind()
# column bind
cbind(new_data, result)
## employ address debtinc creddebt othdebt score recommendation
## 1 40 4 12 0.856075 3.277652 641 GOOD