1 Behaviour Scoring Scorecard Workflow

Istilah scorecard atau ‘predictive scorecard’ mengacu pada sebuah model kredit kuantitatif yang digunakan untuk menilai kelayakan dan kemampuan bayar peminjam berdasarkan credit score yang dihasilkan.

1.1 Problem Statement

Kita akan mencoba membuat suatu scorecard berdasarkan analisa dari suatu dataset behaviour customer di suatu bank di Taiwan.

1.2 Library loading

Berikut beberapa library yang digunakan selama pemodelan klasifikasi dan pembentukan scorecard

source("package_classification.R")

1.3 Data Preparation

Kita akan mencoba untuk membaca data dengan format excel. Di R, kita bisa gunakan syntax berikut: read_xlsx("lokasi working directory") dari library(readxl) dari data

# read data
data <- read_xlsx("data_input/credit_taiwan.xlsx")
data

Dataset memiliki 25 kolom dan 30,952 dengan penjelasan sebagai berikut:

  • id = id debitur
  • limit_bal = Besaran kredit limit yang diberikan dalam dolar NT
  • sex = jenis kelamin
    • 1 = laki-laki
    • 2 = perempuan
  • education = Pendidikan terakhir
    • 1 = pascasarjana (s2 & s3)
    • 2 = universitas (s1)
    • 3 = high school (SMA)
    • 4 = lain-lain
  • marriage = Status pernikahan
    • 1 = menikah
    • 2 = lajang
    • 3 = lainnya
  • age = Usia dalam tahun
  • pay_* = Status pembayaran dalam bulan April (1) - September (6).
    • 0 = pembayaran tepat waktu
    • 1 = keterlambatan pembayaran satu bulan
    • 2 = keterlambatan pembayaran dua bulan
    • 8 = keterlambatan pembayaran delapan bulan atau lebih
  • bill_amt* = Jumlah tagihan pada bulan April (1) - September (6) dalam dolar NT
  • pay_amt* = Jumlah pembayaran/pengeluaran sebelumnya pada bulan April (1) - September(6) dalam dolar NT
  • gb_flag = Flagging pembayaran default (gagal bayar) pada bulan berikutnya
    • 1 = default
    • 0 = not default

1.4 Data Cleansing

data_clean <- data %>% 
  select(-id) %>% 
  mutate_at(.vars = c("sex", "education", "marriage", "gb_flag"), as.factor)
glimpse(data_clean)
#> Rows: 30,952
#> Columns: 24
#> $ limit_bal <dbl> 20000, 100000, 80000, 280000, 130000, 340000, 260000, 20000,…
#> $ sex       <fct> 2, 1, 2, 2, 1, 2, 1, 2, 1, 2, 2, 2, 2, 1, 2, 2, 1, 2, 2, 2, …
#> $ education <fct> 3, 3, 1, 2, 2, 2, 3, 3, 2, 3, 1, 2, 4, 2, 2, 2, 1, 1, 3, 3, …
#> $ marriage  <fct> 2, 2, 2, 1, 1, 2, 1, 2, 1, 2, 2, 2, 2, 1, 1, 2, 1, 1, 1, 1, …
#> $ age       <dbl> 39, 49, 26, 31, 53, 47, 49, 54, 35, 27, 31, 28, 38, 36, 40, …
#> $ pay_1     <dbl> 0, 0, 2, 0, 2, 0, 0, 0, 0, 1, 0, 0, 1, 2, 2, 0, 0, 1, 0, 0, …
#> $ pay_2     <dbl> 0, 0, 0, 0, 2, 0, 0, 0, 0, 4, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, …
#> $ pay_3     <dbl> 2, 0, 0, 0, 2, 0, 0, 0, 0, 3, 0, 0, 0, 2, 0, 2, 0, 0, 0, 0, …
#> $ pay_4     <dbl> 2, 0, 2, 2, 0, 0, 0, 0, 2, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, …
#> $ pay_5     <dbl> 3, 0, 2, 2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, …
#> $ pay_6     <dbl> 2, 0, 2, 2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, …
#> $ bill_amt1 <dbl> 12241, 1440, 37097, 127609, 109994, 196112, 8654, 16120, 105…
#> $ bill_amt2 <dbl> 16020, 0, 38174, 76057, 125138, 183453, 15260, 17160, 11312,…
#> $ bill_amt3 <dbl> 16457, 0, 40550, 75377, 114344, 167592, 6790, 18083, 13050, …
#> $ bill_amt4 <dbl> 20906.0, 0.0, 41577.0, 68277.0, 96995.0, 162628.0, 10070.0, …
#> $ bill_amt5 <dbl> 20289.0, 0.0, 41595.0, 72042.0, 100086.0, 142779.0, 585.0, 1…
#> $ bill_amt6 <dbl> 20407, 0, 43264, 65921, 92344, 139548, 435, 15720, 13179, 48…
#> $ pay_amt1  <dbl> 4000, 0, 2000, 6000, 17100, 6446, 15328, 1308, 1200, 0, 586,…
#> $ pay_amt2  <dbl> 1000, 0, 3000, 6000, 17, 6207, 6914, 1231, 1900, 0, 3660, 60…
#> $ pay_amt3  <dbl> 4750, 0, 2000, 0, 4000, 5758, 10098, 1244, 0, 0, 5754, 5000,…
#> $ pay_amt4  <dbl> 0, 0, 1000, 4800, 5000, 4853, 585, 544, 1600, 1790, 5666, 50…
#> $ pay_amt5  <dbl> 600, 0, 2500, 0, 4000, 4940, 435, 565, 0, 1890, 5666, 4000, …
#> $ pay_amt6  <dbl> 0, 0, 1000, 2226, 4000, 4600, 427, 1500, 500, 1740, 6246, 50…
#> $ gb_flag   <fct> 1, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, …

