Session 19. Cross-validation in classification problems. An introduction to Decision Trees: complicated classification problems and powerful solutions. Postpruning of a Decision Tree model.

Feedback should be send to goran.milovanovic@datakolektiv.com. These notebooks accompany the Intro to Data Science: Non-Technical Background course 2020/21.


What do we want to do today?

We will first consider cross-validation in classification problems and contrast it with previously introduced approaches to model selection. Then we begin to go beyond Linear Models: we will introduce Decision Trees for classification and regression problems. In this session we go for an intuitive and practical approach to Decision Trees in R; in the next session we will introduce the basic elements of Information Theory and dig deeper into the theory of Decision Trees and even more powerful Random Forests.

0. Setup

install.packages('rpart')
install.packages ('rpart.plot')

Grab the HR_comma_sep.csv dataset from the Kaggle and place it in your _data directory for this session.

dataDir <- paste0(getwd(), "/_data/")
library(tidyverse)
Registered S3 methods overwritten by 'dbplyr':
  method         from
  print.tbl_lazy     
  print.tbl_sql      
── Attaching packages ────────────────────────────────────────────────────────────────────────────── tidyverse 1.3.1 ──
✔ ggplot2 3.3.5     ✔ purrr   0.3.4
✔ tibble  3.1.6     ✔ dplyr   1.0.8
✔ tidyr   1.2.0     ✔ stringr 1.4.0
✔ readr   2.0.2     ✔ forcats 0.5.1
── Conflicts ───────────────────────────────────────────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag()    masks stats::lag()
library(data.table)
Registered S3 method overwritten by 'data.table':
  method           from
  print.data.table     
data.table 1.14.2 using 1 threads (see ?getDTthreads).  Latest news: r-datatable.com
**********
This installation of data.table has not detected OpenMP support. It should still work but in single-threaded mode.
This is a Mac. Please read https://mac.r-project.org/openmp/. Please engage with Apple and ask them for support. Check r-datatable.com for updates, and our Mac instructions here: https://github.com/Rdatatable/data.table/wiki/Installation. After several years of many reports of installation problems on Mac, it's time to gingerly point out that there have been no similar problems on Windows or Linux.
**********

Attaching package: ‘data.table’

The following objects are masked from ‘package:dplyr’:

    between, first, last

The following object is masked from ‘package:purrr’:

    transpose
library(rpart)
library(rpart.plot)

1. Cross-Validation in Classification Problems

Consider the HR_comma_sep.csv dataset:

dataSet <- read.csv(paste0('_data/', 'HR_comma_sep.csv'), 
                    header = T, 
                    check.names = F,
                    stringsAsFactors = F)
head(dataSet)
table(dataSet$left)

    0     1 
11428  3571 

The task is to predict the value of left - whether the employee has left the company or not - from a set of predictors encompassing the following:

glimpse(dataSet)
Rows: 14,999
Columns: 10
$ satisfaction_level    <dbl> 0.38, 0.80, 0.11, 0.72, 0.37, 0.41, 0.10, 0.92, 0.89, 0.42, 0.45, 0.11, 0.84, 0.41, 0.3…
$ last_evaluation       <dbl> 0.53, 0.86, 0.88, 0.87, 0.52, 0.50, 0.77, 0.85, 1.00, 0.53, 0.54, 0.81, 0.92, 0.55, 0.5…
$ number_project        <int> 2, 5, 7, 5, 2, 2, 6, 5, 5, 2, 2, 6, 4, 2, 2, 2, 2, 4, 2, 5, 6, 2, 6, 2, 2, 5, 4, 2, 2, …
$ average_montly_hours  <int> 157, 262, 272, 223, 159, 153, 247, 259, 224, 142, 135, 305, 234, 148, 137, 143, 160, 25…
$ time_spend_company    <int> 3, 6, 4, 5, 3, 3, 4, 5, 5, 3, 3, 4, 5, 3, 3, 3, 3, 6, 3, 5, 4, 3, 4, 3, 3, 5, 5, 3, 3, …
$ Work_accident         <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, …
$ left                  <int> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, …
$ promotion_last_5years <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, …
$ sales                 <chr> "sales", "sales", "sales", "sales", "sales", "sales", "sales", "sales", "sales", "sales…
$ salary                <chr> "low", "medium", "medium", "low", "low", "low", "low", "low", "low", "low", "low", "low…
  • satisfaction_level: a measure of employee’s level of satisfaction
  • last_evaluation: the result of a last evaluation
  • number_projects: in how many projects did the employee took part
  • average_monthly_hours: how working hours monthly on average
  • time_spend_company: for how long is the employee with us
  • Work_accident: any work accidents?
  • promotion_last_5years: did the promotion occur in the last five years?
  • sales: department (sales, accounting, hr, technical, support, management, IT, product_mng, marketing, RandD)
  • salary: salary class (low, medium, high)

Let’s formulate a Binomial Logistic Regression model to try to predict left from satisfaction_level, last_evaluation, sales, and salary:

# - setups
dataSet$left <- factor(dataSet$left, 
                       levels = c(0, 1))
dataSet$salary <- factor(dataSet$salary)
dataSet$salary <- relevel(dataSet$salary,
                          ref = 'high')
dataSet$sales <- factor(dataSet$sales)
dataSet$sales <- relevel(dataSet$sales,
                         ref = 'RandD')
# - model
blr_model1 <- glm(left ~ satisfaction_level + last_evaluation + sales + salary,
                  data = dataSet,
                  family = "binomial")
modelsummary <- summary(blr_model1)
print(modelsummary)

Call:
glm(formula = left ~ satisfaction_level + last_evaluation + sales + 
    salary, family = "binomial", data = dataSet)

Deviance Residuals: 
    Min       1Q   Median       3Q      Max  
-1.7093  -0.6970  -0.4741  -0.1928   2.7917  

Coefficients:
                   Estimate Std. Error z value Pr(>|z|)    
(Intercept)        -1.46022    0.18380  -7.945 1.95e-15 ***
satisfaction_level -3.87156    0.08923 -43.387  < 2e-16 ***
last_evaluation     0.53218    0.12208   4.359 1.31e-05 ***
salesaccounting     0.67072    0.14023   4.783 1.73e-06 ***
saleshr             0.88149    0.13928   6.329 2.47e-10 ***
salesIT             0.49930    0.13124   3.804 0.000142 ***
salesmanagement     0.26267    0.16531   1.589 0.112068    
salesmarketing      0.65029    0.13890   4.682 2.84e-06 ***
salesproduct_mng    0.51352    0.13853   3.707 0.000210 ***
salessales          0.62675    0.11422   5.487 4.08e-08 ***
salessupport        0.66090    0.12001   5.507 3.65e-08 ***
salestechnical      0.67753    0.11769   5.757 8.57e-09 ***
salarylow           1.77870    0.12287  14.476  < 2e-16 ***
salarymedium        1.28847    0.12400  10.391  < 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: 16465  on 14998  degrees of freedom
Residual deviance: 13733  on 14985  degrees of freedom
AIC: 13761

Number of Fisher Scoring iterations: 5

And take a look at the regression coefficients:

exp(coefficients(blr_model1))
       (Intercept) satisfaction_level    last_evaluation    salesaccounting            saleshr            salesIT 
        0.23218577         0.02082584         1.70263484         1.95564264         2.41448902         1.64756456 
   salesmanagement     salesmarketing   salesproduct_mng         salessales       salessupport     salestechnical 
        1.30039267         1.91608809         1.67116051         1.87152647         1.93652695         1.96900961 
         salarylow       salarymedium 
        5.92213412         3.62722234 

The Akaike Information Criterion is:

blr_model1$aic
[1] 13761.14

Now consider a model encompassing all predictors from the HR_comma_sep.csv:

# - setups
dataSet$Work_accident <- factor(dataSet$Work_accident,
                                levels = c(0, 1))
dataSet$promotion_last_5years <- factor(dataSet$promotion_last_5years,
                                        levels = c(0, 1))

# - model
blr_model2 <- glm(left ~ .,
                  data = dataSet,
                  family = "binomial")
modelsummary <- summary(blr_model2)
print(modelsummary)

Call:
glm(formula = left ~ ., family = "binomial", data = dataSet)

Deviance Residuals: 
    Min       1Q   Median       3Q      Max  
-2.2248  -0.6645  -0.4026  -0.1177   3.0688  

Coefficients:
                         Estimate Std. Error z value Pr(>|z|)    
(Intercept)            -2.0586521  0.2035965 -10.111  < 2e-16 ***
satisfaction_level     -4.1356889  0.0980538 -42.178  < 2e-16 ***
last_evaluation         0.7309032  0.1491787   4.900 9.61e-07 ***
number_project         -0.3150787  0.0213248 -14.775  < 2e-16 ***
average_montly_hours    0.0044603  0.0005161   8.643  < 2e-16 ***
time_spend_company      0.2677537  0.0155736  17.193  < 2e-16 ***
Work_accident1         -1.5298283  0.0895473 -17.084  < 2e-16 ***
promotion_last_5years1 -1.4301364  0.2574958  -5.554 2.79e-08 ***
salesaccounting         0.5823659  0.1448848   4.020 5.83e-05 ***
saleshr                 0.8147438  0.1439439   5.660 1.51e-08 ***
salesIT                 0.4016480  0.1355936   2.962  0.00306 ** 
salesmanagement         0.1339423  0.1704829   0.786  0.43206    
salesmarketing          0.5702777  0.1445326   3.946 7.96e-05 ***
salesproduct_mng        0.4291129  0.1428822   3.003  0.00267 ** 
salessales              0.5435800  0.1181590   4.600 4.22e-06 ***
salessupport            0.6323910  0.1241337   5.094 3.50e-07 ***
salestechnical          0.6525123  0.1217267   5.360 8.30e-08 ***
salarylow               1.9440627  0.1286272  15.114  < 2e-16 ***
salarymedium            1.4132244  0.1293534  10.925  < 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: 16465  on 14998  degrees of freedom
Residual deviance: 12850  on 14980  degrees of freedom
AIC: 12888

Number of Fisher Scoring iterations: 5
exp(coefficients(blr_model2))
           (Intercept)     satisfaction_level        last_evaluation         number_project   average_montly_hours 
            0.12762588             0.01599164             2.07695560             0.72973146             1.00447026 
    time_spend_company         Work_accident1 promotion_last_5years1        salesaccounting                saleshr 
            1.30702513             0.21657284             0.23927628             1.79026897             2.25859684 
               salesIT        salesmanagement         salesmarketing       salesproduct_mng             salessales 
            1.49428520             1.14332679             1.76875818             1.53589447             1.72216110 
          salessupport         salestechnical              salarylow           salarymedium 
            1.88210526             1.92035920             6.98708012             4.10918362 

The Akaike Information Criterion is:

blr_model2$aic
[1] 12887.9

Let’s cross-validate our blr_model1 and blr_model2 now. We will perform the k-fold CV in the following way:

  • define four folds in the dataSet by randomly assigning each observation to fold 1, 2, 3, or 4;
  • for each i in folds: estimate the model on the remaining dataSet[-i, ] folds taken together
  • predict the observations in fold i from the fitted model,
  • compute model accuracy, FA rate, Hit rate for fold i,
  • observe the average accuracy (ROC) across all four folds.

Here we go: define folds first.

dataSet$fold <- sample(1:4, size = dim(dataSet)[1], replace = T)
table(dataSet$fold)

   1    2    3    4 
3647 3762 3790 3800 

First for the narrower model:

cv1 <- lapply(1:4, function(x) {
  
  # - test and train datasets
  test <- dataSet %>% 
    dplyr::filter(fold == x) %>% 
    dplyr::select(-fold)
  train <- dataSet %>% 
    dplyr::filter(fold != x) %>% 
    dplyr::select(-fold)
  
  # - model on the training dataset
  blrModel <- glm(left ~ satisfaction_level + last_evaluation + sales + salary,
                  data = train,
                  family = "binomial")
  
  # - predict on the test dataset
  predictions <- predict(blrModel, 
                         newdata = test, 
                         type = "response")
  predictions <- ifelse(predictions > .5, 1, 0)
  
  # - ROC analysis
  acc <- sum(test$left == predictions)
  acc <- acc/dim(test)[1]
  hit <- sum(test$left == 1 & predictions == 1)
  hit <- hit/sum(test$left == 1)
  fa <- sum(test$left == 0 & predictions == 1)
  fa <- fa/sum(test$left == 0)
  return(data.frame(acc, hit, fa))
})

