Introduction
I will write something later…
| Bad |
300 |
8.92 |
| Good |
3064 |
91.08 |
library(tidymodels)
set.seed(123)
members_split <- initial_split(hmeq_full, prop = 0.7, strata = BAD)
members_train <- training(members_split)
members_test <- testing(members_split)
set.seed(123)
members_folds <- vfold_cv(members_train, strata = BAD, v = 5, repeats = 3)
library(recipes)
library(themis)
members_rec <- recipe(BAD ~ ., data = members_train) %>%
step_dummy(all_nominal(), -BAD) %>%
step_smote(BAD)
glm_spec <- logistic_reg() %>%
set_engine("glm")
rf_spec <- rand_forest(trees = 1000) %>%
set_mode("classification") %>%
set_engine("ranger")
members_wf <- workflow() %>%
add_recipe(members_rec)
members_metrics <- metric_set(roc_auc, accuracy, sensitivity, specificity)
glm_rs <- members_wf %>%
add_model(glm_spec) %>%
fit_resamples(resamples = members_folds,
metrics = members_metrics,
control = control_resamples(save_pred = TRUE))
rf_rs <- members_wf %>%
add_model(rf_spec) %>%
fit_resamples(resamples = members_folds,
metrics = members_metrics,
control = control_resamples(save_pred = TRUE))
members_final_glm <- members_wf %>%
add_model(glm_spec) %>%
last_fit(members_split)
collect_predictions(members_final_glm) -> glm_pd
members_final_rf <- members_wf %>%
add_model(rf_spec) %>%
last_fit(members_split)
collect_predictions(members_final_rf) -> rf_pd
calculate_some_metrics <- function(threshold_selected, pd_selected) {
pd_selected %>%
mutate(label_predicted_threshold = case_when(.pred_Bad >= threshold_selected ~ "Bad", TRUE ~ "Good")) -> df_result
sum(df_result$label_predicted_threshold == df_result$BAD) / nrow(df_result) -> accuracy_threshold
df_result %>% filter(BAD == "Bad") -> actual_is_died
sum(actual_is_died$label_predicted_threshold == "Bad") / nrow(actual_is_died) -> sensitivity_threshold
df_result %>% filter(BAD != "Bad") -> actual_is_survived
# Calculate Specificity:
sum(actual_is_survived$label_predicted_threshold != "Bad") / nrow(actual_is_survived) -> spec
# Report results in form of data frame:
data.frame(Accuracy = accuracy_threshold,
Sensitivity = sensitivity_threshold,
Specificity = spec,
FPR = 1 - spec,
# AUC = roc_auc,
Threshold = threshold_selected) -> df_reporting
return(df_reporting)
}
range_threshold <- c(0.5, seq(0, 1, length.out = 99))
lapply(range_threshold, function(x) {calculate_some_metrics(x, pd_selected = glm_pd)}) -> list_glm
lapply(range_threshold, function(x) {calculate_some_metrics(x, pd_selected = rf_pd)}) -> list_rf
do.call("bind_rows", list_glm) -> glm_results_thres
do.call("bind_rows", list_rf) -> rf_results_thres
bind_rows(glm_results_thres %>% mutate(Model = "GLM"),
rf_results_thres %>% mutate(Model = "RF")) -> df_compare
df_compare %>%
gather(Metric, Value, -Threshold, -Model) -> df_long
library(pROC)
glm_auc <- roc(case_when(members_test$BAD == "Bad" ~ 1, TRUE ~ 0), glm_pd$.pred_Bad)$auc %>% round(3)
rf_auc <- roc(case_when(members_test$BAD == "Bad" ~ 1, TRUE ~ 0), rf_pd$.pred_Bad)$auc %>% round(3)
library(extrafont)
theme_set(theme_minimal())
my_font <- "Roboto Condensed"
df_long %>%
ggplot(aes(Threshold, Value, color = Model)) +
geom_line() +
geom_point(data = df_long %>% filter(Threshold == 0.5)) +
facet_wrap(~ Metric) +
theme(text = element_text(family = my_font)) +
theme(plot.margin = unit(rep(0.8, 4), "cm")) +
scale_y_continuous(labels = scales::percent) +
labs(subtitle = paste0("AUC for GLM = ", glm_auc, ", AUC for RF = ", rf_auc),
title = "Figure 1: Model Performance by Threshold based on Test Data",
y = NULL)

