Motivation

Trong post trước chúng ta đã sử dụng AUC để lựa chọn biến số cho mô hình Logistic để đạt được AUC = 0.903. Chúng ta có thể đạt được thứ hạng cao hơn nữa (cao hơn vị trí thứ hai của Team yoshida có AUC trên Test Data = 0.93293) với kĩ thuật lựa chọn biến số dựa trên Information Value (IV). Bạn đọc quan tâm có thể tham khảo thêm về IV tại trang 81 của textbook Credit Risk Scorecards: Developing and Implementing Intelligent Credit Scoring.

Vẫn như ở post trước chúng ta vẫn sử dụng bộ dữ liệu (download tại đây) đã được sử dụng cho cuộc thi Corporate Bankruptcy Prediction 2021 trên Kaggle. Trước hết load bộ dữ liệu rồi thực hiện một số thao tác xử lí sơ bộ ban đầu:

# Clear our R environment: 
rm(list = ls())

# Load tidyverse package: 
library(tidyverse)

# Load data: 
read_csv("F:/data.csv") -> data

# Rename for all columns: 

old_names <- names(data)

old_names %>% str_replace_all("[^a-z|^A-Z]", "") -> new_names

names(data) <- new_names

# Remove NetIncomeFlag column: 
data %>% select(-NetIncomeFlag)-> df

# Set response and predictors: 

response <- "Bankrupt"

predictors <- names(df %>% select(-response))

Do thời hạn cuộc thi đã hết nên chúng ta có thể phân chia dữ liệu theo tỉ lệ 70-30 như đã mô tả và sử dụng như là proxy để so sánh kết quả của những mô hình phân loại mà chúng ta xây dựng với kết quả của những đội đã tham gia cuộc thi với lưu ý rằng Validation Data có số lượng đúng bằng số quan sát ở Test Data:

# Split our data: 

library(caret)

set.seed(1)

id <- createDataPartition(y = df %>% pull(response), p = 0.7, list = FALSE)

# 70% data for training và validation: 
train_valid <- df[id, ] 

# 30% data will be used for evaluating model performance: 

df_test <- df[-id, ] # Test data.  

set.seed(1)

id_new <- createDataPartition(y = train_valid %>% pull(response), p = nrow(df_test) / nrow(train_valid), list = FALSE)

df_train <- train_valid[-id_new, ] # Train data. 

df_valid <- train_valid[id_new, ] # Validation data. 

Using IV for Feature Selection

Nhắc lại rằng 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ố. Việc lựa chọn biến số cho mô hình phân loại sẽ căn cứ vào giá trị IV của nó (xem trang 81 cuốn Credit Risk Scorecards: Developing and Implementing Intelligent Credit Scoring). Chúng ta có thể sử dụng thư viện scorecard thể thực hiện Binning dữ liệu cho 94 biến số như sau:

# Load scorecard package: 
library(scorecard)

# Binning data: 

bins <- woebin(df_train, y = response)
## [INFO] creating woe binning ... 
## [INFO] Binning on 2729 rows and 95 columns in 00:00:32
df_train_woe <- woebin_ply(df_train, bins = bins) %>% 
  mutate(Bankrupt = case_when(Bankrupt == 1 ~ "Bankrupt", TRUE ~ "NonBankrupt")) %>% 
  mutate(Bankrupt = as.factor(Bankrupt))
## [INFO] converting into woe values ...
df_valid_woe <- woebin_ply(df_valid, bins = bins) %>% 
  mutate(Bankrupt = case_when(Bankrupt == 1 ~ "Bankrupt", TRUE ~ "NonBankrupt")) %>% 
  mutate(Bankrupt = as.factor(Bankrupt))
## [INFO] converting into woe values ...
df_test_woe <- woebin_ply(df_test %>% select(-response), bins = bins) 
## [INFO] converting into woe values ...
# True label from Test Data: 

true_test_labels <- case_when(df_test$Bankrupt == 1 ~ "Bankrupt", TRUE ~ "NonBankrupt") %>% as.factor()

# Total IV for 94 features: 

do.call("bind_rows", lapply(1:length(predictors), function(j) {bins[[j]]})) %>% 
  arrange(-total_iv) %>% 
  mutate(variable_woe = str_c(variable, "_woe")) %>% 
  select(variable, variable_woe, total_iv) %>% 
  as_tibble() %>% 
  filter(!duplicated(variable)) -> df_iv

Dưới đây là IV của 6 features có Total IV cao nhất:

df_iv %>% 
  select(variable, total_iv) %>% 
  head()
## # A tibble: 6 x 2
##   variable                                        total_iv
##   <chr>                                              <dbl>
## 1 RetainedEarningstoTotalAssets                       2.82
## 2 NetprofitbeforetaxPaidincapital                     2.77
## 3 NetIncometoTotalAssets                              2.65
## 4 PerShareNetprofitbeforetaxYuan                      2.62
## 5 InterestExpenseRatio                                2.55
## 6 ROACbeforeinterestanddepreciationbeforeinterest     2.53

Dưới đây là R codes khảo sát AUC trên Validation Data trước một loạt ngưỡng IV được lựa chọn của biến số:

# Function extracts ROC/AUC on valid data: 

library(pROC)

returnROC_AUC_ValidData <- function(predictor_selected) {
  
  f <- as.formula(paste0(response, " ~ ", paste(predictor_selected, collapse = " + ")))
  
  logit <- glm(f, family = "binomial", data = df_train_woe)
  
  prob_pred <- predict(logit, df_valid_woe, type = "response")
  
  my_auc <- roc(df_valid_woe$Bankrupt, prob_pred)$auc %>% as.numeric()
  
  return(my_auc)
  
}

# Set a sequence of thresholds: 

iv_thresholds <- seq(min(df_iv$total_iv), max(df_iv$total_iv), 0.01)

auc_space <- NULL

# AUC by threshold: 

for (j in iv_thresholds) {
  
  df_iv %>% 
    filter(total_iv >= j) %>% 
    pull(variable_woe) -> predictors_for_modelling
  
  returnROC_AUC_ValidData(predictors_for_modelling) -> my_auc
  
  auc_space <- c(auc_space, my_auc)
  
}

tibble(iv_thresholds = iv_thresholds, auc = auc_space) -> df_auc_threshold