1.5 Data Preprocessing - Cross Validation

Cross Validation adalah metode yang kita gunakan untuk mengetahui seberapa baik performa model kita memprediksi terhadap data baru.

Lantas, bagaimana cara mengetahui apakah model yang kita buat telah baik dalam memprediksi data baru? Di sinilah mengapa kita melakukan Train-test splitting. Kita membagi data kita menjadi 2 kelompok, yaitu data train dan test.

Fungsi initial_split() dari paket rsample memiliki beberapa parameter:

  • data = data frame awal
  • prop = proporsi dari data yang akan digunakan sebagai data train (80:20 atau 70:30)

Selanjutnya, fungsi training() and testing() dilakukan untuk mengekstrak data train dan test dari hasil split.

# dipakai untuk melakukan CV satu paket dengan set seed
RNGkind(sample.kind= "Rounding")
set.seed(123)
# membuat binary split data menjadi set data training dan testing
splitter <- initial_split(data_clean, prop = 0.8) # 0.8 di data train, 0.2 di data test

# splitting
train <- training(splitter)
test <- testing(splitter)
test

1.6 Initial Characteristic Analysis

Initial Characteristic Analysis merupakan langkah awal dalam mengidentifikasi dan menganalisis karakteristik atau variabel yang potensial untuk dimasukkan ke dalam model behaviour scoring. Tujuan utama dari langkah ini adalah untuk memahami hubungan antara variabel-variabel yang tersedia dengan perilaku pembayaran kredit yang akan diprediksi oleh skor kredit.

1.6.1 Weight of Evidence (WoE)

Pembentukan WOE bertujuan melakukan binning / kategorisasi seluruh variabel prediktor yang ada pada data, kemudian menghitung kekuatan masing-masing hasil kategori dalam memisahkan kelas positif dan kelas negatif.

📐 Formula WOE:

\[ ln(\frac{Distr_{Good}}{Distr_{Bad}}) \]

Keterangan:

  • \(Distr_{Good}\) : proporsi target debitur good pada kategori bin
  • \(Distr_{Bad}\) : proporsi target debitur bad pada kategori bin

Note: Nilai WoE negatif menunjukkan bahwa banyak sebaran kelas negatif pada kelompok tersebut -> kekuatannya negatif. Begitupula sebaliknya.

📌 Cara membuat binning dan menghitung WOE:

Gunakan fungsi woebin(), dengan parameter:

  • dt : data frame train
  • y : nama variabel y pada data
  • positive: kelas positive (debitur good)
binning <- woebin(dt = train,
                  y = "gb_flag",
                  positive = 1)
