WOE Transformation and Logistic Regression’s Performance

#=================================
#  State 1: Data Pre-processing
#=================================

# Load một số packages cho data manipulation: 
library(tidyverse)
library(magrittr)

# Import data: 
hmeq <- read.csv("http://www.creditriskanalytics.net/uploads/1/9/5/1/19511601/hmeq.csv")

# Function replaces NA by mean: 
replace_by_mean <- function(x) {
  x[is.na(x)] <- mean(x, na.rm = TRUE)
  return(x)
}

# A function imputes NA observations for categorical variables: 

replace_na_categorical <- function(x) {
  x %>% 
    table() %>% 
    as.data.frame() %>% 
    arrange(-Freq) ->> my_df
  
  n_obs <- sum(my_df$Freq)
  pop <- my_df$. %>% as.character()
  set.seed(29)
  x[is.na(x)] <- sample(pop, sum(is.na(x)), replace = TRUE, prob = my_df$Freq)
  return(x)
}

# Use the two functions: 
df <- hmeq %>% 
  mutate_if(is.factor, as.character) %>% 
  mutate(REASON = case_when(REASON == "" ~ NA_character_, TRUE ~ REASON), 
         JOB = case_when(JOB == "" ~ NA_character_, TRUE ~ JOB)) %>%
  mutate_if(is_character, as.factor) %>% 
  mutate_if(is.numeric, replace_by_mean) %>% 
  mutate_if(is.factor, replace_na_categorical)

# Check again missing cases: 

df %>% sapply(function(x) {sum(is.na(x))})
##     BAD    LOAN MORTDUE   VALUE  REASON     JOB     YOJ   DEROG  DELINQ 
##       0       0       0       0       0       0       0       0       0 
##   CLAGE    NINQ    CLNO DEBTINC 
##       0       0       0       0
# Phân chia dữ liệu: 
set.seed(2)
df_train <- df %>% group_by(BAD) %>% sample_frac(0.5, replace = FALSE)
df_test <- setdiff(df, df_train)


#====================================================================
#  Kịch bản 1: So sánh Logistic Regression với biến nguyên bản 
#  và WOE Transformation. Tham khảo thêm: 
#  https://cran.r-project.org/web/packages/scorecard/scorecard.pdf
#====================================================================


# Load package cho binning WOE: 
library(scorecard)

# Thực hiện binning dữ liệu: 
bins <- woebin(df_train, y = "BAD")
## [INFO] creating woe binning ...
# Thực hiện WOE Transformation: 
df_train_woe <- woebin_ply(df_train, bins = bins) %>% 
  mutate(BAD = case_when(BAD == 1 ~ "Bad", TRUE ~ "Good") %>% as.factor())
## [INFO] converting into woe values ...
df_test_woe <- woebin_ply(df_test, bins = bins) %>% 
  mutate(BAD = case_when(BAD == 1 ~ "Bad", TRUE ~ "Good") %>% as.factor())
## [INFO] converting into woe values ...
# Thực hiện Logistic với biến được làm WOE Transformation: 
model_full_woe <- glm(BAD ~ ., family = "binomial", data = df_train_woe)

# Kết quả của mô hình: 
summary(model_full_woe)
## 
## Call:
## glm(formula = BAD ~ ., family = "binomial", data = df_train_woe)
## 
## Deviance Residuals: 
##     Min       1Q   Median       3Q      Max  
## -3.0877   0.1436   0.2543   0.4713   2.8750  
## 
## Coefficients:
##             Estimate Std. Error z value Pr(>|z|)    
## (Intercept)  1.38655    0.06273  22.102  < 2e-16 ***
## LOAN_woe    -0.59605    0.17417  -3.422 0.000621 ***
## MORTDUE_woe -0.64188    0.21875  -2.934 0.003343 ** 
## VALUE_woe   -0.53917    0.15658  -3.443 0.000574 ***
## REASON_woe  -0.41469    0.49978  -0.830 0.406685    
## JOB_woe     -0.65059    0.19607  -3.318 0.000906 ***
## YOJ_woe     -1.01467    0.21061  -4.818 1.45e-06 ***
## DEROG_woe   -0.60753    0.10027  -6.059 1.37e-09 ***
## DELINQ_woe  -0.96626    0.08067 -11.978  < 2e-16 ***
## CLAGE_woe   -0.74682    0.11984  -6.232 4.61e-10 ***
## NINQ_woe    -0.45039    0.14699  -3.064 0.002183 ** 
## CLNO_woe    -1.14173    0.19179  -5.953 2.63e-09 ***
## DEBTINC_woe -0.92948    0.04861 -19.121  < 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: 2976.8  on 2979  degrees of freedom
## Residual deviance: 1837.2  on 2967  degrees of freedom
## AIC: 1863.2
## 
## Number of Fisher Scoring iterations: 6
# Viết hàm tính toán ROC/AUC: 