cv1 <- rbindlist(cv1)
cv1$fold <- 1:4
cv1 <- tidyr::pivot_longer(cv1,
                           cols = -fold,
                           names_to = 'measure',
                           values_to = 'value')
cv1$model <- 1
print(cv1)

Now for the full model:

cv2 <- lapply(1:4, function(x) {
  test <- dataSet %>% 
    dplyr::filter(fold == x) %>% 
    dplyr::select(-fold)
  train <- dataSet %>% 
    dplyr::filter(fold != x) %>% 
    dplyr::select(-fold)
  blrModel <- glm(left ~ .,
                  data = train,
                  family = "binomial")
  predictions <- predict(blrModel, 
                         newdata = test, 
                         type = "response")
  predictions <- ifelse(predictions > .5, 1, 0)
  acc <- sum(test$left == predictions)
  acc <- acc/dim(test)[1]
  hit <- sum(test$left == 1 & predictions == 1)
  hit <- hit/sum(test$left == 1)
  fa <- sum(test$left == 0 & predictions == 1)
  fa <- fa/sum(test$left == 0)
  return(data.frame(acc, hit, fa))
})
cv2 <- rbindlist(cv2)
cv2$fold <- 1:4
cv2 <- tidyr::pivot_longer(cv2,
                           cols = -fold,
                           names_to = 'measure',
                           values_to = 'value')
cv2$model <- 2
print(cv2)

Compare:

modelSelection <- rbind(cv1, cv2)
modelSelection$model <- ifelse(modelSelection$model == 1,
                               "Partial", "Full")
modelSelection$model <- factor(modelSelection$model)
ggplot(data = modelSelection, 
       aes(x = fold, 
           y = value, 
           group = model, 
           color = model, 
           fill = model)) + 
  geom_path(size = .5) + 
  geom_point(size = 3) + 
  scale_color_manual(values = c('darkred', 'darkorange')) + 
  ylim(0, 1) +
  facet_wrap(~measure) + 
  theme_bw() + 
  theme(panel.border = element_blank()) + 
  theme(legend.text = element_text(size = 20)) +
  theme(legend.title = element_text(size = 20)) +
  theme(legend.position = "top") +
  theme(strip.background = element_blank()) + 
  theme(strip.text = element_text(size = 20)) +
  theme(axis.title.x = element_text(size = 18)) + 
  theme(axis.title.y = element_text(size = 18)) + 
  theme(axis.text.x = element_text(size = 17)) + 
  theme(axis.text.y = element_text(size = 17))

The average ROC from k-fold CV for both models:

modelSelection %>%
  dplyr::select(-fold) %>% 
  dplyr::group_by(model, measure) %>% 
  dplyr::summarise(mean = round(mean(value), 5)) %>% 
  tidyr::pivot_wider(id_cols = model, 
                     names_from = 'measure', 
                     values_from = 'mean')
`summarise()` has grouped output by 'model'. You can override using the `.groups` argument.

Suppose we set the decision trashold to be p = .2. First for the narrow model:

cv1 <- lapply(1:4, function(x) {
  
  # - test and train datasets
  test <- dataSet %>% 
    dplyr::filter(fold == x) %>% 
    dplyr::select(-fold)
  train <- dataSet %>% 
    dplyr::filter(fold != x) %>% 
    dplyr::select(-fold)
  
  # - model on the training dataset
  blrModel <- glm(left ~ satisfaction_level + last_evaluation + sales + salary,
                  data = train,
                  family = "binomial")
  
  # - predict on the test dataset
  predictions <- predict(blrModel, 
                         newdata = test, 
                         type = "response")
  predictions <- ifelse(predictions > .2, 1, 0)
  
  # - ROC analysis
  acc <- sum(test$left == predictions)
  acc <- acc/dim(test)[1]
  hit <- sum(test$left == 1 & predictions == 1)
  hit <- hit/sum(test$left == 1)
  fa <- sum(test$left == 0 & predictions == 1)
  fa <- fa/sum(test$left == 0)
  return(data.frame(acc, hit, fa))
})

cv1 <- rbindlist(cv1)
cv1$fold <- 1:4
cv1 <- tidyr::pivot_longer(cv1,
                           cols = -fold,
                           names_to = 'measure',
                           values_to = 'value')
cv1$model <- 1

Now for the full model, decision treshold is p = .2:

cv2 <- lapply(1:4, function(x) {
  test <- dataSet %>% 
    dplyr::filter(fold == x) %>% 
    dplyr::select(-fold)
  train <- dataSet %>% 
    dplyr::filter(fold != x) %>% 
    dplyr::select(-fold)
  blrModel <- glm(left ~ .,
                  data = train,
                  family = "binomial")
  predictions <- predict(blrModel, 
                         newdata = test, 
                         type = "response")
  predictions <- ifelse(predictions > .2, 1, 0)
  acc <- sum(test$left == predictions)
  acc <- acc/dim(test)[1]
  hit <- sum(test$left == 1 & predictions == 1)
  hit <- hit/sum(test$left == 1)
  fa <- sum(test$left == 0 & predictions == 1)
  fa <- fa/sum(test$left == 0)
  return(data.frame(acc, hit, fa))
})
cv2 <- rbindlist(cv2)
cv2$fold <- 1:4
cv2 <- tidyr::pivot_longer(cv2,
                           cols = -fold,
                           names_to = 'measure',
                           values_to = 'value')
cv2$model <- 2

Compare:

modelSelection <- rbind(cv1, cv2)
modelSelection$model <- ifelse(modelSelection$model == 1,
                               "Partial", "Full")
modelSelection$model <- factor(modelSelection$model)
ggplot(data = modelSelection, 
       aes(x = fold, 
           y = value, 
           group = model, 
           color = model, 
           fill = model)) + 
  geom_path(size = .5) + 
  geom_point(size = 3) + 
  scale_color_manual(values = c('darkred', 'darkorange')) + 
  ylim(0, 1) +
  facet_wrap(~measure) + 
  theme_bw() + 
  theme(panel.border = element_blank()) + 
  theme(legend.text = element_text(size = 20)) +
  theme(legend.title = element_text(size = 20)) +
  theme(legend.position = "top") +
  theme(strip.background = element_blank()) + 
  theme(strip.text = element_text(size = 20)) +
  theme(axis.title.x = element_text(size = 18)) + 
  theme(axis.title.y = element_text(size = 18)) + 
  theme(axis.text.x = element_text(size = 17)) + 
  theme(axis.text.y = element_text(size = 17))

Across a range of decision criteria, dec_criterion <- seq(.01, .99, .01), initial model with four predictors:

cv1 <- lapply(1:4, function(x) {
  
  # - test and train datasets
  test <- dataSet %>% 
    dplyr::filter(fold == x) %>% 
    dplyr::select(-fold)
  train <- dataSet %>% 
    dplyr::filter(fold != x) %>% 
    dplyr::select(-fold)
  
  # - model on the training dataset
  blrModel <- glm(left ~ satisfaction_level + last_evaluation + sales + salary,
                  data = train,
                  family = "binomial")
  
  # - predict on the test dataset
  predictions <- predict(blrModel, 
                         newdata = test, 
                         type = "response")
  dec_criterion <- seq(.01, .99, .01)
  predictions <- lapply(dec_criterion, function(y) {
    return(
      ifelse(predictions > y, 1, 0)
    )  
  })
  predictions <- t(Reduce(rbind, predictions))
  roc <- apply(predictions, 2, function(y) {
    # - ROC analysis
    acc <- sum(test$left == y)
    acc <- acc/dim(test)[1]
    hit <- sum(test$left == 1 & y == 1)
    hit <- hit/sum(test$left == 1)
    fa <- sum(test$left == 0 & y == 1)
    fa <- fa/sum(test$left == 0)
    return(data.frame(acc, hit, fa))
  })
  roc <- rbindlist(roc)
  roc$dec_criterion <- dec_criterion
  roc$fold <- x
  return(roc)
})

cv1 <- rbindlist(cv1)
cv1 <- cv1 %>% 
  dplyr::group_by(dec_criterion) %>%
  dplyr::summarise(acc = mean(acc),
                   hit = mean(hit),
                   fa = mean(fa))
cv1 <- tidyr::pivot_longer(cv1,
                           cols = -dec_criterion,
                           names_to = 'measure',
                           values_to = 'value')
cv1$model <- 1

For the full model, dec_criterion <- seq(.01, .99, .01):

cv2 <- lapply(1:4, function(x) {
  
  # - test and train datasets
  test <- dataSet %>% 
    dplyr::filter(fold == x) %>% 
    dplyr::select(-fold)
  train <- dataSet %>% 
    dplyr::filter(fold != x) %>% 
    dplyr::select(-fold)
  
  # - model on the training dataset
  blrModel <- glm(left ~ .,
                  data = train,
                  family = "binomial")
  
  # - predict on the test dataset
  predictions <- predict(blrModel, 
                         newdata = test, 
                         type = "response")
  dec_criterion <- seq(.01, .99, .01)
  predictions <- lapply(dec_criterion, function(y) {
    return(
      ifelse(predictions > y, 1, 0)
    )  
  })
  predictions <- t(Reduce(rbind, predictions))
  roc <- apply(predictions, 2, function(y) {
    # - ROC analysis
    acc <- sum(test$left == y)
    acc <- acc/dim(test)[1]
    hit <- sum(test$left == 1 & y == 1)
    hit <- hit/sum(test$left == 1)
    fa <- sum(test$left == 0 & y == 1)
    fa <- fa/sum(test$left == 0)
    return(data.frame(acc, hit, fa))
  })
  roc <- rbindlist(roc)
  roc$dec_criterion <- dec_criterion
  roc$fold <- x
  return(roc)
})

cv2 <- rbindlist(cv2)
cv2 <- cv2 %>% 
  dplyr::group_by(dec_criterion) %>%
  dplyr::summarise(acc = mean(acc),
                   hit = mean(hit),
                   fa = mean(fa))
cv2 <- tidyr::pivot_longer(cv2,
                           cols = -dec_criterion,
                           names_to = 'measure',
                           values_to = 'value')
cv2$model <- 2

Compare ROC curves:

ROC_results <- rbind(cv1, cv2)
ROC_results$model <- ifelse(ROC_results$model == 1,
                               "Partial", "Full")
ROC_results <- ROC_results %>% 
  pivot_wider(id_cols = c('dec_criterion', 'model'), 
              names_from = measure,
              values_from = value)

ROC_results$model <- factor(ROC_results$model)

ggplot(data = ROC_results, 
       aes(x = fa, 
           y = hit, 
           group = model,
           color = model, 
           fill = model)) +
  ylab("Hit Rate (TPR)") + 
  xlab("FA Rate (FPR)") +
  geom_point(size = 1) + geom_path(size = .1) + 
  geom_abline(intercept = 0, slope = 1, size = .5) + 
  ggtitle("ROC analysis for the Binomial Regression Model") +
  theme_bw() + 
  theme(plot.title = element_text(hjust = .5)) + 
  theme_bw() + 
  theme(panel.border = element_blank()) + 
  theme(plot.title = element_text(hjust = .5, size = 20)) + 
  theme(legend.text = element_text(size = 20)) +
  theme(legend.title = element_text(size = 20)) +
  theme(legend.position = "top") +
  theme(axis.title.x = element_text(size = 18)) + 
  theme(axis.title.y = element_text(size = 18)) + 
  theme(axis.text.x = element_text(size = 17)) + 
  theme(axis.text.y = element_text(size = 17))

2. Decision Trees for Classification Problems

What is a Decision Tree classifier? Let’s introduce the Decision Tree by an example before diving into theory in the next session. We will use the HR_comma_sep.csv dataset again:

# - load HR_comma_sep.csv again
dataSet <- read.csv(paste0('_data/', 'HR_comma_sep.csv'), 
                    header = T, 
                    check.names = F,
                    stringsAsFactors = F)

Let’s split dataSet into a training and test subsets:

# - Test and Train data:
ix <- rbinom(dim(dataSet)[1] , 1, .5)
table(ix)/sum(table(ix))
ix
        0         1 
0.4942329 0.5057671 
train <- dataSet[ix == 1, ]
test <- dataSet[ix == 0,]

Train one Decision Tree on train:

# - Base Model
classTree <- rpart(left ~ ., 
                   data = train, 
                   method = "class")

Visualize the model with prp():

prp(classTree, 
    cex = .8)

Decision Trees can easily overfit because of the intrinsinc complexity of the model. Pruning is one of the methods to prevent the Decision Tree for overfitting: we prune the tree by relying on the complexity parameter (cp) to discard the branches that were developed to fit potentially idiosyncratic information present in the data. The CP (complexity parameter) is used to control tree growth: if the cost of adding a variable is higher then the value of CP then tree growth stops.