#> ✔ Binning on 24761 rows and 24 columns in 00:00:07
binning
#> $limit_bal
#>     variable             bin count count_distr  neg  pos   posprob        woe
#> 1: limit_bal    [-Inf,50000)  4862  0.19635717 1296 3566 0.7334430  1.1717014
#> 2: limit_bal  [50000,150000)  9973  0.40277049 4550 5423 0.5437682  0.3350614
#> 3: limit_bal [150000,360000)  7744  0.31274989 5663 2081 0.2687242 -0.8415657
#> 4: limit_bal   [360000, Inf)  2182  0.08812245 1857  325 0.1489459 -1.5833529
#>        bin_iv  total_iv breaks is_special_values
#> 1: 0.25306622 0.6761588  50000             FALSE
#> 2: 0.04539898 0.6761588 150000             FALSE
#> 3: 0.20287037 0.6761588 360000             FALSE
#> 4: 0.17482326 0.6761588    Inf             FALSE
#> 
#> $sex
#>    variable bin count count_distr  neg  pos   posprob        woe      bin_iv
#> 1:      sex   1 10226   0.4129882 5118 5108 0.4995111  0.1575837 0.010298870
#> 2:      sex   2 14535   0.5870118 8248 6287 0.4325421 -0.1119472 0.007316302
#>      total_iv breaks is_special_values
#> 1: 0.01761517      1             FALSE
#> 2: 0.01761517      2             FALSE
#> 
#> $education
#>     variable   bin count count_distr  neg  pos   posprob        woe      bin_iv
#> 1: education     1  7962   0.3215541 5087 2875 0.3610902 -0.4110961 0.052739067
#> 2: education     2 12073   0.4875813 5979 6094 0.5047627  0.1785909 0.015620792
#> 3: education 3%,%4  4726   0.1908647 2300 2426 0.5133305  0.2128742 0.008689948
#>      total_iv breaks is_special_values
#> 1: 0.07704981      1             FALSE
#> 2: 0.07704981      2             FALSE
#> 3: 0.07704981  3%,%4             FALSE
#> 
#> $marriage
#>    variable   bin count count_distr  neg  pos   posprob         woe
#> 1: marriage 0%,%1 11369   0.4591495 6086 5283 0.4646847  0.01804257
#> 2: marriage 2%,%3 13392   0.5408505 7280 6112 0.4563919 -0.01533730
#>          bin_iv    total_iv breaks is_special_values
#> 1: 0.0001495723 0.000276718  0%,%1             FALSE
#> 2: 0.0001271457 0.000276718  2%,%3             FALSE
#> 
#> $age
#>    variable       bin count count_distr  neg  pos   posprob         woe
#> 1:      age [-Inf,26)  3662  0.14789387 1550 2112 0.5767340  0.46891994
#> 2:      age   [26,28)  2274  0.09183797 1212 1062 0.4670185  0.02742154
#> 3:      age   [28,46) 14924  0.60272202 8607 6317 0.4232779 -0.14979191
#> 4:      age [46, Inf)  3901  0.15754614 1997 1904 0.4880800  0.11185039
#>           bin_iv   total_iv breaks is_special_values
#> 1: 0.03253299297 0.04799841     26             FALSE
#> 2: 0.00006912784 0.04799841     28             FALSE
#> 3: 0.01341856563 0.04799841     46             FALSE
#> 4: 0.00197772528 0.04799841    Inf             FALSE
#> 
#> $pay_1
#>    variable      bin count count_distr   neg  pos   posprob        woe
#> 1:    pay_1 [-Inf,1) 15681   0.6332943 11152 4529 0.2888209 -0.7415782
#> 2:    pay_1    [1,2)  5120   0.2067768  1286 3834 0.7488281  1.2519115
#> 3:    pay_1 [2, Inf)  3960   0.1599289   928 3032 0.7656566  1.3434855
#>       bin_iv  total_iv breaks is_special_values
#> 1: 0.3239961 0.8889654      1             FALSE
#> 2: 0.3007706 0.8889654      2             FALSE
#> 3: 0.2641987 0.8889654    Inf             FALSE
#> 
#> $pay_2
#>    variable      bin count count_distr   neg  pos   posprob        woe
#> 1:    pay_2 [-Inf,2) 17485   0.7061508 11743 5742 0.3283958 -0.5559102
#> 2:    pay_2 [2, Inf)  7276   0.2938492  1623 5653 0.7769379  1.4074496
#>       bin_iv  total_iv breaks is_special_values
#> 1: 0.2082814 0.7356067      2             FALSE
#> 2: 0.5273253 0.7356067    Inf             FALSE
#> 
#> $pay_3
#>    variable      bin count count_distr   neg  pos   posprob        woe
#> 1:    pay_3 [-Inf,2) 18311   0.7395097 12086 6225 0.3399596 -0.5039348
#> 2:    pay_3 [2, Inf)  6450   0.2604903  1280 5170 0.8015504  1.5555521
#>       bin_iv  total_iv breaks is_special_values
#> 1: 0.1803796 0.7371777      2             FALSE
#> 2: 0.5567980 0.7371777    Inf             FALSE
#> 
#> $pay_4
#>    variable      bin count count_distr   neg  pos   posprob        woe
#> 1:    pay_4 [-Inf,1) 19538   0.7890634 12699 6839 0.3500358 -0.4593422
#> 2:    pay_4 [1, Inf)  5223   0.2109366   667 4556 0.8722956  2.0809498
#>       bin_iv  total_iv breaks is_special_values
#> 1: 0.1607338 0.8889034      1             FALSE
#> 2: 0.7281696 0.8889034    Inf             FALSE
#> 
#> $pay_5
#>    variable      bin count count_distr   neg  pos   posprob        woe
#> 1:    pay_5 [-Inf,2) 20251   0.8178587 12889 7362 0.3635376 -0.4005031
#> 2:    pay_5 [2, Inf)  4510   0.1821413   477 4033 0.8942350  2.2942888
#>       bin_iv  total_iv breaks is_special_values
#> 1: 0.1274559 0.8575895      2             FALSE
#> 2: 0.7301335 0.8575895    Inf             FALSE
#> 
#> $pay_6
#>    variable      bin count count_distr   neg  pos   posprob       woe    bin_iv
#> 1:    pay_6 [-Inf,2) 20166   0.8144259 12721 7445 0.3691858 -0.376172 0.1122446
#> 2:    pay_6 [2, Inf)  4595   0.1855741   645 3950 0.8596300  1.971760 0.5883466
#>     total_iv breaks is_special_values
#> 1: 0.7005912      2             FALSE
#> 2: 0.7005912    Inf             FALSE
#> 
#> $bill_amt1
#>     variable          bin count count_distr  neg  pos   posprob        woe
#> 1: bill_amt1  [-Inf,5000)  5553  0.22426396 4145 1408 0.2535566 -0.9201930
#> 2: bill_amt1 [5000,10000)  2161  0.08727434 1279  882 0.4081444 -0.2121022
#> 3: bill_amt1 [10000, Inf) 17047  0.68846169 7942 9105 0.5341116  0.2961981
#>         bin_iv  total_iv breaks is_special_values
#> 1: 0.171664081 0.2362164   5000             FALSE
#> 2: 0.003878966 0.2362164  10000             FALSE
#> 3: 0.060673346 0.2362164    Inf             FALSE
#> 
#> $bill_amt2
#>     variable            bin count count_distr  neg  pos   posprob        woe
#> 1: bill_amt2    [-Inf,5000)  5719   0.2309681 4335 1384 0.2420003 -0.9822042
#> 2: bill_amt2   [5000,15000)  3616   0.1460361 2072 1544 0.4269912 -0.1345984
#> 3: bill_amt2 [15000,115000) 12428   0.5019183 5792 6636 0.5339556  0.2955712
#> 4: bill_amt2  [115000, Inf)  2998   0.1210775 1167 1831 0.6107405  0.6099654
#>         bin_iv  total_iv breaks is_special_values
#> 1: 0.199263323 0.2906929   5000             FALSE
#> 2: 0.002627653 0.2906929  15000             FALSE
#> 3: 0.044046722 0.2906929 115000             FALSE
#> 4: 0.044755251 0.2906929    Inf             FALSE
#> 
#> $bill_amt3
#>     variable            bin count count_distr  neg  pos   posprob        woe
#> 1: bill_amt3    [-Inf,5000)  5785  0.23363354 4395 1390 0.2402766 -0.9916243
#> 2: bill_amt3   [5000,10000)  2035  0.08218570 1247  788 0.3872236 -0.2994583
#> 3: bill_amt3 [10000,155000) 15227  0.61495901 7126 8101 0.5320155  0.2877770
#> 4: bill_amt3  [155000, Inf)  1714  0.06922176  598 1116 0.6511085  0.7834549
#>         bin_iv  total_iv breaks is_special_values
#> 1: 0.205103665 0.3051728   5000             FALSE
#> 2: 0.007229909 0.3051728  10000             FALSE
#> 3: 0.051161575 0.3051728 155000             FALSE
#> 4: 0.041677690 0.3051728    Inf             FALSE
#> 
#> $bill_amt4
#>     variable            bin count count_distr  neg  pos   posprob        woe
#> 1: bill_amt4    [-Inf,5000)  6019  0.24308388 4563 1456 0.2419006 -0.9827478
#> 2: bill_amt4   [5000,10000)  2036  0.08222608 1252  784 0.3850688 -0.3085490
#> 3: bill_amt4 [10000,135000) 14734  0.59504867 6904 7830 0.5314239  0.2854011
#> 4: bill_amt4  [135000, Inf)  1972  0.07964137  647 1325 0.6719067  0.8763610
#>         bin_iv total_iv breaks is_special_values
#> 1: 0.209927969 0.325774   5000             FALSE
#> 2: 0.007673121 0.325774  10000             FALSE
#> 3: 0.048691988 0.325774 135000             FALSE
#> 4: 0.059480954 0.325774    Inf             FALSE
#> 
#> $bill_amt5
#>     variable            bin count count_distr  neg  pos   posprob        woe
#> 1: bill_amt5   [-Inf,10000)  8454  0.34142401 6128 2326 0.2751360 -0.8091789
#> 2: bill_amt5 [10000,130000) 14391  0.58119624 6639 7752 0.5386700  0.3145290
#> 3: bill_amt5  [130000, Inf)  1916  0.07737975  599 1317 0.6873695  0.9473896
#>        bin_iv  total_iv breaks is_special_values
#> 1: 0.20581636 0.3305998  10000             FALSE
#> 2: 0.05774448 0.3305998 130000             FALSE
#> 3: 0.06703900 0.3305998    Inf             FALSE
#> 
#> $bill_amt6
#>     variable            bin count count_distr  neg  pos   posprob        woe
#> 1: bill_amt6   [-Inf,10000)  9013  0.36399984 6509 2504 0.2778209 -0.7957569
#> 2: bill_amt6 [10000,130000) 13966  0.56403215 6309 7657 0.5482601  0.3531826
#> 3: bill_amt6  [130000, Inf)  1782  0.07196801  548 1234 0.6924804  0.9712804
#>        bin_iv  total_iv breaks is_special_values
#> 1: 0.21265520 0.3486324  10000             FALSE
#> 2: 0.07061633 0.3486324 130000             FALSE
#> 3: 0.06536092 0.3486324    Inf             FALSE
#> 
#> $pay_amt1
#>    variable          bin count count_distr  neg  pos   posprob            woe
#> 1: pay_amt1   [-Inf,500)  5963  0.24082226 2675 3288 0.5514003  0.36586960324
#> 2: pay_amt1   [500,5000) 12696  0.51274181 6853 5843 0.4602237  0.00009735736
#> 3: pay_amt1 [5000,16000)  4838  0.19538791 2864 1974 0.4080198 -0.21261780170
#> 4: pay_amt1 [16000, Inf)  1264  0.05104802  974  290 0.2294304 -1.05199087427
#>               bin_iv   total_iv breaks is_special_values
#> 1: 0.032347606745362 0.09096089    500             FALSE
#> 2: 0.000000004860019 0.09096089   5000             FALSE
#> 3: 0.008726079449159 0.09096089  16000             FALSE
#> 4: 0.049887202211970 0.09096089    Inf             FALSE
#> 
#> $pay_amt2
#>    variable          bin count count_distr  neg  pos   posprob         woe
#> 1: pay_amt2   [-Inf,500)  5887  0.23775292 2797 3090 0.5248853  0.25916318
#> 2: pay_amt2   [500,9500) 16208  0.65457776 8653 7555 0.4661278  0.02384302
#> 3: pay_amt2 [9500,15500)  1384  0.05589435  857  527 0.3807803 -0.32669786
#> 4: pay_amt2 [15500, Inf)  1282  0.05177497 1059  223 0.1739470 -1.39836907
#>          bin_iv total_iv breaks is_special_values
#> 1: 0.0160446007 0.105683    500             FALSE
#> 2: 0.0003724565 0.105683   9500             FALSE
#> 3: 0.0058379460 0.105683  15500             FALSE
#> 4: 0.0834279536 0.105683    Inf             FALSE
#> 
#> $pay_amt3
#>    variable          bin count count_distr   neg  pos   posprob         woe
#> 1: pay_amt3  [-Inf,5000) 19465  0.78611526 10009 9456 0.4857950  0.10270428
#> 2: pay_amt3  [5000,6000)  1252  0.05056339   779  473 0.3777955 -0.33937615
#> 3: pay_amt3 [6000,12500)  2457  0.09922863  1377 1080 0.4395604 -0.08340667
#> 4: pay_amt3 [12500, Inf)  1587  0.06409273  1201  386 0.2432262 -0.97553295
#>          bin_iv   total_iv breaks is_special_values
#> 1: 0.0083187700 0.06930934   5000             FALSE
#> 2: 0.0056922797 0.06930934   6000             FALSE
#> 3: 0.0006876199 0.06930934  12500             FALSE
#> 4: 0.0546106750 0.06930934    Inf             FALSE
#> 
#> $pay_amt4
#>    variable          bin count count_distr  neg  pos   posprob         woe
#> 1: pay_amt4   [-Inf,800)  8915   0.3600420 4763 4152 0.4657319  0.02225193
#> 2: pay_amt4   [800,4400) 10587   0.4275675 5378 5209 0.4920185  0.12761085
#> 3: pay_amt4 [4400,13800)  3893   0.1572231 2223 1670 0.4289751 -0.12649450
#> 4: pay_amt4 [13800, Inf)  1366   0.0551674 1002  364 0.2664714 -0.85305991
#>          bin_iv   total_iv breaks is_special_values
#> 1: 0.0001784249 0.04636773    800             FALSE
#> 2: 0.0069887500 0.04636773   4400             FALSE
#> 3: 0.0024997863 0.04636773  13800             FALSE
#> 4: 0.0367007677 0.04636773    Inf             FALSE
#> 
#> $pay_amt5
#>    variable          bin count count_distr  neg  pos   posprob          woe
#> 1: pay_amt5  [-Inf,1000)  9550  0.38568717 5170 4380 0.4586387 -0.006284458
#> 2: pay_amt5 [1000,14000) 13928  0.56249748 7205 6723 0.4826967  0.090298760
#> 3: pay_amt5 [14000, Inf)  1283  0.05181535  991  292 0.2275916 -1.062421226
#>           bin_iv  total_iv breaks is_special_values
#> 1: 0.00001522863 0.0561618   1000             FALSE
#> 2: 0.00459993581 0.0561618  14000             FALSE
#> 3: 0.05154663223 0.0561618    Inf             FALSE
#> 
#> $pay_amt6
#>    variable         bin count count_distr   neg  pos   posprob         woe
#> 1: pay_amt6 [-Inf,5000) 20048  0.80966035 10340 9708 0.4842378  0.09646993
#> 2: pay_amt6 [5000,9800)  2607  0.10528654  1463 1144 0.4388186 -0.08641872
#> 3: pay_amt6 [9800, Inf)  2106  0.08505311  1563  543 0.2578348 -0.89771350
#>          bin_iv   total_iv breaks is_special_values
#> 1: 0.0075582201 0.07054034   5000             FALSE
#> 2: 0.0007831196 0.07054034   9800             FALSE
#> 3: 0.0621989972 0.07054034    Inf             FALSE