library(pROC)
my_roc_fun <- function(model_selected, df_test_selected) {
  my_roc <- roc(df_test_selected$BAD, 
                predict(model_selected, df_test_selected %>% select(-BAD), 
                        type = "response"))
  return(my_roc$auc)
  
}

# Chuẩn bị dữ liệu cho Logistic mà biến là nguyên bản: 
df_train_ori <- df_train %>% 
  mutate(BAD = case_when(BAD == 1 ~ "Bad", TRUE ~ "Good") %>% as.factor())
  
df_test_ori <- df_test %>% 
  mutate(BAD = case_when(BAD == 1 ~ "Bad", TRUE ~ "Good") %>% as.factor())


# Thực hiện Logistic với biến nguyên bản: 
model_full_ori <- glm(BAD ~ ., family = "binomial", data = df_train_ori)

# Xem kết quả: 
summary(model_full_ori)
## 
## Call:
## glm(formula = BAD ~ ., family = "binomial", data = df_train_ori)
## 
## Deviance Residuals: 
##     Min       1Q   Median       3Q      Max  
## -3.9693   0.2547   0.4212   0.5955   2.1117  
## 
## Coefficients:
##                 Estimate Std. Error z value Pr(>|z|)    
## (Intercept)    2.718e+00  3.780e-01   7.190 6.47e-13 ***
## LOAN           1.157e-05  5.643e-06   2.051  0.04030 *  
## MORTDUE        4.375e-06  2.111e-06   2.072  0.03824 *  
## VALUE         -3.390e-06  1.629e-06  -2.081  0.03740 *  
## REASONHomeImp -3.263e-01  1.190e-01  -2.741  0.00613 ** 
## JOBOffice      4.779e-01  2.091e-01   2.286  0.02227 *  
## JOBOther      -2.564e-01  1.639e-01  -1.564  0.11777    
## JOBProfExe    -8.934e-02  1.905e-01  -0.469  0.63915    
## JOBSales      -8.338e-01  3.467e-01  -2.405  0.01618 *  
## JOBSelf       -5.574e-01  3.063e-01  -1.820  0.06882 .  
## YOJ            1.527e-02  8.327e-03   1.834  0.06671 .  
## DEROG         -5.373e-01  7.022e-02  -7.650 2.00e-14 ***
## DELINQ        -7.496e-01  5.317e-02 -14.098  < 2e-16 ***
## CLAGE          6.341e-03  7.953e-04   7.973 1.54e-15 ***
## NINQ          -1.567e-01  2.958e-02  -5.297 1.17e-07 ***
## CLNO           1.720e-02  6.113e-03   2.813  0.00490 ** 
## DEBTINC       -5.923e-02  8.743e-03  -6.775 1.24e-11 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## (Dispersion parameter for binomial family taken to be 1)
## 
##     Null deviance: 2976.8  on 2979  degrees of freedom
## Residual deviance: 2303.6  on 2963  degrees of freedom
## AIC: 2337.6
## 
## Number of Fisher Scoring iterations: 5
# So sánh AUC/ROC: 
my_roc_fun(model_full_woe, df_test_woe)
## Area under the curve: 0.8917
my_roc_fun(model_full_ori, df_train_ori)
## Area under the curve: 0.8071
# Viết hàm extract ra ROC/AUC của nhiều lần thử nghiệm cho mục đích so sánh:

my_roc_for_comparision <- function(so_lan_thu) {
  
  auc_woe <- c() 
  auc_ori <- c()
  
  for (i in 1:so_lan_thu) {
    
    # Chuẩn bị dữ liệu: 
    set.seed(i)
    df_train <- df %>% group_by(BAD) %>% sample_frac(0.5, replace = FALSE)
    df_test <- setdiff(df, df_train)
    
    # Thực hiện binning dữ liệu: 
    bins <- woebin(df_train, y = "BAD")
    
    # Thực hiện WOE Transformation: 
    df_train_woe <- woebin_ply(df_train, bins = bins) %>% 
      mutate(BAD = case_when(BAD == 1 ~ "Bad", TRUE ~ "Good") %>% as.factor())
    
    df_test_woe <- woebin_ply(df_test, bins = bins) %>% 
      mutate(BAD = case_when(BAD == 1 ~ "Bad", TRUE ~ "Good") %>% as.factor())
    
    # Thực hiện Logistic với biến được làm WOE Transformation: 
    model_full_woe <- glm(BAD ~ ., family = "binomial", data = df_train_woe)
    
    auc_woe_i <- my_roc_fun(model_full_woe, df_test_woe)
    auc_woe <- c(auc_woe, auc_woe_i)
    
    
    # Chuẩn bị dữ liệu cho Logistic mà biến là nguyên bản: 
    df_train_ori <- df_train %>% 
      mutate(BAD = case_when(BAD == 1 ~ "Bad", TRUE ~ "Good") %>% as.factor())
    
    df_test_ori <- df_test %>% 
      mutate(BAD = case_when(BAD == 1 ~ "Bad", TRUE ~ "Good") %>% as.factor())
    
    # Thực hiện Logistic với biến nguyên bản: 
    model_full_ori <- glm(BAD ~ ., family = "binomial", data = df_train_ori)
    auc_ori_i <- my_roc_fun(model_full_ori, df_test_ori)
    auc_ori <- c(auc_ori, auc_ori_i)
    
  }
  
  return(data.frame(AUC_woe = auc_woe, AUC_ori = auc_ori))
}


# Thử nghiệm trên 10 lần chạy mẫu. Cả 10 lần thử nghiệm có thể thấy
# ROC/AUC của mô hình Logistic với biến được thực hiện WOE Transformation
# là cao hơn: 

