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