Output hasil woebin:

  • variable : nama variabel
  • bin : hasil binning kategori
  • count : jumlah data pada masing-masing bin
  • count_distr : distribusi masing-masing bin/total data
  • neg : banyaknya kelas negatif pada bin (default)
  • pos : banyaknya kelas postif pada bin
  • posprob : pos/count masing-masing bin
  • woe : nilai woe untuk bin tersebut.

Mari kita pahami menggunakan variable pay_2!

binning$pay_2

Perhitungan manual:

# WOE pay 2
## binning 1
distr_good_1 <- binning$pay_2$pos[1] / sum(binning$pay_2$pos)
distr_bad_1 <- binning$pay_2$neg[1] / sum(binning$pay_2$neg)
log(distr_good_1/distr_bad_1)
#> [1] -0.5559102
## binning 2
distr_good_2 <- binning$pay_2$pos[2] / sum(binning$pay_2$pos)
distr_bad_2 <- binning$pay_2$neg[2] / sum(binning$pay_2$neg)
log(distr_good_2/distr_bad_2)
#> [1] 1.40745

💡 Insight:

  • Bin dengan nilai pay_2 = [-Inf,2) sebesar 0.5559102 menunjukkan sebaran kelas positif > sebaran kelas negatif pada kategori tersebut
  • Bin dengan nilai pay_2 = [2, Inf) sebesar -1.40745 menunjukkan debitur dengan kategori ini memiliki sebaran kelas negatif > sebaran kelas positif.

