Note: Some results may differ from the hard copy book due to the changing of sampling procedures introduced in R 3.6.0. See http://bit.ly/35D1SW7 for more details. Access and run the source code for this notebook here.
Hidden chapter requirements used in the book to set the plotting theme and load packages used in hidden code chunks:
# Set the graphical theme
ggplot2::theme_set(ggplot2::theme_light())
# Set global knitr chunk options
knitr::opts_chunk$set(
fig.align = "center",
fig.height = 3.5
)
Prerequisites
For this section we’ll use the following packages:
# Helper packages
library(dplyr) # for data wrangling
library(ggplot2) # for awesome plotting
library(rsample) # for data splitting
# Modeling packages
library(caret) # for logistic regression modeling
# Model interpretability packages
library(vip) # variable importance
To illustrate logistic regression concepts we’ll use the employee attrition data:
df <- attrition %>% mutate_if(is.ordered, factor, ordered = FALSE)
# Create training (70%) and test (30%) sets for the
# rsample::attrition data.
set.seed(123) # for reproducibility
churn_split <- initial_split(df, prop = .7, strata = "Attrition")
churn_train <- training(churn_split)
churn_test <- testing(churn_split)
Why logistic regression
Figure 5.1:
p1 <- ISLR::Default %>%
mutate(prob = ifelse(default == "Yes", 1, 0)) %>%
ggplot(aes(balance, prob)) +
geom_point(alpha = .15) +
geom_smooth(method = "lm") +
ggtitle("Linear regression model fit") +
xlab("Balance") +
ylab("Probability of Default")
p2 <- ISLR::Default %>%
mutate(prob = ifelse(default == "Yes", 1, 0)) %>%
ggplot(aes(balance, prob)) +
geom_point(alpha = .15) +
geom_smooth(method = "glm", method.args = list(family = "binomial")) +
ggtitle("Logistic regression model fit") +
xlab("Balance") +
ylab("Probability of Default")
gridExtra::grid.arrange(p1, p2, nrow = 1)
Simple logistic regression
model1 <- glm(Attrition ~ MonthlyIncome, family = "binomial", data = churn_train)
model2 <- glm(Attrition ~ OverTime, family = "binomial", data = churn_train)
Figure 5.2:
churn_train2 <- churn_train %>% mutate(prob = ifelse(Attrition == "Yes", 1, 0))
churn_train2 <- broom::augment(model2, churn_train2) %>% mutate(.fitted = exp(.fitted))
p1 <- ggplot(churn_train2, aes(MonthlyIncome, prob)) +
geom_point(alpha = 0.15) +
geom_smooth(method = "glm", method.args = list(family = "binomial")) +
ggtitle("Predicted probabilities for model1") +
xlab("Monthly Income") +
ylab("Probability of Attrition")
p2 <- ggplot(churn_train2, aes(OverTime, .fitted, color = OverTime)) +
geom_boxplot(show.legend = FALSE) +
geom_rug(sides = "b", position = "jitter", alpha = 0.2, show.legend = FALSE) +
ggtitle("Predicted probabilities for model2") +
xlab("Over Time") +
scale_y_continuous("Probability of Attrition", limits = c(0, 1))
gridExtra::grid.arrange(p1, p2, nrow = 1)
tidy(model1)
tidy(model2)
exp(coef(model1))
(Intercept) MonthlyIncome
0.3970771 0.9998697
exp(coef(model2))
(Intercept) OverTimeYes
0.1126126 4.0812121
confint(model1) # for odds, you can use `exp(confint(model1))`
Waiting for profiling to be done...
2.5 % 97.5 %
(Intercept) -1.2267754960 -0.61800619157
MonthlyIncome -0.0001849796 -0.00008107634
confint(model2)
Waiting for profiling to be done...
2.5 % 97.5 %
(Intercept) -2.430458 -1.952330
OverTimeYes 1.063246 1.752879
Multiple logistic regression
model3 <- glm(
Attrition ~ MonthlyIncome + OverTime,
family = "binomial",
data = churn_train
)
tidy(model3)
Figure 5.3:
churn_train3 <- churn_train %>% mutate(prob = ifelse(Attrition == "Yes", 1, 0))
churn_train3 <- broom::augment(model3, churn_train3) %>% mutate(.fitted = exp(.fitted))
ggplot(churn_train3, aes(MonthlyIncome, prob, color = OverTime)) +
geom_point(alpha = .15) +
geom_smooth(method = "glm", method.args = list(family = "binomial"), se = FALSE) +
ggtitle("Predicted probabilities for model3") +
xlab("Monthly Income") +
ylab("Probability of Attrition")
Assessing model accuracy
set.seed(123)
cv_model1 <- train(
Attrition ~ MonthlyIncome,
data = churn_train,
method = "glm",
family = "binomial",
trControl = trainControl(method = "cv", number = 10)
)
set.seed(123)
cv_model2 <- train(
Attrition ~ MonthlyIncome + OverTime,
data = churn_train,
method = "glm",
family = "binomial",
trControl = trainControl(method = "cv", number = 10)
)
set.seed(123)
cv_model3 <- train(
Attrition ~ .,
data = churn_train,
method = "glm",
family = "binomial",
trControl = trainControl(method = "cv", number = 10)
)
# extract out of sample performance measures
summary(
resamples(
list(
model1 = cv_model1,
model2 = cv_model2,
model3 = cv_model3
)
)
)$statistics$Accuracy
Min. 1st Qu. Median Mean 3rd Qu. Max. NA's
model1 0.8349515 0.8349515 0.8365385 0.8388478 0.8431373 0.8446602 0
model2 0.8349515 0.8349515 0.8365385 0.8388478 0.8431373 0.8446602 0
model3 0.8365385 0.8495146 0.8792476 0.8757893 0.8907767 0.9313725 0
# predict class
pred_class <- predict(cv_model3, churn_train)
# create confusion matrix
confusionMatrix(
data = relevel(pred_class, ref = "Yes"),
reference = relevel(churn_train$Attrition, ref = "Yes")
)
Confusion Matrix and Statistics
Reference
Prediction Yes No
Yes 93 25
No 73 839
Accuracy : 0.9049
95% CI : (0.8853, 0.9221)
No Information Rate : 0.8388
P-Value [Acc > NIR] : 0.000000000536
Kappa : 0.6016
Mcnemar's Test P-Value : 0.000002057257
Sensitivity : 0.56024
Specificity : 0.97106
Pos Pred Value : 0.78814
Neg Pred Value : 0.91996
Prevalence : 0.16117
Detection Rate : 0.09029
Detection Prevalence : 0.11456
Balanced Accuracy : 0.76565
'Positive' Class : Yes
library(ROCR)
# Compute predicted probabilities
m1_prob <- predict(cv_model1, churn_train, type = "prob")$Yes
m3_prob <- predict(cv_model3, churn_train, type = "prob")$Yes
# Compute AUC metrics for cv_model1 and cv_model3
perf1 <- prediction(m1_prob, churn_train$Attrition) %>%
performance(measure = "tpr", x.measure = "fpr")
perf2 <- prediction(m3_prob, churn_train$Attrition) %>%
performance(measure = "tpr", x.measure = "fpr")
# Plot ROC curves for cv_model1 and cv_model3
plot(perf1, col = "black", lty = 2)
plot(perf2, add = TRUE, col = "blue")
legend(0.8, 0.2, legend = c("cv_model1", "cv_model3"),
col = c("black", "blue"), lty = 2:1, cex = 0.6)
# Perform 10-fold CV on a PLS model tuning the number of PCs to
# use as predictors
set.seed(123)
cv_model_pls <- train(
Attrition ~ .,
data = churn_train,
method = "pls",
family = "binomial",
trControl = trainControl(method = "cv", number = 10),
preProcess = c("zv", "center", "scale"),
tuneLength = 16
)
# Model with lowest RMSE
cv_model_pls$bestTune
# Plot cross-validated RMSE
ggplot(cv_model_pls)
Feature interpretation
vip(cv_model3, num_features = 20)
Figure 5.7:
pred.fun <- function(object, newdata) {
Yes <- mean(predict(object, newdata, type = "prob")$Yes)
as.data.frame(Yes)
}
p1 <- pdp::partial(cv_model3, pred.var = "OverTime", pred.fun = pred.fun) %>%
autoplot(rug = TRUE) + ylim(c(0, 1))
p2 <- pdp::partial(cv_model3, pred.var = "JobSatisfaction", pred.fun = pred.fun) %>%
autoplot() + ylim(c(0, 1))
p3 <- pdp::partial(cv_model3, pred.var = "NumCompaniesWorked", pred.fun = pred.fun, gr = 10) %>%
autoplot() + scale_x_continuous(breaks = 0:9) + ylim(c(0, 1))
p4 <- pdp::partial(cv_model3, pred.var = "EnvironmentSatisfaction", pred.fun = pred.fun) %>%
autoplot() + ylim(c(0, 1))
grid.arrange(p1, p2, p3, p4, nrow = 2)
# clean up
rm(list = ls())
LS0tCnRpdGxlOiAiQ2hhcHRlciA1OiBMb2dpc2l0aWMgUmVncmVzc2lvbiIKb3V0cHV0OiBodG1sX25vdGVib29rCi0tLQoKX19Ob3RlX186IFNvbWUgcmVzdWx0cyBtYXkgZGlmZmVyIGZyb20gdGhlIGhhcmQgY29weSBib29rIGR1ZSB0byB0aGUgY2hhbmdpbmcgb2Ygc2FtcGxpbmcgcHJvY2VkdXJlcyBpbnRyb2R1Y2VkIGluIFIgMy42LjAuIFNlZSBodHRwOi8vYml0Lmx5LzM1RDFTVzcgZm9yIG1vcmUgZGV0YWlscy4gQWNjZXNzIGFuZCBydW4gdGhlIHNvdXJjZSBjb2RlIGZvciB0aGlzIG5vdGVib29rIFtoZXJlXShodHRwczovL3JzdHVkaW8uY2xvdWQvcHJvamVjdC84MDExODUpLiAKCkhpZGRlbiBjaGFwdGVyIHJlcXVpcmVtZW50cyB1c2VkIGluIHRoZSBib29rIHRvIHNldCB0aGUgcGxvdHRpbmcgdGhlbWUgYW5kIGxvYWQgcGFja2FnZXMgdXNlZCBpbiBoaWRkZW4gY29kZSBjaHVua3M6CgpgYGB7ciBzZXR1cH0KIyBTZXQgdGhlIGdyYXBoaWNhbCB0aGVtZQpnZ3Bsb3QyOjp0aGVtZV9zZXQoZ2dwbG90Mjo6dGhlbWVfbGlnaHQoKSkKCiMgU2V0IGdsb2JhbCBrbml0ciBjaHVuayBvcHRpb25zCmtuaXRyOjpvcHRzX2NodW5rJHNldCgKICBmaWcuYWxpZ24gPSAiY2VudGVyIiwKICBmaWcuaGVpZ2h0ID0gMy41CikKYGBgCgojIyBQcmVyZXF1aXNpdGVzCgpGb3IgdGhpcyBzZWN0aW9uIHdlJ2xsIHVzZSB0aGUgZm9sbG93aW5nIHBhY2thZ2VzOgoKYGBge3IgMDgtcGtncywgbWVzc2FnZT1GQUxTRX0KIyBIZWxwZXIgcGFja2FnZXMKbGlicmFyeShkcGx5cikgICAgICMgZm9yIGRhdGEgd3JhbmdsaW5nCmxpYnJhcnkoZ2dwbG90MikgICAjIGZvciBhd2Vzb21lIHBsb3R0aW5nCmxpYnJhcnkocnNhbXBsZSkgICAjIGZvciBkYXRhIHNwbGl0dGluZwoKIyBNb2RlbGluZyBwYWNrYWdlcwpsaWJyYXJ5KGNhcmV0KSAgICAgIyBmb3IgbG9naXN0aWMgcmVncmVzc2lvbiBtb2RlbGluZwoKIyBNb2RlbCBpbnRlcnByZXRhYmlsaXR5IHBhY2thZ2VzCmxpYnJhcnkodmlwKSAgICAgICAjIHZhcmlhYmxlIGltcG9ydGFuY2UKYGBgCgpUbyBpbGx1c3RyYXRlIGxvZ2lzdGljIHJlZ3Jlc3Npb24gY29uY2VwdHMgd2UnbGwgdXNlIHRoZSBlbXBsb3llZSBhdHRyaXRpb24gZGF0YToKCmBgYHtyIGxvZ2l0LWRhdGEtaW1wb3J0fQpkZiA8LSBhdHRyaXRpb24gJT4lIG11dGF0ZV9pZihpcy5vcmRlcmVkLCBmYWN0b3IsIG9yZGVyZWQgPSBGQUxTRSkKCiMgQ3JlYXRlIHRyYWluaW5nICg3MCUpIGFuZCB0ZXN0ICgzMCUpIHNldHMgZm9yIHRoZSAKIyByc2FtcGxlOjphdHRyaXRpb24gZGF0YS4Kc2V0LnNlZWQoMTIzKSAgIyBmb3IgcmVwcm9kdWNpYmlsaXR5CmNodXJuX3NwbGl0IDwtIGluaXRpYWxfc3BsaXQoZGYsIHByb3AgPSAuNywgc3RyYXRhID0gIkF0dHJpdGlvbiIpCmNodXJuX3RyYWluIDwtIHRyYWluaW5nKGNodXJuX3NwbGl0KQpjaHVybl90ZXN0ICA8LSB0ZXN0aW5nKGNodXJuX3NwbGl0KQpgYGAKCgojIyBXaHkgbG9naXN0aWMgcmVncmVzc2lvbgoKRmlndXJlIDUuMToKCmBgYHtyIHdoeWxvZ2l0LCBlY2hvPVRSVUUsIGZpZy5oZWlnaHQ9MywgZmlnLndpZHRoPTgsIGZpZy5jYXA9IkNvbXBhcmluZyB0aGUgcHJlZGljdGVkIHByb2JhYmlsaXRpZXMgb2YgbGluZWFyIHJlZ3Jlc3Npb24gKGxlZnQpIHRvIGxvZ2lzdGljIHJlZ3Jlc3Npb24gKHJpZ2h0KS4gUHJlZGljdGVkIHByb2JhYmlsaXRpZXMgdXNpbmcgbGluZWFyIHJlZ3Jlc3Npb24gcmVzdWx0cyBpbiBmbGF3ZWQgbG9naWMgd2hlcmVhcyBwcmVkaWN0ZWQgdmFsdWVzIGZyb20gbG9naXN0aWMgcmVncmVzc2lvbiB3aWxsIGFsd2F5cyBsaWUgYmV0d2VlbiAwIGFuZCAxLiJ9CnAxIDwtIElTTFI6OkRlZmF1bHQgJT4lCiAgbXV0YXRlKHByb2IgPSBpZmVsc2UoZGVmYXVsdCA9PSAiWWVzIiwgMSwgMCkpICU+JQogIGdncGxvdChhZXMoYmFsYW5jZSwgcHJvYikpICsKICBnZW9tX3BvaW50KGFscGhhID0gLjE1KSArCiAgZ2VvbV9zbW9vdGgobWV0aG9kID0gImxtIikgKwogIGdndGl0bGUoIkxpbmVhciByZWdyZXNzaW9uIG1vZGVsIGZpdCIpICsKICB4bGFiKCJCYWxhbmNlIikgKwogIHlsYWIoIlByb2JhYmlsaXR5IG9mIERlZmF1bHQiKQoKcDIgPC0gSVNMUjo6RGVmYXVsdCAlPiUKICBtdXRhdGUocHJvYiA9IGlmZWxzZShkZWZhdWx0ID09ICJZZXMiLCAxLCAwKSkgJT4lCiAgZ2dwbG90KGFlcyhiYWxhbmNlLCBwcm9iKSkgKwogIGdlb21fcG9pbnQoYWxwaGEgPSAuMTUpICsKICBnZW9tX3Ntb290aChtZXRob2QgPSAiZ2xtIiwgbWV0aG9kLmFyZ3MgPSBsaXN0KGZhbWlseSA9ICJiaW5vbWlhbCIpKSArCiAgZ2d0aXRsZSgiTG9naXN0aWMgcmVncmVzc2lvbiBtb2RlbCBmaXQiKSArCiAgeGxhYigiQmFsYW5jZSIpICsKICB5bGFiKCJQcm9iYWJpbGl0eSBvZiBEZWZhdWx0IikKCmdyaWRFeHRyYTo6Z3JpZC5hcnJhbmdlKHAxLCBwMiwgbnJvdyA9IDEpCmBgYAoKIyMgU2ltcGxlIGxvZ2lzdGljIHJlZ3Jlc3Npb24KCmBgYHtyIGdsbS1tb2RlbDF9Cm1vZGVsMSA8LSBnbG0oQXR0cml0aW9uIH4gTW9udGhseUluY29tZSwgZmFtaWx5ID0gImJpbm9taWFsIiwgZGF0YSA9IGNodXJuX3RyYWluKQptb2RlbDIgPC0gZ2xtKEF0dHJpdGlvbiB+IE92ZXJUaW1lLCBmYW1pbHkgPSAiYmlub21pYWwiLCBkYXRhID0gY2h1cm5fdHJhaW4pCmBgYAoKRmlndXJlIDUuMjoKCmBgYHtyIGdsbS1zaWdtb2lkLCBlY2hvPVRSVUUsIGZpZy53aWR0aD04LCBmaWcuaGVpZ2h0PTMsIGZpZy5jYXA9IlByZWRpY3RlZCBwcm9iYWJsaWxpdGllcyBvZiBlbXBsb3llZSBhdHRyaXRpb24gYmFzZWQgb24gbW9udGhseSBpbmNvbWUgKGxlZnQpIGFuZCBvdmVydGltZSAocmlnaHQpLiBBcyBtb250aGx5IGluY29tZSBpbmNyZWFzZXMsIGBtb2RlbDFgIHByZWRpY3RzIGEgZGVjcmVhc2VkIHByb2JhYmlsaXR5IG9mIGF0dHJpdGlvbiBhbmQgaWYgZW1wbG95ZWVzIHdvcmsgb3ZlcnRpbWUgYG1vZGVsMmAgcHJlZGljdHMgYW4gaW5jcmVhc2VkIHByb2JhYmlsaXR5LiJ9CmNodXJuX3RyYWluMiA8LSBjaHVybl90cmFpbiAlPiUgbXV0YXRlKHByb2IgPSBpZmVsc2UoQXR0cml0aW9uID09ICJZZXMiLCAxLCAwKSkKY2h1cm5fdHJhaW4yIDwtIGJyb29tOjphdWdtZW50KG1vZGVsMiwgY2h1cm5fdHJhaW4yKSAlPiUgbXV0YXRlKC5maXR0ZWQgPSBleHAoLmZpdHRlZCkpCgpwMSA8LSBnZ3Bsb3QoY2h1cm5fdHJhaW4yLCBhZXMoTW9udGhseUluY29tZSwgcHJvYikpICsKICBnZW9tX3BvaW50KGFscGhhID0gMC4xNSkgKwogIGdlb21fc21vb3RoKG1ldGhvZCA9ICJnbG0iLCBtZXRob2QuYXJncyA9IGxpc3QoZmFtaWx5ID0gImJpbm9taWFsIikpICsKICBnZ3RpdGxlKCJQcmVkaWN0ZWQgcHJvYmFiaWxpdGllcyBmb3IgbW9kZWwxIikgKwogIHhsYWIoIk1vbnRobHkgSW5jb21lIikgKwogIHlsYWIoIlByb2JhYmlsaXR5IG9mIEF0dHJpdGlvbiIpCgpwMiA8LSBnZ3Bsb3QoY2h1cm5fdHJhaW4yLCBhZXMoT3ZlclRpbWUsIC5maXR0ZWQsIGNvbG9yID0gT3ZlclRpbWUpKSArCiAgZ2VvbV9ib3hwbG90KHNob3cubGVnZW5kID0gRkFMU0UpICsKICBnZW9tX3J1ZyhzaWRlcyA9ICJiIiwgcG9zaXRpb24gPSAiaml0dGVyIiwgYWxwaGEgPSAwLjIsIHNob3cubGVnZW5kID0gRkFMU0UpICsKICBnZ3RpdGxlKCJQcmVkaWN0ZWQgcHJvYmFiaWxpdGllcyBmb3IgbW9kZWwyIikgKwogIHhsYWIoIk92ZXIgVGltZSIpICsKICBzY2FsZV95X2NvbnRpbnVvdXMoIlByb2JhYmlsaXR5IG9mIEF0dHJpdGlvbiIsIGxpbWl0cyA9IGMoMCwgMSkpCgpncmlkRXh0cmE6OmdyaWQuYXJyYW5nZShwMSwgcDIsIG5yb3cgPSAxKQpgYGAKCmBgYHtyfQp0aWR5KG1vZGVsMSkKdGlkeShtb2RlbDIpCmBgYAoKYGBge3IgY29udmVydC1vZGRzLXByb2JzfQpleHAoY29lZihtb2RlbDEpKQpleHAoY29lZihtb2RlbDIpKQpgYGAKCmBgYHtyIGNvZWYtY29uZmludH0KY29uZmludChtb2RlbDEpICAjIGZvciBvZGRzLCB5b3UgY2FuIHVzZSBgZXhwKGNvbmZpbnQobW9kZWwxKSlgCmNvbmZpbnQobW9kZWwyKQpgYGAKCiMjIE11bHRpcGxlIGxvZ2lzdGljIHJlZ3Jlc3Npb24KCmBgYHtyIGdsbS1tb2RlbDN9Cm1vZGVsMyA8LSBnbG0oCiAgQXR0cml0aW9uIH4gTW9udGhseUluY29tZSArIE92ZXJUaW1lLAogIGZhbWlseSA9ICJiaW5vbWlhbCIsIAogIGRhdGEgPSBjaHVybl90cmFpbgogICkKCnRpZHkobW9kZWwzKQpgYGAKCkZpZ3VyZSA1LjM6CgpgYGB7ciBnbG0tc2lnbW9pZDIsIGVjaG89VFJVRSwgZmlnLndpZHRoPTYsIGZpZy5oZWlnaHQ9MywgZmlnLmNhcD0iUHJlZGljdGVkIHByb2JhYmlsaXR5IG9mIGF0dHJpdGlvbiBiYXNlZCBvbiBtb250aGx5IGluY29tZSBhbmQgd2hldGhlciBvciBub3QgZW1wbG95ZWVzIHdvcmsgb3ZlcnRpbWUuIn0KY2h1cm5fdHJhaW4zIDwtIGNodXJuX3RyYWluICU+JSBtdXRhdGUocHJvYiA9IGlmZWxzZShBdHRyaXRpb24gPT0gIlllcyIsIDEsIDApKQpjaHVybl90cmFpbjMgPC0gYnJvb206OmF1Z21lbnQobW9kZWwzLCBjaHVybl90cmFpbjMpICU+JSBtdXRhdGUoLmZpdHRlZCA9IGV4cCguZml0dGVkKSkKCmdncGxvdChjaHVybl90cmFpbjMsIGFlcyhNb250aGx5SW5jb21lLCBwcm9iLCBjb2xvciA9IE92ZXJUaW1lKSkgKwogIGdlb21fcG9pbnQoYWxwaGEgPSAuMTUpICsKICBnZW9tX3Ntb290aChtZXRob2QgPSAiZ2xtIiwgbWV0aG9kLmFyZ3MgPSBsaXN0KGZhbWlseSA9ICJiaW5vbWlhbCIpLCBzZSA9IEZBTFNFKSArCiAgZ2d0aXRsZSgiUHJlZGljdGVkIHByb2JhYmlsaXRpZXMgZm9yIG1vZGVsMyIpICsKICB4bGFiKCJNb250aGx5IEluY29tZSIpICsKICB5bGFiKCJQcm9iYWJpbGl0eSBvZiBBdHRyaXRpb24iKQpgYGAKCgojIyBBc3Nlc3NpbmcgbW9kZWwgYWNjdXJhY3kKCmBgYHtyIG11bHQtbW9kZWxzLWxvZ2lzdGljfQpzZXQuc2VlZCgxMjMpCmN2X21vZGVsMSA8LSB0cmFpbigKICBBdHRyaXRpb24gfiBNb250aGx5SW5jb21lLCAKICBkYXRhID0gY2h1cm5fdHJhaW4sIAogIG1ldGhvZCA9ICJnbG0iLAogIGZhbWlseSA9ICJiaW5vbWlhbCIsCiAgdHJDb250cm9sID0gdHJhaW5Db250cm9sKG1ldGhvZCA9ICJjdiIsIG51bWJlciA9IDEwKQopCgpzZXQuc2VlZCgxMjMpCmN2X21vZGVsMiA8LSB0cmFpbigKICBBdHRyaXRpb24gfiBNb250aGx5SW5jb21lICsgT3ZlclRpbWUsIAogIGRhdGEgPSBjaHVybl90cmFpbiwgCiAgbWV0aG9kID0gImdsbSIsCiAgZmFtaWx5ID0gImJpbm9taWFsIiwKICB0ckNvbnRyb2wgPSB0cmFpbkNvbnRyb2wobWV0aG9kID0gImN2IiwgbnVtYmVyID0gMTApCikKCnNldC5zZWVkKDEyMykKY3ZfbW9kZWwzIDwtIHRyYWluKAogIEF0dHJpdGlvbiB+IC4sIAogIGRhdGEgPSBjaHVybl90cmFpbiwgCiAgbWV0aG9kID0gImdsbSIsCiAgZmFtaWx5ID0gImJpbm9taWFsIiwKICB0ckNvbnRyb2wgPSB0cmFpbkNvbnRyb2wobWV0aG9kID0gImN2IiwgbnVtYmVyID0gMTApCikKCiMgZXh0cmFjdCBvdXQgb2Ygc2FtcGxlIHBlcmZvcm1hbmNlIG1lYXN1cmVzCnN1bW1hcnkoCiAgcmVzYW1wbGVzKAogICAgbGlzdCgKICAgICAgbW9kZWwxID0gY3ZfbW9kZWwxLCAKICAgICAgbW9kZWwyID0gY3ZfbW9kZWwyLCAKICAgICAgbW9kZWwzID0gY3ZfbW9kZWwzCiAgICApCiAgKQopJHN0YXRpc3RpY3MkQWNjdXJhY3kKYGBgCgpgYGB7ciBnbG0tY29uZnVzaW9uLW1hdHJpeH0KIyBwcmVkaWN0IGNsYXNzCnByZWRfY2xhc3MgPC0gcHJlZGljdChjdl9tb2RlbDMsIGNodXJuX3RyYWluKQoKIyBjcmVhdGUgY29uZnVzaW9uIG1hdHJpeApjb25mdXNpb25NYXRyaXgoCiAgZGF0YSA9IHJlbGV2ZWwocHJlZF9jbGFzcywgcmVmID0gIlllcyIpLCAKICByZWZlcmVuY2UgPSByZWxldmVsKGNodXJuX3RyYWluJEF0dHJpdGlvbiwgcmVmID0gIlllcyIpCikKYGBgCgpgYGB7ciBsb2dpc3RpYy1yZWdyZXNzaW9uLXJvYywgZmlnLndpZHRoPTYsIGZpZy5oZWlnaHQ9NC41LCBmaWcuY2FwPSJST0MgY3VydmUgZm9yIGNyb3NzLXZhbGlkYXRlZCBtb2RlbHMgMSBhbmQgMy4gVGhlIGluY3JlYXNlIGluIHRoZSBBVUMgcmVwcmVzZW50cyB0aGUgJ2xpZnQnIHRoYXQgd2UgYWNoaWV2ZSB3aXRoIG1vZGVsIDMuIiwgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0KbGlicmFyeShST0NSKQoKIyBDb21wdXRlIHByZWRpY3RlZCBwcm9iYWJpbGl0aWVzCm0xX3Byb2IgPC0gcHJlZGljdChjdl9tb2RlbDEsIGNodXJuX3RyYWluLCB0eXBlID0gInByb2IiKSRZZXMKbTNfcHJvYiA8LSBwcmVkaWN0KGN2X21vZGVsMywgY2h1cm5fdHJhaW4sIHR5cGUgPSAicHJvYiIpJFllcwoKIyBDb21wdXRlIEFVQyBtZXRyaWNzIGZvciBjdl9tb2RlbDEgYW5kIGN2X21vZGVsMwpwZXJmMSA8LSBwcmVkaWN0aW9uKG0xX3Byb2IsIGNodXJuX3RyYWluJEF0dHJpdGlvbikgJT4lCiAgcGVyZm9ybWFuY2UobWVhc3VyZSA9ICJ0cHIiLCB4Lm1lYXN1cmUgPSAiZnByIikKcGVyZjIgPC0gcHJlZGljdGlvbihtM19wcm9iLCBjaHVybl90cmFpbiRBdHRyaXRpb24pICU+JQogIHBlcmZvcm1hbmNlKG1lYXN1cmUgPSAidHByIiwgeC5tZWFzdXJlID0gImZwciIpCgojIFBsb3QgUk9DIGN1cnZlcyBmb3IgY3ZfbW9kZWwxIGFuZCBjdl9tb2RlbDMKcGxvdChwZXJmMSwgY29sID0gImJsYWNrIiwgbHR5ID0gMikKcGxvdChwZXJmMiwgYWRkID0gVFJVRSwgY29sID0gImJsdWUiKQpsZWdlbmQoMC44LCAwLjIsIGxlZ2VuZCA9IGMoImN2X21vZGVsMSIsICJjdl9tb2RlbDMiKSwKICAgICAgIGNvbCA9IGMoImJsYWNrIiwgImJsdWUiKSwgbHR5ID0gMjoxLCBjZXggPSAwLjYpCmBgYAoKYGBge3IgcGxzLWxvZ2lzdGljLXJlZ3Jlc3Npb24sIGZpZy5oZWlnaHQ9My41LCBmaWcud2lkdGg9NiwgZmlnLmNhcD0iVGhlIDEwLWZvbGQgY3Jvc3MtdmFsaWRhdGlvbiBSTVNFIG9idGFpbmVkIHVzaW5nIFBMUyB3aXRoIDEtLTE2IHByaW5jaXBhbCBjb21wb25lbnRzLiJ9CiMgUGVyZm9ybSAxMC1mb2xkIENWIG9uIGEgUExTIG1vZGVsIHR1bmluZyB0aGUgbnVtYmVyIG9mIFBDcyB0byAKIyB1c2UgYXMgcHJlZGljdG9ycwpzZXQuc2VlZCgxMjMpCmN2X21vZGVsX3BscyA8LSB0cmFpbigKICBBdHRyaXRpb24gfiAuLCAKICBkYXRhID0gY2h1cm5fdHJhaW4sIAogIG1ldGhvZCA9ICJwbHMiLAogIGZhbWlseSA9ICJiaW5vbWlhbCIsCiAgdHJDb250cm9sID0gdHJhaW5Db250cm9sKG1ldGhvZCA9ICJjdiIsIG51bWJlciA9IDEwKSwKICBwcmVQcm9jZXNzID0gYygienYiLCAiY2VudGVyIiwgInNjYWxlIiksCiAgdHVuZUxlbmd0aCA9IDE2CikKCiMgTW9kZWwgd2l0aCBsb3dlc3QgUk1TRQpjdl9tb2RlbF9wbHMkYmVzdFR1bmUKCiMgUGxvdCBjcm9zcy12YWxpZGF0ZWQgUk1TRQpnZ3Bsb3QoY3ZfbW9kZWxfcGxzKQpgYGAKCiMjIEZlYXR1cmUgaW50ZXJwcmV0YXRpb24KCmBgYHtyIGdsbS12aXAsIGZpZy5jYXA9IlRvcCAyMCBtb3N0IGltcG9ydGFudCB2YXJpYWJsZXMgZm9yIHRoZSBQTFMgbW9kZWwuIn0KdmlwKGN2X21vZGVsMywgbnVtX2ZlYXR1cmVzID0gMjApCmBgYAoKRmlndXJlIDUuNzoKCmBgYHtyIGdsbS1wZHAsIGVjaG89VFJVRSwgZmlnLmhlaWdodD01LCBmaWcud2lkdGg9NywgZmlnLmNhcD0iUGFydGlhbCBkZXBlbmRlbmNlIHBsb3RzIGZvciB0aGUgZmlyc3QgZm91ciBtb3N0IGltcG9ydGFudCB2YXJpYWJsZXMuICBXZSBjYW4gc2VlIGhvdyB0aGUgcHJlZGljdGVkIHByb2JhYmlsaXR5IG9mIGF0dHJpdGlvbiBjaGFuZ2VzIGZvciBlYWNoIHZhbHVlIG9mIHRoZSBpbmZsdWVudGlhbCBwcmVkaWN0b3JzLiJ9CnByZWQuZnVuIDwtIGZ1bmN0aW9uKG9iamVjdCwgbmV3ZGF0YSkgewogIFllcyA8LSBtZWFuKHByZWRpY3Qob2JqZWN0LCBuZXdkYXRhLCB0eXBlID0gInByb2IiKSRZZXMpCiAgYXMuZGF0YS5mcmFtZShZZXMpCn0KCnAxIDwtIHBkcDo6cGFydGlhbChjdl9tb2RlbDMsIHByZWQudmFyID0gIk92ZXJUaW1lIiwgcHJlZC5mdW4gPSBwcmVkLmZ1bikgJT4lIAogIGF1dG9wbG90KHJ1ZyA9IFRSVUUpICsgeWxpbShjKDAsIDEpKQoKcDIgPC0gcGRwOjpwYXJ0aWFsKGN2X21vZGVsMywgcHJlZC52YXIgPSAiSm9iU2F0aXNmYWN0aW9uIiwgcHJlZC5mdW4gPSBwcmVkLmZ1bikgJT4lIAogIGF1dG9wbG90KCkgKyB5bGltKGMoMCwgMSkpCgpwMyA8LSBwZHA6OnBhcnRpYWwoY3ZfbW9kZWwzLCBwcmVkLnZhciA9ICJOdW1Db21wYW5pZXNXb3JrZWQiLCBwcmVkLmZ1biA9IHByZWQuZnVuLCBnciA9IDEwKSAlPiUgCiAgYXV0b3Bsb3QoKSArIHNjYWxlX3hfY29udGludW91cyhicmVha3MgPSAwOjkpICsgeWxpbShjKDAsIDEpKQogIAoKcDQgPC0gcGRwOjpwYXJ0aWFsKGN2X21vZGVsMywgcHJlZC52YXIgPSAiRW52aXJvbm1lbnRTYXRpc2ZhY3Rpb24iLCBwcmVkLmZ1biA9IHByZWQuZnVuKSAlPiUgCiAgYXV0b3Bsb3QoKSArIHlsaW0oYygwLCAxKSkKCmdyaWQuYXJyYW5nZShwMSwgcDIsIHAzLCBwNCwgbnJvdyA9IDIpCmBgYAoKYGBge3J9CiMgY2xlYW4gdXAKcm0obGlzdCA9IGxzKCkpCmBgYAoK