my_roc_for_comparision(so_lan_thu = 10) %>% knitr::kable()
## [INFO] creating woe binning ... 
## [INFO] converting into woe values ... 
## [INFO] converting into woe values ... 
## [INFO] creating woe binning ... 
## [INFO] converting into woe values ... 
## [INFO] converting into woe values ... 
## [INFO] creating woe binning ... 
## [INFO] converting into woe values ... 
## [INFO] converting into woe values ... 
## [INFO] creating woe binning ... 
## [INFO] converting into woe values ... 
## [INFO] converting into woe values ... 
## [INFO] creating woe binning ... 
## [INFO] converting into woe values ... 
## [INFO] converting into woe values ... 
## [INFO] creating woe binning ... 
## [INFO] converting into woe values ... 
## [INFO] converting into woe values ... 
## [INFO] creating woe binning ... 
## [INFO] converting into woe values ... 
## [INFO] converting into woe values ... 
## [INFO] creating woe binning ... 
## [INFO] converting into woe values ... 
## [INFO] converting into woe values ... 
## [INFO] creating woe binning ... 
## [INFO] converting into woe values ... 
## [INFO] converting into woe values ... 
## [INFO] creating woe binning ... 
## [INFO] converting into woe values ... 
## [INFO] converting into woe values ...
AUC_woe AUC_ori
0.8917636 0.7982665
0.8916594 0.7856294
0.8954537 0.8020408
0.8873914 0.7834667
0.8755457 0.7862284
0.8910502 0.7986259
0.8873203 0.7965386
0.8813748 0.7905925
0.8852872 0.7858239
0.8801638 0.7941004
LS0tDQp0aXRsZTogIkRvZXMgV09FIFRyYW5zZm9ybWF0aW9uIEltcHJvdmUgTG9naXN0aWMgUmVncmVzc2lvbidzIFBlcmZvcm1hbmNlPyIgDQpzdWJ0aXRsZTogIlRoZSBTZXJpb3VzIFByb2JsZW0gTXVzdCBCZSBIYW5kbGVkIg0KYXV0aG9yOiAiTmd1eWVuIENoaSBEdW5nIg0Kb3V0cHV0Og0KICBodG1sX2RvY3VtZW50OiANCiAgICBjb2RlX2Rvd25sb2FkOiB0cnVlDQogICAgIyBjb2RlX2ZvbGRpbmc6IGhpZGUNCiAgICBoaWdobGlnaHQ6IHB5Z21lbnRzDQogICAgIyBudW1iZXJfc2VjdGlvbnM6IHllcw0KICAgIHRoZW1lOiAiZmxhdGx5Ig0KICAgIHRvYzogVFJVRQ0KICAgIHRvY19mbG9hdDogVFJVRQ0KLS0tDQoNCmBgYHtyIHNldHVwLGluY2x1ZGU9RkFMU0V9DQprbml0cjo6b3B0c19jaHVuayRzZXQoZWNobyA9IFRSVUUsIHdhcm5pbmcgPSBGQUxTRSwgbWVzc2FnZSA9IEZBTFNFKQ0KYGBgDQoNCiMgV09FIFRyYW5zZm9ybWF0aW9uIGFuZCBMb2dpc3RpYyBSZWdyZXNzaW9uJ3MgUGVyZm9ybWFuY2UNCg0KYGBge3J9DQojPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09DQojICBTdGF0ZSAxOiBEYXRhIFByZS1wcm9jZXNzaW5nDQojPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09DQoNCiMgTG9hZCBt4buZdCBz4buRIHBhY2thZ2VzIGNobyBkYXRhIG1hbmlwdWxhdGlvbjogDQpsaWJyYXJ5KHRpZHl2ZXJzZSkNCmxpYnJhcnkobWFncml0dHIpDQoNCiMgSW1wb3J0IGRhdGE6IA0KaG1lcSA8LSByZWFkLmNzdigiaHR0cDovL3d3dy5jcmVkaXRyaXNrYW5hbHl0aWNzLm5ldC91cGxvYWRzLzEvOS81LzEvMTk1MTE2MDEvaG1lcS5jc3YiKQ0KDQojIEZ1bmN0aW9uIHJlcGxhY2VzIE5BIGJ5IG1lYW46IA0KcmVwbGFjZV9ieV9tZWFuIDwtIGZ1bmN0aW9uKHgpIHsNCiAgeFtpcy5uYSh4KV0gPC0gbWVhbih4LCBuYS5ybSA9IFRSVUUpDQogIHJldHVybih4KQ0KfQ0KDQojIEEgZnVuY3Rpb24gaW1wdXRlcyBOQSBvYnNlcnZhdGlvbnMgZm9yIGNhdGVnb3JpY2FsIHZhcmlhYmxlczogDQoNCnJlcGxhY2VfbmFfY2F0ZWdvcmljYWwgPC0gZnVuY3Rpb24oeCkgew0KICB4ICU+JSANCiAgICB0YWJsZSgpICU+JSANCiAgICBhcy5kYXRhLmZyYW1lKCkgJT4lIA0KICAgIGFycmFuZ2UoLUZyZXEpIC0+PiBteV9kZg0KICANCiAgbl9vYnMgPC0gc3VtKG15X2RmJEZyZXEpDQogIHBvcCA8LSBteV9kZiQuICU+JSBhcy5jaGFyYWN0ZXIoKQ0KICBzZXQuc2VlZCgyOSkNCiAgeFtpcy5uYSh4KV0gPC0gc2FtcGxlKHBvcCwgc3VtKGlzLm5hKHgpKSwgcmVwbGFjZSA9IFRSVUUsIHByb2IgPSBteV9kZiRGcmVxKQ0KICByZXR1cm4oeCkNCn0NCg0KIyBVc2UgdGhlIHR3byBmdW5jdGlvbnM6IA0KZGYgPC0gaG1lcSAlPiUgDQogIG11dGF0ZV9pZihpcy5mYWN0b3IsIGFzLmNoYXJhY3RlcikgJT4lIA0KICBtdXRhdGUoUkVBU09OID0gY2FzZV93aGVuKFJFQVNPTiA9PSAiIiB+IE5BX2NoYXJhY3Rlcl8sIFRSVUUgfiBSRUFTT04pLCANCiAgICAgICAgIEpPQiA9IGNhc2Vfd2hlbihKT0IgPT0gIiIgfiBOQV9jaGFyYWN0ZXJfLCBUUlVFIH4gSk9CKSkgJT4lDQogIG11dGF0ZV9pZihpc19jaGFyYWN0ZXIsIGFzLmZhY3RvcikgJT4lIA0KICBtdXRhdGVfaWYoaXMubnVtZXJpYywgcmVwbGFjZV9ieV9tZWFuKSAlPiUgDQogIG11dGF0ZV9pZihpcy5mYWN0b3IsIHJlcGxhY2VfbmFfY2F0ZWdvcmljYWwpDQoNCiMgQ2hlY2sgYWdhaW4gbWlzc2luZyBjYXNlczogDQoNCmRmICU+JSBzYXBwbHkoZnVuY3Rpb24oeCkge3N1bShpcy5uYSh4KSl9KQ0KDQojIFBow6JuIGNoaWEgZOG7ryBsaeG7h3U6IA0Kc2V0LnNlZWQoMikNCmRmX3RyYWluIDwtIGRmICU+JSBncm91cF9ieShCQUQpICU+JSBzYW1wbGVfZnJhYygwLjUsIHJlcGxhY2UgPSBGQUxTRSkNCmRmX3Rlc3QgPC0gc2V0ZGlmZihkZiwgZGZfdHJhaW4pDQoNCg0KIz09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09DQojICBL4buLY2ggYuG6o24gMTogU28gc8OhbmggTG9naXN0aWMgUmVncmVzc2lvbiB24bubaSBiaeG6v24gbmd1ecOqbiBi4bqjbiANCiMgIHbDoCBXT0UgVHJhbnNmb3JtYXRpb24uIFRoYW0ga2jhuqNvIHRow6ptOiANCiMgIGh0dHBzOi8vY3Jhbi5yLXByb2plY3Qub3JnL3dlYi9wYWNrYWdlcy9zY29yZWNhcmQvc2NvcmVjYXJkLnBkZg0KIz09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09DQoNCg0KIyBMb2FkIHBhY2thZ2UgY2hvIGJpbm5pbmcgV09FOiANCmxpYnJhcnkoc2NvcmVjYXJkKQ0KDQojIFRo4buxYyBoaeG7h24gYmlubmluZyBk4buvIGxp4buHdTogDQpiaW5zIDwtIHdvZWJpbihkZl90cmFpbiwgeSA9ICJCQUQiKQ0KDQojIFRo4buxYyBoaeG7h24gV09FIFRyYW5zZm9ybWF0aW9uOiANCmRmX3RyYWluX3dvZSA8LSB3b2ViaW5fcGx5KGRmX3RyYWluLCBiaW5zID0gYmlucykgJT4lIA0KICBtdXRhdGUoQkFEID0gY2FzZV93aGVuKEJBRCA9PSAxIH4gIkJhZCIsIFRSVUUgfiAiR29vZCIpICU+JSBhcy5mYWN0b3IoKSkNCg0KZGZfdGVzdF93b2UgPC0gd29lYmluX3BseShkZl90ZXN0LCBiaW5zID0gYmlucykgJT4lIA0KICBtdXRhdGUoQkFEID0gY2FzZV93aGVuKEJBRCA9PSAxIH4gIkJhZCIsIFRSVUUgfiAiR29vZCIpICU+JSBhcy5mYWN0b3IoKSkNCiAgDQojIFRo4buxYyBoaeG7h24gTG9naXN0aWMgduG7m2kgYmnhur9uIMSRxrDhu6NjIGzDoG0gV09FIFRyYW5zZm9ybWF0aW9uOiANCm1vZGVsX2Z1bGxfd29lIDwtIGdsbShCQUQgfiAuLCBmYW1pbHkgPSAiYmlub21pYWwiLCBkYXRhID0gZGZfdHJhaW5fd29lKQ0KDQojIEvhur90IHF14bqjIGPhu6dhIG3DtCBow6xuaDogDQpzdW1tYXJ5KG1vZGVsX2Z1bGxfd29lKQ0KDQojIFZp4bq/dCBow6BtIHTDrW5oIHRvw6FuIFJPQy9BVUM6IA0KDQpsaWJyYXJ5KHBST0MpDQpteV9yb2NfZnVuIDwtIGZ1bmN0aW9uKG1vZGVsX3NlbGVjdGVkLCBkZl90ZXN0X3NlbGVjdGVkKSB7DQogIG15X3JvYyA8LSByb2MoZGZfdGVzdF9zZWxlY3RlZCRCQUQsIA0KICAgICAgICAgICAgICAgIHByZWRpY3QobW9kZWxfc2VsZWN0ZWQsIGRmX3Rlc3Rfc2VsZWN0ZWQgJT4lIHNlbGVjdCgtQkFEKSwgDQogICAgICAgICAgICAgICAgICAgICAgICB0eXBlID0gInJlc3BvbnNlIikpDQogIHJldHVybihteV9yb2MkYXVjKQ0KICANCn0NCg0KIyBDaHXhuqluIGLhu4sgZOG7ryBsaeG7h3UgY2hvIExvZ2lzdGljIG3DoCBiaeG6v24gbMOgIG5ndXnDqm4gYuG6o246IA0KZGZfdHJhaW5fb3JpIDwtIGRmX3RyYWluICU+JSANCiAgbXV0YXRlKEJBRCA9IGNhc2Vfd2hlbihCQUQgPT0gMSB+ICJCYWQiLCBUUlVFIH4gIkdvb2QiKSAlPiUgYXMuZmFjdG9yKCkpDQogIA0KZGZfdGVzdF9vcmkgPC0gZGZfdGVzdCAlPiUgDQogIG11dGF0ZShCQUQgPSBjYXNlX3doZW4oQkFEID09IDEgfiAiQmFkIiwgVFJVRSB+ICJHb29kIikgJT4lIGFzLmZhY3RvcigpKQ0KDQoNCiMgVGjhu7FjIGhp4buHbiBMb2dpc3RpYyB24bubaSBiaeG6v24gbmd1ecOqbiBi4bqjbjogDQptb2RlbF9mdWxsX29yaSA8LSBnbG0oQkFEIH4gLiwgZmFtaWx5ID0gImJpbm9taWFsIiwgZGF0YSA9IGRmX3RyYWluX29yaSkNCg0KIyBYZW0ga+G6v3QgcXXhuqM6IA0Kc3VtbWFyeShtb2RlbF9mdWxsX29yaSkNCg0KIyBTbyBzw6FuaCBBVUMvUk9DOiANCm15X3JvY19mdW4obW9kZWxfZnVsbF93b2UsIGRmX3Rlc3Rfd29lKQ0KbXlfcm9jX2Z1bihtb2RlbF9mdWxsX29yaSwgZGZfdHJhaW5fb3JpKQ0KDQoNCiMgVmnhur90IGjDoG0gZXh0cmFjdCByYSBST0MvQVVDIGPhu6dhIG5oaeG7gXUgbOG6p24gdGjhu60gbmdoaeG7h20gY2hvIG3hu6VjIMSRw61jaCBzbyBzw6FuaDoNCg0KbXlfcm9jX2Zvcl9jb21wYXJpc2lvbiA8LSBmdW5jdGlvbihzb19sYW5fdGh1KSB7DQogIA0KICBhdWNfd29lIDwtIGMoKSANCiAgYXVjX29yaSA8LSBjKCkNCiAgDQogIGZvciAoaSBpbiAxOnNvX2xhbl90aHUpIHsNCiAgICANCiAgICAjIENodeG6qW4gYuG7iyBk4buvIGxp4buHdTogDQogICAgc2V0LnNlZWQoaSkNCiAgICBkZl90cmFpbiA8LSBkZiAlPiUgZ3JvdXBfYnkoQkFEKSAlPiUgc2FtcGxlX2ZyYWMoMC41LCByZXBsYWNlID0gRkFMU0UpDQogICAgZGZfdGVzdCA8LSBzZXRkaWZmKGRmLCBkZl90cmFpbikNCiAgICANCiAgICAjIFRo4buxYyBoaeG7h24gYmlubmluZyBk4buvIGxp4buHdTogDQogICAgYmlucyA8LSB3b2ViaW4oZGZfdHJhaW4sIHkgPSAiQkFEIikNCiAgICANCiAgICAjIFRo4buxYyBoaeG7h24gV09FIFRyYW5zZm9ybWF0aW9uOiANCiAgICBkZl90cmFpbl93b2UgPC0gd29lYmluX3BseShkZl90cmFpbiwgYmlucyA9IGJpbnMpICU+JSANCiAgICAgIG11dGF0ZShCQUQgPSBjYXNlX3doZW4oQkFEID09IDEgfiAiQmFkIiwgVFJVRSB+ICJHb29kIikgJT4lIGFzLmZhY3RvcigpKQ0KICAgIA0KICAgIGRmX3Rlc3Rfd29lIDwtIHdvZWJpbl9wbHkoZGZfdGVzdCwgYmlucyA9IGJpbnMpICU+JSANCiAgICAgIG11dGF0ZShCQUQgPSBjYXNlX3doZW4oQkFEID09IDEgfiAiQmFkIiwgVFJVRSB+ICJHb29kIikgJT4lIGFzLmZhY3RvcigpKQ0KICAgIA0KICAgICMgVGjhu7FjIGhp4buHbiBMb2dpc3RpYyB24bubaSBiaeG6v24gxJHGsOG7o2MgbMOgbSBXT0UgVHJhbnNmb3JtYXRpb246IA0KICAgIG1vZGVsX2Z1bGxfd29lIDwtIGdsbShCQUQgfiAuLCBmYW1pbHkgPSAiYmlub21pYWwiLCBkYXRhID0gZGZfdHJhaW5fd29lKQ0KICAgIA0KICAgIGF1Y193b2VfaSA8LSBteV9yb2NfZnVuKG1vZGVsX2Z1bGxfd29lLCBkZl90ZXN0X3dvZSkNCiAgICBhdWNfd29lIDwtIGMoYXVjX3dvZSwgYXVjX3dvZV9pKQ0KICAgIA0KICAgIA0KICAgICMgQ2h14bqpbiBi4buLIGThu68gbGnhu4d1IGNobyBMb2dpc3RpYyBtw6AgYmnhur9uIGzDoCBuZ3V5w6puIGLhuqNuOiANCiAgICBkZl90cmFpbl9vcmkgPC0gZGZfdHJhaW4gJT4lIA0KICAgICAgbXV0YXRlKEJBRCA9IGNhc2Vfd2hlbihCQUQgPT0gMSB+ICJCYWQiLCBUUlVFIH4gIkdvb2QiKSAlPiUgYXMuZmFjdG9yKCkpDQogICAgDQogICAgZGZfdGVzdF9vcmkgPC0gZGZfdGVzdCAlPiUgDQogICAgICBtdXRhdGUoQkFEID0gY2FzZV93aGVuKEJBRCA9PSAxIH4gIkJhZCIsIFRSVUUgfiAiR29vZCIpICU+JSBhcy5mYWN0b3IoKSkNCiAgICANCiAgICAjIFRo4buxYyBoaeG7h24gTG9naXN0aWMgduG7m2kgYmnhur9uIG5ndXnDqm4gYuG6o246IA0KICAgIG1vZGVsX2Z1bGxfb3JpIDwtIGdsbShCQUQgfiAuLCBmYW1pbHkgPSAiYmlub21pYWwiLCBkYXRhID0gZGZfdHJhaW5fb3JpKQ0KICAgIGF1Y19vcmlfaSA8LSBteV9yb2NfZnVuKG1vZGVsX2Z1bGxfb3JpLCBkZl90ZXN0X29yaSkNCiAgICBhdWNfb3JpIDwtIGMoYXVjX29yaSwgYXVjX29yaV9pKQ0KICAgIA0KICB9DQogIA0KICByZXR1cm4oZGF0YS5mcmFtZShBVUNfd29lID0gYXVjX3dvZSwgQVVDX29yaSA9IGF1Y19vcmkpKQ0KfQ0KDQoNCiMgVGjhu60gbmdoaeG7h20gdHLDqm4gMTAgbOG6p24gY2jhuqF5IG3huqt1LiBD4bqjIDEwIGzhuqduIHRo4butIG5naGnhu4dtIGPDsyB0aOG7gyB0aOG6pXkNCiMgUk9DL0FVQyBj4bunYSBtw7QgaMOsbmggTG9naXN0aWMgduG7m2kgYmnhur9uIMSRxrDhu6NjIHRo4buxYyBoaeG7h24gV09FIFRyYW5zZm9ybWF0aW9uDQojIGzDoCBjYW8gaMahbjogDQoNCm15X3JvY19mb3JfY29tcGFyaXNpb24oc29fbGFuX3RodSA9IDEwKSAlPiUga25pdHI6OmthYmxlKCkNCg0KYGBgDQoNCg==