1.6.1.1 Mengubah dataframe ke dalam WOE

Setelah kita mengetahui nilai WOE dan binning pada data, nilai data/values di data train maupun test kita perlu dikonversi ke nilai WOE sesuai kategori/bins yang sudah dihitung. Kita bisa gunakan fungsi woebin_ply() dari library scorecard, dengan parameter:

  • dt : dataframe yang akan ditransformasi -> data train
  • bins : informasi binning yang dihasilkan dari woebin
# data train
train_woe <- woebin_ply(dt = train,
                        bins = binning)
#> ✔ Woe transformating on 24761 rows and 23 columns in 00:00:01
nrow(train_woe)
#> [1] 24761
# data test
test_woe <- woebin_ply(dt = test,
                       bins = binning)
#> ✔ Woe transformating on 6191 rows and 23 columns in 00:00:00

1.6.2 Information Value (IV)

Information Value digunakan untuk melihat feature importance pada data behavior kita. Nilai IV yang tinggi mengindikasikan kekuatan suatu feature/variabel dalam memprediksi target.

📐 Formula IV:

\[ \sum_{i=1}^n (Distr_{Good_i} - Distr_{Bad_i}) \times ln(\frac{Distr_{Good_i}}{Distr_{Bad_i}}) \]

📌 Cara mengeluarkan nilai information value (iv) pada data:

Gunakan fungsi iv(), dengan parameter:

  • dt : data frame train hasil woe
  • y : nama variabel y pada data
  • positive: kelas positive (debitur good)
iv(dt = train_woe,
   y = "gb_flag",
   positive = 1)

Menurut (Siddiqi, Naeem), skor IV dapat dikategorikan menjadi nilai berikut:

  • IV dibawah 0.02 -> unpredictive
  • IV diantara 0.02 - 0.1 -> ‘weak’
  • IV diantara 0.1 sampai 0.3 dikategorikan sebagai ‘medium’
  • IV diatas 0.3 dikategorikan sebagai ‘strong’

❓ Nilai IV yang sudah kita dapat digunakan sebagai feature elimination, variabel apa saja yang akan dieliminasi?

  • sex
  • marriage
# hasil data setelah feature elimination
train_woe_final <- train_woe %>% 
  select(-sex_woe, -marriage_woe)

test_woe_final <- test_woe %>% 
  select(-sex_woe, -marriage_woe)

1.7 Modeling - Logistic Regression

1.7.1 📐 Theory - Linear Regression vs Logistic Regression

Logistic regression merupakan salah satu metode klasifikasi yang konsepnya hampir mirip dengan regresi linear. Hanya saja, dalam binary logistic regression tidak menghitung secara spesifik nilai prediksi target variabel, namun menghitung kemungkinan pada masing-masing kelas target.

  • Range regression: -inf s.d inf
  • Range probability: 0 s.d 1

Persamaan linear regression

\[ y = b_0 + b_1x \]

Untuk mengubah nilai linear ke dalam nilai logistic dibutuhkan fungsi logit:

\[ y = log(\frac{p}{1-p}) \]

Nilai y ini merupakan nilai log of odds. Nilai ini tidak bisa diinterpretasikan. Sehingga terkadang harus diubah terlebih dahulu ke dalam nilai odds

\[ odds = \frac{p}{1-p} \]

Selanjutnya elakukan substitusi nilai logit/log of odds ke dalam persamaan linear.

\[ log(\frac{p}{1-p}) = b_0 + b_1x \]

Output dari hasil prediksi model logistic regression adalah probability atau peluang. Dalam bentuk seperti berikut:

\[ p = \frac{1}{1+e^{-y}} = \frac{1}{1+e^{-(b_0 + b_1x)}} \]

model <- glm(gb_flag ~ .,
             family = "binomial",  
             data = train_woe_final)

summary(model) 
#> 
#> Call:
#> glm(formula = gb_flag ~ ., family = "binomial", data = train_woe_final)
#> 
#> Coefficients:
#>               Estimate Std. Error z value Pr(>|z|)    
#> (Intercept)   -0.09518    0.01826  -5.212 1.87e-07 ***
#> limit_bal_woe  0.79347    0.02492  31.844  < 2e-16 ***
#> education_woe  0.18240    0.06431   2.836 0.004566 ** 
#> age_woe        0.04454    0.07850   0.567 0.570502    
#> pay_1_woe      0.60082    0.02105  28.547  < 2e-16 ***
#> pay_2_woe      0.10380    0.02752   3.772 0.000162 ***
#> pay_3_woe      0.34498    0.02548  13.540  < 2e-16 ***
#> pay_4_woe      0.36329    0.02441  14.882  < 2e-16 ***
#> pay_5_woe      0.28930    0.02651  10.913  < 2e-16 ***
#> pay_6_woe      0.35695    0.02622  13.611  < 2e-16 ***
#> bill_amt1_woe -0.72112    0.07208 -10.005  < 2e-16 ***
#> bill_amt2_woe  0.67595    0.08265   8.179 2.87e-16 ***
#> bill_amt3_woe  0.28239    0.07988   3.535 0.000408 ***
#> bill_amt4_woe  0.31885    0.07642   4.172 3.01e-05 ***
#> bill_amt5_woe  0.17014    0.06859   2.481 0.013116 *  
#> bill_amt6_woe  0.55784    0.05549  10.054  < 2e-16 ***
#> pay_amt1_woe   0.57165    0.07975   7.168 7.59e-13 ***
#> pay_amt2_woe   0.49593    0.06866   7.223 5.08e-13 ***
#> pay_amt3_woe   0.12716    0.07939   1.602 0.109212    
#> pay_amt4_woe  -0.31722    0.08895  -3.566 0.000362 ***
#> pay_amt5_woe   0.26646    0.07771   3.429 0.000606 ***
#> pay_amt6_woe   0.05179    0.07377   0.702 0.482632    
#> ---
#> Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
#> 
#> (Dispersion parameter for binomial family taken to be 1)
#> 
#>     Null deviance: 34169  on 24760  degrees of freedom
#> Residual deviance: 21294  on 24739  degrees of freedom
#> AIC: 21338
#> 
#> Number of Fisher Scoring iterations: 5