#========================
calculate_profit <- function(threshold_selected, pd_selected) {
members_test %>%
as_tibble() %>%
mutate(pd = pd_selected$.pred_Bad) %>%
mutate(predicted = case_when(pd >= threshold_selected ~ "Bad", TRUE ~ "Good")) -> df_selected
# Set conditions for calculating average profit at given threshold:
n <- 50
rate <- 0.07
profit_space <- NULL
# Calculate net profit for each sample randomly selected from test data:
for (j in 1:n) {
set.seed(j)
df_results <- df_selected %>%
sample_frac(0.7)
# Profit from true negative cases:
df_results %>%
filter(predicted == "Good", BAD == "Good") %>%
mutate(profit = rate*LOAN) %>%
pull(profit) %>%
sum() -> total_profit
# Loss causes from false negative cases:
df_results %>%
filter(predicted == "Good", BAD == "Bad") %>%
pull(LOAN) %>%
sum() -> total_loss
# Net profit:
net_profit <- total_profit - total_loss
profit_space <- c(profit_space, net_profit)
}
# Average net profit at given threshold:
data.frame(Profit_avg = mean(profit_space), Threshold = threshold_selected) -> df_prof_thres
return(df_prof_thres)
}
lapply(range_threshold, function(x) {calculate_profit(threshold_selected = x, pd_selected = rf_pd)}) -> list_prof
do.call("bind_rows", list_prof) -> df_prof
df_prof %>% filter(Threshold == 0.5) -> default_prof
df_prof %>% slice(which.max(Profit_avg)) -> max_prof
df_prof %>%
ggplot(aes(Threshold, Profit_avg)) +
geom_line(color = "#00006E") +
geom_point(data = max_prof, color = "red", size = 3) +
geom_point(data = default_prof, color = "blue", size = 3) +
geom_text(data = max_prof %>% mutate(Profit_avg = 640000), family = my_font,
aes(label = "Threshold that\nmaximizes Profit"), size = 3.5) +
geom_text(data = default_prof %>% mutate(Profit_avg = 550000),
aes(label = "Profit at\ndefault threshold"), size = 3.5, family = my_font) +
scale_y_continuous(labels = scales::dollar_format()) +
theme(text = element_text(family = my_font)) +
theme(plot.margin = unit(rep(0.8, 4), "cm")) +
labs(y = NULL, title = "Figure 2: Maximum Profit by Threshold for Random Forest")

lapply(range_threshold, function(x) {calculate_profit(threshold_selected = x, pd_selected = glm_pd)}) -> list_prof_glm
do.call("bind_rows", list_prof_glm) -> df_prof_glm
df_prof_glm %>% filter(Threshold == 0.5) -> default_prof_glm
df_prof_glm %>% slice(which.max(Profit_avg)) -> max_prof_glm
df_prof_glm %>%
ggplot(aes(Threshold, Profit_avg)) +
geom_line(color = "#00006E") +
geom_point(data = max_prof_glm, color = "red", size = 3) +
geom_point(data = default_prof_glm, color = "blue", size = 3) +
geom_text(data = max_prof_glm %>% mutate(Threshold = 0.5), family = my_font,
aes(label = "Threshold that\nmaximizes Profit"), size = 3.5) +
geom_text(data = default_prof_glm %>% mutate(Profit_avg = 290000),
aes(label = "Profit at\ndefault threshold"), size = 3.5, family = my_font) +
scale_y_continuous(labels = scales::dollar_format()) +
theme(text = element_text(family = my_font)) +
theme(plot.margin = unit(rep(0.8, 4), "cm")) +
labs(y = NULL, title = "Figure 3: Maximum Profit by Threshold for Logistic")