The CP parameters has to do with an internal cross-validation procedure performed by {Rpart} during the training of a Decision Tree model (to be explained in our live session).

# - Base Model
classTree <- rpart(left ~ ., 
                   data = train, 
                   method = "class",
                   control = rpart.control(cp = 0))
# - Inspect model:
prp(classTree, 
    cex = .8)

# - Examine the complexity plot
cptable <- as.data.frame(classTree$cptable)
print(cptable)
plotcp(classTree)

The one with least cross-validated error (xerror) is the optimal value of CP.

cptable[which.min(cptable$xerror), ]

ROC analysis for the base model:

# - Base Model Accuracy
test$pred <- predict(classTree,
                     test,
                     type = "class")
# - silly, but I need to do this...
test$pred <- as.numeric(as.character(test$pred))
base_accuracy <- mean(test$pred == test$left)
print(paste0("Base model acc: ", base_accuracy))
[1] "Base model acc: 0.971266693646297"
# - Base Model ROC
test$hit <- ifelse(test$pred == 1 & test$left == 1, T, F)
test$FA <- ifelse(test$pred == 1 & test$left == 0, T, F)
hitRate <- sum(test$hit)/length(test$hit)
print(paste0("Base model Hit rate: ", hitRate))
[1] "Base model Hit rate: 0.222851746931067"
FARate <- sum(test$FA)/length(test$FA)
print(paste0("Base model FA rate: ", FARate))
[1] "Base model FA rate: 0.0078240928099285"
test$miss <- ifelse(test$pred == 0 & test$left == 1, T, F)
missRate <- sum(test$miss)/length(test$miss)
print(paste0("Base model Miss rate: ", missRate))
[1] "Base model Miss rate: 0.0209092135437745"
# - Prune the classTree based on the optimal cp value
optimal_cp <- cptable$CP[which.min(cptable$xerror)]
classTree_prunned <- prune(classTree, 
                           cp = optimal_cp)
prp(classTree_prunned, 
    cex = .75)

# - The accuracy of the pruned tree
test$pred <- predict(classTree_prunned, 
                     test, 
                     type = "class")
accuracy_postprun <- mean(test$pred == test$left)
print(paste0("Pruned model acc: ", accuracy_postprun))
[1] "Pruned model acc: 0.971131795494402"
# - Pruned Model ROC
test$hit <- ifelse(test$pred == 1 & test$left == 1, T, F)
test$FA <- ifelse(test$pred == 1 & test$left == 0, T, F)
hitRate <- sum(test$hit)/length(test$hit)
print(paste0("Pruned Hit rate: ", hitRate))
[1] "Pruned Hit rate: 0.219614191285579"
FARate <- sum(test$FA)/length(test$FA)
print(paste0("Pruned FA rate: ", FARate))
[1] "Pruned FA rate: 0.00472143531633617"
test$miss <- ifelse(test$pred == 0 & test$left == 1, T, F)
missRate <- sum(test$miss)/length(test$miss)
print(paste0("Pruned Miss rate: ", missRate))
[1] "Pruned Miss rate: 0.0241467691892621"
test$CR <- ifelse(test$pred == 0 & test$left == 0, T, F)
CRRate <- sum(test$CR)/length(test$CR)
print(paste0("Pruned CR rate: ", CRRate))
[1] "Pruned CR rate: 0.751517604208822"

For pruning with {rpart} Decison Trees in R, see the following Stack Overflow discussion: Selecting cp value for decision tree pruning using rpart.


Further Readings


Goran S. Milovanović

DataKolektiv, 2020/21

contact:


License: GPLv3 This Notebook is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This Notebook is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this Notebook. If not, see http://www.gnu.org/licenses/.