Nagelkereke R squared

\[ 1- \frac{D}{D_0} \]

1- (21294/34169)
#> [1] 0.3768035

1.8 Prediction

Setelah model didapatkan, prediksi dapat dilakukan terhadap data test yang sudah ditransformasi ke dalam WOE. Prediksi ini dilakukan dengan tujuan 2 hal:

  • Mengetahui hasil prediksi debitur akan gagal bayar pinjaman (loan default) pada bulan berikutnya atau tidak
  • Memberikan hasil evaluasi dari model apakah prediksi yang dihasilkan sudah akurat dengan realitanya atau belum.

Pada tahapan ini dapat menggunakan fungsi predict.

📌 Syntax: predict(model, newdata, type = "response")

Keterangan:

  • model : model logistic regression
  • new_data : data untuk diprediksi bisa data test ataupun data baru
  • type = "response" untuk menghasilkan prediksi berupa peluang

❓ Prediksikan data test hasil transformasi WOE. Simpan pada kolom baru bernama pred_Risk

# Melakukan prediksi terhadap data test
test_woe_final$pred_Risk <- predict(model, 
                                    newdata = test_woe_final,
                                    type = "response")
head(test_woe_final)

📌 Ingat kembali, keluaran dari model logistic regression adalah dalam bentuk peluang.

Sehingga ntuk menyatakan prediksi jatuh ke kelas default(1) atau not default(0) dapat menggunakan threshold/ambang batas.

❓ Klasifikasikan data test_woe_final berdasarkan pred_Risk dan simpan ke dalam kolom baru bernama pred_label

# case when
test_woe_final <- test_woe_final %>% 
  mutate(pred_label = case_when(pred_Risk > 0.5 ~ 1,
                                TRUE ~ 0))
tail(test_woe_final)

1.9 Scorecard

Pada model logistic regression, output yang dihasilkan berupa prediksi apakah default atau tidak default dalam bentuk probabilitas.

Untuk mempremudah pengambilan keputusan apakah seorang debitur akan default dan tidak default maka hasil binning dapat dibuat menjadi penilaian dalam bentuk score point untuk setiap bin yang diakumulasikan menjadi scorecard.

Manfaat pembentukan scorecard antara lain:

  • Pengambilan keputusan yang konsisten: Hal ini mengurangi tingkat subjektivitas dalam proses pengambilan keputusan dan memberikan dasar yang lebih objektif untuk menilai kredit.
  • Penilaian risiko individu: Scorecard membantu memberikan gambaran tentang seberapa besar risiko default yang dimiliki oleh debitur berdasarkan karakteristik peminjam.
  • Pengelolaan risiko lebih baik: Dengan scorecard dapat mengidentifikasi risiko, dan menentukan keketatan risiko berdasarkan pemahaman yang lebih mendalam tentang profil risiko debitur.

Rumus umum pembentukan Points score dalam setiap binning (Siddiqi, Naeem)

\[ Points_{ij} = (\frac{pdo}{ln(2)}) \times -Coef_i \times WOE_{ij} \]

Keterangan:

  • Coef : Hasil nilai coefficient dari logistic regression
  • WOE : Nilai WOE dari setiap binning untuk setiap variabelnya

📌 Untuk membuat scorecard dapat menggunakan fungsi scorecard() dengan parameter:

  • bins: object binning hasil woebin()
  • model: object model hasil glm()
  • pdo: points to double the odds, ketika skor kredit suatu individu naik sebesar PDO, itu mengindikasikan bahwa peluang (odds) untuk individu itu mengalami perubahan sebanyak dua kali lipat. Sebaliknya, jika skor kredit turun sebesar PDO, peluangnya mengalami penurunan setengahnya (ditentukan oleh user).
  • odds0: Tingkat keketatan / target odds, rasio kemungkinan default terhadap kemungkinan tidak default. Contoh: 1/19, artinya 1 yg akan default dibanding 19 yang tidak default. Semakin kecil, semakin ketat (ditentukan oleh user)
  • points0: Target points ketika odds0 tercapai. Contoh: points0 = 600 dan odds0 = 1/19, maka pada poin 600 berarti terdapat 1 debitur default terhadap 19 debitur lain yang tidak default (ditentukan oleh user)

pdo = setiap kenaikan pdo, akan meningkatkan peluang berhasil bayar sebanyak 2 kali lipat

Ingin dihasilkan sebuah scorecard sesuai dengan kebutuhan user dengan odds 19:1 pada poin 600 dan ingin peluangnya berlipat ganda setiap kenaikan 20 poin, maka bagaimana pembentukan scorecardnya?

# membentuk scorecard 1
score_card <- scorecard(bins = binning, 
                        model = model, 
                        points0 = 600,
                        odds0 = 1/19,
                        pdo = 20)

# percobaan 2 membentuk scorecard dengan pdo 50
score_card_2 <- scorecard(bins = binning, 
                        model = model, 
                        points0 = 600,
                        odds0 = 1/19,
                        pdo = 50)

# percobaan 2 membentuk scorecard dengan odds 1/40
score_card_3 <- scorecard(bins = binning, 
                        model = model, 
                        points0 = 600,
                        odds0 = 1/40,
                        pdo = 20)
# menampilkan untuk variabel limit_bal
score_card$limit_bal