# Features create max AUC on validation data: 

auc_max <- df_auc_threshold %>% slice(which.max(auc))

auc_max
## # A tibble: 1 x 2
##   iv_thresholds   auc
##           <dbl> <dbl>
## 1          1.21 0.936

Như vậy khi chọn các biến số mà thỏa mãn IV >= 1.21 thì AUC trên Validation Data sẽ là lớn nhất và bằng 0.936. Chúng ta kì vọng rằng xu hướng này sẽ vẫn đúng cho Test Data. Figure 1 cho thấy biến động của AUC trên Validation data có dạng hình chữ U ngược trong đó điểm màu đỏ tương ứng với ngưỡng IV để AUC trên Validation Data cực đại:

# Features with IV >= 1.21: 

df_iv %>% 
  filter(total_iv >= auc_max$iv_thresholds) %>% 
  pull(variable_woe) -> var_auc_936

df_auc_threshold %>% 
  ggplot(aes(iv_thresholds, auc)) + 
  geom_line(size = 1, color = "blue") + 
  geom_point(data = auc_max, aes(iv_thresholds, auc), color = "red", size = 2) + 
  labs(x = "IV Threshold", 
       y = "AUC on Valid Data", 
       title = "Figure 1: AUC on Validation Data by Information Value Threshold")

Sử dụng các biến số mà IV >= 1.21 (có 35 biến) cho mô hình Logistic:

f936 <- as.formula(paste0(response, " ~ ", paste(var_auc_936, collapse = " + ")))

logit936 <- glm(f936, family = "binomial", data = df_train_woe)

Sử dụng mô hình Logistic này đánh giá hiệu quả dự báo - phân loại trên Test Data:

prob_pred936 <- predict(logit936, df_test_woe, type = "response")

my_auc <- roc(true_test_labels, prob_pred936)$auc %>% as.numeric()

my_auc
## [1] 0.9325832

Như vậy AUC trên Test Data = 0.93258 (Team yoshida có AUC = 0.93203, xếp thứ hai ở bảng Public Score).

Automated Machine Learning

Dựa trên kết quả thu được ở trên chúng ta có thể đạt được thứ hạng cao hơn nữa bằng sử dụng cách tiếp cận Automated Machine Learning với inputs là các biến có IV >= 1.21. Dưới đây là R codes:

# Load h2o package for Automated Machine Learning: 

library(h2o)
h2o.init(nthreads = 20, max_mem_size = "32g")
##  Connection successful!
## 
## R is connected to the H2O cluster: 
##     H2O cluster uptime:         4 hours 33 minutes 
##     H2O cluster timezone:       Asia/Bangkok 
##     H2O data parsing timezone:  UTC 
##     H2O cluster version:        3.32.1.5 
##     H2O cluster version age:    6 months and 15 days !!! 
##     H2O cluster name:           H2O_started_from_R_ADMIN_xfr920 
##     H2O cluster total nodes:    1 
##     H2O cluster total memory:   6.59 GB 
##     H2O cluster total cores:    40 
##     H2O cluster allowed cores:  20 
##     H2O cluster healthy:        TRUE 
##     H2O Connection ip:          localhost 
##     H2O Connection port:        54321 
##     H2O Connection proxy:       NA 
##     H2O Internal Security:      FALSE 
##     H2O API Extensions:         Amazon S3, Algos, AutoML, Core V3, TargetEncoder, Core V4 
##     R Version:                  R version 4.1.2 (2021-11-01)
h2o.no_progress()

# Prepare data: 

train_h2o <- as.h2o(df_train_woe) # Train data. 

valid_h2o <- as.h2o(df_valid_woe) # Validation data. 

test_h2o <- as.h2o(df_test_woe) # Convert test data to h2o frame. 


#===================================
#  Training Auto Machine Learning
#===================================

# Train Auto Machine Learning: 

autoML <- h2o.automl(x = var_auc_936, 
                     y = response, 
                     training_frame = train_h2o, 
                     leaderboard_frame = valid_h2o, 
                     stopping_metric = "AUC", 
                     stopping_rounds = 10, 
                     stopping_tolerance = 0.025, 
                     max_models = 15, 
                     max_runtime_secs = 60*60, 
                     seed = 1, 
                     sort_metric = "AUC")
## 
## 19:50:43.934: AutoML: XGBoost is not available; skipping it.

Sử dụng mô hình có AUC lớn nhất trên Validation Data và đánh giá hiệu quả phân loại - dự báo của mô hình này bằng Test Data:

prob_h20 <- h2o.predict(autoML@leader, test_h2o) %>% 
  as.data.frame() %>% 
  pull(Bankrupt)

roc(true_test_labels, prob_h20)$auc %>% as.numeric()
## [1] 0.937021

Kết quả AUC này trên Test Data này chỉ thua kém không đáng kể so với AUC = 0.93798 của Team xếp thứ nhất (Team KotaShimomura).

Summary

  1. Sử dụng IV cho bước lựa chọn biến số (Feature Selection) và chỉ sử dụng mô hình thống kê truyền thống đơn giản là Logistic chúng ta cũng có thể đặt được một kết quả rất khả quan. Thực nghiệm trên bộ dữ liệu Test Data được AUC = 0.93258.

  2. Sử dụng danh sách các biến số có IV >= 1.21 và sử dụng Automated Machine Learning có thể đạt được AUC cao hơn nữa trên Test Data. Kết quả thực nhiệm chỉ ra rằng AUC trên Test Data khi sử dụng Automated Machine Learning xấp xỉ với AUC của Team đang đứng vị trí thứ nhất trong cuộc thi Corporate Bankruptcy Prediction 2021.

  3. Đây là dữ liệu bất cân bằng rất cao (chỉ có 3.2% các quan sát là Bankrupt) nên có thể cần xem xét đến khả năng sử dụng các giải pháp resampling dữ liệu như SMOTE, upsampling - downsampling để đạt kết quả tốt hơn nữa.

R Environment and OS