LS0tCnRpdGxlOiBJbnRybyB0byBEYXRhIFNjaWVuY2UgKE5vbi1UZWNobmljYWwgQmFja2dyb3VuZCwgUikgLSBTZXNzaW9uMTkKYXV0aG9yOgotIG5hbWU6IEdvcmFuIFMuIE1pbG92YW5vdmnEhywgUGhECiAgYWZmaWxpYXRpb246IERhdGFLb2xla3RpdiwgQ2hpZWYgU2NpZW50aXN0ICYgT3duZXI7IERhdGEgU2NpZW50aXN0IGZvciBXaWtpZGF0YSwgV01ERQphYnN0cmFjdDogCm91dHB1dDoKICBodG1sX25vdGVib29rOgogICAgY29kZV9mb2xkaW5nOiBzaG93CiAgICB0aGVtZTogc3BhY2VsYWIKICAgIHRvYzogeWVzCiAgICB0b2NfZmxvYXQ6IHllcwogICAgdG9jX2RlcHRoOiA1CiAgaHRtbF9kb2N1bWVudDoKICAgIHRvYzogeWVzCiAgICB0b2NfZGVwdGg6IDUKLS0tCgohW10oLi4vX2ltZy9ES19Mb2dvXzEwMC5wbmcpCgoqKioKIyBTZXNzaW9uIDE5LiBDcm9zcy12YWxpZGF0aW9uIGluIGNsYXNzaWZpY2F0aW9uIHByb2JsZW1zLiBBbiBpbnRyb2R1Y3Rpb24gdG8gRGVjaXNpb24gVHJlZXM6IGNvbXBsaWNhdGVkIGNsYXNzaWZpY2F0aW9uIHByb2JsZW1zIGFuZCBwb3dlcmZ1bCBzb2x1dGlvbnMuIFBvc3RwcnVuaW5nIG9mIGEgRGVjaXNpb24gVHJlZSBtb2RlbC4KCioqRmVlZGJhY2sqKiBzaG91bGQgYmUgc2VuZCB0byBgZ29yYW4ubWlsb3Zhbm92aWNAZGF0YWtvbGVrdGl2LmNvbWAuIApUaGVzZSBub3RlYm9va3MgYWNjb21wYW55IHRoZSBJbnRybyB0byBEYXRhIFNjaWVuY2U6IE5vbi1UZWNobmljYWwgQmFja2dyb3VuZCBjb3Vyc2UgMjAyMC8yMS4KCioqKgoKIyMjIFdoYXQgZG8gd2Ugd2FudCB0byBkbyB0b2RheT8KCldlIHdpbGwgZmlyc3QgY29uc2lkZXIgY3Jvc3MtdmFsaWRhdGlvbiBpbiBjbGFzc2lmaWNhdGlvbiBwcm9ibGVtcyBhbmQgY29udHJhc3QgaXQgd2l0aCBwcmV2aW91c2x5IGludHJvZHVjZWQgYXBwcm9hY2hlcyB0byBtb2RlbCBzZWxlY3Rpb24uIFRoZW4gd2UgYmVnaW4gdG8gZ28gYmV5b25kIExpbmVhciBNb2RlbHM6IHdlIHdpbGwgaW50cm9kdWNlICoqRGVjaXNpb24gVHJlZXMqKiBmb3IgY2xhc3NpZmljYXRpb24gYW5kIHJlZ3Jlc3Npb24gcHJvYmxlbXMuIEluIHRoaXMgc2Vzc2lvbiB3ZSBnbyBmb3IgYW4gaW50dWl0aXZlIGFuZCBwcmFjdGljYWwgYXBwcm9hY2ggdG8gRGVjaXNpb24gVHJlZXMgaW4gUjsgaW4gdGhlIG5leHQgc2Vzc2lvbiB3ZSB3aWxsIGludHJvZHVjZSB0aGUgYmFzaWMgZWxlbWVudHMgb2YgKipJbmZvcm1hdGlvbiBUaGVvcnkqKiBhbmQgZGlnIGRlZXBlciBpbnRvIHRoZSB0aGVvcnkgb2YgRGVjaXNpb24gVHJlZXMgYW5kIGV2ZW4gbW9yZSBwb3dlcmZ1bCBSYW5kb20gRm9yZXN0cy4KCiMjIyAwLiBTZXR1cAoKYGBge3IgZWNobyA9IFQsIGV2YWwgPSBGfQppbnN0YWxsLnBhY2thZ2VzKCdycGFydCcpCmluc3RhbGwucGFja2FnZXMgKCdycGFydC5wbG90JykKYGBgCgpHcmFiIHRoZSBgSFJfY29tbWFfc2VwLmNzdmAgZGF0YXNldCBmcm9tIHRoZSBbS2FnZ2xlXShodHRwczovL3d3dy5rYWdnbGUuY29tL2xpdWppYXFpL2hyLWNvbW1hLXNlcGNzdikgYW5kIHBsYWNlIGl0IGluIHlvdXIgYF9kYXRhYCBkaXJlY3RvcnkgZm9yIHRoaXMgc2Vzc2lvbi4KCmBgYHtyIGVjaG8gPSBULCBtZXNzYWdlID0gRiwgd2FybmluZyA9IEZ9CmRhdGFEaXIgPC0gcGFzdGUwKGdldHdkKCksICIvX2RhdGEvIikKbGlicmFyeSh0aWR5dmVyc2UpCmxpYnJhcnkoZGF0YS50YWJsZSkKbGlicmFyeShycGFydCkKbGlicmFyeShycGFydC5wbG90KQpgYGAKCgojIyMgMS4gQ3Jvc3MtVmFsaWRhdGlvbiBpbiBDbGFzc2lmaWNhdGlvbiBQcm9ibGVtcwoKQ29uc2lkZXIgdGhlIGBIUl9jb21tYV9zZXAuY3N2YCBkYXRhc2V0OgoKYGBge3IgZWNobyA9IFQsIG1lc3NhZ2UgPSBGLCB3YXJuaW5nID0gRn0KZGF0YVNldCA8LSByZWFkLmNzdihwYXN0ZTAoJ19kYXRhLycsICdIUl9jb21tYV9zZXAuY3N2JyksIAogICAgICAgICAgICAgICAgICAgIGhlYWRlciA9IFQsIAogICAgICAgICAgICAgICAgICAgIGNoZWNrLm5hbWVzID0gRiwKICAgICAgICAgICAgICAgICAgICBzdHJpbmdzQXNGYWN0b3JzID0gRikKaGVhZChkYXRhU2V0KQpgYGAKCmBgYHtyIGVjaG8gPSBULCBtZXNzYWdlID0gRiwgd2FybmluZyA9IEZ9CnRhYmxlKGRhdGFTZXQkbGVmdCkKYGBgCgpUaGUgdGFzayBpcyB0byBwcmVkaWN0IHRoZSB2YWx1ZSBvZiBgbGVmdGAgLSB3aGV0aGVyIHRoZSBlbXBsb3llZSBoYXMgbGVmdCB0aGUgY29tcGFueSBvciBub3QgLSBmcm9tIGEgc2V0IG9mIHByZWRpY3RvcnMgZW5jb21wYXNzaW5nIHRoZSBmb2xsb3dpbmc6CgpgYGB7ciBlY2hvID0gVCwgbWVzc2FnZSA9IEYsIHdhcm5pbmcgPSBGfQpnbGltcHNlKGRhdGFTZXQpCmBgYAoKLSAqKnNhdGlzZmFjdGlvbl9sZXZlbCoqOiBhIG1lYXN1cmUgb2YgZW1wbG95ZWUncyBsZXZlbCBvZiBzYXRpc2ZhY3Rpb24KLSAqKmxhc3RfZXZhbHVhdGlvbioqOiB0aGUgcmVzdWx0IG9mIGEgbGFzdCBldmFsdWF0aW9uCi0gKipudW1iZXJfcHJvamVjdHMqKjogaW4gaG93IG1hbnkgcHJvamVjdHMgZGlkIHRoZSBlbXBsb3llZSB0b29rIHBhcnQKLSAqKmF2ZXJhZ2VfbW9udGhseV9ob3VycyoqOiBob3cgd29ya2luZyBob3VycyBtb250aGx5IG9uIGF2ZXJhZ2UKLSAqKnRpbWVfc3BlbmRfY29tcGFueSoqOiBmb3IgaG93IGxvbmcgaXMgdGhlIGVtcGxveWVlIHdpdGggdXMKLSAqKldvcmtfYWNjaWRlbnQqKjogYW55IHdvcmsgYWNjaWRlbnRzPwotICoqcHJvbW90aW9uX2xhc3RfNXllYXJzKio6IGRpZCB0aGUgcHJvbW90aW9uIG9jY3VyIGluIHRoZSBsYXN0IGZpdmUgeWVhcnM/Ci0gKipzYWxlcyoqOiBkZXBhcnRtZW50IChzYWxlcywgYWNjb3VudGluZywgaHIsIHRlY2huaWNhbCwgc3VwcG9ydCwgbWFuYWdlbWVudCwgSVQsIHByb2R1Y3RfbW5nLCBtYXJrZXRpbmcsIFJhbmREKQotICoqc2FsYXJ5Kio6IHNhbGFyeSBjbGFzcyAobG93LCBtZWRpdW0sIGhpZ2gpCgpMZXQncyBmb3JtdWxhdGUgYSBCaW5vbWlhbCBMb2dpc3RpYyBSZWdyZXNzaW9uIG1vZGVsIHRvIHRyeSB0byBwcmVkaWN0IGBsZWZ0YCBmcm9tIGBzYXRpc2ZhY3Rpb25fbGV2ZWxgLCBgbGFzdF9ldmFsdWF0aW9uYCwgYHNhbGVzYCwgYW5kIGBzYWxhcnlgOgoKYGBge3IgZWNobyA9IFQsIG1lc3NhZ2UgPSBGLCB3YXJuaW5nID0gRn0KIyAtIHNldHVwcwpkYXRhU2V0JGxlZnQgPC0gZmFjdG9yKGRhdGFTZXQkbGVmdCwgCiAgICAgICAgICAgICAgICAgICAgICAgbGV2ZWxzID0gYygwLCAxKSkKZGF0YVNldCRzYWxhcnkgPC0gZmFjdG9yKGRhdGFTZXQkc2FsYXJ5KQpkYXRhU2V0JHNhbGFyeSA8LSByZWxldmVsKGRhdGFTZXQkc2FsYXJ5LAogICAgICAgICAgICAgICAgICAgICAgICAgIHJlZiA9ICdoaWdoJykKZGF0YVNldCRzYWxlcyA8LSBmYWN0b3IoZGF0YVNldCRzYWxlcykKZGF0YVNldCRzYWxlcyA8LSByZWxldmVsKGRhdGFTZXQkc2FsZXMsCiAgICAgICAgICAgICAgICAgICAgICAgICByZWYgPSAnUmFuZEQnKQojIC0gbW9kZWwKYmxyX21vZGVsMSA8LSBnbG0obGVmdCB+IHNhdGlzZmFjdGlvbl9sZXZlbCArIGxhc3RfZXZhbHVhdGlvbiArIHNhbGVzICsgc2FsYXJ5LAogICAgICAgICAgICAgICAgICBkYXRhID0gZGF0YVNldCwKICAgICAgICAgICAgICAgICAgZmFtaWx5ID0gImJpbm9taWFsIikKbW9kZWxzdW1tYXJ5IDwtIHN1bW1hcnkoYmxyX21vZGVsMSkKcHJpbnQobW9kZWxzdW1tYXJ5KQpgYGAKCkFuZCB0YWtlIGEgbG9vayBhdCB0aGUgcmVncmVzc2lvbiBjb2VmZmljaWVudHM6CgpgYGB7ciBlY2hvID0gVCwgbWVzc2FnZSA9IEYsIHdhcm5pbmcgPSBGfQpleHAoY29lZmZpY2llbnRzKGJscl9tb2RlbDEpKQpgYGAKClRoZSBBa2Fpa2UgSW5mb3JtYXRpb24gQ3JpdGVyaW9uIGlzOgoKYGBge3IgZWNobyA9IFQsIG1lc3NhZ2UgPSBGLCB3YXJuaW5nID0gRn0KYmxyX21vZGVsMSRhaWMKYGBgCgpOb3cgY29uc2lkZXIgYSBtb2RlbCBlbmNvbXBhc3NpbmcgYWxsIHByZWRpY3RvcnMgZnJvbSB0aGUgYEhSX2NvbW1hX3NlcC5jc3ZgOgoKYGBge3IgZWNobyA9IFQsIG1lc3NhZ2UgPSBGLCB3YXJuaW5nID0gRn0KIyAtIHNldHVwcwpkYXRhU2V0JFdvcmtfYWNjaWRlbnQgPC0gZmFjdG9yKGRhdGFTZXQkV29ya19hY2NpZGVudCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBsZXZlbHMgPSBjKDAsIDEpKQpkYXRhU2V0JHByb21vdGlvbl9sYXN0XzV5ZWFycyA8LSBmYWN0b3IoZGF0YVNldCRwcm9tb3Rpb25fbGFzdF81eWVhcnMsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBsZXZlbHMgPSBjKDAsIDEpKQoKIyAtIG1vZGVsCmJscl9tb2RlbDIgPC0gZ2xtKGxlZnQgfiAuLAogICAgICAgICAgICAgICAgICBkYXRhID0gZGF0YVNldCwKICAgICAgICAgICAgICAgICAgZmFtaWx5ID0gImJpbm9taWFsIikKbW9kZWxzdW1tYXJ5IDwtIHN1bW1hcnkoYmxyX21vZGVsMikKcHJpbnQobW9kZWxzdW1tYXJ5KQpgYGAKCmBgYHtyIGVjaG8gPSBULCBtZXNzYWdlID0gRiwgd2FybmluZyA9IEZ9CmV4cChjb2VmZmljaWVudHMoYmxyX21vZGVsMikpCmBgYAoKVGhlIEFrYWlrZSBJbmZvcm1hdGlvbiBDcml0ZXJpb24gaXM6CgpgYGB7ciBlY2hvID0gVCwgbWVzc2FnZSA9IEYsIHdhcm5pbmcgPSBGfQpibHJfbW9kZWwyJGFpYwpgYGAKCkxldCdzIGNyb3NzLXZhbGlkYXRlIG91ciBgYmxyX21vZGVsMWAgYW5kIGBibHJfbW9kZWwyYCBub3cuIFdlIHdpbGwgcGVyZm9ybSB0aGUgKmstZm9sZCogQ1YgaW4gdGhlIGZvbGxvd2luZyB3YXk6CgotIGRlZmluZSBmb3VyIGZvbGRzIGluIHRoZSBkYXRhU2V0IGJ5IHJhbmRvbWx5IGFzc2lnbmluZyBlYWNoIG9ic2VydmF0aW9uIHRvIGZvbGQgMSwgMiwgMywgb3IgNDsKLSBmb3IgZWFjaCBgaWAgaW4gYGZvbGRzYDogZXN0aW1hdGUgdGhlIG1vZGVsIG9uIHRoZSByZW1haW5pbmcgKmRhdGFTZXRbLWksIF0qIGZvbGRzIHRha2VuIHRvZ2V0aGVyCi0gcHJlZGljdCB0aGUgb2JzZXJ2YXRpb25zIGluIGZvbGQgYGlgIGZyb20gdGhlIGZpdHRlZCBtb2RlbCwKLSBjb21wdXRlIG1vZGVsIGFjY3VyYWN5LCBGQSByYXRlLCBIaXQgcmF0ZSBmb3IgZm9sZCBgaWAsCi0gb2JzZXJ2ZSB0aGUgYXZlcmFnZSBhY2N1cmFjeSAoUk9DKSBhY3Jvc3MgYWxsIGZvdXIgZm9sZHMuCgpIZXJlIHdlIGdvOiBkZWZpbmUgZm9sZHMgZmlyc3QuCgpgYGB7ciBlY2hvID0gVCwgbWVzc2FnZSA9IEYsIHdhcm5pbmcgPSBGfQpkYXRhU2V0JGZvbGQgPC0gc2FtcGxlKDE6NCwgc2l6ZSA9IGRpbShkYXRhU2V0KVsxXSwgcmVwbGFjZSA9IFQpCnRhYmxlKGRhdGFTZXQkZm9sZCkKYGBgCgpGaXJzdCBmb3IgdGhlIG5hcnJvd2VyIG1vZGVsOgoKYGBge3IgZWNobyA9IFQsIG1lc3NhZ2UgPSBGLCB3YXJuaW5nID0gRn0KY3YxIDwtIGxhcHBseSgxOjQsIGZ1bmN0aW9uKHgpIHsKICAKICAjIC0gdGVzdCBhbmQgdHJhaW4gZGF0YXNldHMKICB0ZXN0IDwtIGRhdGFTZXQgJT4lIAogICAgZHBseXI6OmZpbHRlcihmb2xkID09IHgpICU+JSAKICAgIGRwbHlyOjpzZWxlY3QoLWZvbGQpCiAgdHJhaW4gPC0gZGF0YVNldCAlPiUgCiAgICBkcGx5cjo6ZmlsdGVyKGZvbGQgIT0geCkgJT4lIAogICAgZHBseXI6OnNlbGVjdCgtZm9sZCkKICAKICAjIC0gbW9kZWwgb24gdGhlIHRyYWluaW5nIGRhdGFzZXQKICBibHJNb2RlbCA8LSBnbG0obGVmdCB+IHNhdGlzZmFjdGlvbl9sZXZlbCArIGxhc3RfZXZhbHVhdGlvbiArIHNhbGVzICsgc2FsYXJ5LAogICAgICAgICAgICAgICAgICBkYXRhID0gdHJhaW4sCiAgICAgICAgICAgICAgICAgIGZhbWlseSA9ICJiaW5vbWlhbCIpCiAgCiAgIyAtIHByZWRpY3Qgb24gdGhlIHRlc3QgZGF0YXNldAogIHByZWRpY3Rpb25zIDwtIHByZWRpY3QoYmxyTW9kZWwsIAogICAgICAgICAgICAgICAgICAgICAgICAgbmV3ZGF0YSA9IHRlc3QsIAogICAgICAgICAgICAgICAgICAgICAgICAgdHlwZSA9ICJyZXNwb25zZSIpCiAgcHJlZGljdGlvbnMgPC0gaWZlbHNlKHByZWRpY3Rpb25zID4gLjUsIDEsIDApCiAgCiAgIyAtIFJPQyBhbmFseXNpcwogIGFjYyA8LSBzdW0odGVzdCRsZWZ0ID09IHByZWRpY3Rpb25zKQogIGFjYyA8LSBhY2MvZGltKHRlc3QpWzFdCiAgaGl0IDwtIHN1bSh0ZXN0JGxlZnQgPT0gMSAmIHByZWRpY3Rpb25zID09IDEpCiAgaGl0IDwtIGhpdC9zdW0odGVzdCRsZWZ0ID09IDEpCiAgZmEgPC0gc3VtKHRlc3QkbGVmdCA9PSAwICYgcHJlZGljdGlvbnMgPT0gMSkKICBmYSA8LSBmYS9zdW0odGVzdCRsZWZ0ID09IDApCiAgcmV0dXJuKGRhdGEuZnJhbWUoYWNjLCBoaXQsIGZhKSkKfSkKCmN2MSA8LSByYmluZGxpc3QoY3YxKQpjdjEkZm9sZCA8LSAxOjQKY3YxIDwtIHRpZHlyOjpwaXZvdF9sb25nZXIoY3YxLAogICAgICAgICAgICAgICAgICAgICAgICAgICBjb2xzID0gLWZvbGQsCiAgICAgICAgICAgICAgICAgICAgICAgICAgIG5hbWVzX3RvID0gJ21lYXN1cmUnLAogICAgICAgICAgICAgICAgICAgICAgICAgICB2YWx1ZXNfdG8gPSAndmFsdWUnKQpjdjEkbW9kZWwgPC0gMQpwcmludChjdjEpCmBgYAoKTm93IGZvciB0aGUgZnVsbCBtb2RlbDoKCmBgYHtyIGVjaG8gPSBULCBtZXNzYWdlID0gRiwgd2FybmluZyA9IEZ9CmN2MiA8LSBsYXBwbHkoMTo0LCBmdW5jdGlvbih4KSB7CiAgdGVzdCA8LSBkYXRhU2V0ICU+JSAKICAgIGRwbHlyOjpmaWx0ZXIoZm9sZCA9PSB4KSAlPiUgCiAgICBkcGx5cjo6c2VsZWN0KC1mb2xkKQogIHRyYWluIDwtIGRhdGFTZXQgJT4lIAogICAgZHBseXI6OmZpbHRlcihmb2xkICE9IHgpICU+JSAKICAgIGRwbHlyOjpzZWxlY3QoLWZvbGQpCiAgYmxyTW9kZWwgPC0gZ2xtKGxlZnQgfiAuLAogICAgICAgICAgICAgICAgICBkYXRhID0gdHJhaW4sCiAgICAgICAgICAgICAgICAgIGZhbWlseSA9ICJiaW5vbWlhbCIpCiAgcHJlZGljdGlvbnMgPC0gcHJlZGljdChibHJNb2RlbCwgCiAgICAgICAgICAgICAgICAgICAgICAgICBuZXdkYXRhID0gdGVzdCwgCiAgICAgICAgICAgICAgICAgICAgICAgICB0eXBlID0gInJlc3BvbnNlIikKICBwcmVkaWN0aW9ucyA8LSBpZmVsc2UocHJlZGljdGlvbnMgPiAuNSwgMSwgMCkKICBhY2MgPC0gc3VtKHRlc3QkbGVmdCA9PSBwcmVkaWN0aW9ucykKICBhY2MgPC0gYWNjL2RpbSh0ZXN0KVsxXQogIGhpdCA8LSBzdW0odGVzdCRsZWZ0ID09IDEgJiBwcmVkaWN0aW9ucyA9PSAxKQogIGhpdCA8LSBoaXQvc3VtKHRlc3QkbGVmdCA9PSAxKQogIGZhIDwtIHN1bSh0ZXN0JGxlZnQgPT0gMCAmIHByZWRpY3Rpb25zID09IDEpCiAgZmEgPC0gZmEvc3VtKHRlc3QkbGVmdCA9PSAwKQogIHJldHVybihkYXRhLmZyYW1lKGFjYywgaGl0LCBmYSkpCn0pCmN2MiA8LSByYmluZGxpc3QoY3YyKQpjdjIkZm9sZCA8LSAxOjQKY3YyIDwtIHRpZHlyOjpwaXZvdF9sb25nZXIoY3YyLAogICAgICAgICAgICAgICAgICAgICAgICAgICBjb2xzID0gLWZvbGQsCiAgICAgICAgICAgICAgICAgICAgICAgICAgIG5hbWVzX3RvID0gJ21lYXN1cmUnLAogICAgICAgICAgICAgICAgICAgICAgICAgICB2YWx1ZXNfdG8gPSAndmFsdWUnKQpjdjIkbW9kZWwgPC0gMgpwcmludChjdjIpCmBgYAoKQ29tcGFyZToKCmBgYHtyIGVjaG8gPSBULCBtZXNzYWdlID0gRiwgd2FybmluZyA9IEYsIGZpZy53aWR0aCA9IDEwLCBmaWcuaGVpZ2h0PTMuNX0KbW9kZWxTZWxlY3Rpb24gPC0gcmJpbmQoY3YxLCBjdjIpCm1vZGVsU2VsZWN0aW9uJG1vZGVsIDwtIGlmZWxzZShtb2RlbFNlbGVjdGlvbiRtb2RlbCA9PSAxLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIlBhcnRpYWwiLCAiRnVsbCIpCm1vZGVsU2VsZWN0aW9uJG1vZGVsIDwtIGZhY3Rvcihtb2RlbFNlbGVjdGlvbiRtb2RlbCkKZ2dwbG90KGRhdGEgPSBtb2RlbFNlbGVjdGlvbiwgCiAgICAgICBhZXMoeCA9IGZvbGQsIAogICAgICAgICAgIHkgPSB2YWx1ZSwgCiAgICAgICAgICAgZ3JvdXAgPSBtb2RlbCwgCiAgICAgICAgICAgY29sb3IgPSBtb2RlbCwgCiAgICAgICAgICAgZmlsbCA9IG1vZGVsKSkgKyAKICBnZW9tX3BhdGgoc2l6ZSA9IC41KSArIAogIGdlb21fcG9pbnQoc2l6ZSA9IDMpICsgCiAgc2NhbGVfY29sb3JfbWFudWFsKHZhbHVlcyA9IGMoJ2RhcmtyZWQnLCAnZGFya29yYW5nZScpKSArIAogIHlsaW0oMCwgMSkgKwogIGZhY2V0X3dyYXAofm1lYXN1cmUpICsgCiAgdGhlbWVfYncoKSArIAogIHRoZW1lKHBhbmVsLmJvcmRlciA9IGVsZW1lbnRfYmxhbmsoKSkgKyAKICB0aGVtZShsZWdlbmQudGV4dCA9IGVsZW1lbnRfdGV4dChzaXplID0gMjApKSArCiAgdGhlbWUobGVnZW5kLnRpdGxlID0gZWxlbWVudF90ZXh0KHNpemUgPSAyMCkpICsKICB0aGVtZShsZWdlbmQucG9zaXRpb24gPSAidG9wIikgKwogIHRoZW1lKHN0cmlwLmJhY2tncm91bmQgPSBlbGVtZW50X2JsYW5rKCkpICsgCiAgdGhlbWUoc3RyaXAudGV4dCA9IGVsZW1lbnRfdGV4dChzaXplID0gMjApKSArCiAgdGhlbWUoYXhpcy50aXRsZS54ID0gZWxlbWVudF90ZXh0KHNpemUgPSAxOCkpICsgCiAgdGhlbWUoYXhpcy50aXRsZS55ID0gZWxlbWVudF90ZXh0KHNpemUgPSAxOCkpICsgCiAgdGhlbWUoYXhpcy50ZXh0LnggPSBlbGVtZW50X3RleHQoc2l6ZSA9IDE3KSkgKyAKICB0aGVtZShheGlzLnRleHQueSA9IGVsZW1lbnRfdGV4dChzaXplID0gMTcpKQpgYGAKClRoZSBhdmVyYWdlIFJPQyBmcm9tIGstZm9sZCBDViBmb3IgYm90aCBtb2RlbHM6CgpgYGB7ciBlY2hvID0gVCwgbWVzc2FnZSA9IEYsIHdhcm5pbmcgPSBGfQptb2RlbFNlbGVjdGlvbiAlPiUKICBkcGx5cjo6c2VsZWN0KC1mb2xkKSAlPiUgCiAgZHBseXI6Omdyb3VwX2J5KG1vZGVsLCBtZWFzdXJlKSAlPiUgCiAgZHBseXI6OnN1bW1hcmlzZShtZWFuID0gcm91bmQobWVhbih2YWx1ZSksIDUpKSAlPiUgCiAgdGlkeXI6OnBpdm90X3dpZGVyKGlkX2NvbHMgPSBtb2RlbCwgCiAgICAgICAgICAgICAgICAgICAgIG5hbWVzX2Zyb20gPSAnbWVhc3VyZScsIAogICAgICAgICAgICAgICAgICAgICB2YWx1ZXNfZnJvbSA9ICdtZWFuJykKYGBgClN1cHBvc2Ugd2Ugc2V0IHRoZSBkZWNpc2lvbiB0cmFzaG9sZCB0byBiZSBgcCA9IC4yYC4gRmlyc3QgZm9yIHRoZSBuYXJyb3cgbW9kZWw6CgpgYGB7ciBlY2hvID0gVCwgbWVzc2FnZSA9IEYsIHdhcm5pbmcgPSBGfQpjdjEgPC0gbGFwcGx5KDE6NCwgZnVuY3Rpb24oeCkgewogIAogICMgLSB0ZXN0IGFuZCB0cmFpbiBkYXRhc2V0cwogIHRlc3QgPC0gZGF0YVNldCAlPiUgCiAgICBkcGx5cjo6ZmlsdGVyKGZvbGQgPT0geCkgJT4lIAogICAgZHBseXI6OnNlbGVjdCgtZm9sZCkKICB0cmFpbiA8LSBkYXRhU2V0ICU+JSAKICAgIGRwbHlyOjpmaWx0ZXIoZm9sZCAhPSB4KSAlPiUgCiAgICBkcGx5cjo6c2VsZWN0KC1mb2xkKQogIAogICMgLSBtb2RlbCBvbiB0aGUgdHJhaW5pbmcgZGF0YXNldAogIGJsck1vZGVsIDwtIGdsbShsZWZ0IH4gc2F0aXNmYWN0aW9uX2xldmVsICsgbGFzdF9ldmFsdWF0aW9uICsgc2FsZXMgKyBzYWxhcnksCiAgICAgICAgICAgICAgICAgIGRhdGEgPSB0cmFpbiwKICAgICAgICAgICAgICAgICAgZmFtaWx5ID0gImJpbm9taWFsIikKICAKICAjIC0gcHJlZGljdCBvbiB0aGUgdGVzdCBkYXRhc2V0CiAgcHJlZGljdGlvbnMgPC0gcHJlZGljdChibHJNb2RlbCwgCiAgICAgICAgICAgICAgICAgICAgICAgICBuZXdkYXRhID0gdGVzdCwgCiAgICAgICAgICAgICAgICAgICAgICAgICB0eXBlID0gInJlc3BvbnNlIikKICBwcmVkaWN0aW9ucyA8LSBpZmVsc2UocHJlZGljdGlvbnMgPiAuMiwgMSwgMCkKICAKICAjIC0gUk9DIGFuYWx5c2lzCiAgYWNjIDwtIHN1bSh0ZXN0JGxlZnQgPT0gcHJlZGljdGlvbnMpCiAgYWNjIDwtIGFjYy9kaW0odGVzdClbMV0KICBoaXQgPC0gc3VtKHRlc3QkbGVmdCA9PSAxICYgcHJlZGljdGlvbnMgPT0gMSkKICBoaXQgPC0gaGl0L3N1bSh0ZXN0JGxlZnQgPT0gMSkKICBmYSA8LSBzdW0odGVzdCRsZWZ0ID09IDAgJiBwcmVkaWN0aW9ucyA9PSAxKQogIGZhIDwtIGZhL3N1bSh0ZXN0JGxlZnQgPT0gMCkKICByZXR1cm4oZGF0YS5mcmFtZShhY2MsIGhpdCwgZmEpKQp9KQoKY3YxIDwtIHJiaW5kbGlzdChjdjEpCmN2MSRmb2xkIDwtIDE6NApjdjEgPC0gdGlkeXI6OnBpdm90X2xvbmdlcihjdjEsCiAgICAgICAgICAgICAgICAgICAgICAgICAgIGNvbHMgPSAtZm9sZCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgbmFtZXNfdG8gPSAnbWVhc3VyZScsCiAgICAgICAgICAgICAgICAgICAgICAgICAgIHZhbHVlc190byA9ICd2YWx1ZScpCmN2MSRtb2RlbCA8LSAxCmBgYAoKTm93IGZvciB0aGUgZnVsbCBtb2RlbCwgZGVjaXNpb24gdHJlc2hvbGQgaXMgYHAgPSAuMmA6CgpgYGB7ciBlY2hvID0gVCwgbWVzc2FnZSA9IEYsIHdhcm5pbmcgPSBGfQpjdjIgPC0gbGFwcGx5KDE6NCwgZnVuY3Rpb24oeCkgewogIHRlc3QgPC0gZGF0YVNldCAlPiUgCiAgICBkcGx5cjo6ZmlsdGVyKGZvbGQgPT0geCkgJT4lIAogICAgZHBseXI6OnNlbGVjdCgtZm9sZCkKICB0cmFpbiA8LSBkYXRhU2V0ICU+JSAKICAgIGRwbHlyOjpmaWx0ZXIoZm9sZCAhPSB4KSAlPiUgCiAgICBkcGx5cjo6c2VsZWN0KC1mb2xkKQogIGJsck1vZGVsIDwtIGdsbShsZWZ0IH4gLiwKICAgICAgICAgICAgICAgICAgZGF0YSA9IHRyYWluLAogICAgICAgICAgICAgICAgICBmYW1pbHkgPSAiYmlub21pYWwiKQogIHByZWRpY3Rpb25zIDwtIHByZWRpY3QoYmxyTW9kZWwsIAogICAgICAgICAgICAgICAgICAgICAgICAgbmV3ZGF0YSA9IHRlc3QsIAogICAgICAgICAgICAgICAgICAgICAgICAgdHlwZSA9ICJyZXNwb25zZSIpCiAgcHJlZGljdGlvbnMgPC0gaWZlbHNlKHByZWRpY3Rpb25zID4gLjIsIDEsIDApCiAgYWNjIDwtIHN1bSh0ZXN0JGxlZnQgPT0gcHJlZGljdGlvbnMpCiAgYWNjIDwtIGFjYy9kaW0odGVzdClbMV0KICBoaXQgPC0gc3VtKHRlc3QkbGVmdCA9PSAxICYgcHJlZGljdGlvbnMgPT0gMSkKICBoaXQgPC0gaGl0L3N1bSh0ZXN0JGxlZnQgPT0gMSkKICBmYSA8LSBzdW0odGVzdCRsZWZ0ID09IDAgJiBwcmVkaWN0aW9ucyA9PSAxKQogIGZhIDwtIGZhL3N1bSh0ZXN0JGxlZnQgPT0gMCkKICByZXR1cm4oZGF0YS5mcmFtZShhY2MsIGhpdCwgZmEpKQp9KQpjdjIgPC0gcmJpbmRsaXN0KGN2MikKY3YyJGZvbGQgPC0gMTo0CmN2MiA8LSB0aWR5cjo6cGl2b3RfbG9uZ2VyKGN2MiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgY29scyA9IC1mb2xkLAogICAgICAgICAgICAgICAgICAgICAgICAgICBuYW1lc190byA9ICdtZWFzdXJlJywKICAgICAgICAgICAgICAgICAgICAgICAgICAgdmFsdWVzX3RvID0gJ3ZhbHVlJykKY3YyJG1vZGVsIDwtIDIKYGBgCgpDb21wYXJlOgoKYGBge3IgZWNobyA9IFQsIG1lc3NhZ2UgPSBGLCB3YXJuaW5nID0gRiwgZmlnLndpZHRoID0gMTAsIGZpZy5oZWlnaHQ9My41fQptb2RlbFNlbGVjdGlvbiA8LSByYmluZChjdjEsIGN2MikKbW9kZWxTZWxlY3Rpb24kbW9kZWwgPC0gaWZlbHNlKG1vZGVsU2VsZWN0aW9uJG1vZGVsID09IDEsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiUGFydGlhbCIsICJGdWxsIikKbW9kZWxTZWxlY3Rpb24kbW9kZWwgPC0gZmFjdG9yKG1vZGVsU2VsZWN0aW9uJG1vZGVsKQpnZ3Bsb3QoZGF0YSA9IG1vZGVsU2VsZWN0aW9uLCAKICAgICAgIGFlcyh4ID0gZm9sZCwgCiAgICAgICAgICAgeSA9IHZhbHVlLCAKICAgICAgICAgICBncm91cCA9IG1vZGVsLCAKICAgICAgICAgICBjb2xvciA9IG1vZGVsLCAKICAgICAgICAgICBmaWxsID0gbW9kZWwpKSArIAogIGdlb21fcGF0aChzaXplID0gLjUpICsgCiAgZ2VvbV9wb2ludChzaXplID0gMykgKyAKICBzY2FsZV9jb2xvcl9tYW51YWwodmFsdWVzID0gYygnZGFya3JlZCcsICdkYXJrb3JhbmdlJykpICsgCiAgeWxpbSgwLCAxKSArCiAgZmFjZXRfd3JhcCh+bWVhc3VyZSkgKyAKICB0aGVtZV9idygpICsgCiAgdGhlbWUocGFuZWwuYm9yZGVyID0gZWxlbWVudF9ibGFuaygpKSArIAogIHRoZW1lKGxlZ2VuZC50ZXh0ID0gZWxlbWVudF90ZXh0KHNpemUgPSAyMCkpICsKICB0aGVtZShsZWdlbmQudGl0bGUgPSBlbGVtZW50X3RleHQoc2l6ZSA9IDIwKSkgKwogIHRoZW1lKGxlZ2VuZC5wb3NpdGlvbiA9ICJ0b3AiKSArCiAgdGhlbWUoc3RyaXAuYmFja2dyb3VuZCA9IGVsZW1lbnRfYmxhbmsoKSkgKyAKICB0aGVtZShzdHJpcC50ZXh0ID0gZWxlbWVudF90ZXh0KHNpemUgPSAyMCkpICsKICB0aGVtZShheGlzLnRpdGxlLnggPSBlbGVtZW50X3RleHQoc2l6ZSA9IDE4KSkgKyAKICB0aGVtZShheGlzLnRpdGxlLnkgPSBlbGVtZW50X3RleHQoc2l6ZSA9IDE4KSkgKyAKICB0aGVtZShheGlzLnRleHQueCA9IGVsZW1lbnRfdGV4dChzaXplID0gMTcpKSArIAogIHRoZW1lKGF4aXMudGV4dC55ID0gZWxlbWVudF90ZXh0KHNpemUgPSAxNykpCmBgYAoKQWNyb3NzIGEgcmFuZ2Ugb2YgZGVjaXNpb24gY3JpdGVyaWEsIGBkZWNfY3JpdGVyaW9uIDwtIHNlcSguMDEsIC45OSwgLjAxKWAsIGluaXRpYWwgbW9kZWwgd2l0aCBmb3VyIHByZWRpY3RvcnM6CgpgYGB7ciBlY2hvID0gVCwgbWVzc2FnZSA9IEYsIHdhcm5pbmcgPSBGfQpjdjEgPC0gbGFwcGx5KDE6NCwgZnVuY3Rpb24oeCkgewogIAogICMgLSB0ZXN0IGFuZCB0cmFpbiBkYXRhc2V0cwogIHRlc3QgPC0gZGF0YVNldCAlPiUgCiAgICBkcGx5cjo6ZmlsdGVyKGZvbGQgPT0geCkgJT4lIAogICAgZHBseXI6OnNlbGVjdCgtZm9sZCkKICB0cmFpbiA8LSBkYXRhU2V0ICU+JSAKICAgIGRwbHlyOjpmaWx0ZXIoZm9sZCAhPSB4KSAlPiUgCiAgICBkcGx5cjo6c2VsZWN0KC1mb2xkKQogIAogICMgLSBtb2RlbCBvbiB0aGUgdHJhaW5pbmcgZGF0YXNldAogIGJsck1vZGVsIDwtIGdsbShsZWZ0IH4gc2F0aXNmYWN0aW9uX2xldmVsICsgbGFzdF9ldmFsdWF0aW9uICsgc2FsZXMgKyBzYWxhcnksCiAgICAgICAgICAgICAgICAgIGRhdGEgPSB0cmFpbiwKICAgICAgICAgICAgICAgICAgZmFtaWx5ID0gImJpbm9taWFsIikKICAKICAjIC0gcHJlZGljdCBvbiB0aGUgdGVzdCBkYXRhc2V0CiAgcHJlZGljdGlvbnMgPC0gcHJlZGljdChibHJNb2RlbCwgCiAgICAgICAgICAgICAgICAgICAgICAgICBuZXdkYXRhID0gdGVzdCwgCiAgICAgICAgICAgICAgICAgICAgICAgICB0eXBlID0gInJlc3BvbnNlIikKICBkZWNfY3JpdGVyaW9uIDwtIHNlcSguMDEsIC45OSwgLjAxKQogIHByZWRpY3Rpb25zIDwtIGxhcHBseShkZWNfY3JpdGVyaW9uLCBmdW5jdGlvbih5KSB7CiAgICByZXR1cm4oCiAgICAgIGlmZWxzZShwcmVkaWN0aW9ucyA+IHksIDEsIDApCiAgICApICAKICB9KQogIHByZWRpY3Rpb25zIDwtIHQoUmVkdWNlKHJiaW5kLCBwcmVkaWN0aW9ucykpCiAgcm9jIDwtIGFwcGx5KHByZWRpY3Rpb25zLCAyLCBmdW5jdGlvbih5KSB7CiAgICAjIC0gUk9DIGFuYWx5c2lzCiAgICBhY2MgPC0gc3VtKHRlc3QkbGVmdCA9PSB5KQogICAgYWNjIDwtIGFjYy9kaW0odGVzdClbMV0KICAgIGhpdCA8LSBzdW0odGVzdCRsZWZ0ID09IDEgJiB5ID09IDEpCiAgICBoaXQgPC0gaGl0L3N1bSh0ZXN0JGxlZnQgPT0gMSkKICAgIGZhIDwtIHN1bSh0ZXN0JGxlZnQgPT0gMCAmIHkgPT0gMSkKICAgIGZhIDwtIGZhL3N1bSh0ZXN0JGxlZnQgPT0gMCkKICAgIHJldHVybihkYXRhLmZyYW1lKGFjYywgaGl0LCBmYSkpCiAgfSkKICByb2MgPC0gcmJpbmRsaXN0KHJvYykKICByb2MkZGVjX2NyaXRlcmlvbiA8LSBkZWNfY3JpdGVyaW9uCiAgcm9jJGZvbGQgPC0geAogIHJldHVybihyb2MpCn0pCgpjdjEgPC0gcmJpbmRsaXN0KGN2MSkKY3YxIDwtIGN2MSAlPiUgCiAgZHBseXI6Omdyb3VwX2J5KGRlY19jcml0ZXJpb24pICU+JQogIGRwbHlyOjpzdW1tYXJpc2UoYWNjID0gbWVhbihhY2MpLAogICAgICAgICAgICAgICAgICAgaGl0ID0gbWVhbihoaXQpLAogICAgICAgICAgICAgICAgICAgZmEgPSBtZWFuKGZhKSkKY3YxIDwtIHRpZHlyOjpwaXZvdF9sb25nZXIoY3YxLAogICAgICAgICAgICAgICAgICAgICAgICAgICBjb2xzID0gLWRlY19jcml0ZXJpb24sCiAgICAgICAgICAgICAgICAgICAgICAgICAgIG5hbWVzX3RvID0gJ21lYXN1cmUnLAogICAgICAgICAgICAgICAgICAgICAgICAgICB2YWx1ZXNfdG8gPSAndmFsdWUnKQpjdjEkbW9kZWwgPC0gMQpgYGAKCkZvciB0aGUgZnVsbCBtb2RlbCwgYGRlY19jcml0ZXJpb24gPC0gc2VxKC4wMSwgLjk5LCAuMDEpYDoKCmBgYHtyIGVjaG8gPSBULCBtZXNzYWdlID0gRiwgd2FybmluZyA9IEZ9CmN2MiA8LSBsYXBwbHkoMTo0LCBmdW5jdGlvbih4KSB7CiAgCiAgIyAtIHRlc3QgYW5kIHRyYWluIGRhdGFzZXRzCiAgdGVzdCA8LSBkYXRhU2V0ICU+JSAKICAgIGRwbHlyOjpmaWx0ZXIoZm9sZCA9PSB4KSAlPiUgCiAgICBkcGx5cjo6c2VsZWN0KC1mb2xkKQogIHRyYWluIDwtIGRhdGFTZXQgJT4lIAogICAgZHBseXI6OmZpbHRlcihmb2xkICE9IHgpICU+JSAKICAgIGRwbHlyOjpzZWxlY3QoLWZvbGQpCiAgCiAgIyAtIG1vZGVsIG9uIHRoZSB0cmFpbmluZyBkYXRhc2V0CiAgYmxyTW9kZWwgPC0gZ2xtKGxlZnQgfiAuLAogICAgICAgICAgICAgICAgICBkYXRhID0gdHJhaW4sCiAgICAgICAgICAgICAgICAgIGZhbWlseSA9ICJiaW5vbWlhbCIpCiAgCiAgIyAtIHByZWRpY3Qgb24gdGhlIHRlc3QgZGF0YXNldAogIHByZWRpY3Rpb25zIDwtIHByZWRpY3QoYmxyTW9kZWwsIAogICAgICAgICAgICAgICAgICAgICAgICAgbmV3ZGF0YSA9IHRlc3QsIAogICAgICAgICAgICAgICAgICAgICAgICAgdHlwZSA9ICJyZXNwb25zZSIpCiAgZGVjX2NyaXRlcmlvbiA8LSBzZXEoLjAxLCAuOTksIC4wMSkKICBwcmVkaWN0aW9ucyA8LSBsYXBwbHkoZGVjX2NyaXRlcmlvbiwgZnVuY3Rpb24oeSkgewogICAgcmV0dXJuKAogICAgICBpZmVsc2UocHJlZGljdGlvbnMgPiB5LCAxLCAwKQogICAgKSAgCiAgfSkKICBwcmVkaWN0aW9ucyA8LSB0KFJlZHVjZShyYmluZCwgcHJlZGljdGlvbnMpKQogIHJvYyA8LSBhcHBseShwcmVkaWN0aW9ucywgMiwgZnVuY3Rpb24oeSkgewogICAgIyAtIFJPQyBhbmFseXNpcwogICAgYWNjIDwtIHN1bSh0ZXN0JGxlZnQgPT0geSkKICAgIGFjYyA8LSBhY2MvZGltKHRlc3QpWzFdCiAgICBoaXQgPC0gc3VtKHRlc3QkbGVmdCA9PSAxICYgeSA9PSAxKQogICAgaGl0IDwtIGhpdC9zdW0odGVzdCRsZWZ0ID09IDEpCiAgICBmYSA8LSBzdW0odGVzdCRsZWZ0ID09IDAgJiB5ID09IDEpCiAgICBmYSA8LSBmYS9zdW0odGVzdCRsZWZ0ID09IDApCiAgICByZXR1cm4oZGF0YS5mcmFtZShhY2MsIGhpdCwgZmEpKQogIH0pCiAgcm9jIDwtIHJiaW5kbGlzdChyb2MpCiAgcm9jJGRlY19jcml0ZXJpb24gPC0gZGVjX2NyaXRlcmlvbgogIHJvYyRmb2xkIDwtIHgKICByZXR1cm4ocm9jKQp9KQoKY3YyIDwtIHJiaW5kbGlzdChjdjIpCmN2MiA8LSBjdjIgJT4lIAogIGRwbHlyOjpncm91cF9ieShkZWNfY3JpdGVyaW9uKSAlPiUKICBkcGx5cjo6c3VtbWFyaXNlKGFjYyA9IG1lYW4oYWNjKSwKICAgICAgICAgICAgICAgICAgIGhpdCA9IG1lYW4oaGl0KSwKICAgICAgICAgICAgICAgICAgIGZhID0gbWVhbihmYSkpCmN2MiA8LSB0aWR5cjo6cGl2b3RfbG9uZ2VyKGN2MiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgY29scyA9IC1kZWNfY3JpdGVyaW9uLAogICAgICAgICAgICAgICAgICAgICAgICAgICBuYW1lc190byA9ICdtZWFzdXJlJywKICAgICAgICAgICAgICAgICAgICAgICAgICAgdmFsdWVzX3RvID0gJ3ZhbHVlJykKY3YyJG1vZGVsIDwtIDIKYGBgCgpDb21wYXJlIFJPQyBjdXJ2ZXM6CgpgYGB7ciBlY2hvID0gVCwgbWVzc2FnZSA9IEYsIHdhcm5pbmcgPSBGLCBmaWcud2lkdGggPSA0LCBmaWcuaGVpZ2h0PTN9ClJPQ19yZXN1bHRzIDwtIHJiaW5kKGN2MSwgY3YyKQpST0NfcmVzdWx0cyRtb2RlbCA8LSBpZmVsc2UoUk9DX3Jlc3VsdHMkbW9kZWwgPT0gMSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJQYXJ0aWFsIiwgIkZ1bGwiKQpST0NfcmVzdWx0cyA8LSBST0NfcmVzdWx0cyAlPiUgCiAgcGl2b3Rfd2lkZXIoaWRfY29scyA9IGMoJ2RlY19jcml0ZXJpb24nLCAnbW9kZWwnKSwgCiAgICAgICAgICAgICAgbmFtZXNfZnJvbSA9IG1lYXN1cmUsCiAgICAgICAgICAgICAgdmFsdWVzX2Zyb20gPSB2YWx1ZSkKClJPQ19yZXN1bHRzJG1vZGVsIDwtIGZhY3RvcihST0NfcmVzdWx0cyRtb2RlbCkKCmdncGxvdChkYXRhID0gUk9DX3Jlc3VsdHMsIAogICAgICAgYWVzKHggPSBmYSwgCiAgICAgICAgICAgeSA9IGhpdCwgCiAgICAgICAgICAgZ3JvdXAgPSBtb2RlbCwKICAgICAgICAgICBjb2xvciA9IG1vZGVsLCAKICAgICAgICAgICBmaWxsID0gbW9kZWwpKSArCiAgeWxhYigiSGl0IFJhdGUgKFRQUikiKSArIAogIHhsYWIoIkZBIFJhdGUgKEZQUikiKSArCiAgZ2VvbV9wb2ludChzaXplID0gMSkgKyBnZW9tX3BhdGgoc2l6ZSA9IC4xKSArIAogIGdlb21fYWJsaW5lKGludGVyY2VwdCA9IDAsIHNsb3BlID0gMSwgc2l6ZSA9IC41KSArIAogIGdndGl0bGUoIlJPQyBhbmFseXNpcyBmb3IgdGhlIEJpbm9taWFsIFJlZ3Jlc3Npb24gTW9kZWwiKSArCiAgdGhlbWVfYncoKSArIAogIHRoZW1lKHBsb3QudGl0bGUgPSBlbGVtZW50X3RleHQoaGp1c3QgPSAuNSkpICsgCiAgdGhlbWVfYncoKSArIAogIHRoZW1lKHBhbmVsLmJvcmRlciA9IGVsZW1lbnRfYmxhbmsoKSkgKyAKICB0aGVtZShwbG90LnRpdGxlID0gZWxlbWVudF90ZXh0KGhqdXN0ID0gLjUsIHNpemUgPSAyMCkpICsgCiAgdGhlbWUobGVnZW5kLnRleHQgPSBlbGVtZW50X3RleHQoc2l6ZSA9IDIwKSkgKwogIHRoZW1lKGxlZ2VuZC50aXRsZSA9IGVsZW1lbnRfdGV4dChzaXplID0gMjApKSArCiAgdGhlbWUobGVnZW5kLnBvc2l0aW9uID0gInRvcCIpICsKICB0aGVtZShheGlzLnRpdGxlLnggPSBlbGVtZW50X3RleHQoc2l6ZSA9IDE4KSkgKyAKICB0aGVtZShheGlzLnRpdGxlLnkgPSBlbGVtZW50X3RleHQoc2l6ZSA9IDE4KSkgKyAKICB0aGVtZShheGlzLnRleHQueCA9IGVsZW1lbnRfdGV4dChzaXplID0gMTcpKSArIAogIHRoZW1lKGF4aXMudGV4dC55ID0gZWxlbWVudF90ZXh0KHNpemUgPSAxNykpCmBgYAoKIyMjIDIuIERlY2lzaW9uIFRyZWVzIGZvciBDbGFzc2lmaWNhdGlvbiBQcm9ibGVtcwoKV2hhdCBpcyBhIERlY2lzaW9uIFRyZWUgY2xhc3NpZmllcj8gTGV0J3MgaW50cm9kdWNlIHRoZSBEZWNpc2lvbiBUcmVlIGJ5IGFuIGV4YW1wbGUgYmVmb3JlIGRpdmluZyBpbnRvIHRoZW9yeSBpbiB0aGUgbmV4dCBzZXNzaW9uLiBXZSB3aWxsIHVzZSB0aGUgYEhSX2NvbW1hX3NlcC5jc3ZgIGRhdGFzZXQgYWdhaW46CgpgYGB7ciBlY2hvID0gVCwgbWVzc2FnZSA9IEYsIHdhcm5pbmcgPSBGfQojIC0gbG9hZCBIUl9jb21tYV9zZXAuY3N2IGFnYWluCmRhdGFTZXQgPC0gcmVhZC5jc3YocGFzdGUwKCdfZGF0YS8nLCAnSFJfY29tbWFfc2VwLmNzdicpLCAKICAgICAgICAgICAgICAgICAgICBoZWFkZXIgPSBULCAKICAgICAgICAgICAgICAgICAgICBjaGVjay5uYW1lcyA9IEYsCiAgICAgICAgICAgICAgICAgICAgc3RyaW5nc0FzRmFjdG9ycyA9IEYpCmBgYAoKTGV0J3Mgc3BsaXQgYGRhdGFTZXRgIGludG8gYSAqdHJhaW5pbmcqIGFuZCAqdGVzdCogc3Vic2V0czoKCmBgYHtyIGVjaG8gPSBULCBtZXNzYWdlID0gRiwgd2FybmluZyA9IEZ9CiMgLSBUZXN0IGFuZCBUcmFpbiBkYXRhOgppeCA8LSByYmlub20oZGltKGRhdGFTZXQpWzFdICwgMSwgLjUpCnRhYmxlKGl4KS9zdW0odGFibGUoaXgpKQpgYGAKCmBgYHtyIGVjaG8gPSBULCBtZXNzYWdlID0gRiwgd2FybmluZyA9IEZ9CnRyYWluIDwtIGRhdGFTZXRbaXggPT0gMSwgXQp0ZXN0IDwtIGRhdGFTZXRbaXggPT0gMCxdCmBgYAoKVHJhaW4gb25lIERlY2lzaW9uIFRyZWUgb24gYHRyYWluYDoKCmBgYHtyIGVjaG8gPSBULCBtZXNzYWdlID0gRiwgd2FybmluZyA9IEZ9CiMgLSBCYXNlIE1vZGVsCmNsYXNzVHJlZSA8LSBycGFydChsZWZ0IH4gLiwgCiAgICAgICAgICAgICAgICAgICBkYXRhID0gdHJhaW4sIAogICAgICAgICAgICAgICAgICAgbWV0aG9kID0gImNsYXNzIikKYGBgCgpWaXN1YWxpemUgdGhlIG1vZGVsIHdpdGggYHBycCgpYDoKCmBgYHtyIGVjaG8gPSBULCBtZXNzYWdlID0gRiwgd2FybmluZyA9IEZ9CnBycChjbGFzc1RyZWUsIAogICAgY2V4ID0gLjgpCmBgYApEZWNpc2lvbiBUcmVlcyBjYW4gZWFzaWx5ICpvdmVyZml0KiBiZWNhdXNlIG9mIHRoZSBpbnRyaW5zaW5jIGNvbXBsZXhpdHkgb2YgdGhlIG1vZGVsLiAqUHJ1bmluZyogaXMgb25lIG9mIHRoZSBtZXRob2RzIHRvIHByZXZlbnQgdGhlIERlY2lzaW9uIFRyZWUgZm9yIG92ZXJmaXR0aW5nOiB3ZSAqcHJ1bmUqIHRoZSB0cmVlIGJ5IHJlbHlpbmcgb24gdGhlICpjb21wbGV4aXR5IHBhcmFtZXRlciAoY3ApKiB0byBkaXNjYXJkIHRoZSBicmFuY2hlcyB0aGF0IHdlcmUgZGV2ZWxvcGVkIHRvIGZpdCBwb3RlbnRpYWxseSBpZGlvc3luY3JhdGljIGluZm9ybWF0aW9uIHByZXNlbnQgaW4gdGhlIGRhdGEuIFRoZSBDUCAoY29tcGxleGl0eSBwYXJhbWV0ZXIpIGlzIHVzZWQgdG8gY29udHJvbCB0cmVlIGdyb3d0aDogaWYgdGhlICpjb3N0IG9mIGFkZGluZyBhIHZhcmlhYmxlKiBpcyBoaWdoZXIgdGhlbiB0aGUgdmFsdWUgb2YgQ1AgdGhlbiB0cmVlIGdyb3d0aCBzdG9wcy4KClRoZSBDUCBwYXJhbWV0ZXJzIGhhcyB0byBkbyB3aXRoICphbiBpbnRlcm5hbCBjcm9zcy12YWxpZGF0aW9uIHByb2NlZHVyZSogcGVyZm9ybWVkIGJ5IHtScGFydH0gZHVyaW5nIHRoZSB0cmFpbmluZyBvZiBhIERlY2lzaW9uIFRyZWUgbW9kZWwgKHRvIGJlIGV4cGxhaW5lZCBpbiBvdXIgbGl2ZSBzZXNzaW9uKS4KCmBgYHtyIGVjaG8gPSBULCBtZXNzYWdlID0gRiwgd2FybmluZyA9IEZ9CiMgLSBCYXNlIE1vZGVsCmNsYXNzVHJlZSA8LSBycGFydChsZWZ0IH4gLiwgCiAgICAgICAgICAgICAgICAgICBkYXRhID0gdHJhaW4sIAogICAgICAgICAgICAgICAgICAgbWV0aG9kID0gImNsYXNzIiwKICAgICAgICAgICAgICAgICAgIGNvbnRyb2wgPSBycGFydC5jb250cm9sKGNwID0gMCkpCiMgLSBJbnNwZWN0IG1vZGVsOgpwcnAoY2xhc3NUcmVlLCAKICAgIGNleCA9IC44KQpgYGAKCgpgYGB7ciBlY2hvID0gVCwgbWVzc2FnZSA9IEYsIHdhcm5pbmcgPSBGfQojIC0gRXhhbWluZSB0aGUgY29tcGxleGl0eSBwbG90CmNwdGFibGUgPC0gYXMuZGF0YS5mcmFtZShjbGFzc1RyZWUkY3B0YWJsZSkKcHJpbnQoY3B0YWJsZSkKYGBgCgpgYGB7ciBlY2hvID0gVCwgbWVzc2FnZSA9IEYsIHdhcm5pbmcgPSBGfQpwbG90Y3AoY2xhc3NUcmVlKQpgYGAKVGhlIG9uZSB3aXRoIGxlYXN0ICoqY3Jvc3MtdmFsaWRhdGVkIGVycm9yICh4ZXJyb3IpKiogaXMgdGhlIG9wdGltYWwgdmFsdWUgb2YgQ1AuCgpgYGB7ciBlY2hvID0gVCwgbWVzc2FnZSA9IEYsIHdhcm5pbmcgPSBGfQpjcHRhYmxlW3doaWNoLm1pbihjcHRhYmxlJHhlcnJvciksIF0KYGBgCgpST0MgYW5hbHlzaXMgZm9yIHRoZSBiYXNlIG1vZGVsOgoKYGBge3IgZWNobyA9IFQsIG1lc3NhZ2UgPSBGLCB3YXJuaW5nID0gRn0KIyAtIEJhc2UgTW9kZWwgQWNjdXJhY3kKdGVzdCRwcmVkIDwtIHByZWRpY3QoY2xhc3NUcmVlLAogICAgICAgICAgICAgICAgICAgICB0ZXN0LAogICAgICAgICAgICAgICAgICAgICB0eXBlID0gImNsYXNzIikKIyAtIHNpbGx5LCBidXQgSSBuZWVkIHRvIGRvIHRoaXMuLi4KdGVzdCRwcmVkIDwtIGFzLm51bWVyaWMoYXMuY2hhcmFjdGVyKHRlc3QkcHJlZCkpCmJhc2VfYWNjdXJhY3kgPC0gbWVhbih0ZXN0JHByZWQgPT0gdGVzdCRsZWZ0KQpwcmludChwYXN0ZTAoIkJhc2UgbW9kZWwgYWNjOiAiLCBiYXNlX2FjY3VyYWN5KSkKYGBgCgpgYGB7ciBlY2hvID0gVCwgbWVzc2FnZSA9IEYsIHdhcm5pbmcgPSBGfQojIC0gQmFzZSBNb2RlbCBST0MKdGVzdCRoaXQgPC0gaWZlbHNlKHRlc3QkcHJlZCA9PSAxICYgdGVzdCRsZWZ0ID09IDEsIFQsIEYpCnRlc3QkRkEgPC0gaWZlbHNlKHRlc3QkcHJlZCA9PSAxICYgdGVzdCRsZWZ0ID09IDAsIFQsIEYpCmhpdFJhdGUgPC0gc3VtKHRlc3QkaGl0KS9sZW5ndGgodGVzdCRoaXQpCnByaW50KHBhc3RlMCgiQmFzZSBtb2RlbCBIaXQgcmF0ZTogIiwgaGl0UmF0ZSkpCmBgYAoKYGBge3IgZWNobyA9IFQsIG1lc3NhZ2UgPSBGLCB3YXJuaW5nID0gRn0KRkFSYXRlIDwtIHN1bSh0ZXN0JEZBKS9sZW5ndGgodGVzdCRGQSkKcHJpbnQocGFzdGUwKCJCYXNlIG1vZGVsIEZBIHJhdGU6ICIsIEZBUmF0ZSkpCmBgYAoKYGBge3IgZWNobyA9IFQsIG1lc3NhZ2UgPSBGLCB3YXJuaW5nID0gRn0KdGVzdCRtaXNzIDwtIGlmZWxzZSh0ZXN0JHByZWQgPT0gMCAmIHRlc3QkbGVmdCA9PSAxLCBULCBGKQptaXNzUmF0ZSA8LSBzdW0odGVzdCRtaXNzKS9sZW5ndGgodGVzdCRtaXNzKQpwcmludChwYXN0ZTAoIkJhc2UgbW9kZWwgTWlzcyByYXRlOiAiLCBtaXNzUmF0ZSkpCmBgYAoKYGBge3IgZWNobyA9IFQsIG1lc3NhZ2UgPSBGLCB3YXJuaW5nID0gRn0KIyAtIFBydW5lIHRoZSBjbGFzc1RyZWUgYmFzZWQgb24gdGhlIG9wdGltYWwgY3AgdmFsdWUKb3B0aW1hbF9jcCA8LSBjcHRhYmxlJENQW3doaWNoLm1pbihjcHRhYmxlJHhlcnJvcildCmNsYXNzVHJlZV9wcnVubmVkIDwtIHBydW5lKGNsYXNzVHJlZSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgIGNwID0gb3B0aW1hbF9jcCkKYGBgCgpgYGB7ciBlY2hvID0gVCwgbWVzc2FnZSA9IEYsIHdhcm5pbmcgPSBGfQpwcnAoY2xhc3NUcmVlX3BydW5uZWQsIAogICAgY2V4ID0gLjc1KQpgYGAKCmBgYHtyIGVjaG8gPSBULCBtZXNzYWdlID0gRiwgd2FybmluZyA9IEZ9CiMgLSBUaGUgYWNjdXJhY3kgb2YgdGhlIHBydW5lZCB0cmVlCnRlc3QkcHJlZCA8LSBwcmVkaWN0KGNsYXNzVHJlZV9wcnVubmVkLCAKICAgICAgICAgICAgICAgICAgICAgdGVzdCwgCiAgICAgICAgICAgICAgICAgICAgIHR5cGUgPSAiY2xhc3MiKQphY2N1cmFjeV9wb3N0cHJ1biA8LSBtZWFuKHRlc3QkcHJlZCA9PSB0ZXN0JGxlZnQpCnByaW50KHBhc3RlMCgiUHJ1bmVkIG1vZGVsIGFjYzogIiwgYWNjdXJhY3lfcG9zdHBydW4pKQpgYGAKCmBgYHtyIGVjaG8gPSBULCBtZXNzYWdlID0gRiwgd2FybmluZyA9IEZ9CiMgLSBQcnVuZWQgTW9kZWwgUk9DCnRlc3QkaGl0IDwtIGlmZWxzZSh0ZXN0JHByZWQgPT0gMSAmIHRlc3QkbGVmdCA9PSAxLCBULCBGKQp0ZXN0JEZBIDwtIGlmZWxzZSh0ZXN0JHByZWQgPT0gMSAmIHRlc3QkbGVmdCA9PSAwLCBULCBGKQpoaXRSYXRlIDwtIHN1bSh0ZXN0JGhpdCkvbGVuZ3RoKHRlc3QkaGl0KQpwcmludChwYXN0ZTAoIlBydW5lZCBIaXQgcmF0ZTogIiwgaGl0UmF0ZSkpCmBgYAoKYGBge3IgZWNobyA9IFQsIG1lc3NhZ2UgPSBGLCB3YXJuaW5nID0gRn0KRkFSYXRlIDwtIHN1bSh0ZXN0JEZBKS9sZW5ndGgodGVzdCRGQSkKcHJpbnQocGFzdGUwKCJQcnVuZWQgRkEgcmF0ZTogIiwgRkFSYXRlKSkKYGBgCgpgYGB7ciBlY2hvID0gVCwgbWVzc2FnZSA9IEYsIHdhcm5pbmcgPSBGfQp0ZXN0JG1pc3MgPC0gaWZlbHNlKHRlc3QkcHJlZCA9PSAwICYgdGVzdCRsZWZ0ID09IDEsIFQsIEYpCm1pc3NSYXRlIDwtIHN1bSh0ZXN0JG1pc3MpL2xlbmd0aCh0ZXN0JG1pc3MpCnByaW50KHBhc3RlMCgiUHJ1bmVkIE1pc3MgcmF0ZTogIiwgbWlzc1JhdGUpKQpgYGAKCmBgYHtyIGVjaG8gPSBULCBtZXNzYWdlID0gRiwgd2FybmluZyA9IEZ9CnRlc3QkQ1IgPC0gaWZlbHNlKHRlc3QkcHJlZCA9PSAwICYgdGVzdCRsZWZ0ID09IDAsIFQsIEYpCkNSUmF0ZSA8LSBzdW0odGVzdCRDUikvbGVuZ3RoKHRlc3QkQ1IpCnByaW50KHBhc3RlMCgiUHJ1bmVkIENSIHJhdGU6ICIsIENSUmF0ZSkpCmBgYAoKRm9yIHBydW5pbmcgd2l0aCB7cnBhcnR9IERlY2lzb24gVHJlZXMgaW4gUiwgc2VlIHRoZSBmb2xsb3dpbmcgU3RhY2sgT3ZlcmZsb3cgZGlzY3Vzc2lvbjogW1NlbGVjdGluZyBjcCB2YWx1ZSBmb3IgZGVjaXNpb24gdHJlZSBwcnVuaW5nIHVzaW5nIHJwYXJ0XShodHRwczovL3N0YWNrb3ZlcmZsb3cuY29tL3F1ZXN0aW9ucy8zNzcyMTA0Ny9zZWxlY3RpbmctY3AtdmFsdWUtZm9yLWRlY2lzaW9uLXRyZWUtcHJ1bmluZy11c2luZy1ycGFydCkuCgoqKioKCiMjIyBGdXJ0aGVyIFJlYWRpbmdzCgorIFtUaGUgRWxlbWVudHMgb2YgU3RhdGlzdGljYWwgTGVhcm5pbmcsIEhhc3RpZSwgVC4sIFRpYnNoaXJhbmksIFIuICYgRnJpZWRtYW4sIEouLCAxMnRoIHByaW50aW5nIHdpdGggY29ycmVjdGlvbnMgYW5kIHRhYmxlIG9mIGNvbnRlbnRzLCBKYW4gMjAxNywgQ2hhcHRlciA5LjIgVHJlZS1CYXNlZCBNZXRob2RzKV0oaHR0cHM6Ly93ZWIuc3RhbmZvcmQuZWR1L35oYXN0aWUvRWxlbVN0YXRMZWFybi9wcmludGluZ3MvRVNMSUlfcHJpbnQxMl90b2MucGRmKQoKKyBbQW4gSW50cm9kdWN0aW9uIHRvIFJlY3Vyc2l2ZSBQYXJ0aXRpb25pbmcgVXNpbmcgdGhlIFJQQVJUIFJvdXRpbmVzLCBUZXJyeSBNLiBUaGVybmVhdSwgRWxpemFiZXRoIEouIEF0a2luc29uLCBNYXlvIEZvdW5kYXRpb24sIEFwcmlsIDExLCAyMDE5XShodHRwczovL2NyYW4uci1wcm9qZWN0Lm9yZy93ZWIvcGFja2FnZXMvcnBhcnQvdmlnbmV0dGVzL2xvbmdpbnRyby5wZGYpCgoKKioqCkdvcmFuIFMuIE1pbG92YW5vdmnEhwoKRGF0YUtvbGVrdGl2LCAyMDIwLzIxCgpjb250YWN0OiBnb3Jhbi5taWxvdmFub3ZpY0BkYXRha29sZWt0aXYuY29tCgohW10oLi4vX2ltZy9ES19Mb2dvXzEwMC5wbmcpCgoqKioKTGljZW5zZTogW0dQTHYzXShodHRwOi8vd3d3LmdudS5vcmcvbGljZW5zZXMvZ3BsLTMuMC50eHQpClRoaXMgTm90ZWJvb2sgaXMgZnJlZSBzb2Z0d2FyZTogeW91IGNhbiByZWRpc3RyaWJ1dGUgaXQgYW5kL29yIG1vZGlmeSBpdCB1bmRlciB0aGUgdGVybXMgb2YgdGhlIEdOVSBHZW5lcmFsIFB1YmxpYyBMaWNlbnNlIGFzIHB1Ymxpc2hlZCBieSB0aGUgRnJlZSBTb2Z0d2FyZSBGb3VuZGF0aW9uLCBlaXRoZXIgdmVyc2lvbiAzIG9mIHRoZSBMaWNlbnNlLCBvciAoYXQgeW91ciBvcHRpb24pIGFueSBsYXRlciB2ZXJzaW9uLgpUaGlzIE5vdGVib29rIGlzIGRpc3RyaWJ1dGVkIGluIHRoZSBob3BlIHRoYXQgaXQgd2lsbCBiZSB1c2VmdWwsIGJ1dCBXSVRIT1VUIEFOWSBXQVJSQU5UWTsgd2l0aG91dCBldmVuIHRoZSBpbXBsaWVkIHdhcnJhbnR5IG9mIE1FUkNIQU5UQUJJTElUWSBvciBGSVRORVNTIEZPUiBBIFBBUlRJQ1VMQVIgUFVSUE9TRS4gIFNlZSB0aGUgR05VIEdlbmVyYWwgUHVibGljIExpY2Vuc2UgZm9yIG1vcmUgZGV0YWlscy4KWW91IHNob3VsZCBoYXZlIHJlY2VpdmVkIGEgY29weSBvZiB0aGUgR05VIEdlbmVyYWwgUHVibGljIExpY2Vuc2UgYWxvbmcgd2l0aCB0aGlzIE5vdGVib29rLiBJZiBub3QsIHNlZSA8aHR0cDovL3d3dy5nbnUub3JnL2xpY2Vuc2VzLz4uCgoqKioKCg==