LS0tDQp0aXRsZTogIkNvbXBhcmUgTWF4aW11bSBQcm9maXQgYmV0d2VlbiBSYW5kb20gRm9yZXN0IGFuZCBMb2dpc3RpYyAodGlkeW1vZGVscykiDQphdXRob3I6ICJOZ3V5ZW4gQ2hpIER1bmciDQpzdWJ0aXRsZTogIlIgTWFjaGluZSBMZWFybmluZyBTZXJpZXMiDQpvdXRwdXQ6DQogIGh0bWxfZG9jdW1lbnQ6IA0KICAgIGNvZGVfZG93bmxvYWQ6IHRydWUNCiAgICAjIGNvZGVfZm9sZGluZzogaGlkZQ0KICAgIGhpZ2hsaWdodDogemVuYnVybg0KICAgICMgbnVtYmVyX3NlY3Rpb25zOiB5ZXMNCiAgICB0aGVtZTogImZsYXRseSINCiAgICB0b2M6IFRSVUUNCiAgICB0b2NfZmxvYXQ6IFRSVUUNCi0tLQ0KDQpgYGB7ciBzZXR1cCxpbmNsdWRlPUZBTFNFfQ0Ka25pdHI6Om9wdHNfY2h1bmskc2V0KGVjaG8gPSBUUlVFLCB3YXJuaW5nID0gRkFMU0UsIG1lc3NhZ2UgPSBGQUxTRSwgZmlnLndpZHRoID0gMTAsIGZpZy5oZWlnaHQgPSA2KQ0KYGBgDQoNCg0KDQojIEludHJvZHVjdGlvbg0KDQpJIHdpbGwgd3JpdGUgc29tZXRoaW5nIGxhdGVyLi4uDQoNCg0KYGBge3J9DQojIGh0dHBzOi8vZ2l0aHViLmNvbS90aWR5bW9kZWxzL3RoZW1pcw0KIyBodHRwczovL3d3dy50aWR5bW9kZWxzLm9yZy8NCiMgaHR0cHM6Ly93d3cudG13ci5vcmcvDQojIGh0dHBzOi8vanVsaWFzaWxnZS5jb20vYmxvZy94Z2Jvb3N0LXR1bmUtdm9sbGV5YmFsbC8NCg0KIyBDbGVhciBvdXIgd29ya3NwYWNlOiANCnJtKGxpc3QgPSBscygpKQ0KDQojIExvYWQgaG1lcS5jc3YgZGF0YXNldDogDQpsaWJyYXJ5KHRpZHl2ZXJzZSkNCmhtZXEgPC0gcmVhZF9jc3YoImh0dHA6Ly93d3cuY3JlZGl0cmlza2FuYWx5dGljcy5uZXQvdXBsb2Fkcy8xLzkvNS8xLzE5NTExNjAxL2htZXEuY3N2IikNCmhtZXEgJT4lIG5hLm9taXQoKSAtPiBobWVxX2Z1bGwNCg0KIyBSZWxhYmVsIGZvciBCQUQgYW5kIGNvbnZlcnQgdG8gZmFjdG9yIGZvciBjaGFyYWN0ZXIgY29sdW1uczogDQpobWVxX2Z1bGwgJT4lIA0KICBtdXRhdGUoQkFEID0gY2FzZV93aGVuKEJBRCA9PSAxIH4gIkJhZCIsIFRSVUUgfiAiR29vZCIpKSAlPiUgDQogIG11dGF0ZV9pZihpcy5jaGFyYWN0ZXIsIGFzLmZhY3RvcikgLT4gaG1lcV9mdWxsDQoNCiMgRGlzdHJpYnV0aW9uIG9mIEJBRDogDQoNCmxpYnJhcnkoa25pdHIpDQoNCmhtZXFfZnVsbCAlPiUgDQogIGdyb3VwX2J5KEJBRCkgJT4lIA0KICBjb3VudCgpICU+JSANCiAgdW5ncm91cCgpICU+JSANCiAgbXV0YXRlKHBlcmNlbnQgPSAxMDAqbiAvIHN1bShuKSkgJT4lIA0KICBtdXRhdGUocGVyY2VudCA9IHJvdW5kKHBlcmNlbnQsIDIpKSAtPiBkZl9kaXMNCg0KZGZfZGlzICU+JSANCiAga2FibGUoKQ0KDQoNCmxpYnJhcnkodGlkeW1vZGVscykNCg0Kc2V0LnNlZWQoMTIzKQ0KbWVtYmVyc19zcGxpdCA8LSBpbml0aWFsX3NwbGl0KGhtZXFfZnVsbCwgcHJvcCA9IDAuNywgc3RyYXRhID0gQkFEKQ0KbWVtYmVyc190cmFpbiA8LSB0cmFpbmluZyhtZW1iZXJzX3NwbGl0KQ0KbWVtYmVyc190ZXN0IDwtIHRlc3RpbmcobWVtYmVyc19zcGxpdCkNCg0Kc2V0LnNlZWQoMTIzKQ0KbWVtYmVyc19mb2xkcyA8LSB2Zm9sZF9jdihtZW1iZXJzX3RyYWluLCBzdHJhdGEgPSBCQUQsIHYgPSA1LCByZXBlYXRzID0gMykNCg0KbGlicmFyeShyZWNpcGVzKQ0KbGlicmFyeSh0aGVtaXMpDQoNCm1lbWJlcnNfcmVjIDwtIHJlY2lwZShCQUQgfiAuLCBkYXRhID0gbWVtYmVyc190cmFpbikgJT4lDQogIHN0ZXBfZHVtbXkoYWxsX25vbWluYWwoKSwgLUJBRCkgJT4lDQogIHN0ZXBfc21vdGUoQkFEKQ0KDQpnbG1fc3BlYyA8LSBsb2dpc3RpY19yZWcoKSAlPiUNCiAgc2V0X2VuZ2luZSgiZ2xtIikNCg0KcmZfc3BlYyA8LSByYW5kX2ZvcmVzdCh0cmVlcyA9IDEwMDApICU+JQ0KICBzZXRfbW9kZSgiY2xhc3NpZmljYXRpb24iKSAlPiUNCiAgc2V0X2VuZ2luZSgicmFuZ2VyIikNCg0KbWVtYmVyc193ZiA8LSB3b3JrZmxvdygpICU+JQ0KICBhZGRfcmVjaXBlKG1lbWJlcnNfcmVjKQ0KDQptZW1iZXJzX21ldHJpY3MgPC0gbWV0cmljX3NldChyb2NfYXVjLCBhY2N1cmFjeSwgc2Vuc2l0aXZpdHksIHNwZWNpZmljaXR5KQ0KDQpnbG1fcnMgPC0gbWVtYmVyc193ZiAlPiUNCiAgYWRkX21vZGVsKGdsbV9zcGVjKSAlPiUNCiAgZml0X3Jlc2FtcGxlcyhyZXNhbXBsZXMgPSBtZW1iZXJzX2ZvbGRzLA0KICAgICAgICAgICAgICAgIG1ldHJpY3MgPSBtZW1iZXJzX21ldHJpY3MsDQogICAgICAgICAgICAgICAgY29udHJvbCA9IGNvbnRyb2xfcmVzYW1wbGVzKHNhdmVfcHJlZCA9IFRSVUUpKQ0KDQpyZl9ycyA8LSBtZW1iZXJzX3dmICU+JQ0KICBhZGRfbW9kZWwocmZfc3BlYykgJT4lDQogIGZpdF9yZXNhbXBsZXMocmVzYW1wbGVzID0gbWVtYmVyc19mb2xkcywNCiAgICAgICAgICAgICAgICBtZXRyaWNzID0gbWVtYmVyc19tZXRyaWNzLA0KICAgICAgICAgICAgICAgIGNvbnRyb2wgPSBjb250cm9sX3Jlc2FtcGxlcyhzYXZlX3ByZWQgPSBUUlVFKSkNCg0KbWVtYmVyc19maW5hbF9nbG0gPC0gbWVtYmVyc193ZiAlPiUNCiAgYWRkX21vZGVsKGdsbV9zcGVjKSAlPiUNCiAgbGFzdF9maXQobWVtYmVyc19zcGxpdCkNCg0KY29sbGVjdF9wcmVkaWN0aW9ucyhtZW1iZXJzX2ZpbmFsX2dsbSkgLT4gZ2xtX3BkDQoNCm1lbWJlcnNfZmluYWxfcmYgPC0gbWVtYmVyc193ZiAlPiUNCiAgYWRkX21vZGVsKHJmX3NwZWMpICU+JQ0KICBsYXN0X2ZpdChtZW1iZXJzX3NwbGl0KQ0KDQpjb2xsZWN0X3ByZWRpY3Rpb25zKG1lbWJlcnNfZmluYWxfcmYpIC0+IHJmX3BkDQoNCmNhbGN1bGF0ZV9zb21lX21ldHJpY3MgPC0gZnVuY3Rpb24odGhyZXNob2xkX3NlbGVjdGVkLCBwZF9zZWxlY3RlZCkgew0KICANCiAgcGRfc2VsZWN0ZWQgJT4lIA0KICAgIG11dGF0ZShsYWJlbF9wcmVkaWN0ZWRfdGhyZXNob2xkID0gY2FzZV93aGVuKC5wcmVkX0JhZCA+PSB0aHJlc2hvbGRfc2VsZWN0ZWQgfiAiQmFkIiwgVFJVRSB+ICJHb29kIikpIC0+IGRmX3Jlc3VsdA0KICANCiAgc3VtKGRmX3Jlc3VsdCRsYWJlbF9wcmVkaWN0ZWRfdGhyZXNob2xkID09IGRmX3Jlc3VsdCRCQUQpIC8gbnJvdyhkZl9yZXN1bHQpIC0+IGFjY3VyYWN5X3RocmVzaG9sZA0KICANCiAgZGZfcmVzdWx0ICU+JSBmaWx0ZXIoQkFEID09ICJCYWQiKSAtPiBhY3R1YWxfaXNfZGllZA0KICANCiAgc3VtKGFjdHVhbF9pc19kaWVkJGxhYmVsX3ByZWRpY3RlZF90aHJlc2hvbGQgPT0gIkJhZCIpIC8gbnJvdyhhY3R1YWxfaXNfZGllZCkgLT4gc2Vuc2l0aXZpdHlfdGhyZXNob2xkDQogIA0KICBkZl9yZXN1bHQgJT4lIGZpbHRlcihCQUQgIT0gIkJhZCIpIC0+IGFjdHVhbF9pc19zdXJ2aXZlZA0KICANCiAgIyBDYWxjdWxhdGUgU3BlY2lmaWNpdHk6IA0KICBzdW0oYWN0dWFsX2lzX3N1cnZpdmVkJGxhYmVsX3ByZWRpY3RlZF90aHJlc2hvbGQgIT0gIkJhZCIpIC8gbnJvdyhhY3R1YWxfaXNfc3Vydml2ZWQpIC0+IHNwZWMgDQogIA0KICAjIFJlcG9ydCByZXN1bHRzIGluIGZvcm0gb2YgZGF0YSBmcmFtZTogDQogIGRhdGEuZnJhbWUoQWNjdXJhY3kgPSBhY2N1cmFjeV90aHJlc2hvbGQsIA0KICAgICAgICAgICAgIFNlbnNpdGl2aXR5ID0gc2Vuc2l0aXZpdHlfdGhyZXNob2xkLCANCiAgICAgICAgICAgICBTcGVjaWZpY2l0eSA9IHNwZWMsIA0KICAgICAgICAgICAgIEZQUiA9IDEgLSBzcGVjLCANCiAgICAgICAgICAgICAjIEFVQyA9IHJvY19hdWMsIA0KICAgICAgICAgICAgIFRocmVzaG9sZCA9IHRocmVzaG9sZF9zZWxlY3RlZCkgLT4gZGZfcmVwb3J0aW5nDQogIHJldHVybihkZl9yZXBvcnRpbmcpDQp9DQoNCnJhbmdlX3RocmVzaG9sZCA8LSBjKDAuNSwgc2VxKDAsIDEsIGxlbmd0aC5vdXQgPSA5OSkpDQoNCmxhcHBseShyYW5nZV90aHJlc2hvbGQsIGZ1bmN0aW9uKHgpIHtjYWxjdWxhdGVfc29tZV9tZXRyaWNzKHgsIHBkX3NlbGVjdGVkID0gZ2xtX3BkKX0pIC0+IGxpc3RfZ2xtDQpsYXBwbHkocmFuZ2VfdGhyZXNob2xkLCBmdW5jdGlvbih4KSB7Y2FsY3VsYXRlX3NvbWVfbWV0cmljcyh4LCBwZF9zZWxlY3RlZCA9IHJmX3BkKX0pIC0+IGxpc3RfcmYNCg0KZG8uY2FsbCgiYmluZF9yb3dzIiwgbGlzdF9nbG0pIC0+IGdsbV9yZXN1bHRzX3RocmVzDQpkby5jYWxsKCJiaW5kX3Jvd3MiLCBsaXN0X3JmKSAtPiByZl9yZXN1bHRzX3RocmVzDQoNCmJpbmRfcm93cyhnbG1fcmVzdWx0c190aHJlcyAlPiUgbXV0YXRlKE1vZGVsID0gIkdMTSIpLCANCiAgICAgICAgICByZl9yZXN1bHRzX3RocmVzICU+JSBtdXRhdGUoTW9kZWwgPSAiUkYiKSkgLT4gZGZfY29tcGFyZQ0KDQpkZl9jb21wYXJlICU+JSANCiAgZ2F0aGVyKE1ldHJpYywgVmFsdWUsIC1UaHJlc2hvbGQsIC1Nb2RlbCkgLT4gZGZfbG9uZw0KDQpsaWJyYXJ5KHBST0MpDQpnbG1fYXVjIDwtIHJvYyhjYXNlX3doZW4obWVtYmVyc190ZXN0JEJBRCA9PSAiQmFkIiB+IDEsIFRSVUUgfiAwKSwgZ2xtX3BkJC5wcmVkX0JhZCkkYXVjICU+JSByb3VuZCgzKQ0KcmZfYXVjIDwtIHJvYyhjYXNlX3doZW4obWVtYmVyc190ZXN0JEJBRCA9PSAiQmFkIiB+IDEsIFRSVUUgfiAwKSwgcmZfcGQkLnByZWRfQmFkKSRhdWMgJT4lIHJvdW5kKDMpDQoNCmxpYnJhcnkoZXh0cmFmb250KQ0KdGhlbWVfc2V0KHRoZW1lX21pbmltYWwoKSkNCm15X2ZvbnQgPC0gIlJvYm90byBDb25kZW5zZWQiDQoNCmRmX2xvbmcgJT4lIA0KICBnZ3Bsb3QoYWVzKFRocmVzaG9sZCwgVmFsdWUsIGNvbG9yID0gTW9kZWwpKSArIA0KICBnZW9tX2xpbmUoKSArIA0KICBnZW9tX3BvaW50KGRhdGEgPSBkZl9sb25nICU+JSBmaWx0ZXIoVGhyZXNob2xkID09IDAuNSkpICsgDQogIGZhY2V0X3dyYXAofiBNZXRyaWMpICsgDQogIHRoZW1lKHRleHQgPSBlbGVtZW50X3RleHQoZmFtaWx5ID0gbXlfZm9udCkpICsgDQogIHRoZW1lKHBsb3QubWFyZ2luID0gdW5pdChyZXAoMC44LCA0KSwgImNtIikpICsgDQogIHNjYWxlX3lfY29udGludW91cyhsYWJlbHMgPSBzY2FsZXM6OnBlcmNlbnQpICsgDQogIGxhYnMoc3VidGl0bGUgPSBwYXN0ZTAoIkFVQyBmb3IgR0xNID0gIiwgZ2xtX2F1YywgIiwgQVVDIGZvciBSRiA9ICIsIHJmX2F1YyksIA0KICAgICAgIHRpdGxlID0gIkZpZ3VyZSAxOiBNb2RlbCBQZXJmb3JtYW5jZSBieSBUaHJlc2hvbGQgYmFzZWQgb24gVGVzdCBEYXRhIiwgDQogICAgICAgeSA9IE5VTEwpDQoNCg0KIz09PT09PT09PT09PT09PT09PT09PT09PQ0KDQpjYWxjdWxhdGVfcHJvZml0IDwtIGZ1bmN0aW9uKHRocmVzaG9sZF9zZWxlY3RlZCwgcGRfc2VsZWN0ZWQpIHsNCiAgDQogIG1lbWJlcnNfdGVzdCAlPiUgDQogICAgYXNfdGliYmxlKCkgJT4lIA0KICAgIG11dGF0ZShwZCA9IHBkX3NlbGVjdGVkJC5wcmVkX0JhZCkgJT4lIA0KICAgIG11dGF0ZShwcmVkaWN0ZWQgPSBjYXNlX3doZW4ocGQgPj0gdGhyZXNob2xkX3NlbGVjdGVkIH4gIkJhZCIsIFRSVUUgfiAiR29vZCIpKSAtPiBkZl9zZWxlY3RlZA0KICANCiAgIyBTZXQgY29uZGl0aW9ucyBmb3IgY2FsY3VsYXRpbmcgYXZlcmFnZSBwcm9maXQgYXQgZ2l2ZW4gdGhyZXNob2xkOiANCiAgbiA8LSA1MA0KICByYXRlIDwtIDAuMDcNCiAgcHJvZml0X3NwYWNlIDwtIE5VTEwNCiAgDQogICMgQ2FsY3VsYXRlIG5ldCBwcm9maXQgZm9yIGVhY2ggc2FtcGxlIHJhbmRvbWx5IHNlbGVjdGVkIGZyb20gdGVzdCBkYXRhOg0KICANCiAgZm9yIChqIGluIDE6bikgew0KICAgIA0KICAgIHNldC5zZWVkKGopDQogICAgDQogICAgZGZfcmVzdWx0cyA8LSBkZl9zZWxlY3RlZCAlPiUgDQogICAgICBzYW1wbGVfZnJhYygwLjcpDQogICAgDQogICAgIyBQcm9maXQgZnJvbSB0cnVlIG5lZ2F0aXZlIGNhc2VzOiANCiAgICANCiAgICBkZl9yZXN1bHRzICU+JSANCiAgICAgIGZpbHRlcihwcmVkaWN0ZWQgPT0gIkdvb2QiLCBCQUQgPT0gIkdvb2QiKSAlPiUgDQogICAgICBtdXRhdGUocHJvZml0ID0gcmF0ZSpMT0FOKSAlPiUgDQogICAgICBwdWxsKHByb2ZpdCkgJT4lIA0KICAgICAgc3VtKCkgLT4gdG90YWxfcHJvZml0DQogICAgDQogICAgIyBMb3NzIGNhdXNlcyBmcm9tIGZhbHNlIG5lZ2F0aXZlIGNhc2VzOiANCiAgICANCiAgICBkZl9yZXN1bHRzICU+JSANCiAgICAgIGZpbHRlcihwcmVkaWN0ZWQgPT0gIkdvb2QiLCBCQUQgPT0gIkJhZCIpICU+JSANCiAgICAgIHB1bGwoTE9BTikgJT4lIA0KICAgICAgc3VtKCkgLT4gdG90YWxfbG9zcw0KICAgIA0KICAgICMgTmV0IHByb2ZpdDogDQogICAgbmV0X3Byb2ZpdCA8LSB0b3RhbF9wcm9maXQgLSB0b3RhbF9sb3NzDQogICAgcHJvZml0X3NwYWNlIDwtIGMocHJvZml0X3NwYWNlLCBuZXRfcHJvZml0KQ0KICAgIA0KICB9DQogIA0KICAjIEF2ZXJhZ2UgbmV0IHByb2ZpdCBhdCBnaXZlbiB0aHJlc2hvbGQ6IA0KICBkYXRhLmZyYW1lKFByb2ZpdF9hdmcgPSBtZWFuKHByb2ZpdF9zcGFjZSksIFRocmVzaG9sZCA9IHRocmVzaG9sZF9zZWxlY3RlZCkgLT4gZGZfcHJvZl90aHJlcw0KICByZXR1cm4oZGZfcHJvZl90aHJlcykNCiAgDQp9DQoNCmxhcHBseShyYW5nZV90aHJlc2hvbGQsIGZ1bmN0aW9uKHgpIHtjYWxjdWxhdGVfcHJvZml0KHRocmVzaG9sZF9zZWxlY3RlZCA9IHgsIHBkX3NlbGVjdGVkID0gcmZfcGQpfSkgLT4gbGlzdF9wcm9mDQoNCmRvLmNhbGwoImJpbmRfcm93cyIsIGxpc3RfcHJvZikgLT4gZGZfcHJvZg0KDQpkZl9wcm9mICU+JSBmaWx0ZXIoVGhyZXNob2xkID09IDAuNSkgLT4gZGVmYXVsdF9wcm9mDQoNCmRmX3Byb2YgJT4lIHNsaWNlKHdoaWNoLm1heChQcm9maXRfYXZnKSkgLT4gbWF4X3Byb2YNCg0KZGZfcHJvZiAlPiUgDQogIGdncGxvdChhZXMoVGhyZXNob2xkLCBQcm9maXRfYXZnKSkgKyANCiAgZ2VvbV9saW5lKGNvbG9yID0gIiMwMDAwNkUiKSArIA0KICBnZW9tX3BvaW50KGRhdGEgPSBtYXhfcHJvZiwgY29sb3IgPSAicmVkIiwgc2l6ZSA9IDMpICsgDQogIGdlb21fcG9pbnQoZGF0YSA9IGRlZmF1bHRfcHJvZiwgY29sb3IgPSAiYmx1ZSIsIHNpemUgPSAzKSArIA0KICBnZW9tX3RleHQoZGF0YSA9IG1heF9wcm9mICU+JSBtdXRhdGUoUHJvZml0X2F2ZyA9IDY0MDAwMCksIGZhbWlseSA9IG15X2ZvbnQsICANCiAgICAgICAgICAgIGFlcyhsYWJlbCA9ICJUaHJlc2hvbGQgdGhhdFxubWF4aW1pemVzIFByb2ZpdCIpLCBzaXplID0gMy41KSArIA0KICBnZW9tX3RleHQoZGF0YSA9IGRlZmF1bHRfcHJvZiAlPiUgbXV0YXRlKFByb2ZpdF9hdmcgPSA1NTAwMDApLA0KICAgICAgICAgICAgYWVzKGxhYmVsID0gIlByb2ZpdCBhdFxuZGVmYXVsdCB0aHJlc2hvbGQiKSwgc2l6ZSA9IDMuNSwgZmFtaWx5ID0gbXlfZm9udCkgKyANCiAgc2NhbGVfeV9jb250aW51b3VzKGxhYmVscyA9IHNjYWxlczo6ZG9sbGFyX2Zvcm1hdCgpKSArIA0KICB0aGVtZSh0ZXh0ID0gZWxlbWVudF90ZXh0KGZhbWlseSA9IG15X2ZvbnQpKSArIA0KICB0aGVtZShwbG90Lm1hcmdpbiA9IHVuaXQocmVwKDAuOCwgNCksICJjbSIpKSArIA0KICBsYWJzKHkgPSBOVUxMLCB0aXRsZSA9ICJGaWd1cmUgMjogTWF4aW11bSBQcm9maXQgYnkgVGhyZXNob2xkIGZvciBSYW5kb20gRm9yZXN0IikNCg0KbGFwcGx5KHJhbmdlX3RocmVzaG9sZCwgZnVuY3Rpb24oeCkge2NhbGN1bGF0ZV9wcm9maXQodGhyZXNob2xkX3NlbGVjdGVkID0geCwgcGRfc2VsZWN0ZWQgPSBnbG1fcGQpfSkgLT4gbGlzdF9wcm9mX2dsbQ0KDQpkby5jYWxsKCJiaW5kX3Jvd3MiLCBsaXN0X3Byb2ZfZ2xtKSAtPiBkZl9wcm9mX2dsbQ0KDQpkZl9wcm9mX2dsbSAlPiUgZmlsdGVyKFRocmVzaG9sZCA9PSAwLjUpIC0+IGRlZmF1bHRfcHJvZl9nbG0NCg0KZGZfcHJvZl9nbG0gJT4lIHNsaWNlKHdoaWNoLm1heChQcm9maXRfYXZnKSkgLT4gbWF4X3Byb2ZfZ2xtDQoNCmRmX3Byb2ZfZ2xtICU+JSANCiAgZ2dwbG90KGFlcyhUaHJlc2hvbGQsIFByb2ZpdF9hdmcpKSArIA0KICBnZW9tX2xpbmUoY29sb3IgPSAiIzAwMDA2RSIpICsgDQogIGdlb21fcG9pbnQoZGF0YSA9IG1heF9wcm9mX2dsbSwgY29sb3IgPSAicmVkIiwgc2l6ZSA9IDMpICsgDQogIGdlb21fcG9pbnQoZGF0YSA9IGRlZmF1bHRfcHJvZl9nbG0sIGNvbG9yID0gImJsdWUiLCBzaXplID0gMykgKyANCiAgZ2VvbV90ZXh0KGRhdGEgPSBtYXhfcHJvZl9nbG0gJT4lIG11dGF0ZShUaHJlc2hvbGQgPSAwLjUpLCBmYW1pbHkgPSBteV9mb250LCAgDQogICAgICAgICAgICBhZXMobGFiZWwgPSAiVGhyZXNob2xkIHRoYXRcbm1heGltaXplcyBQcm9maXQiKSwgc2l6ZSA9IDMuNSkgKyANCiAgZ2VvbV90ZXh0KGRhdGEgPSBkZWZhdWx0X3Byb2ZfZ2xtICU+JSBtdXRhdGUoUHJvZml0X2F2ZyA9IDI5MDAwMCksDQogICAgICAgICAgICBhZXMobGFiZWwgPSAiUHJvZml0IGF0XG5kZWZhdWx0IHRocmVzaG9sZCIpLCBzaXplID0gMy41LCBmYW1pbHkgPSBteV9mb250KSArIA0KICBzY2FsZV95X2NvbnRpbnVvdXMobGFiZWxzID0gc2NhbGVzOjpkb2xsYXJfZm9ybWF0KCkpICsgDQogIHRoZW1lKHRleHQgPSBlbGVtZW50X3RleHQoZmFtaWx5ID0gbXlfZm9udCkpICsgDQogIHRoZW1lKHBsb3QubWFyZ2luID0gdW5pdChyZXAoMC44LCA0KSwgImNtIikpICsgDQogIGxhYnMoeSA9IE5VTEwsIHRpdGxlID0gIkZpZ3VyZSAzOiBNYXhpbXVtIFByb2ZpdCBieSBUaHJlc2hvbGQgZm9yIExvZ2lzdGljIikNCg0KDQpgYGANCg0K