sessionInfo()
## R version 4.1.2 (2021-11-01)
## Platform: x86_64-w64-mingw32/x64 (64-bit)
## Running under: Windows 10 x64 (build 19043)
## 
## Matrix products: default
## 
## locale:
## [1] LC_COLLATE=English_United States.1252 
## [2] LC_CTYPE=English_United States.1252   
## [3] LC_MONETARY=English_United States.1252
## [4] LC_NUMERIC=C                          
## [5] LC_TIME=English_United States.1252    
## 
## attached base packages:
## [1] stats     graphics  grDevices utils     datasets  methods   base     
## 
## other attached packages:
##  [1] h2o_3.32.1.5    pROC_1.17.0.1   scorecard_0.3.6 caret_6.0-88   
##  [5] lattice_0.20-45 forcats_0.5.1   stringr_1.4.0   dplyr_1.0.7    
##  [9] purrr_0.3.4     readr_2.0.0     tidyr_1.1.3     tibble_3.1.3   
## [13] ggplot2_3.3.5   tidyverse_1.3.1
## 
## loaded via a namespace (and not attached):
##  [1] nlme_3.1-153         bitops_1.0-7         fs_1.5.0            
##  [4] lubridate_1.7.10     bit64_4.0.5          doParallel_1.0.16   
##  [7] httr_1.4.2           tools_4.1.2          backports_1.2.1     
## [10] bslib_0.2.5.1        utf8_1.2.2           R6_2.5.1            
## [13] rpart_4.1-15         DBI_1.1.1            colorspace_2.0-2    
## [16] nnet_7.3-16          withr_2.4.2          tidyselect_1.1.1    
## [19] gridExtra_2.3        bit_4.0.4            compiler_4.1.2      
## [22] cli_3.0.1            rvest_1.0.1          xml2_1.3.2          
## [25] labeling_0.4.2       sass_0.4.0           scales_1.1.1        
## [28] digest_0.6.27        rmarkdown_2.9        pkgconfig_2.0.3     
## [31] htmltools_0.5.1.1    highr_0.9            dbplyr_2.1.1        
## [34] rlang_0.4.11         readxl_1.3.1         rstudioapi_0.13     
## [37] farver_2.1.0         jquerylib_0.1.4      generics_0.1.0      
## [40] jsonlite_1.7.2       vroom_1.5.4          zip_2.2.0           
## [43] ModelMetrics_1.2.2.2 RCurl_1.98-1.4       magrittr_2.0.1      
## [46] Matrix_1.3-4         Rcpp_1.0.7           munsell_0.5.0       
## [49] fansi_0.5.0          lifecycle_1.0.0      stringi_1.7.4       
## [52] yaml_2.2.1           MASS_7.3-54          plyr_1.8.6          
## [55] recipes_0.1.16       grid_4.1.2           parallel_4.1.2      
## [58] crayon_1.4.1         haven_2.4.1          splines_4.1.2       
## [61] hms_1.1.0            knitr_1.33           pillar_1.6.2        
## [64] reshape2_1.4.4       codetools_0.2-18     stats4_4.1.2        
## [67] reprex_2.0.0         glue_1.4.2           evaluate_0.14       
## [70] data.table_1.14.0    modelr_0.1.8         vctrs_0.3.8         
## [73] tzdb_0.1.2           foreach_1.5.1        cellranger_1.1.0    
## [76] gtable_0.3.0         assertthat_0.2.1     openxlsx_4.2.4      
## [79] xfun_0.25            gower_0.2.2          prodlim_2019.11.13  
## [82] broom_0.7.7          class_7.3-19         survival_3.2-13     
## [85] timeDate_3043.102    iterators_1.0.13     lava_1.6.9          
## [88] ellipsis_0.3.2       ipred_0.9-11
LS0tDQp0aXRsZTogJ0luZm9ybWF0aW9uIFZhbHVlIGZvciBGZWF0dXJlIFNlbGVjdGlvbjogQ29ycG9yYXRlIEJhbmtydXB0Y3kgUHJlZGljdGlvbiBmcm9tIEthZ2dsZSAyMDIxIENvbXBldGl0aW9uJw0KYXV0aG9yOiAnQXV0aG9yOiBOZ3V5ZW4gQ2hpIER1bmcnDQpzdWJ0aXRsZTogIlIgRGF0YSBTY2llbmNlIFNlcmllcyINCm91dHB1dDoNCiAgaHRtbF9kb2N1bWVudDogDQogICAgY29kZV9kb3dubG9hZDogdHJ1ZQ0KICAgICMgY29kZV9mb2xkaW5nOiBoaWRlDQogICAgaGlnaGxpZ2h0OiB6ZW5idXJuDQogICAgIyBudW1iZXJfc2VjdGlvbnM6IHllcw0KICAgIHRoZW1lOiAiZmxhdGx5Ig0KICAgIHRvYzogVFJVRQ0KICAgIHRvY19mbG9hdDogVFJVRQ0KLS0tDQoNCmBgYHtyIHNldHVwLGluY2x1ZGU9RkFMU0V9DQprbml0cjo6b3B0c19jaHVuayRzZXQoZWNobyA9IFRSVUUsIHdhcm5pbmcgPSBGQUxTRSwgbWVzc2FnZSA9IEZBTFNFLCBjYWNoZSA9IFRSVUUpDQoNCmBgYA0KDQoNCiFbXShDOi9Vc2Vycy9BZG1pbi9Eb2N1bWVudHMvYmFua3J1cHQuanBnKQ0KDQojIE1vdGl2YXRpb24NCg0KVHJvbmcgW3Bvc3QgdHLGsOG7m2NdKGh0dHBzOi8vcnB1YnMuY29tL2NoaWR1bmdrdC84NjcyNzIpIGNow7puZyB0YSDEkcOjIHPhu60gZOG7pW5nIEFVQyDEkeG7gyBs4buxYSBjaOG7jW4gYmnhur9uIHPhu5EgY2hvIG3DtCBow6xuaCBMb2dpc3RpYyDEkeG7gyDEkeG6oXQgxJHGsOG7o2MgQVVDID0gMC45MDMuIENow7puZyB0YSBjw7MgdGjhu4MgxJHhuqF0IMSRxrDhu6NjIHRo4bupIGjhuqFuZyBjYW8gaMahbiBu4buvYSAoY2FvIGjGoW4gduG7iyB0csOtIHRo4bupIGhhaSBj4bunYSBUZWFtIHlvc2hpZGEgY8OzIEFVQyB0csOqbiBUZXN0IERhdGEgPSAwLjkzMjkzKSB24bubaSBrxKkgdGh14bqtdCBs4buxYSBjaOG7jW4gYmnhur9uIHPhu5EgZOG7sWEgdHLDqm4gSW5mb3JtYXRpb24gVmFsdWUgKElWKS4gQuG6oW4gxJHhu41jIHF1YW4gdMOibSBjw7MgdGjhu4MgdGhhbSBraOG6o28gdGjDqm0gduG7gSBJViB04bqhaSB0cmFuZyA4MSBj4bunYSB0ZXh0Ym9vayBbQ3JlZGl0IFJpc2sgU2NvcmVjYXJkczogRGV2ZWxvcGluZyBhbmQgSW1wbGVtZW50aW5nIEludGVsbGlnZW50IENyZWRpdCBTY29yaW5nXShodHRwczovL3d3dy5hbWF6b24uY29tL0NyZWRpdC1SaXNrLVNjb3JlY2FyZHMtSW1wbGVtZW50aW5nLUludGVsbGlnZW50L2RwLzA0NzE3NTQ1MVgpLiANCg0KVuG6q24gbmjGsCDhu58gcG9zdCB0csaw4bubYyBjaMO6bmcgdGEgduG6q24gc+G7rSBk4bulbmcgYuG7mSBk4buvIGxp4buHdSAoZG93bmxvYWQgW3ThuqFpIMSRw6J5XShodHRwczovL3d3dy5rYWdnbGUuY29tL2ZlZGVzb3JpYW5vL2NvbXBhbnktYmFua3J1cHRjeS1wcmVkaWN0aW9uKSkgxJHDoyDEkcaw4bujYyBz4butIGThu6VuZyBjaG8gY3Xhu5ljIHRoaSBbQ29ycG9yYXRlIEJhbmtydXB0Y3kgUHJlZGljdGlvbiAyMDIxXShodHRwczovL3d3dy5rYWdnbGUuY29tL2MvMTA1NmxhYi1jb3Jwb3JhdGUtYmFua3J1cHRjeS1wcmVkaWN0aW9uLTIwMjEvbGVhZGVyYm9hcmQ/dGFiPXB1YmxpYykgdHLDqm4gS2FnZ2xlLiBUcsaw4bubYyBo4bq/dCBsb2FkIGLhu5kgZOG7ryBsaeG7h3UgcuG7k2kgdGjhu7FjIGhp4buHbiBt4buZdCBz4buRIHRoYW8gdMOhYyB44butIGzDrSBzxqEgYuG7mSBiYW4gxJHhuqd1OiANCg0KYGBge3J9DQojIENsZWFyIG91ciBSIGVudmlyb25tZW50OiANCnJtKGxpc3QgPSBscygpKQ0KDQojIExvYWQgdGlkeXZlcnNlIHBhY2thZ2U6IA0KbGlicmFyeSh0aWR5dmVyc2UpDQoNCiMgTG9hZCBkYXRhOiANCnJlYWRfY3N2KCJGOi9kYXRhLmNzdiIpIC0+IGRhdGENCg0KIyBSZW5hbWUgZm9yIGFsbCBjb2x1bW5zOiANCg0Kb2xkX25hbWVzIDwtIG5hbWVzKGRhdGEpDQoNCm9sZF9uYW1lcyAlPiUgc3RyX3JlcGxhY2VfYWxsKCJbXmEtenxeQS1aXSIsICIiKSAtPiBuZXdfbmFtZXMNCg0KbmFtZXMoZGF0YSkgPC0gbmV3X25hbWVzDQoNCiMgUmVtb3ZlIE5ldEluY29tZUZsYWcgY29sdW1uOiANCmRhdGEgJT4lIHNlbGVjdCgtTmV0SW5jb21lRmxhZyktPiBkZg0KDQojIFNldCByZXNwb25zZSBhbmQgcHJlZGljdG9yczogDQoNCnJlc3BvbnNlIDwtICJCYW5rcnVwdCINCg0KcHJlZGljdG9ycyA8LSBuYW1lcyhkZiAlPiUgc2VsZWN0KC1yZXNwb25zZSkpDQpgYGANCg0KRG8gdGjhu51pIGjhuqFuIGN14buZYyB0aGkgxJHDoyBo4bq/dCBuw6puIGNow7puZyB0YSBjw7MgdGjhu4MgcGjDom4gY2hpYSBk4buvIGxp4buHdSB0aGVvIHThu4kgbOG7hyA3MC0zMCBuaMawIMSRw6MgbcO0IHThuqMgdsOgIHPhu60gZOG7pW5nIG5oxrAgbMOgIHByb3h5IMSR4buDIHNvIHPDoW5oIGvhur90IHF14bqjIGPhu6dhIG5o4buvbmcgbcO0IGjDrG5oIHBow6JuIGxv4bqhaSBtw6AgY2jDum5nIHRhIHjDonkgZOG7sW5nIHbhu5tpIGvhur90IHF14bqjIGPhu6dhIG5o4buvbmcgxJHhu5lpIMSRw6MgdGhhbSBnaWEgY3Xhu5ljIHRoaSB24bubaSBsxrB1IMO9IHLhurFuZyBWYWxpZGF0aW9uIERhdGEgY8OzIHPhu5EgbMaw4bujbmcgxJHDum5nIGLhurFuZyBz4buRIHF1YW4gc8OhdCDhu58gVGVzdCBEYXRhOiANCg0KYGBge3J9DQojIFNwbGl0IG91ciBkYXRhOiANCg0KbGlicmFyeShjYXJldCkNCg0Kc2V0LnNlZWQoMSkNCg0KaWQgPC0gY3JlYXRlRGF0YVBhcnRpdGlvbih5ID0gZGYgJT4lIHB1bGwocmVzcG9uc2UpLCBwID0gMC43LCBsaXN0ID0gRkFMU0UpDQoNCiMgNzAlIGRhdGEgZm9yIHRyYWluaW5nIHbDoCB2YWxpZGF0aW9uOiANCnRyYWluX3ZhbGlkIDwtIGRmW2lkLCBdIA0KDQojIDMwJSBkYXRhIHdpbGwgYmUgdXNlZCBmb3IgZXZhbHVhdGluZyBtb2RlbCBwZXJmb3JtYW5jZTogDQoNCmRmX3Rlc3QgPC0gZGZbLWlkLCBdICMgVGVzdCBkYXRhLiAgDQoNCnNldC5zZWVkKDEpDQoNCmlkX25ldyA8LSBjcmVhdGVEYXRhUGFydGl0aW9uKHkgPSB0cmFpbl92YWxpZCAlPiUgcHVsbChyZXNwb25zZSksIHAgPSBucm93KGRmX3Rlc3QpIC8gbnJvdyh0cmFpbl92YWxpZCksIGxpc3QgPSBGQUxTRSkNCg0KZGZfdHJhaW4gPC0gdHJhaW5fdmFsaWRbLWlkX25ldywgXSAjIFRyYWluIGRhdGEuIA0KDQpkZl92YWxpZCA8LSB0cmFpbl92YWxpZFtpZF9uZXcsIF0gIyBWYWxpZGF0aW9uIGRhdGEuIA0KYGBgDQoNCiMgVXNpbmcgSVYgZm9yIEZlYXR1cmUgU2VsZWN0aW9uIA0KDQpOaOG6r2MgbOG6oWkgcuG6sW5nIFdPRSBCaW5uaW5nIGzDoCBt4buZdCBrxKkgdGh14bqtdCB0aHXhu5ljIG5ow7NtIERhdGEgVHJhbnNmb3JtYXRpb24gbcOgIHRyb25nIMSRw7MgbeG7mXQgYmnhur9uIGxpw6puIGzhu6VjIMSRxrDhu6NjIOKAnHLhu51pIHLhuqFjIGjDs2HigJ0gKHRodeG6rXQgbmfhu68gdGnhur9uZyBBbmggbMOgIERpc2NyZXRpemF0aW9uKSB0aMOgbmggY8OhYyBuaMOzbSAoZ+G7jWkgbMOgIGJpbm5pbmcpLiBWaeG7h2MgY2hpYSBiaeG6v24gbGnDqm4gdOG7pWMgdGjDoG5oIGPDoWMgbmjDs20gbmjGsCB24bqteSDEkcaw4bujYyB0aOG7sWMgaGnhu4duIHbhu5tpIG3hu6VjIMSRw61uaCBsw6AgdMOtbmggdG/DoW4gV29FIChi4bqxbmcgY2jhu6luZyBjw7MgdHLhu41uZyBz4buRIFdvRSAtIFdlaWdodCBvZiBFdmlkZW5jZSkgdsOgIGdpw6EgdHLhu4sgdGjDtG5nIHRpbiBJViAoSW5mb3JtYXRpb24gVmFsdWUpIGPhu6dhIGJp4bq/biBz4buRLiBWaeG7h2MgbOG7sWEgY2jhu41uIGJp4bq/biBz4buRIGNobyBtw7QgaMOsbmggcGjDom4gbG/huqFpIHPhur0gY8SDbiBj4bupIHbDoG8gZ2nDoSB0cuG7iyBJViBj4bunYSBuw7MgKHhlbSB0cmFuZyA4MSBjdeG7kW4gQ3JlZGl0IFJpc2sgU2NvcmVjYXJkczogRGV2ZWxvcGluZyBhbmQgSW1wbGVtZW50aW5nIEludGVsbGlnZW50IENyZWRpdCBTY29yaW5nKS4gQ2jDum5nIHRhIGPDsyB0aOG7gyBz4butIGThu6VuZyB0aMawIHZp4buHbiBbc2NvcmVjYXJkXShodHRwczovL2NyYW4uci1wcm9qZWN0Lm9yZy93ZWIvcGFja2FnZXMvc2NvcmVjYXJkL3Njb3JlY2FyZC5wZGYpIHRo4buDIHRo4buxYyBoaeG7h24gQmlubmluZyBk4buvIGxp4buHdSBjaG8gOTQgYmnhur9uIHPhu5EgbmjGsCBzYXU6IA0KDQpgYGB7cn0NCiMgTG9hZCBzY29yZWNhcmQgcGFja2FnZTogDQpsaWJyYXJ5KHNjb3JlY2FyZCkNCg0KIyBCaW5uaW5nIGRhdGE6IA0KDQpiaW5zIDwtIHdvZWJpbihkZl90cmFpbiwgeSA9IHJlc3BvbnNlKQ0KDQpkZl90cmFpbl93b2UgPC0gd29lYmluX3BseShkZl90cmFpbiwgYmlucyA9IGJpbnMpICU+JSANCiAgbXV0YXRlKEJhbmtydXB0ID0gY2FzZV93aGVuKEJhbmtydXB0ID09IDEgfiAiQmFua3J1cHQiLCBUUlVFIH4gIk5vbkJhbmtydXB0IikpICU+JSANCiAgbXV0YXRlKEJhbmtydXB0ID0gYXMuZmFjdG9yKEJhbmtydXB0KSkNCg0KZGZfdmFsaWRfd29lIDwtIHdvZWJpbl9wbHkoZGZfdmFsaWQsIGJpbnMgPSBiaW5zKSAlPiUgDQogIG11dGF0ZShCYW5rcnVwdCA9IGNhc2Vfd2hlbihCYW5rcnVwdCA9PSAxIH4gIkJhbmtydXB0IiwgVFJVRSB+ICJOb25CYW5rcnVwdCIpKSAlPiUgDQogIG11dGF0ZShCYW5rcnVwdCA9IGFzLmZhY3RvcihCYW5rcnVwdCkpDQoNCmRmX3Rlc3Rfd29lIDwtIHdvZWJpbl9wbHkoZGZfdGVzdCAlPiUgc2VsZWN0KC1yZXNwb25zZSksIGJpbnMgPSBiaW5zKSANCg0KIyBUcnVlIGxhYmVsIGZyb20gVGVzdCBEYXRhOiANCg0KdHJ1ZV90ZXN0X2xhYmVscyA8LSBjYXNlX3doZW4oZGZfdGVzdCRCYW5rcnVwdCA9PSAxIH4gIkJhbmtydXB0IiwgVFJVRSB+ICJOb25CYW5rcnVwdCIpICU+JSBhcy5mYWN0b3IoKQ0KDQojIFRvdGFsIElWIGZvciA5NCBmZWF0dXJlczogDQoNCmRvLmNhbGwoImJpbmRfcm93cyIsIGxhcHBseSgxOmxlbmd0aChwcmVkaWN0b3JzKSwgZnVuY3Rpb24oaikge2JpbnNbW2pdXX0pKSAlPiUgDQogIGFycmFuZ2UoLXRvdGFsX2l2KSAlPiUgDQogIG11dGF0ZSh2YXJpYWJsZV93b2UgPSBzdHJfYyh2YXJpYWJsZSwgIl93b2UiKSkgJT4lIA0KICBzZWxlY3QodmFyaWFibGUsIHZhcmlhYmxlX3dvZSwgdG90YWxfaXYpICU+JSANCiAgYXNfdGliYmxlKCkgJT4lIA0KICBmaWx0ZXIoIWR1cGxpY2F0ZWQodmFyaWFibGUpKSAtPiBkZl9pdg0KYGBgDQoNCkTGsOG7m2kgxJHDonkgbMOgIElWIGPhu6dhIDYgZmVhdHVyZXMgY8OzIFRvdGFsIElWIGNhbyBuaOG6pXQ6IA0KDQpgYGB7cn0NCmRmX2l2ICU+JSANCiAgc2VsZWN0KHZhcmlhYmxlLCB0b3RhbF9pdikgJT4lIA0KICBoZWFkKCkNCmBgYA0KDQpExrDhu5tpIMSRw6J5IGzDoCBSIGNvZGVzIGto4bqjbyBzw6F0IEFVQyB0csOqbiBWYWxpZGF0aW9uIERhdGEgdHLGsOG7m2MgbeG7mXQgbG/huqF0IG5nxrDhu6FuZyBJViDEkcaw4bujYyBs4buxYSBjaOG7jW4gY+G7p2EgYmnhur9uIHPhu5E6ICANCg0KDQpgYGB7cn0NCiMgRnVuY3Rpb24gZXh0cmFjdHMgUk9DL0FVQyBvbiB2YWxpZCBkYXRhOiANCg0KbGlicmFyeShwUk9DKQ0KDQpyZXR1cm5ST0NfQVVDX1ZhbGlkRGF0YSA8LSBmdW5jdGlvbihwcmVkaWN0b3Jfc2VsZWN0ZWQpIHsNCiAgDQogIGYgPC0gYXMuZm9ybXVsYShwYXN0ZTAocmVzcG9uc2UsICIgfiAiLCBwYXN0ZShwcmVkaWN0b3Jfc2VsZWN0ZWQsIGNvbGxhcHNlID0gIiArICIpKSkNCiAgDQogIGxvZ2l0IDwtIGdsbShmLCBmYW1pbHkgPSAiYmlub21pYWwiLCBkYXRhID0gZGZfdHJhaW5fd29lKQ0KICANCiAgcHJvYl9wcmVkIDwtIHByZWRpY3QobG9naXQsIGRmX3ZhbGlkX3dvZSwgdHlwZSA9ICJyZXNwb25zZSIpDQogIA0KICBteV9hdWMgPC0gcm9jKGRmX3ZhbGlkX3dvZSRCYW5rcnVwdCwgcHJvYl9wcmVkKSRhdWMgJT4lIGFzLm51bWVyaWMoKQ0KICANCiAgcmV0dXJuKG15X2F1YykNCiAgDQp9DQoNCiMgU2V0IGEgc2VxdWVuY2Ugb2YgdGhyZXNob2xkczogDQoNCml2X3RocmVzaG9sZHMgPC0gc2VxKG1pbihkZl9pdiR0b3RhbF9pdiksIG1heChkZl9pdiR0b3RhbF9pdiksIDAuMDEpDQoNCmF1Y19zcGFjZSA8LSBOVUxMDQoNCiMgQVVDIGJ5IHRocmVzaG9sZDogDQoNCmZvciAoaiBpbiBpdl90aHJlc2hvbGRzKSB7DQogIA0KICBkZl9pdiAlPiUgDQogICAgZmlsdGVyKHRvdGFsX2l2ID49IGopICU+JSANCiAgICBwdWxsKHZhcmlhYmxlX3dvZSkgLT4gcHJlZGljdG9yc19mb3JfbW9kZWxsaW5nDQogIA0KICByZXR1cm5ST0NfQVVDX1ZhbGlkRGF0YShwcmVkaWN0b3JzX2Zvcl9tb2RlbGxpbmcpIC0+IG15X2F1Yw0KICANCiAgYXVjX3NwYWNlIDwtIGMoYXVjX3NwYWNlLCBteV9hdWMpDQogIA0KfQ0KDQp0aWJibGUoaXZfdGhyZXNob2xkcyA9IGl2X3RocmVzaG9sZHMsIGF1YyA9IGF1Y19zcGFjZSkgLT4gZGZfYXVjX3RocmVzaG9sZA0KDQojIEZlYXR1cmVzIGNyZWF0ZSBtYXggQVVDIG9uIHZhbGlkYXRpb24gZGF0YTogDQoNCmF1Y19tYXggPC0gZGZfYXVjX3RocmVzaG9sZCAlPiUgc2xpY2Uod2hpY2gubWF4KGF1YykpDQoNCmF1Y19tYXgNCmBgYA0KDQpOaMawIHbhuq15IGtoaSBjaOG7jW4gY8OhYyBiaeG6v24gc+G7kSBtw6AgdGjhu49hIG3Do24gSVYgPj0gMS4yMSB0aMOsIEFVQyB0csOqbiBWYWxpZGF0aW9uIERhdGEgc+G6vSBsw6AgbOG7m24gbmjhuqV0IHbDoCBi4bqxbmcgMC45MzYuIENow7puZyB0YSBrw6wgduG7jW5nIHLhurFuZyB4dSBoxrDhu5tuZyBuw6B5IHPhur0gduG6q24gxJHDum5nIGNobyBUZXN0IERhdGEuIEZpZ3VyZSAxIGNobyB0aOG6pXkgYmnhur9uIMSR4buZbmcgY+G7p2EgQVVDIHRyw6puIFZhbGlkYXRpb24gZGF0YSBjw7MgZOG6oW5nIGjDrG5oIGNo4buvIFUgbmfGsOG7o2MgdHJvbmcgxJHDsyDEkWnhu4NtIG3DoHUgxJHhu48gdMawxqFuZyDhu6luZyB24bubaSBuZ8aw4buhbmcgSVYgxJHhu4MgQVVDIHRyw6puIFZhbGlkYXRpb24gRGF0YSBj4buxYyDEkeG6oWk6ICANCg0KYGBge3J9DQojIEZlYXR1cmVzIHdpdGggSVYgPj0gMS4yMTogDQoNCmRmX2l2ICU+JSANCiAgZmlsdGVyKHRvdGFsX2l2ID49IGF1Y19tYXgkaXZfdGhyZXNob2xkcykgJT4lIA0KICBwdWxsKHZhcmlhYmxlX3dvZSkgLT4gdmFyX2F1Y185MzYNCg0KZGZfYXVjX3RocmVzaG9sZCAlPiUgDQogIGdncGxvdChhZXMoaXZfdGhyZXNob2xkcywgYXVjKSkgKyANCiAgZ2VvbV9saW5lKHNpemUgPSAxLCBjb2xvciA9ICJibHVlIikgKyANCiAgZ2VvbV9wb2ludChkYXRhID0gYXVjX21heCwgYWVzKGl2X3RocmVzaG9sZHMsIGF1YyksIGNvbG9yID0gInJlZCIsIHNpemUgPSAyKSArIA0KICBsYWJzKHggPSAiSVYgVGhyZXNob2xkIiwgDQogICAgICAgeSA9ICJBVUMgb24gVmFsaWQgRGF0YSIsIA0KICAgICAgIHRpdGxlID0gIkZpZ3VyZSAxOiBBVUMgb24gVmFsaWRhdGlvbiBEYXRhIGJ5IEluZm9ybWF0aW9uIFZhbHVlIFRocmVzaG9sZCIpDQpgYGANCg0KU+G7rSBk4bulbmcgY8OhYyBiaeG6v24gc+G7kSBtw6AgSVYgPj0gMS4yMSAoY8OzIDM1IGJp4bq/bikgY2hvIG3DtCBow6xuaCBMb2dpc3RpYzoNCg0KYGBge3J9DQpmOTM2IDwtIGFzLmZvcm11bGEocGFzdGUwKHJlc3BvbnNlLCAiIH4gIiwgcGFzdGUodmFyX2F1Y185MzYsIGNvbGxhcHNlID0gIiArICIpKSkNCg0KbG9naXQ5MzYgPC0gZ2xtKGY5MzYsIGZhbWlseSA9ICJiaW5vbWlhbCIsIGRhdGEgPSBkZl90cmFpbl93b2UpDQpgYGANCg0KU+G7rSBk4bulbmcgbcO0IGjDrG5oIExvZ2lzdGljIG7DoHkgxJHDoW5oIGdpw6EgaGnhu4d1IHF14bqjIGThu7EgYsOhbyAtIHBow6JuIGxv4bqhaSB0csOqbiBUZXN0IERhdGE6IA0KDQpgYGB7cn0NCnByb2JfcHJlZDkzNiA8LSBwcmVkaWN0KGxvZ2l0OTM2LCBkZl90ZXN0X3dvZSwgdHlwZSA9ICJyZXNwb25zZSIpDQoNCm15X2F1YyA8LSByb2ModHJ1ZV90ZXN0X2xhYmVscywgcHJvYl9wcmVkOTM2KSRhdWMgJT4lIGFzLm51bWVyaWMoKQ0KDQpteV9hdWMNCmBgYA0KDQpOaMawIHbhuq15IEFVQyB0csOqbiBUZXN0IERhdGEgPSAwLjkzMjU4IChUZWFtIHlvc2hpZGEgY8OzIEFVQyA9IDAuOTMyMDMsIHjhur9wIHRo4bupIGhhaSDhu58gYuG6o25nIFB1YmxpYyBTY29yZSkuIA0KDQojIEF1dG9tYXRlZCBNYWNoaW5lIExlYXJuaW5nDQoNCkThu7FhIHRyw6puIGvhur90IHF14bqjIHRodSDEkcaw4bujYyDhu58gdHLDqm4gY2jDum5nIHRhIGPDsyB0aOG7gyDEkeG6oXQgxJHGsOG7o2MgdGjhu6kgaOG6oW5nIGNhbyBoxqFuIG7hu69hIGLhurFuZyBz4butIGThu6VuZyBjw6FjaCB0aeG6v3AgY+G6rW4gQXV0b21hdGVkIE1hY2hpbmUgTGVhcm5pbmcgduG7m2kgaW5wdXRzIGzDoCBjw6FjIGJp4bq/biBjw7MgSVYgPj0gMS4yMS4gRMaw4bubaSDEkcOieSBsw6AgUiBjb2RlczogDQoNCmBgYHtyfQ0KIyBMb2FkIGgybyBwYWNrYWdlIGZvciBBdXRvbWF0ZWQgTWFjaGluZSBMZWFybmluZzogDQoNCmxpYnJhcnkoaDJvKQ0KaDJvLmluaXQobnRocmVhZHMgPSAyMCwgbWF4X21lbV9zaXplID0gIjMyZyIpDQpoMm8ubm9fcHJvZ3Jlc3MoKQ0KDQojIFByZXBhcmUgZGF0YTogDQoNCnRyYWluX2gybyA8LSBhcy5oMm8oZGZfdHJhaW5fd29lKSAjIFRyYWluIGRhdGEuIA0KDQp2YWxpZF9oMm8gPC0gYXMuaDJvKGRmX3ZhbGlkX3dvZSkgIyBWYWxpZGF0aW9uIGRhdGEuIA0KDQp0ZXN0X2gybyA8LSBhcy5oMm8oZGZfdGVzdF93b2UpICMgQ29udmVydCB0ZXN0IGRhdGEgdG8gaDJvIGZyYW1lLiANCg0KDQojPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0NCiMgIFRyYWluaW5nIEF1dG8gTWFjaGluZSBMZWFybmluZw0KIz09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09DQoNCiMgVHJhaW4gQXV0byBNYWNoaW5lIExlYXJuaW5nOiANCg0KYXV0b01MIDwtIGgyby5hdXRvbWwoeCA9IHZhcl9hdWNfOTM2LCANCiAgICAgICAgICAgICAgICAgICAgIHkgPSByZXNwb25zZSwgDQogICAgICAgICAgICAgICAgICAgICB0cmFpbmluZ19mcmFtZSA9IHRyYWluX2gybywgDQogICAgICAgICAgICAgICAgICAgICBsZWFkZXJib2FyZF9mcmFtZSA9IHZhbGlkX2gybywgDQogICAgICAgICAgICAgICAgICAgICBzdG9wcGluZ19tZXRyaWMgPSAiQVVDIiwgDQogICAgICAgICAgICAgICAgICAgICBzdG9wcGluZ19yb3VuZHMgPSAxMCwgDQogICAgICAgICAgICAgICAgICAgICBzdG9wcGluZ190b2xlcmFuY2UgPSAwLjAyNSwgDQogICAgICAgICAgICAgICAgICAgICBtYXhfbW9kZWxzID0gMTUsIA0KICAgICAgICAgICAgICAgICAgICAgbWF4X3J1bnRpbWVfc2VjcyA9IDYwKjYwLCANCiAgICAgICAgICAgICAgICAgICAgIHNlZWQgPSAxLCANCiAgICAgICAgICAgICAgICAgICAgIHNvcnRfbWV0cmljID0gIkFVQyIpDQpgYGANCg0KU+G7rSBk4bulbmcgbcO0IGjDrG5oIGPDsyBBVUMgbOG7m24gbmjhuqV0IHRyw6puIFZhbGlkYXRpb24gRGF0YSB2w6AgxJHDoW5oIGdpw6EgaGnhu4d1IHF14bqjIHBow6JuIGxv4bqhaSAtIGThu7EgYsOhbyBj4bunYSBtw7QgaMOsbmggbsOgeSBi4bqxbmcgVGVzdCBEYXRhOiANCg0KYGBge3J9DQpwcm9iX2gyMCA8LSBoMm8ucHJlZGljdChhdXRvTUxAbGVhZGVyLCB0ZXN0X2gybykgJT4lIA0KICBhcy5kYXRhLmZyYW1lKCkgJT4lIA0KICBwdWxsKEJhbmtydXB0KQ0KDQpyb2ModHJ1ZV90ZXN0X2xhYmVscywgcHJvYl9oMjApJGF1YyAlPiUgYXMubnVtZXJpYygpDQoNCmBgYA0KDQpL4bq/dCBxdeG6oyBBVUMgbsOgeSB0csOqbiBUZXN0IERhdGEgbsOgeSBjaOG7iSB0aHVhIGvDqW0ga2jDtG5nIMSRw6FuZyBr4buDIHNvIHbhu5tpIEFVQyA9IDAuOTM3OTggY+G7p2EgVGVhbSB44bq/cCB0aOG7qSBuaOG6pXQgKFRlYW0gS290YVNoaW1vbXVyYSkuIA0KDQojIFN1bW1hcnkNCg0KMS4gU+G7rSBk4bulbmcgSVYgY2hvIGLGsOG7m2MgbOG7sWEgY2jhu41uIGJp4bq/biBz4buRIChGZWF0dXJlIFNlbGVjdGlvbikgdsOgIGNo4buJIHPhu60gZOG7pW5nIG3DtCBow6xuaCB0aOG7kW5nIGvDqiB0cnV54buBbiB0aOG7kW5nIMSRxqFuIGdp4bqjbiBsw6AgTG9naXN0aWMgY2jDum5nIHRhIGPFqW5nIGPDsyB0aOG7gyDEkeG6t3QgxJHGsOG7o2MgbeG7mXQga+G6v3QgcXXhuqMgcuG6pXQga2jhuqMgcXVhbi4gVGjhu7FjIG5naGnhu4dtIHRyw6puIGLhu5kgZOG7ryBsaeG7h3UgVGVzdCBEYXRhIMSRxrDhu6NjIEFVQyA9IDAuOTMyNTguIA0KDQoyLiBT4butIGThu6VuZyBkYW5oIHPDoWNoIGPDoWMgYmnhur9uIHPhu5EgY8OzIElWID49IDEuMjEgdsOgIHPhu60gZOG7pW5nIEF1dG9tYXRlZCBNYWNoaW5lIExlYXJuaW5nIGPDsyB0aOG7gyDEkeG6oXQgxJHGsOG7o2MgQVVDIGNhbyBoxqFuIG7hu69hIHRyw6puIFRlc3QgRGF0YS4gS+G6v3QgcXXhuqMgdGjhu7FjIG5oaeG7h20gY2jhu4kgcmEgcuG6sW5nIEFVQyB0csOqbiBUZXN0IERhdGEga2hpIHPhu60gZOG7pW5nIEF1dG9tYXRlZCBNYWNoaW5lIExlYXJuaW5nIHjhuqVwIHjhu4kgduG7m2kgQVVDIGPhu6dhIFRlYW0gxJFhbmcgxJHhu6luZyB24buLIHRyw60gdGjhu6kgbmjhuqV0IHRyb25nIGN14buZYyB0aGkgQ29ycG9yYXRlIEJhbmtydXB0Y3kgUHJlZGljdGlvbiAyMDIxLiANCg0KMy4gxJDDonkgbMOgIGThu68gbGnhu4d1IGLhuqV0IGPDom4gYuG6sW5nIHLhuqV0IGNhbyAoY2jhu4kgY8OzIDMuMiUgY8OhYyBxdWFuIHPDoXQgbMOgIEJhbmtydXB0KSBuw6puIGPDsyB0aOG7gyBj4bqnbiB4ZW0geMOpdCDEkeG6v24ga2jhuqMgbsSDbmcgc+G7rSBk4bulbmcgY8OhYyBnaeG6o2kgcGjDoXAgcmVzYW1wbGluZyBk4buvIGxp4buHdSBuaMawIFNNT1RFLCB1cHNhbXBsaW5nIC0gZG93bnNhbXBsaW5nIMSR4buDIMSR4bqhdCBr4bq/dCBxdeG6oyB04buRdCBoxqFuIG7hu69hLiANCg0KIyBSIEVudmlyb25tZW50IGFuZCBPUw0KDQpgYGB7cn0NCnNlc3Npb25JbmZvKCkNCmBgYA0KDQoNCg==