WOE Binning là một kĩ thuật thuộc nhóm Data Transformation mà trong đó một biến liên lục được “rời rạc hóa” (thuật ngữ tiếng Anh là Discretization) thành các nhóm (gọi là binning). Việc chia biến liên tục thành các nhóm như vậy được thực hiện với mục đính là tính toán WoE (bằng chứng có trọng số WoE - Weight of Evidence) và giá trị thông tin IV (Information Value) của biến số. Một ví dụ minh họa được trình bày ở trang 80 cuốn Credit Risk Scorecards: Developing and Implementing Intelligent Credit Scoring áp dụng cho biến tuổi (Age) như sau:
Và tính toán WoE/IV cũng như sử dụng tiêu chuẩn IV cho chọn biến số như sau (trang 81):
Lưu ý rằng kĩ thuật binning biến này còn áp dụng cho cả biến định tính chứ không riêng gì biến định lượng.
Chúng ta có thể viết một hàm binning biến số đồng thời tính toán luôn WoE/IV cũng như thực hiện cái gọi là WoE Transformation cho biến số và sẽ sử dụng hàm này cho bộ số liệu hmeq.csv của cuốn Credit Risk Analytics: Measurement Techniques, Applications, and Examples in SAS như sau:
# Load một số packages cho data manipulation:
library(tidyverse)
library(magrittr)
library(SciViews)
library(Hmisc)
# Import data:
hmeq <- read.csv("http://www.creditriskanalytics.net/uploads/1/9/5/1/19511601/hmeq.csv")
# Hàm thực hiện binning và tính toán WOE/IV + thực hiện luôn cái gọi là "WOE Transformation":
manually_binning_WOE_trans <- function(your_df, y, x, break_points_selected) {
# Chuẩn bị dữ liệu:
df_for_woe <- your_df[, c(x, y)] %>% dplyr::mutate_if(is.integer, as.numeric)
names(df_for_woe) <- c("Var_selected", "Y")
df_for_woe %<>%
mutate(Y = case_when(Y == 1 ~ "Bad", TRUE ~ "Good"),
Var_selected = case_when(Var_selected %in% c(-Inf, Inf) ~ NA %>% as.numeric(),
TRUE ~ Var_selected))
# Tính toán WOE/IV table:
df_for_woe %>%
mutate(Cut_Point = cut2(Var_selected, break_points_selected, onlycuts = T) %>% as.character()) %>%
mutate(Cut_Point = case_when(is.na(Cut_Point) ~ "Missing", TRUE ~ Cut_Point)) %>%
group_by(Cut_Point, Y) %>%
count() %>%
ungroup() %>%
spread(Y, n) %>%
na.omit() %>%
mutate(Total = Bad + Good) %>%
mutate(BadRate = Bad / Total) %>%
mutate(Per_bin = Total / sum(Total)) %>%
mutate(DG = Good / sum(Good), DB = Bad / sum(Bad)) %>%
mutate(WOE = ln(DG / DB)) %>%
mutate(WOE = case_when(Bad == 0 ~ 2, Good == 0 ~ -2, TRUE ~ WOE)) %>%
mutate(IV = (DG - DB)*WOE,
IV_Total = sum(IV), Var_Name = x) -> df_woe_cal
cut_point <- df_woe_cal %>% pull(Cut_Point)
n_bins <- cut_point[!str_detect(cut_point, "Missing")] %>% length()
df_woe_cal %<>% mutate(N_bins = n_bins)
# Tính toán WOE Transformation:
df_for_woe %>%
mutate(Cut_Point = cut2(Var_selected, break_points_selected, onlycuts = T) %>% as.character()) %>%
mutate(Cut_Point = case_when(is.na(Cut_Point) ~ "Missing", TRUE ~ Cut_Point)) %>%
right_join(df_woe_cal, by = "Cut_Point") %>%
mutate(woe_trans = WOE) %>%
pull(woe_trans) -> woe_trans_data
return(list(iv_table_final = df_woe_cal, woe_trans_data = woe_trans_data))
}
# Sử dụng hàm:
manually_binning_WOE_trans(hmeq,
y = "BAD",
x = "LOAN",
break_points_selected = c(6000, 8000, 16000, 38000)) -> binning_results
# Kết quả:
binning_results$iv_table_final %>%
select(1:12) %>%
mutate_if(is.numeric, function(x) {round(x, 3)}) %>%
knitr::kable()
Cut_Point | Bad | Good | Total | BadRate | Per_bin | DG | DB | WOE | IV | IV_Total | Var_Name |
---|---|---|---|---|---|---|---|---|---|---|---|
[ 1100, 6000) | 142 | 162 | 304 | 0.467 | 0.051 | 0.034 | 0.119 | -1.258 | 0.107 | 0.167 | LOAN |
[ 6000, 8000) | 92 | 250 | 342 | 0.269 | 0.057 | 0.052 | 0.077 | -0.390 | 0.010 | 0.167 | LOAN |
[ 8000,16000) | 456 | 1772 | 2228 | 0.205 | 0.374 | 0.371 | 0.384 | -0.032 | 0.000 | 0.167 | LOAN |
[16000,38000) | 415 | 2323 | 2738 | 0.152 | 0.459 | 0.487 | 0.349 | 0.333 | 0.046 | 0.167 | LOAN |
[38000,89900] | 84 | 264 | 348 | 0.241 | 0.058 | 0.055 | 0.071 | -0.244 | 0.004 | 0.167 | LOAN |
Hệ quả trước tiên của việc sử dụng biến số được thực hiện WoE Transformation khi thực hiện hồi quy Logistic đơn biến như sau:
Hai hệ quả trên có thể được kiểm tra lại với bộ dữ liệu hmeq như sau:
# Consequences of using WOE for Logistic Regression:
df_woe <- data.frame(BAD = hmeq$BAD, LOAN_woe = binning_results$woe_trans_data)
glm(BAD ~ LOAN_woe, family = "binomial", data = df_woe) %>% summary()
##
## Call:
## glm(formula = BAD ~ LOAN_woe, family = "binomial", data = df_woe)
##
## Deviance Residuals:
## Min 1Q Median 3Q Max
## -1.1220 -0.6767 -0.5734 -0.5734 1.9425
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) -1.38944 0.03305 -42.05 <2e-16 ***
## LOAN_woe -1.00000 0.07624 -13.12 <2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for binomial family taken to be 1)
##
## Null deviance: 5956.5 on 5959 degrees of freedom
## Residual deviance: 5791.0 on 5958 degrees of freedom
## AIC: 5795
##
## Number of Fisher Scoring iterations: 4
Hệ quả thứ ba là chúng ta có thể tính tỉ lệ hồ sơ Bad cho từng nhóm (hay khoảng binning) từ mô hình Logistic. Ví dụ, với nhóm từ 1100 đến 6000 thì tỉ lệ Bad được tính theo công thức sau:
# Consequences of using WOE for Logistic Regression:
ts <- exp(-1*-1.258 - 1.38944)
ts / (ts + 1)
## [1] 0.4671872
Tương tự là cho nhóm từ 6000 đến 8000:
ts <- exp(-1*-0.390 - 1.38944)
ts / (ts + 1)
## [1] 0.2690515
Hai kết quả 0.467 và 0.269 chính là tỉ lệ hồ sơ Bad (46.7% và 26.9%) tương ứng với các nhóm này mà chúng ta đã biết ở bảng IV Table khi sử dụng hàm binning mà chúng ta đã viết ở trên.
Hiện R có rất nhiều package sử dụng cho binning và với mục đích là Scorecard Modelling thì scorecard là thuận lợi hơn cả vì package được viết nhằm hiện thực hóa cách thức xây dựng Scorecard thường được thực hiện bởi các ngân hàng được mô tả trong cuốn Credit Risk Scorecards: Developing and Implementing Intelligent Credit Scoring. Tuy nhiên tác giả của package này đã “nhầm lẫn” khi tính toán WoE như sau:
Điều này dẫn đến các giá trị WoE sẽ ngược dấu hoàn toàn với những tính toán được mô tả trong tài liệu của Naeem Siddiqi như chúng ta có thể thấy:
# Tính WoE cho biến LOAN bằng sử dụng các hàm của gói scorecard:
library(scorecard)
bins <- woebin(hmeq, y = "BAD", positive = "BAD|1")
## [INFO] creating woe binning ...
df2_woe <- woebin_ply(hmeq, bins = bins)
## [INFO] converting into woe values ...
bins$LOAN[, 1:10] %>%
mutate_if(is.numeric, function(x) {round(x, 3)}) %>%
knitr::kable()
variable | bin | count | count_distr | good | bad | badprob | woe | bin_iv | total_iv |
---|---|---|---|---|---|---|---|---|---|
LOAN | [-Inf,6000) | 304 | 0.051 | 162 | 142 | 0.467 | 1.258 | 0.107 | 0.167 |
LOAN | [6000,8000) | 342 | 0.057 | 250 | 92 | 0.269 | 0.390 | 0.010 | 0.167 |
LOAN | [8000,16000) | 2228 | 0.374 | 1772 | 456 | 0.205 | 0.032 | 0.000 | 0.167 |
LOAN | [16000,38000) | 2738 | 0.459 | 2323 | 415 | 0.152 | -0.333 | 0.046 | 0.167 |
LOAN | [38000, Inf) | 348 | 0.058 | 264 | 84 | 0.241 | 0.244 | 0.004 | 0.167 |
Do ngược dấu nên hệ số chặn của mô hình hồi quy Logistic lúc này là 1:
glm(BAD ~ LOAN_woe, family = "binomial", data = df2_woe) %>% summary()
##
## Call:
## glm(formula = BAD ~ LOAN_woe, family = "binomial", data = df2_woe)
##
## Deviance Residuals:
## Min 1Q Median 3Q Max
## -1.1220 -0.6767 -0.5734 -0.5734 1.9425
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) -1.38944 0.03305 -42.05 <2e-16 ***
## LOAN_woe 1.00000 0.07624 13.12 <2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for binomial family taken to be 1)
##
## Null deviance: 5956.5 on 5959 degrees of freedom
## Residual deviance: 5791.0 on 5958 degrees of freedom
## AIC: 5795
##
## Number of Fisher Scoring iterations: 4
Chúng ta có thể kiểm tra với một biến số khác:
glm(BAD ~ VALUE_woe, family = "binomial", data = df2_woe) %>% summary()
##
## Call:
## glm(formula = BAD ~ VALUE_woe, family = "binomial", data = df2_woe)
##
## Deviance Residuals:
## Min 1Q Median 3Q Max
## -2.3548 -0.6409 -0.6409 -0.5400 2.2322
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) -1.38944 0.03392 -40.97 <2e-16 ***
## VALUE_woe 1.00000 0.06567 15.23 <2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for binomial family taken to be 1)
##
## Null deviance: 5956.5 on 5959 degrees of freedom
## Residual deviance: 5537.2 on 5958 degrees of freedom
## AIC: 5541.2
##
## Number of Fisher Scoring iterations: 5
Ngoài scorecard thì còn có gói logiBin cũng có thể được sử dụng để binning biến số với mục đích là binning theo hướng monotonic (tức là đơn điệu tăng hoặc giảm cho nhóm hồ sơ Bad hoặc Good theo các khoảng):
detach(package:scorecard)
# Import package:
library(logiBin)
# Binning data cho biến LOAN:
my_bin <- getBins(hmeq, y = "BAD", xVars = c("LOAN"),
minProp = 0.05, minCr = 0.2, nCores = 20)
# Monotonic trend or not:
my_bin$varSummary
## var iv pVal stat ent trend monTrend flipRatio
## 16 LOAN 0.2607 6.741998e-09 33.60785 0.6906 I N 0.5
## numBins purNode varType
## 16 15 N integer
# IV and results:
my_bin$bin %>%
select(-c(1, 9)) %>%
knitr::kable()
bin | count | bads | goods | propn | bad_rate | iv |
---|---|---|---|---|---|---|
LOAN <= 6000 | 332 | 157 | 175 | 5.57 | 47.29 | 0.1222 |
LOAN > 6000 & LOAN <= 8700 | 535 | 114 | 421 | 8.98 | 21.31 | 0.0006 |
LOAN > 8700 & LOAN <= 10000 | 344 | 95 | 249 | 5.77 | 27.62 | 0.0118 |
LOAN > 10000 & LOAN <= 11400 | 365 | 57 | 308 | 6.12 | 15.62 | 0.0049 |
LOAN > 11400 & LOAN <= 12600 | 347 | 76 | 271 | 5.82 | 21.90 | 0.0008 |
LOAN > 12600 & LOAN <= 13900 | 377 | 62 | 315 | 6.33 | 16.45 | 0.0033 |
LOAN > 13900 & LOAN <= 15000 | 322 | 94 | 228 | 5.40 | 29.19 | 0.0157 |
LOAN > 15000 & LOAN <= 17000 | 556 | 91 | 465 | 9.33 | 16.37 | 0.0051 |
LOAN > 17000 & LOAN <= 18700 | 392 | 45 | 347 | 6.58 | 11.48 | 0.0228 |
LOAN > 18700 & LOAN <= 21000 | 464 | 90 | 374 | 7.79 | 19.40 | 0.0001 |
LOAN > 21000 & LOAN <= 23400 | 455 | 50 | 405 | 7.63 | 10.99 | 0.0301 |
LOAN > 23400 & LOAN <= 25000 | 300 | 69 | 231 | 5.03 | 23.00 | 0.0017 |
LOAN > 25000 & LOAN <= 27600 | 360 | 37 | 323 | 6.04 | 10.28 | 0.0284 |
LOAN > 27600 & LOAN <= 37900 | 463 | 68 | 395 | 7.77 | 14.69 | 0.0095 |
LOAN > 37900 | 348 | 84 | 264 | 5.84 | 24.14 | 0.0037 |
Total | 5960 | 1189 | 4771 | 1.00 | 19.95 | 0.2607 |
Chúng ta có thể binning biến LOAN này sao cho monotonic (tăng hoặc giảm):
# Check increasing trend:
forceIncrTrend(my_bin, xVars = c("LOAN")) %>% .[[3]]
## var bin count bads goods propn bad_rate iv ent
## 1 LOAN LOAN <= 37900 5612 1105 4507 94.16 19.69 0.0003 0.7157
## 2 LOAN LOAN > 37900 348 84 264 5.84 24.14 0.0037 0.7973
## 3 LOAN Total 5960 1189 4771 1.00 19.95 0.0040 0.7205
# Check decreasing trend:
my_bin_de <- forceDecrTrend(my_bin, xVars = c("LOAN"))
# IV Table:
iv_table <- my_bin_de$bin %>%
filter(bin != "Total") %>%
mutate(bin = str_replace_all(bin, "LOAN", "")) %>%
mutate(DG = goods / sum(goods), DB = bads / sum(bads)) %>%
mutate(woe = log(DG / DB)) %>%
mutate(iv_total = sum(iv))
Có thể thấy tỉ lệ hồ sơ Bad giảm dần theo từng khoảng tăng dần của LOAN:
iv_table %>%
select(-c(9, 10, 11)) %>%
knitr::kable()
var | bin | count | bads | goods | propn | bad_rate | iv | woe | iv_total |
---|---|---|---|---|---|---|---|---|---|
LOAN | <= 6000 | 332 | 157 | 175 | 5.57 | 47.29 | 0.1222 | -1.2809031 | 0.1678 |
LOAN | > 6000 & <= 10000 | 879 | 209 | 670 | 14.75 | 23.78 | 0.0079 | -0.2244998 | 0.1678 |
LOAN | > 10000 & <= 15000 | 1411 | 289 | 1122 | 23.67 | 20.48 | 0.0003 | -0.0330019 | 0.1678 |
LOAN | > 15000 & <= 17000 | 556 | 91 | 465 | 9.33 | 16.37 | 0.0051 | 0.2417346 | 0.1678 |
LOAN | > 17000 | 2782 | 443 | 2339 | 46.68 | 15.92 | 0.0323 | 0.2744657 | 0.1678 |
Tất nhiên nếu sử dụng biến sau khi thực hiện WoE Transformation thì hệ số góc của mô hình hồi quy đơn biến vẫn là -1:
# WOE Transformation:
createBins(my_bin_de, hmeq, xVars = "LOAN") %>%
select(LOAN, b_LOAN) %>%
right_join(iv_table, by = c("b_LOAN" = "bin")) %>%
pull(woe) -> LOAN_binned
# Logistic Regresion:
hmeq %>%
mutate(LOAN_binned = LOAN_binned) %>%
glm(BAD ~ LOAN_binned, family = "binomial", data = .) %>%
summary()
##
## Call:
## glm(formula = BAD ~ LOAN_binned, family = "binomial", data = .)
##
## Deviance Residuals:
## Min 1Q Median 3Q Max
## -1.132 -0.677 -0.589 -0.589 1.917
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) -1.38944 0.03305 -42.05 <2e-16 ***
## LOAN_binned -1.00000 0.07545 -13.25 <2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for binomial family taken to be 1)
##
## Null deviance: 5956.5 on 5959 degrees of freedom
## Residual deviance: 5789.2 on 5958 degrees of freedom
## AIC: 5793.2
##
## Number of Fisher Scoring iterations: 4
Do cách tính WoE của gói scorecard ngược hoàn toàn nên cần lưu ý để hiệu chỉnh. Tuy nhiên giá trị IV thì vẫn đúng. Nguyên nhân mà IV vẫn đúng trong khi WoE “sai” các bạn có thể giải thích được từ công thức mô tả của gói scorecard. Ngoại trừ “thiếu sót” này thì chúng ta vẫn có thể sử dụng gói này để thực hiện Scorecard Modelling như đã mô tả trong cuốn Credit Risk Scorecards: Developing and Implementing Intelligent Credit Scoring hoặc Credit Risk Analytics: Measurement Techniques, Applications, and Examples in SAS.
Trong tình huống mà cần binning biến để tìm kiếm monotonic trend (đơn điệu tăng hoặc giảm của Bad Rate) thì chúng ta nên sử dụng gói logiBin như đã trình bày ở trên.