Pembuktian:

\[ Points_{ij} = (\frac{pdo}{ln(2)}) \times -Coef_i \times WOE_{ij} \]

Ingin dicari nilai points untuk bin kedua [50000,150000) pada variabel limit_bal

  • Coef limit_bal = -0.79347
  • woe limit_bal bin 2 = -0.3350614
round(20/log(2)*0.79347*-0.3350614,0)
#> [1] -8

📌 Untuk mengubah nilai karakteristik debitur menjadi points dan total points (score) secara otomatis, dapat menggunakan fungsi scorecard_ply() dengan parameter:

  • dt: dataframe yang berisi karakteristik asli debitur. Nama kolom harus sama dengan nama kolom data train.
  • card: Objek hasil fungsi scorecard().
  • only_total_score: TRUE/FALSE, apakah hanya menampilkan total score atau tidak.

Mari kita terapkan scorecard_ply() pada dataframe train.

score <- scorecard_ply(dt = train,
                       card = score_card,
                       only_total_score = F)
score
score2 <- scorecard_ply(dt = train,
                       card = score_card_2,
                       only_total_score = F)
score2
score3 <- scorecard_ply(dt = train,
                       card = score_card_3,
                       only_total_score = F)
score3

Bagaimana score bisa didapatkan?

1.9.1 Pembuktian Cara 1

Menurut (Siddqi, Naeem), rumus score adalah sebagai berikut:

\[ factor = \frac{pdo}{ln(2)} \]

\[ offset = points0 - factor*log(odds0) \]

\[ basepoint = offset - factor*\beta_0 \]

\[ score = basepoint + \sum_{i = 1}^n points_n \]

Perlu diketahui bahwa untuk bisa menggunakan pembuktian di atas, kita memerlukan koefisien logistic regression berupa \(\beta_0\) dan \(\beta_1\). Sehingga penggunaan rumus di atas bersifat eksklusif hanya untuk model logistic regression.

Mari kita buktikan score pada baris pertama

score %>% head(1)
factor <- 20/log(2)
offset <- 600 - (factor*log(19))
basepoint <- offset - factor*-0.09518
basepoint
#> [1] 517.7878

Setelah basepoint didapatkan, selanjutnya mari kita hitung score total untuk masing-masing debitur.

score %>% 
  mutate(total = (limit_bal_points * 1) + (education_points * 1) + (age_points * 1) +
           (pay_1_points * 1) + (pay_2_points * 1) + (pay_3_points * 1) + (pay_4_points * 1) + (pay_5_points * 1) +
           (pay_6_points * 1) + (bill_amt1_points * 1) + (bill_amt2_points * 1) + (bill_amt3_points * 1) +
           (bill_amt4_points * 1) + (bill_amt5_points * 1) + (bill_amt6_points * 1) + 
           (pay_amt1_points * 1) + (pay_amt2_points * 1) + (pay_amt3_points * 1) + (pay_amt4_points * 1) + (pay_amt5_points * 1) + (pay_amt6_points * 1),
         total_again = round(total + (517.7878))) %>% 
  select(score, total, total_again)

Terbukti 🥰

1.9.2 Pembuktian Cara 2

Selain menggunakan cara pertama yang hanya dapat digunakan apabila kita menggunakan model logistic regression, bagaimana menghitung score dengan cara yang lebih general?

# cek data train
head(train_woe_final)

Pada dasarnya, menurut Siddqi Naeem perhitungan general Score dapat dihitung menggunakan rumus berikut

\[ Score = Offset + Factor\times -ln (odds_{default}) \]

\(Odds\) adalah peluang kejadian terjadi dibagi dengan peluang kejadian tidak terjadi. odds adalah besaran perbandingan peluang antara kejadian terjadi dengan kejadian tidak terjadi.

\[ Odds(default) = \frac{P(default)}{1-P(default)} \]

\[ Odds(default) = \frac{P(default)}{P(not.default)} \]

  1. Menghitung nilai log of odds dari hasil prediksi model dengan penambahan nilai laplace sebesar 0.0001 agar tidak terdapat nilai inf pada odds.
# Melakukan prediksi terhadap data train
train_woe_final$pred_Risk <- predict(model, 
                                    newdata = train_woe_final,
                                    type = "response")
head(train_woe_final, 3)
train_woe_final$logodd <- -1*log(
    (train_woe_final$pred_Risk + 0.0001) / (1 - train_woe_final$pred_Risk + 0.0001)
  )

train_woe_final %>% 
  select(pred_Risk, logodd)
  1. Selanjutnya, tentukan nilai Offset dan juga Factor

ketika: pdo = 20, odds0 = 1/19, points0 = 600

pdo = 20
points0 = 600
# ln_odds <- log(19)
ln_odds <- log(19)

factor = pdo/log(2)
factor
#> [1] 28.8539
# offset = points0 - factor * ln_odds # kalo log(19)
offset = points0 - factor * ln_odds # klo log (1/19), rumus yg dipke lib scorecard
offset
#> [1] 515.0414
  1. Hitung masing-masing score Points untuk Debitur

\[ Score = Offset + Factor\times - ln (odds_{default}) \]

train_woe_final$cscore <- round((offset + factor * train_woe_final$logodd),0)
train_woe_final$cscore %>% head()
#> [1] 489 567 503 481 593 498
score$score %>% head()
#> [1] 489 567 503 480 592 498

Terbukti 🥰