1 Market Target Analysis

1.0.1 Introduction

Term deposits are a major source of income for a bank. A term deposit is a cash investment held at a financial institution. Your money is invested for an agreed rate of interest over a fixed amount of time, or term. The bank has various outreach plans to sell term deposits to their customers such as email marketing, advertisements, telephonic marketing, and digital marketing.

1.0.2 Problem Statement

Telephonic marketing campaigns still remain one of the most effective way to reach out to people. However, they require huge investment as large call centers are hired to actually execute these campaigns. Hence, it is crucial to identify the customers most likely to convert beforehand so that they can be specifically targeted via call. The data is related to direct marketing campaigns (phone calls) of a Portuguese banking institution.

1.0.3 Objectives

1.0.3.1 Main Objective

To build a model that predicts if the client will subscribe to a term deposit or not

1.0.3.2 Specific Objective

  1. To determine how the given features are affect Subscription to a term deposit

1.0.4 Metrics Of success

  1. To Answer questions derived from our specific objective.

  2. Find and deal with outliers, anomalies, and missing data within the data set.

  3. Perform EDA.

  4. Building a model to predict if a client will subscribe to a term deposit or not ( best model should have a Balanced Accuracy score above 80)

  5. From our insights provide a conclusion and recommendation.

2 Data Understanding

Loading Important Libraries

library(data.table)
library(dplyr)
library(tidyverse)
library(ggplot2)

We have two sets of data set i.e train and test , will load them separately as follows:

a. Loading the train data set

library(readr)
train <- read_delim("train.csv", delim = ";", 
    escape_double = FALSE, trim_ws = TRUE)

Previewing first six rows

head(train)

Checking number of rows and columns

dim(train)
## [1] 45211    17

We have 45211 rows and 17 columns

Checking the data types

str(train)
## spec_tbl_df [45,211 × 17] (S3: spec_tbl_df/tbl_df/tbl/data.frame)
##  $ age      : num [1:45211] 58 44 33 47 33 35 28 42 58 43 ...
##  $ job      : chr [1:45211] "management" "technician" "entrepreneur" "blue-collar" ...
##  $ marital  : chr [1:45211] "married" "single" "married" "married" ...
##  $ education: chr [1:45211] "tertiary" "secondary" "secondary" "unknown" ...
##  $ default  : chr [1:45211] "no" "no" "no" "no" ...
##  $ balance  : num [1:45211] 2143 29 2 1506 1 ...
##  $ housing  : chr [1:45211] "yes" "yes" "yes" "yes" ...
##  $ loan     : chr [1:45211] "no" "no" "yes" "no" ...
##  $ contact  : chr [1:45211] "unknown" "unknown" "unknown" "unknown" ...
##  $ day      : num [1:45211] 5 5 5 5 5 5 5 5 5 5 ...
##  $ month    : chr [1:45211] "may" "may" "may" "may" ...
##  $ duration : num [1:45211] 261 151 76 92 198 139 217 380 50 55 ...
##  $ campaign : num [1:45211] 1 1 1 1 1 1 1 1 1 1 ...
##  $ pdays    : num [1:45211] -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 ...
##  $ previous : num [1:45211] 0 0 0 0 0 0 0 0 0 0 ...
##  $ poutcome : chr [1:45211] "unknown" "unknown" "unknown" "unknown" ...
##  $ y        : chr [1:45211] "no" "no" "no" "no" ...
##  - attr(*, "spec")=
##   .. cols(
##   ..   age = col_double(),
##   ..   job = col_character(),
##   ..   marital = col_character(),
##   ..   education = col_character(),
##   ..   default = col_character(),
##   ..   balance = col_double(),
##   ..   housing = col_character(),
##   ..   loan = col_character(),
##   ..   contact = col_character(),
##   ..   day = col_double(),
##   ..   month = col_character(),
##   ..   duration = col_double(),
##   ..   campaign = col_double(),
##   ..   pdays = col_double(),
##   ..   previous = col_double(),
##   ..   poutcome = col_character(),
##   ..   y = col_character()
##   .. )
##  - attr(*, "problems")=<externalptr>

We have a mixture of numeric, and categorical variables

b. Loading the test data set

library(readr)
test <- read_delim("test.csv", delim = ";", 
    escape_double = FALSE, trim_ws = TRUE)

Previewing the first six rows

head(test)

Checking the number of rows and columns

dim(test)
## [1] 4521   17

We have 17 columns and 4521 rows

Previewing our test data types

str(test)
## spec_tbl_df [4,521 × 17] (S3: spec_tbl_df/tbl_df/tbl/data.frame)
##  $ age      : num [1:4521] 30 33 35 30 59 35 36 39 41 43 ...
##  $ job      : chr [1:4521] "unemployed" "services" "management" "management" ...
##  $ marital  : chr [1:4521] "married" "married" "single" "married" ...
##  $ education: chr [1:4521] "primary" "secondary" "tertiary" "tertiary" ...
##  $ default  : chr [1:4521] "no" "no" "no" "no" ...
##  $ balance  : num [1:4521] 1787 4789 1350 1476 0 ...
##  $ housing  : chr [1:4521] "no" "yes" "yes" "yes" ...
##  $ loan     : chr [1:4521] "no" "yes" "no" "yes" ...
##  $ contact  : chr [1:4521] "cellular" "cellular" "cellular" "unknown" ...
##  $ day      : num [1:4521] 19 11 16 3 5 23 14 6 14 17 ...
##  $ month    : chr [1:4521] "oct" "may" "apr" "jun" ...
##  $ duration : num [1:4521] 79 220 185 199 226 141 341 151 57 313 ...
##  $ campaign : num [1:4521] 1 1 1 4 1 2 1 2 2 1 ...
##  $ pdays    : num [1:4521] -1 339 330 -1 -1 176 330 -1 -1 147 ...
##  $ previous : num [1:4521] 0 4 1 0 0 3 2 0 0 2 ...
##  $ poutcome : chr [1:4521] "unknown" "failure" "failure" "unknown" ...
##  $ y        : chr [1:4521] "no" "no" "no" "no" ...
##  - attr(*, "spec")=
##   .. cols(
##   ..   age = col_double(),
##   ..   job = col_character(),
##   ..   marital = col_character(),
##   ..   education = col_character(),
##   ..   default = col_character(),
##   ..   balance = col_double(),
##   ..   housing = col_character(),
##   ..   loan = col_character(),
##   ..   contact = col_character(),
##   ..   day = col_double(),
##   ..   month = col_character(),
##   ..   duration = col_double(),
##   ..   campaign = col_double(),
##   ..   pdays = col_double(),
##   ..   previous = col_double(),
##   ..   poutcome = col_character(),
##   ..   y = col_character()
##   .. )
##  - attr(*, "problems")=<externalptr>

3 Data cleaning and Preparation

For cleaning will start cleaning the train data set

Train data set

  1. Checking for null values
is.null(train)
## [1] FALSE
colSums(is.na(train))
##       age       job   marital education   default   balance   housing      loan 
##         0         0         0         0         0         0         0         0 
##   contact       day     month  duration  campaign     pdays  previous  poutcome 
##         0         0         0         0         0         0         0         0 
##         y 
##         0

We have no null values

  1. checking for duplicates
duplicated_rows <- train[duplicated(train),]
duplicated_rows

We have no duplicates

  1. Checking for outliers
boxplot(train$balance, ylab = "average yearly balance, in euros ", main = 'Average Yearly Balance')

We have outliers on the balance column.

ggplot(train) +
  aes(x = "", y =age) +
  geom_boxplot(fill = "#0c4c8a") +
  theme_minimal() + labs(title = 'Age')

We have outlier in age

ggplot(train) +
  aes(x = "", y =day) +
  geom_boxplot(fill = "#0c4c8a") +
  theme_minimal() + labs(title = 'Number of days that passed by after the client was contacted from previous campaign')

There are no outliers on day column.

boxplot(train$duration, ylab = "last contact duration, in seconds ", main = 'Last contact duration')

boxplot(train$campaign, ylab = "number of contacts performed for this client", main = 'Number of contacts')

Most of the numeric columns have outliers but will not drop them since they are significant for our analysis.

Test data set

  1. checking for null values
is.null(test)
## [1] FALSE
colSums(is.na(test))
##       age       job   marital education   default   balance   housing      loan 
##         0         0         0         0         0         0         0         0 
##   contact       day     month  duration  campaign     pdays  previous  poutcome 
##         0         0         0         0         0         0         0         0 
##         y 
##         0

We have no null values

  1. checking for duplicates
duplicated_rows <- test[duplicated(test),]
duplicated_rows

We have no duplicates

Will combine the two tables for EDA

df <- rbind(train, test)
head(df)

4 Exploratory Data Analysis

4.1 1. Uni-variate Analysis

Age distribution of the customers

hist((df$age),  
main = "Customer age distribution",
     xlab = 'Age', 
     ylab = 'count',
     col = "blue")

The age bracket of most clients was 35 years, there was an extreme of 95 years and 18 years

Education level Distribution of the customers

edu <- (df$education)
edu.frequency <- table(edu)
edu.frequency
## edu
##   primary secondary  tertiary   unknown 
##      7529     25508     14651      2044
barplot(edu.frequency,
  main="Distribution of Education level among the customer",
  xlab="Education Level",
  ylab = "Frequency",
  col=c("magenta","blue", "green", "yellow"),
  )

Most of our customers had a form of education with highest having already reached secondary education followed by tertiary level and the least were those who did not disclose their level of education.

Job types distribution

job <- (df$job)
job.frequency <- table(job)
job.frequency
## job
##        admin.   blue-collar  entrepreneur     housemaid    management 
##          5649         10678          1655          1352         10427 
##       retired self-employed      services       student    technician 
##          2494          1762          4571          1022          8365 
##    unemployed       unknown 
##          1431           326
ggplot(df, aes(x=job)) +geom_bar() + ggtitle("Customers Job Type Distribution") + coord_flip()

The clients for the campaign involved most personnel working in blue collar jobs, management and administrative levels with the least being students and thosw who didn’t disclose their jobs.

Marital status

marital <- (df$marital)
marital.frequency <- table(marital)
marital.frequency
## marital
## divorced  married   single 
##     5735    30011    13986
barplot(marital.frequency,
  main="Customers Marital Status",
  xlab="Marital Status",
  ylab = "Frequency",
  col=c("magenta","blue", "red"),
  )

Most of the customers participating in the campaigns were married, followed by single people and finally divorced.

Credit status

default <- (df$default)
default.frequency <- table(default)
default.frequency
## default
##    no   yes 
## 48841   891
barplot(default.frequency,
  main="Distribution of Customers on Default Credit",
  xlab="Has Credit in default",
  ylab = "Frequency",
  col=c("magenta","blue"),
  )

The graph above shows that most customers don’t have credit on default.

Housing Loan

housing <- (df$housing)
housing.frequency <- table(housing)
housing.frequency
## housing
##    no   yes 
## 22043 27689
barplot(housing.frequency,
  main="Customer Housing Loan Distribution",
  xlab="Housing Loan",
  ylab = "Frequency",
  col=c("magenta","blue"),
  )

Most of the customers have a housing loan.

Personal loan

loan <- (df$loan)
loan.frequency <- table(loan)
loan.frequency
## loan
##    no   yes 
## 41797  7935
barplot(loan.frequency,
  main="Customer's Personal Loan Distribution  Status",
  xlab="Personal Loan",
  ylab = "Frequency",
  col=c("magenta","blue"),
  )

Most customers don’t have a personal loan.

Outcome of the previous marketing campaign

outcome <- (df$poutcome)
outcome.frequency <- table(outcome)
outcome.frequency
## outcome
## failure   other success unknown 
##    5391    2037    1640   40664
barplot(outcome.frequency,
  main="Previous Marketing Campaign Outcome",
  xlab="Previous campaign Outcome",
  ylab = "Frequency",
  col=c("magenta","blue", "grey", "black"),
  )

The graph shows most customers outcome of the previous marketing campaign to be unknown, with the least of the current focus group ending in success

Subscription to term deposit

sb <- (df$y)
sb.frequency <- table(sb)
sb.frequency
## sb
##    no   yes 
## 43922  5810
barplot(sb.frequency,
  main="Term Deposit Subscription",
  xlab="Subscription to term deposit",
  ylab = "Frequency",
  col=c("Purple","green"),
  )

The graph shows the outcome towards term deposit subscription where most customers did not subscribe.

4.2 2. Bivariate Analysis

library(reshape2)

Comparing age vs average yearly balance

plot((df$age), (df$balance), 
     main = "Age vs Average yearly Balance",
     xlab = 'Age', 
     ylab = 'Average yearly balance')

There is high concentration of average yearly balance of most customers despite age to be on the lower limit, however, around age 40 to 60 years we have outliers on the upper limit.

Does having a housing loan affect whether a client subscribed to a term deposit or not?

library(plyr)
counts <- ddply(df, .(df$y, df$housing), nrow)
names(counts) <- c("term deposit", "housing loan", "Freq")
counts

The table shows that most people with housing loan didn’t no subscribe to a term deposit.

We can see this visually

ggplot(df, aes(fill=y, x=housing)) + geom_bar(position = "dodge" ) + labs(title = 'Housing loan vs Term deposit subscription', 
    x = 'Housing Loan', y = 'Customer count')

We can therefore answer our objective that indeed having a housing loan affects if someone subscribes to a term deposit or not. We can clearly see most of the people who subscribed to a term deposit did not have a housing loan.

Does having a Personal loan affect whether a client subscribed to a term deposit or not?

loan_counts <- ddply(df, .(df$y, df$loan), nrow)
names(loan_counts) <- c("Term deposit", "Personal loan", "Freq")
loan_counts

The table shows that most people with personal loan did not subscribe to a term deposit.

ggplot(df, aes(fill=y, x=loan)) + geom_bar(position = "dodge" ) + labs(title = 'Personal loan vs Term deposit subscription', 
    x = 'Personal Loan', y = 'Customer count')

We can therefore answer our objective that indeed having a personal loan affects if someone subscribes to a term deposit or not. We can clearly see most of the people who subscribed to a term deposit did not have a personal loan.

Does previous campaign success lead to current campaign success to term deposit subscription?

previous_outcome <- ddply(df, .(df$y, df$poutcome), nrow)
names(previous_outcome) <- c("Term deposit", "Previous outcome", "Freq")
previous_outcome

From this table we can see previous success indeed lead to current success.

ggplot(df, aes(fill=y, x=poutcome)) + geom_bar(position = "dodge" ) + labs(title = 'Previous campaign outcome vs Term deposit subscription', 
    x = 'Previous campaign outcome', y = 'Customer count')

The success of previous campaign had a higher chance of success to the current campaign.

Does having credit on default affect term deposit subscription?

default_count <- ddply(df, .(df$y, df$default), nrow)
names(default_count) <- c("term deposit", "Credit by Default", "Freq")
default_count
ggplot(df, aes(fill=y, x=default)) + geom_bar(position = "dodge" ) + labs(title = 'Customers default credit status vs Term deposit subscription', 
    x = 'Customers default credit status', y = 'Customer count')

The graph and table above shows having a credit on default doesn’t lead to term deposit subscription.

Job type vs Term deposit subscription

job_count <- ddply(df, .(df$job, df$y), nrow)
names(job_count) <- c("Job type", "term deposit", "Freq")
job_count

The table above shows that most people in management subscribed to a term deposit, followed by blue collar and administrative.

Marital status vs Term deposit subscription

maritalstatus <- ddply(df, .(df$marital, df$y), nrow)
names(maritalstatus) <- c("maritalstatus", "Term Deposit", "Freq")
maritalstatus
ggplot(df, aes(fill=y, x=marital)) + geom_bar(position = "dodge" ) + labs(title = 'Customers marital status vs Term deposit subscription', 
    x = 'Customers Marital status', y = 'Customer count')

Most married people subscribed to term deposit, however, they were also the majority in the campaign.

Education Level vs Term deposit subscription

edu_count<- ddply(df, .(df$education, df$y), nrow)
names(edu_count) <- c("Education level", "term deposit", "Freq")
edu_count
ggplot(df, aes(fill=y, x=education)) + geom_bar(position = "dodge" ) +labs(title = 'Customers education level vs Term deposit subscription', 
    x = 'Customers education level', y = 'Customer count')

The graph above shows most customers as previously observed had some level of secondary education. However, proportionally most tertiary educational holder actually subscribed to term deposit compared to other levels of education.

Multiple calls(campaign) contact led to a term deposit or not?

campaign_count<- ddply(df, .(df$campaign, df$y), nrow)
names(campaign_count) <- c("Campaign", "term deposit", "Freq")
campaign_count

The table above shows that multiple contact during the campaign did not result to subscription. Most the people who actually subscribed to term deposit were only contacted once.

4.3 3. Multivariate Analysis

Getting a summary of the variables

summary(df)
##       age            job              marital           education        
##  Min.   :18.00   Length:49732       Length:49732       Length:49732      
##  1st Qu.:33.00   Class :character   Class :character   Class :character  
##  Median :39.00   Mode  :character   Mode  :character   Mode  :character  
##  Mean   :40.96                                                           
##  3rd Qu.:48.00                                                           
##  Max.   :95.00                                                           
##    default             balance         housing              loan          
##  Length:49732       Min.   : -8019   Length:49732       Length:49732      
##  Class :character   1st Qu.:    72   Class :character   Class :character  
##  Mode  :character   Median :   448   Mode  :character   Mode  :character  
##                     Mean   :  1368                                        
##                     3rd Qu.:  1431                                        
##                     Max.   :102127                                        
##    contact               day           month              duration     
##  Length:49732       Min.   : 1.00   Length:49732       Min.   :   0.0  
##  Class :character   1st Qu.: 8.00   Class :character   1st Qu.: 103.0  
##  Mode  :character   Median :16.00   Mode  :character   Median : 180.0  
##                     Mean   :15.82                      Mean   : 258.7  
##                     3rd Qu.:21.00                      3rd Qu.: 320.0  
##                     Max.   :31.00                      Max.   :4918.0  
##     campaign          pdays           previous          poutcome        
##  Min.   : 1.000   Min.   : -1.00   Min.   :  0.0000   Length:49732      
##  1st Qu.: 1.000   1st Qu.: -1.00   1st Qu.:  0.0000   Class :character  
##  Median : 2.000   Median : -1.00   Median :  0.0000   Mode  :character  
##  Mean   : 2.767   Mean   : 40.16   Mean   :  0.5769                     
##  3rd Qu.: 3.000   3rd Qu.: -1.00   3rd Qu.:  0.0000                     
##  Max.   :63.000   Max.   :871.00   Max.   :275.0000                     
##       y            
##  Length:49732      
##  Class :character  
##  Mode  :character  
##                    
##                    
## 

The summary above shows the following:

  * The minimum age was 18 while the maximum was 95 years while the mean was 40.
  
  * The minimum customer's average yearly balance was -8019, the maximum was 102127 while the mean was 1368.
  
  * The minimum number of days that passed by after the client was last contacted from a previous campaign was 1 day, the maximum was 31 days while the mean was 15 days.
  
  * The minimum number of contacts performed during this campaign and for a particular client was 1, the maximum was 63 while the mean was 2. 
  
  
  

Checking for correlation

library(corrplot)
numeric <- df %>%
  select_if(is.numeric) %>%
  select("age", "balance", "duration", "day", "campaign", "pdays", "previous")
corrplot(cor(numeric))

There is no correlation among the numeric columns observed

5 Modeling

Will be performing our modeling using supervised method then challenge with unsupervised learning.

Loading important libraries

library(caTools)
library(party)
library(dplyr)
library(magrittr)
library(randomForest)
library(e1071)
library(caTools)
library(class)
library(rpart)
library(rpart.plot)
library(caret)
library(caretEnsemble)
library(psych)
library(Amelia)
library(mice)
library(GGally)

5.1 A. Pre-processing

Previewing our train data set.

head(df)

Selecting numeric columns

num <- df[, c(1,6,10,12:15)]
head(num)

Selecting categorical columns

cat <- df[, c(2:5,7:9,11,16,17)]
head(cat)

Label encoding our categorical columns

library(superml)
label <- LabelEncoder$new()
cat$job <- label$fit_transform(cat$job)
cat$marital <- label$fit_transform(cat$marital)
cat$education <- label$fit_transform(cat$education)
cat$default <- label$fit_transform(cat$default)
cat$housing <- label$fit_transform(cat$housing)
cat$loan <- label$fit_transform(cat$loan)
cat$contact <- label$fit_transform(cat$contact)
cat$month <- label$fit_transform(cat$month)
cat$poutcome <- label$fit_transform(cat$poutcome)
cat$y <- label$fit_transform(cat$y)
head(cat)

joining now categorical and numeric data

data <-cbind(num, cat)
head(data)

5.2 B. Feature selection

Will also perform feature selection to remove redundant feature in our data set.

  1. Getting a correlation Matrix
correlationMatrix <- cor(data)
  1. Choosing the highly correlated features
highlyCorrelated <- findCorrelation(correlationMatrix, cutoff=0.70)
  1. Removing the redundant (highly correlated) features
Dataset2<-data[-highlyCorrelated]
  1. Previews the correlation matrix
par(mfrow = c(1, 2))
corrplot(correlationMatrix, order = "hclust")
corrplot(cor(Dataset2), order = "hclust")

We can see from the graphs above we don’t have highly correlated feature so none was removed.

5.3 C. Dealing with class Imbalance

Previewing our classes

head(data)
class<- (data$y)
class.frequency <- table(class)
class.frequency
## class
##     0     1 
## 43922  5810

From this frequency table we have a huge class imbalance and will deal with them before moving forward.

library(imbalance)

Selecting the two class in the data set

df_p <- which(data$y == "0")
df_n <- which(data$y == "1")

Under sampling the majority class.

nsample <- 5810
pick_negative <- sample(df_p, nsample)

undersample_df1 <- data[c(df_n, pick_negative), ]

dim(undersample_df1)
## [1] 11620    17

The final product we have a new data set with 11620 rows and 17 columns

Previewing our response variable class

table(undersample_df1$y)
## 
##    0    1 
## 5810 5810

Now will go ahead and split our data into train and test data set

train.size = floor(0.75*nrow(undersample_df1))
train.index = sample(1:nrow(undersample_df1), train.size)
train.set = undersample_df1[train.index,]
test.set = undersample_df1[-train.index,]
x.train = train.set[,-17] 
x.test = test.set[,-17] 
y.train = train.set[,17] 
y.test = test.set[,17]

5.4 KNN Classifier Model

Fitting KNN model

knn.3 <- knn(train = x.train, test = x.test, cl = y.train , k = 5)
def = table(predicted = knn.3, true = y.test)
def
##          true
## predicted    0    1
##         0 1121  338
##         1  353 1093
confusionMatrix(def)
## Confusion Matrix and Statistics
## 
##          true
## predicted    0    1
##         0 1121  338
##         1  353 1093
##                                           
##                Accuracy : 0.7621          
##                  95% CI : (0.7462, 0.7775)
##     No Information Rate : 0.5074          
##     P-Value [Acc > NIR] : <2e-16          
##                                           
##                   Kappa : 0.5242          
##                                           
##  Mcnemar's Test P-Value : 0.5943          
##                                           
##             Sensitivity : 0.7605          
##             Specificity : 0.7638          
##          Pos Pred Value : 0.7683          
##          Neg Pred Value : 0.7559          
##              Prevalence : 0.5074          
##          Detection Rate : 0.3859          
##    Detection Prevalence : 0.5022          
##       Balanced Accuracy : 0.7622          
##                                           
##        'Positive' Class : 0               
## 

The model gives us a balanced accuracy of 76.08 before any hyper parameter tuning is performed.

Parameter tuning

creating Standardization function

standardize = function(x){
  z <- (x - mean(x)) / sd(x)
  return( z)
}

applying the function to the data set

undersample_df2 <-
  apply(undersample_df1, 2, standardize)
head(undersample_df2)
##             age    balance       day  duration   campaign      pdays   previous
## 84   1.48497332  0.2439696 -1.259277 1.8747220 -0.5374291 -0.4841538 -0.3742031
## 87   1.23327436 -0.4747472 -1.259277 3.0799292 -0.5374291 -0.4841538 -0.3742031
## 88  -0.02522044 -0.0916192 -1.259277 2.8587382 -0.5374291 -0.4841538 -0.3742031
## 130  1.14937471  0.2855664 -1.259277 0.5617552 -0.5374291 -0.4841538 -0.3742031
## 169  1.06547506 -0.4312739 -1.259277 0.8283186 -0.1884531 -0.4841538 -0.3742031
## 271  0.05867922 -0.4888213 -1.259277 0.5135469 -0.1884531 -0.4841538 -0.3742031
##            job    marital   education    default    housing      loan   contact
## 84   0.7357723 -0.7841811  0.02026809 -0.1271889 -1.0484622 -0.389973 -1.705916
## 87   0.7357723 -0.7841811  0.02026809 -0.1271889  0.9536957 -0.389973 -1.705916
## 88  -0.8076016 -0.7841811  0.02026809 -0.1271889 -1.0484622 -0.389973 -1.705916
## 130  1.0444471 -0.7841811  0.02026809 -0.1271889 -1.0484622 -0.389973 -1.705916
## 169  0.7357723 -0.7841811 -1.02646594 -0.1271889  0.9536957 -0.389973 -1.705916
## 271 -1.1162764  0.6596763 -1.02646594 -0.1271889 -1.0484622  2.564060 -1.705916
##         month   poutcome        y
## 84  -1.032287 -0.5178625 0.999957
## 87  -1.032287 -0.5178625 0.999957
## 88  -1.032287 -0.5178625 0.999957
## 130 -1.032287 -0.5178625 0.999957
## 169 -1.032287 -0.5178625 0.999957
## 271 -1.032287 -0.5178625 0.999957
train1.size = floor(0.75*nrow(undersample_df2))
train1.index = sample(1:nrow(undersample_df1), train1.size)
train1.set = undersample_df2[train1.index,]
test1.set = undersample_df2[-train1.index,]
x.train1 = train1.set[,-17] 
x.test1 = test1.set[,-17] 
y.train1 = train1.set[,17] 
y.test1 = test1.set[,17]
knn5 <- knn(train = x.train1, test = x.test1, cl = y.train1 , k = 5)
defp = table(predicted = knn5, true = y.test1)
confusionMatrix(defp)
## Confusion Matrix and Statistics
## 
##                     true
## predicted            -0.999956969814305 0.999956969814305
##   -0.999956969814305               1214               331
##   0.999956969814305                 271              1089
##                                             
##                Accuracy : 0.7928            
##                  95% CI : (0.7776, 0.8074)  
##     No Information Rate : 0.5112            
##     P-Value [Acc > NIR] : < 2e-16           
##                                             
##                   Kappa : 0.585             
##                                             
##  Mcnemar's Test P-Value : 0.01619           
##                                             
##             Sensitivity : 0.8175            
##             Specificity : 0.7669            
##          Pos Pred Value : 0.7858            
##          Neg Pred Value : 0.8007            
##              Prevalence : 0.5112            
##          Detection Rate : 0.4179            
##    Detection Prevalence : 0.5318            
##       Balanced Accuracy : 0.7922            
##                                             
##        'Positive' Class : -0.999956969814305
## 

After hyper parameter tuning our model improved to 78.49% balanced accuracy.

5.5 Naive Bayes

Fitting Naive Bayes Model

set.seed(120)  
classifier_cl <- naiveBayes(y.train ~ ., data = x.train)

Predicting on test data’

y_pred <- predict(classifier_cl, newdata = x.test)

Confusion Matrix

cm <- table(y.test, y_pred)
cm
##       y_pred
## y.test    0    1
##      0 1133  341
##      1  334 1097

Model Evaluation

confusionMatrix(cm)
## Confusion Matrix and Statistics
## 
##       y_pred
## y.test    0    1
##      0 1133  341
##      1  334 1097
##                                           
##                Accuracy : 0.7676          
##                  95% CI : (0.7518, 0.7829)
##     No Information Rate : 0.505           
##     P-Value [Acc > NIR] : <2e-16          
##                                           
##                   Kappa : 0.5352          
##                                           
##  Mcnemar's Test P-Value : 0.8174          
##                                           
##             Sensitivity : 0.7723          
##             Specificity : 0.7629          
##          Pos Pred Value : 0.7687          
##          Neg Pred Value : 0.7666          
##              Prevalence : 0.5050          
##          Detection Rate : 0.3900          
##    Detection Prevalence : 0.5074          
##       Balanced Accuracy : 0.7676          
##                                           
##        'Positive' Class : 0               
## 

The model had a balanced accuracy of 74.79% which was lower than knn and also below our metrics of success

5.6 SVM

Fitting SVM to the Training set

classifier = svm(formula = y.train ~ .,
                 data = x.train,
                 type = 'C-classification',
                 kernel = 'linear')

Predicting the Test set results

y_pred = predict(classifier, newdata = x.test)

Making the Confusion Matrix

cm = table(y.test, y_pred)
cm
##       y_pred
## y.test    0    1
##      0 1215  259
##      1  255 1176
confusionMatrix(cm)
## Confusion Matrix and Statistics
## 
##       y_pred
## y.test    0    1
##      0 1215  259
##      1  255 1176
##                                           
##                Accuracy : 0.8231          
##                  95% CI : (0.8087, 0.8368)
##     No Information Rate : 0.506           
##     P-Value [Acc > NIR] : <2e-16          
##                                           
##                   Kappa : 0.6461          
##                                           
##  Mcnemar's Test P-Value : 0.8947          
##                                           
##             Sensitivity : 0.8265          
##             Specificity : 0.8195          
##          Pos Pred Value : 0.8243          
##          Neg Pred Value : 0.8218          
##              Prevalence : 0.5060          
##          Detection Rate : 0.4182          
##    Detection Prevalence : 0.5074          
##       Balanced Accuracy : 0.8230          
##                                           
##        'Positive' Class : 0               
## 

The SVM model had a balanced accuracy of 81.14% making the best model compared to the previous two, and also qualifies with our metric of success.

5.7 Unsupervised Learning using K-Mean Clustering Method

head(undersample_df1)

Selecting the predictor columns

predictorcol <- undersample_df1[, -17]

label <- undersample_df1[, 17]

Fitting the K-mean Clustering model using k=2

kmeans.re <- kmeans(predictorcol, centers = 2, nstart = 20)

Confusion Matrix

kmeancm <- table(label, kmeans.re$cluster)
kmeancm
##      
## label    1    2
##     0 5589  221
##     1 5511  299

The table shows despite being to make correct prediction of the two classes , there was also a case of high mis-prediction for both classes making this model unsuitable

6 Conclusion

  • From our models above we are able to see they performed differently summarized below:

    • KNN model = 78.49% Balanced accuracy
    • Naive Bayes = 74.79% Balanced accuracy
    • SVM = 81.14% Balanced accuracy
  • Overall the best model to determine is a customer subscribe to term deposit or not is SVM. It’s also important to note unsupervised techniques are not suitable for this project.

  • Most Customers who will subscribe to term deposit are those without loan (housing and personal Loan).

  • Making multiple campaign calls to the same customer doesn’t result in them subscribing to term deposit.

  • Having credit on default doesn’t equate term deposit subscription.

6.1 Recommendation

For effectiveness of the campaigns the marketing team would:

  • Don’t call one customer multiple times (more than 2 times) instead spread that time to other customers.

  • Be aware people with previous loan (any form) might not be willing to subscribe to a term deposit.

  • Focusing on customers who without credit default is wise since they will be likely to subscribe to term deposit.

LS0tDQp0aXRsZTogIk1hcmtldCBUYXJnZXQgYW5hbHlzaXMiDQphdXRob3I6ICJUaGUgR2VlayBTcXVhZCINCmRhdGU6ICJgciBTeXMuRGF0ZSgpYCINCm91dHB1dDoNCiAgaHRtbF9kb2N1bWVudDoNCiAgICBkZjogcGFnZWQNCiAgICB0b2M6IHllcw0KICAgIHRvY19kZXB0aDogMw0KICAgIHRvY19mbG9hdDoNCiAgICAgIGNvbGxhcHNlZDogeWVzDQogICAgICBzbW9vdGhfc2Nyb2xsOiB5ZXMNCiAgICBudW1iZXJfc2VjdGlvbnM6IHllcw0KICAgIHRoZW1lOiByZWFkYWJsZQ0KICAgIGhpZ2hsaWdodDogaGFkZG9jaw0KICAgIGNvZGVfZG93bmxvYWQ6IHllcw0KICB3b3JkX2RvY3VtZW50Og0KICAgIHRvYzogeWVzDQogICAgdG9jX2RlcHRoOiAnMycNCi0tLQ0KDQpgYGB7ciBzZXR1cCwgaW5jbHVkZT1GQUxTRX0NCmtuaXRyOjpvcHRzX2NodW5rJHNldChlY2hvID0gVFJVRSkNCmBgYA0KDQojICoqTWFya2V0IFRhcmdldCBBbmFseXNpcyoqDQoNCiMjIyAqKkludHJvZHVjdGlvbioqDQoNCg0KDQpUZXJtIGRlcG9zaXRzIGFyZSBhIG1ham9yIHNvdXJjZSBvZiBpbmNvbWUgZm9yIGEgYmFuay4gQSB0ZXJtIGRlcG9zaXQgaXMgYSBjYXNoIGludmVzdG1lbnQgaGVsZCBhdCBhIGZpbmFuY2lhbCBpbnN0aXR1dGlvbi4gWW91ciBtb25leSBpcyBpbnZlc3RlZCBmb3IgYW4gYWdyZWVkIHJhdGUgb2YgaW50ZXJlc3Qgb3ZlciBhIGZpeGVkIGFtb3VudCBvZiB0aW1lLCBvciB0ZXJtLiBUaGUgYmFuayBoYXMgdmFyaW91cyBvdXRyZWFjaCBwbGFucyB0byBzZWxsIHRlcm0gZGVwb3NpdHMgdG8gdGhlaXIgY3VzdG9tZXJzIHN1Y2ggYXMgZW1haWwgbWFya2V0aW5nLCBhZHZlcnRpc2VtZW50cywgdGVsZXBob25pYyBtYXJrZXRpbmcsIGFuZCBkaWdpdGFsIG1hcmtldGluZy4NCg0KIyMjICoqUHJvYmxlbSBTdGF0ZW1lbnQqKg0KDQpUZWxlcGhvbmljIG1hcmtldGluZyBjYW1wYWlnbnMgc3RpbGwgcmVtYWluIG9uZSBvZiB0aGUgbW9zdCBlZmZlY3RpdmUgd2F5IHRvIHJlYWNoIG91dCB0byBwZW9wbGUuIEhvd2V2ZXIsIHRoZXkgcmVxdWlyZSBodWdlIGludmVzdG1lbnQgYXMgbGFyZ2UgY2FsbCBjZW50ZXJzIGFyZSBoaXJlZCB0byBhY3R1YWxseSBleGVjdXRlIHRoZXNlIGNhbXBhaWducy4gSGVuY2UsIGl0IGlzIGNydWNpYWwgdG8gaWRlbnRpZnkgdGhlIGN1c3RvbWVycyBtb3N0IGxpa2VseSB0byBjb252ZXJ0IGJlZm9yZWhhbmQgc28gdGhhdCB0aGV5IGNhbiBiZSBzcGVjaWZpY2FsbHkgdGFyZ2V0ZWQgdmlhIGNhbGwuDQpUaGUgZGF0YSBpcyByZWxhdGVkIHRvIGRpcmVjdCBtYXJrZXRpbmcgY2FtcGFpZ25zIChwaG9uZSBjYWxscykgb2YgYSBQb3J0dWd1ZXNlIGJhbmtpbmcgaW5zdGl0dXRpb24uIA0KDQojIyMgKipPYmplY3RpdmVzKioNCg0KIyMjIyAqKk1haW4gT2JqZWN0aXZlKioNCg0KVG8gYnVpbGQgYSBtb2RlbCB0aGF0IHByZWRpY3RzIGlmIHRoZSBjbGllbnQgd2lsbCBzdWJzY3JpYmUgdG8gYSB0ZXJtIGRlcG9zaXQgb3Igbm90DQoNCg0KIyMjIyAqKlNwZWNpZmljIE9iamVjdGl2ZSoqDQoNCjEuIFRvIGRldGVybWluZSBob3cgdGhlIGdpdmVuIGZlYXR1cmVzIGFyZSBhZmZlY3QgU3Vic2NyaXB0aW9uIHRvIGEgdGVybSBkZXBvc2l0DQoNCg0KIyMjICoqTWV0cmljcyBPZiBzdWNjZXNzKioNCjEuIFRvIEFuc3dlciBxdWVzdGlvbnMgZGVyaXZlZCBmcm9tIG91ciBzcGVjaWZpYyBvYmplY3RpdmUuDQoNCjIuIEZpbmQgYW5kIGRlYWwgd2l0aCBvdXRsaWVycywgYW5vbWFsaWVzLCBhbmQgbWlzc2luZyBkYXRhIHdpdGhpbiB0aGUgZGF0YSBzZXQuDQoNCjMuIFBlcmZvcm0gRURBLg0KDQo0LiBCdWlsZGluZyBhIG1vZGVsIHRvIHByZWRpY3QgaWYgYSBjbGllbnQgd2lsbCBzdWJzY3JpYmUgdG8gIGEgdGVybSBkZXBvc2l0IG9yIG5vdCAoIGJlc3QgbW9kZWwgc2hvdWxkIGhhdmUgYSBCYWxhbmNlZCBBY2N1cmFjeSBzY29yZSBhYm92ZSA4MCkNCg0KNS4gRnJvbSBvdXIgaW5zaWdodHMgcHJvdmlkZSBhIGNvbmNsdXNpb24gYW5kIHJlY29tbWVuZGF0aW9uLg0KDQoNCiMgKipEYXRhIFVuZGVyc3RhbmRpbmcqKg0KDQpMb2FkaW5nIEltcG9ydGFudCBMaWJyYXJpZXMNCmBgYHtyIG1lc3NhZ2UgPSBGQUxTRX0NCmxpYnJhcnkoZGF0YS50YWJsZSkNCmxpYnJhcnkoZHBseXIpDQpsaWJyYXJ5KHRpZHl2ZXJzZSkNCmxpYnJhcnkoZ2dwbG90MikNCmBgYA0KDQpXZSBoYXZlIHR3byBzZXRzIG9mIGRhdGEgc2V0IGkuZSB0cmFpbiBhbmQgdGVzdCAsIHdpbGwgbG9hZCB0aGVtIHNlcGFyYXRlbHkgYXMgZm9sbG93czogDQoNCioqYS4gTG9hZGluZyB0aGUgdHJhaW4gZGF0YSBzZXQqKg0KYGBge3IgIG1lc3NhZ2UgPSBGQUxTRX0NCmxpYnJhcnkocmVhZHIpDQp0cmFpbiA8LSByZWFkX2RlbGltKCJ0cmFpbi5jc3YiLCBkZWxpbSA9ICI7IiwgDQogICAgZXNjYXBlX2RvdWJsZSA9IEZBTFNFLCB0cmltX3dzID0gVFJVRSkNCmBgYA0KDQpQcmV2aWV3aW5nIGZpcnN0IHNpeCByb3dzDQpgYGB7cn0NCmhlYWQodHJhaW4pDQpgYGANCg0KDQoNCkNoZWNraW5nIG51bWJlciBvZiByb3dzIGFuZCBjb2x1bW5zDQpgYGB7cn0NCmRpbSh0cmFpbikNCmBgYA0KV2UgaGF2ZSA0NTIxMSByb3dzIGFuZCAxNyBjb2x1bW5zDQoNCg0KQ2hlY2tpbmcgdGhlIGRhdGEgdHlwZXMNCmBgYHtyfQ0Kc3RyKHRyYWluKQ0KYGBgDQpXZSBoYXZlIGEgbWl4dHVyZSBvZiBudW1lcmljLCBhbmQgY2F0ZWdvcmljYWwgdmFyaWFibGVzDQoNCg0KKipiLiBMb2FkaW5nIHRoZSB0ZXN0IGRhdGEgc2V0KioNCg0KYGBge3IgIG1lc3NhZ2UgPSBGQUxTRX0NCmxpYnJhcnkocmVhZHIpDQp0ZXN0IDwtIHJlYWRfZGVsaW0oInRlc3QuY3N2IiwgZGVsaW0gPSAiOyIsIA0KICAgIGVzY2FwZV9kb3VibGUgPSBGQUxTRSwgdHJpbV93cyA9IFRSVUUpDQpgYGANCg0KDQpQcmV2aWV3aW5nIHRoZSBmaXJzdCBzaXggcm93cw0KYGBge3J9DQpoZWFkKHRlc3QpDQpgYGANCg0KQ2hlY2tpbmcgdGhlIG51bWJlciBvZiByb3dzIGFuZCBjb2x1bW5zDQpgYGB7cn0NCmRpbSh0ZXN0KQ0KYGBgDQpXZSBoYXZlIDE3IGNvbHVtbnMgYW5kIDQ1MjEgcm93cw0KDQoNClByZXZpZXdpbmcgb3VyIHRlc3QgZGF0YSB0eXBlcw0KYGBge3J9DQpzdHIodGVzdCkNCmBgYA0KDQoNCiMgKipEYXRhIGNsZWFuaW5nIGFuZCBQcmVwYXJhdGlvbioqDQoNCkZvciBjbGVhbmluZyB3aWxsIHN0YXJ0IGNsZWFuaW5nIHRoZSB0cmFpbiBkYXRhIHNldA0KDQoqKlRyYWluIGRhdGEgc2V0KiogDQoNCmEuIENoZWNraW5nIGZvciBudWxsIHZhbHVlcw0KYGBge3J9DQppcy5udWxsKHRyYWluKQ0KYGBgDQpgYGB7cn0NCmNvbFN1bXMoaXMubmEodHJhaW4pKQ0KYGBgDQoNCldlIGhhdmUgbm8gbnVsbCB2YWx1ZXMNCg0KDQpiLiBjaGVja2luZyBmb3IgZHVwbGljYXRlcw0KYGBge3J9DQpkdXBsaWNhdGVkX3Jvd3MgPC0gdHJhaW5bZHVwbGljYXRlZCh0cmFpbiksXQ0KZHVwbGljYXRlZF9yb3dzDQpgYGANCldlIGhhdmUgbm8gZHVwbGljYXRlcw0KDQoNCg0KYy4gQ2hlY2tpbmcgZm9yIG91dGxpZXJzDQoNCmBgYHtyfQ0KYm94cGxvdCh0cmFpbiRiYWxhbmNlLCB5bGFiID0gImF2ZXJhZ2UgeWVhcmx5IGJhbGFuY2UsIGluIGV1cm9zICIsIG1haW4gPSAnQXZlcmFnZSBZZWFybHkgQmFsYW5jZScpDQpgYGANCg0KDQoNCldlIGhhdmUgb3V0bGllcnMgb24gdGhlIGJhbGFuY2UgY29sdW1uLg0KIA0KIA0KIA0KYGBge3J9DQpnZ3Bsb3QodHJhaW4pICsNCiAgYWVzKHggPSAiIiwgeSA9YWdlKSArDQogIGdlb21fYm94cGxvdChmaWxsID0gIiMwYzRjOGEiKSArDQogIHRoZW1lX21pbmltYWwoKSArIGxhYnModGl0bGUgPSAnQWdlJykNCmBgYA0KDQoNCg0KV2UgaGF2ZSBvdXRsaWVyIGluIGFnZQ0KDQoNCmBgYHtyfQ0KZ2dwbG90KHRyYWluKSArDQogIGFlcyh4ID0gIiIsIHkgPWRheSkgKw0KICBnZW9tX2JveHBsb3QoZmlsbCA9ICIjMGM0YzhhIikgKw0KICB0aGVtZV9taW5pbWFsKCkgKyBsYWJzKHRpdGxlID0gJ051bWJlciBvZiBkYXlzIHRoYXQgcGFzc2VkIGJ5IGFmdGVyIHRoZSBjbGllbnQgd2FzIGNvbnRhY3RlZCBmcm9tIHByZXZpb3VzIGNhbXBhaWduJykNCmBgYA0KDQoNCg0KDQpUaGVyZSBhcmUgbm8gb3V0bGllcnMgb24gZGF5IGNvbHVtbi4NCg0KDQpgYGB7cn0NCmJveHBsb3QodHJhaW4kZHVyYXRpb24sIHlsYWIgPSAibGFzdCBjb250YWN0IGR1cmF0aW9uLCBpbiBzZWNvbmRzICIsIG1haW4gPSAnTGFzdCBjb250YWN0IGR1cmF0aW9uJykNCmBgYA0KDQoNCmBgYHtyfQ0KYm94cGxvdCh0cmFpbiRjYW1wYWlnbiwgeWxhYiA9ICJudW1iZXIgb2YgY29udGFjdHMgcGVyZm9ybWVkIGZvciB0aGlzIGNsaWVudCIsIG1haW4gPSAnTnVtYmVyIG9mIGNvbnRhY3RzJykNCmBgYA0KDQpNb3N0IG9mIHRoZSBudW1lcmljIGNvbHVtbnMgaGF2ZSBvdXRsaWVycyBidXQgd2lsbCBub3QgZHJvcCB0aGVtIHNpbmNlIHRoZXkgYXJlIHNpZ25pZmljYW50IGZvciBvdXIgYW5hbHlzaXMuDQoNCg0KDQoNCioqVGVzdCBkYXRhIHNldCoqDQoNCmEuIGNoZWNraW5nIGZvciBudWxsIHZhbHVlcw0KYGBge3J9DQppcy5udWxsKHRlc3QpDQpgYGANCg0KDQpgYGB7cn0NCmNvbFN1bXMoaXMubmEodGVzdCkpDQpgYGANCg0KV2UgaGF2ZSBubyBudWxsIHZhbHVlcw0KDQoNCmIuIGNoZWNraW5nIGZvciBkdXBsaWNhdGVzDQpgYGB7cn0NCmR1cGxpY2F0ZWRfcm93cyA8LSB0ZXN0W2R1cGxpY2F0ZWQodGVzdCksXQ0KZHVwbGljYXRlZF9yb3dzDQpgYGANCldlIGhhdmUgbm8gZHVwbGljYXRlcw0KDQoNCg0KV2lsbCBjb21iaW5lIHRoZSB0d28gdGFibGVzIGZvciBFREENCmBgYHtyfQ0KZGYgPC0gcmJpbmQodHJhaW4sIHRlc3QpDQpoZWFkKGRmKQ0KYGBgDQoNCg0KIyAqKkV4cGxvcmF0b3J5IERhdGEgQW5hbHlzaXMqKg0KDQojIyAqKjEuIFVuaS12YXJpYXRlIEFuYWx5c2lzKioNCg0KKipBZ2UgZGlzdHJpYnV0aW9uIG9mIHRoZSBjdXN0b21lcnMqKg0KDQpgYGB7ciBtZXNzYWdlPUZBTFNFfQ0KaGlzdCgoZGYkYWdlKSwgIA0KbWFpbiA9ICJDdXN0b21lciBhZ2UgZGlzdHJpYnV0aW9uIiwNCiAgICAgeGxhYiA9ICdBZ2UnLCANCiAgICAgeWxhYiA9ICdjb3VudCcsDQogICAgIGNvbCA9ICJibHVlIikNCmBgYA0KDQoNClRoZSBhZ2UgYnJhY2tldCBvZiBtb3N0IGNsaWVudHMgd2FzIDM1IHllYXJzLCB0aGVyZSB3YXMgYW4gZXh0cmVtZSBvZiA5NSB5ZWFycyBhbmQgMTggeWVhcnMNCg0KDQoqKkVkdWNhdGlvbiBsZXZlbCBEaXN0cmlidXRpb24gb2YgdGhlIGN1c3RvbWVycyoqDQoNCmBgYHtyfQ0KZWR1IDwtIChkZiRlZHVjYXRpb24pDQplZHUuZnJlcXVlbmN5IDwtIHRhYmxlKGVkdSkNCmVkdS5mcmVxdWVuY3kNCmBgYA0KDQoNCmBgYHtyfQ0KYmFycGxvdChlZHUuZnJlcXVlbmN5LA0KICBtYWluPSJEaXN0cmlidXRpb24gb2YgRWR1Y2F0aW9uIGxldmVsIGFtb25nIHRoZSBjdXN0b21lciIsDQogIHhsYWI9IkVkdWNhdGlvbiBMZXZlbCIsDQogIHlsYWIgPSAiRnJlcXVlbmN5IiwNCiAgY29sPWMoIm1hZ2VudGEiLCJibHVlIiwgImdyZWVuIiwgInllbGxvdyIpLA0KICApDQpgYGANCg0KDQoNCk1vc3Qgb2Ygb3VyIGN1c3RvbWVycyBoYWQgYSBmb3JtIG9mIGVkdWNhdGlvbiB3aXRoIGhpZ2hlc3QgaGF2aW5nIGFscmVhZHkgcmVhY2hlZCBzZWNvbmRhcnkgZWR1Y2F0aW9uIGZvbGxvd2VkIGJ5IHRlcnRpYXJ5IGxldmVsIGFuZCB0aGUgbGVhc3Qgd2VyZSB0aG9zZSB3aG8gZGlkIG5vdCBkaXNjbG9zZSB0aGVpciBsZXZlbCBvZiBlZHVjYXRpb24uDQoNCg0KKipKb2IgdHlwZXMgZGlzdHJpYnV0aW9uKioNCg0KYGBge3J9DQpqb2IgPC0gKGRmJGpvYikNCmpvYi5mcmVxdWVuY3kgPC0gdGFibGUoam9iKQ0Kam9iLmZyZXF1ZW5jeQ0KYGBgDQoNCmBgYHtyfQ0KDQpgYGANCg0KDQoNCmBgYHtyfQ0KZ2dwbG90KGRmLCBhZXMoeD1qb2IpKSArZ2VvbV9iYXIoKSArIGdndGl0bGUoIkN1c3RvbWVycyBKb2IgVHlwZSBEaXN0cmlidXRpb24iKSArIGNvb3JkX2ZsaXAoKQ0KYGBgDQoNCg0KDQpUaGUgY2xpZW50cyBmb3IgdGhlIGNhbXBhaWduIGludm9sdmVkIG1vc3QgcGVyc29ubmVsIHdvcmtpbmcgaW4gYmx1ZSBjb2xsYXIgam9icywgbWFuYWdlbWVudCBhbmQgYWRtaW5pc3RyYXRpdmUgbGV2ZWxzIHdpdGggdGhlIGxlYXN0IGJlaW5nIHN0dWRlbnRzIGFuZCB0aG9zdyB3aG8gZGlkbid0IGRpc2Nsb3NlIHRoZWlyIGpvYnMuDQoNCg0KDQoqKk1hcml0YWwgc3RhdHVzKioNCmBgYHtyfQ0KbWFyaXRhbCA8LSAoZGYkbWFyaXRhbCkNCm1hcml0YWwuZnJlcXVlbmN5IDwtIHRhYmxlKG1hcml0YWwpDQptYXJpdGFsLmZyZXF1ZW5jeQ0KYGBgDQoNCg0KYGBge3J9DQpiYXJwbG90KG1hcml0YWwuZnJlcXVlbmN5LA0KICBtYWluPSJDdXN0b21lcnMgTWFyaXRhbCBTdGF0dXMiLA0KICB4bGFiPSJNYXJpdGFsIFN0YXR1cyIsDQogIHlsYWIgPSAiRnJlcXVlbmN5IiwNCiAgY29sPWMoIm1hZ2VudGEiLCJibHVlIiwgInJlZCIpLA0KICApDQpgYGANCg0KDQpNb3N0IG9mIHRoZSBjdXN0b21lcnMgcGFydGljaXBhdGluZyBpbiB0aGUgY2FtcGFpZ25zIHdlcmUgbWFycmllZCwgZm9sbG93ZWQgYnkgc2luZ2xlIHBlb3BsZSBhbmQgZmluYWxseSBkaXZvcmNlZC4NCg0KKipDcmVkaXQgc3RhdHVzKioNCmBgYHtyfQ0KZGVmYXVsdCA8LSAoZGYkZGVmYXVsdCkNCmRlZmF1bHQuZnJlcXVlbmN5IDwtIHRhYmxlKGRlZmF1bHQpDQpkZWZhdWx0LmZyZXF1ZW5jeQ0KYGBgDQoNCg0KYGBge3J9DQpiYXJwbG90KGRlZmF1bHQuZnJlcXVlbmN5LA0KICBtYWluPSJEaXN0cmlidXRpb24gb2YgQ3VzdG9tZXJzIG9uIERlZmF1bHQgQ3JlZGl0IiwNCiAgeGxhYj0iSGFzIENyZWRpdCBpbiBkZWZhdWx0IiwNCiAgeWxhYiA9ICJGcmVxdWVuY3kiLA0KICBjb2w9YygibWFnZW50YSIsImJsdWUiKSwNCiAgKQ0KYGBgDQoNCg0KVGhlIGdyYXBoIGFib3ZlIHNob3dzIHRoYXQgbW9zdCBjdXN0b21lcnMgZG9uJ3QgaGF2ZSBjcmVkaXQgb24gZGVmYXVsdC4NCg0KDQoqKkhvdXNpbmcgTG9hbioqDQoNCmBgYHtyfQ0KaG91c2luZyA8LSAoZGYkaG91c2luZykNCmhvdXNpbmcuZnJlcXVlbmN5IDwtIHRhYmxlKGhvdXNpbmcpDQpob3VzaW5nLmZyZXF1ZW5jeQ0KYGBgDQoNCg0KYGBge3J9DQpiYXJwbG90KGhvdXNpbmcuZnJlcXVlbmN5LA0KICBtYWluPSJDdXN0b21lciBIb3VzaW5nIExvYW4gRGlzdHJpYnV0aW9uIiwNCiAgeGxhYj0iSG91c2luZyBMb2FuIiwNCiAgeWxhYiA9ICJGcmVxdWVuY3kiLA0KICBjb2w9YygibWFnZW50YSIsImJsdWUiKSwNCiAgKQ0KYGBgDQoNCg0KTW9zdCBvZiB0aGUgY3VzdG9tZXJzIGhhdmUgYSBob3VzaW5nIGxvYW4uIA0KDQoNCioqUGVyc29uYWwgbG9hbioqDQpgYGB7cn0NCmxvYW4gPC0gKGRmJGxvYW4pDQpsb2FuLmZyZXF1ZW5jeSA8LSB0YWJsZShsb2FuKQ0KbG9hbi5mcmVxdWVuY3kNCmBgYA0KDQoNCmBgYHtyfQ0KYmFycGxvdChsb2FuLmZyZXF1ZW5jeSwNCiAgbWFpbj0iQ3VzdG9tZXIncyBQZXJzb25hbCBMb2FuIERpc3RyaWJ1dGlvbiAgU3RhdHVzIiwNCiAgeGxhYj0iUGVyc29uYWwgTG9hbiIsDQogIHlsYWIgPSAiRnJlcXVlbmN5IiwNCiAgY29sPWMoIm1hZ2VudGEiLCJibHVlIiksDQogICkNCmBgYA0KDQoNCk1vc3QgY3VzdG9tZXJzIGRvbid0IGhhdmUgYSBwZXJzb25hbCBsb2FuLg0KDQoNCg0KDQoNCioqT3V0Y29tZSBvZiB0aGUgcHJldmlvdXMgbWFya2V0aW5nIGNhbXBhaWduKioNCmBgYHtyfQ0Kb3V0Y29tZSA8LSAoZGYkcG91dGNvbWUpDQpvdXRjb21lLmZyZXF1ZW5jeSA8LSB0YWJsZShvdXRjb21lKQ0Kb3V0Y29tZS5mcmVxdWVuY3kNCmBgYA0KDQoNCmBgYHtyfQ0KYmFycGxvdChvdXRjb21lLmZyZXF1ZW5jeSwNCiAgbWFpbj0iUHJldmlvdXMgTWFya2V0aW5nIENhbXBhaWduIE91dGNvbWUiLA0KICB4bGFiPSJQcmV2aW91cyBjYW1wYWlnbiBPdXRjb21lIiwNCiAgeWxhYiA9ICJGcmVxdWVuY3kiLA0KICBjb2w9YygibWFnZW50YSIsImJsdWUiLCAiZ3JleSIsICJibGFjayIpLA0KICApDQpgYGANCg0KDQpUaGUgZ3JhcGggc2hvd3MgbW9zdCBjdXN0b21lcnMgb3V0Y29tZSBvZiB0aGUgcHJldmlvdXMgbWFya2V0aW5nIGNhbXBhaWduIHRvIGJlIHVua25vd24sIHdpdGggdGhlIGxlYXN0IG9mIHRoZSBjdXJyZW50IGZvY3VzIGdyb3VwIGVuZGluZyBpbiBzdWNjZXNzDQoNCg0KKipTdWJzY3JpcHRpb24gdG8gdGVybSBkZXBvc2l0KioNCmBgYHtyfQ0Kc2IgPC0gKGRmJHkpDQpzYi5mcmVxdWVuY3kgPC0gdGFibGUoc2IpDQpzYi5mcmVxdWVuY3kNCmBgYA0KDQpgYGB7cn0NCmJhcnBsb3Qoc2IuZnJlcXVlbmN5LA0KICBtYWluPSJUZXJtIERlcG9zaXQgU3Vic2NyaXB0aW9uIiwNCiAgeGxhYj0iU3Vic2NyaXB0aW9uIHRvIHRlcm0gZGVwb3NpdCIsDQogIHlsYWIgPSAiRnJlcXVlbmN5IiwNCiAgY29sPWMoIlB1cnBsZSIsImdyZWVuIiksDQogICkNCmBgYA0KDQoNClRoZSBncmFwaCBzaG93cyB0aGUgb3V0Y29tZSB0b3dhcmRzIHRlcm0gZGVwb3NpdCBzdWJzY3JpcHRpb24gd2hlcmUgbW9zdCBjdXN0b21lcnMgZGlkIG5vdCBzdWJzY3JpYmUuDQoNCg0KIyMgKioyLiBCaXZhcmlhdGUgQW5hbHlzaXMqKg0KDQpgYGB7ciBtZXNzYWdlPUZBTFNFfQ0KbGlicmFyeShyZXNoYXBlMikNCmBgYA0KDQoqKkNvbXBhcmluZyBhZ2UgdnMgYXZlcmFnZSB5ZWFybHkgYmFsYW5jZSoqDQpgYGB7cn0NCnBsb3QoKGRmJGFnZSksIChkZiRiYWxhbmNlKSwgDQogICAgIG1haW4gPSAiQWdlIHZzIEF2ZXJhZ2UgeWVhcmx5IEJhbGFuY2UiLA0KICAgICB4bGFiID0gJ0FnZScsIA0KICAgICB5bGFiID0gJ0F2ZXJhZ2UgeWVhcmx5IGJhbGFuY2UnKQ0KYGBgDQoNCg0KVGhlcmUgaXMgaGlnaCBjb25jZW50cmF0aW9uIG9mIGF2ZXJhZ2UgeWVhcmx5IGJhbGFuY2Ugb2YgbW9zdCBjdXN0b21lcnMgZGVzcGl0ZSBhZ2UgdG8gYmUgb24gdGhlIGxvd2VyIGxpbWl0LCBob3dldmVyLCBhcm91bmQgYWdlIDQwIHRvIDYwIHllYXJzIHdlIGhhdmUgb3V0bGllcnMgb24gdGhlIHVwcGVyIGxpbWl0Lg0KDQoNCioqRG9lcyBoYXZpbmcgYSBob3VzaW5nIGxvYW4gYWZmZWN0IHdoZXRoZXIgYSBjbGllbnQgc3Vic2NyaWJlZCB0byBhIHRlcm0gZGVwb3NpdCBvciBub3Q/KioNCg0KYGBge3IgbWVzc2FnZT1GQUxTRX0NCmxpYnJhcnkocGx5cikNCmBgYA0KDQoNCg0KYGBge3IgbWVzc2FnZT1GQUxTRX0NCmNvdW50cyA8LSBkZHBseShkZiwgLihkZiR5LCBkZiRob3VzaW5nKSwgbnJvdykNCm5hbWVzKGNvdW50cykgPC0gYygidGVybSBkZXBvc2l0IiwgImhvdXNpbmcgbG9hbiIsICJGcmVxIikNCmNvdW50cw0KYGBgDQpUaGUgdGFibGUgc2hvd3MgdGhhdCBtb3N0IHBlb3BsZSB3aXRoIGhvdXNpbmcgbG9hbiBkaWRuJ3Qgbm8gc3Vic2NyaWJlIHRvIGEgdGVybSBkZXBvc2l0Lg0KDQoNCldlIGNhbiBzZWUgdGhpcyB2aXN1YWxseQ0KYGBge3J9DQpnZ3Bsb3QoZGYsIGFlcyhmaWxsPXksIHg9aG91c2luZykpICsgZ2VvbV9iYXIocG9zaXRpb24gPSAiZG9kZ2UiICkgKyBsYWJzKHRpdGxlID0gJ0hvdXNpbmcgbG9hbiB2cyBUZXJtIGRlcG9zaXQgc3Vic2NyaXB0aW9uJywgDQogICAgeCA9ICdIb3VzaW5nIExvYW4nLCB5ID0gJ0N1c3RvbWVyIGNvdW50JykNCmBgYA0KDQoNCldlIGNhbiB0aGVyZWZvcmUgYW5zd2VyIG91ciBvYmplY3RpdmUgdGhhdCBpbmRlZWQgaGF2aW5nIGEgaG91c2luZyBsb2FuIGFmZmVjdHMgaWYgc29tZW9uZSBzdWJzY3JpYmVzIHRvIGEgdGVybSBkZXBvc2l0IG9yIG5vdC4gV2UgY2FuIGNsZWFybHkgc2VlIG1vc3Qgb2YgdGhlIHBlb3BsZSB3aG8gc3Vic2NyaWJlZCB0byBhIHRlcm0gZGVwb3NpdCBkaWQgbm90IGhhdmUgYSBob3VzaW5nIGxvYW4uIA0KDQoNCioqRG9lcyBoYXZpbmcgYSBQZXJzb25hbCBsb2FuIGFmZmVjdCB3aGV0aGVyIGEgY2xpZW50IHN1YnNjcmliZWQgdG8gYSB0ZXJtIGRlcG9zaXQgb3Igbm90PyoqDQpgYGB7cn0NCmxvYW5fY291bnRzIDwtIGRkcGx5KGRmLCAuKGRmJHksIGRmJGxvYW4pLCBucm93KQ0KbmFtZXMobG9hbl9jb3VudHMpIDwtIGMoIlRlcm0gZGVwb3NpdCIsICJQZXJzb25hbCBsb2FuIiwgIkZyZXEiKQ0KbG9hbl9jb3VudHMNCmBgYA0KVGhlIHRhYmxlIHNob3dzIHRoYXQgbW9zdCBwZW9wbGUgd2l0aCBwZXJzb25hbCBsb2FuIGRpZCBub3Qgc3Vic2NyaWJlIHRvIGEgdGVybSBkZXBvc2l0Lg0KDQpgYGB7cn0NCmdncGxvdChkZiwgYWVzKGZpbGw9eSwgeD1sb2FuKSkgKyBnZW9tX2Jhcihwb3NpdGlvbiA9ICJkb2RnZSIgKSArIGxhYnModGl0bGUgPSAnUGVyc29uYWwgbG9hbiB2cyBUZXJtIGRlcG9zaXQgc3Vic2NyaXB0aW9uJywgDQogICAgeCA9ICdQZXJzb25hbCBMb2FuJywgeSA9ICdDdXN0b21lciBjb3VudCcpDQpgYGANCg0KDQoNCldlIGNhbiB0aGVyZWZvcmUgYW5zd2VyIG91ciBvYmplY3RpdmUgdGhhdCBpbmRlZWQgaGF2aW5nIGEgcGVyc29uYWwgbG9hbiBhZmZlY3RzIGlmIHNvbWVvbmUgc3Vic2NyaWJlcyB0byBhIHRlcm0gZGVwb3NpdCBvciBub3QuIFdlIGNhbiBjbGVhcmx5IHNlZSBtb3N0IG9mIHRoZSBwZW9wbGUgd2hvIHN1YnNjcmliZWQgdG8gYSB0ZXJtIGRlcG9zaXQgZGlkIG5vdCBoYXZlIGEgcGVyc29uYWwgbG9hbi4gDQoNCg0KDQoNCg0KDQoNCioqRG9lcyBwcmV2aW91cyBjYW1wYWlnbiBzdWNjZXNzIGxlYWQgdG8gY3VycmVudCBjYW1wYWlnbiBzdWNjZXNzIHRvIHRlcm0gZGVwb3NpdCBzdWJzY3JpcHRpb24/KioNCmBgYHtyfQ0KcHJldmlvdXNfb3V0Y29tZSA8LSBkZHBseShkZiwgLihkZiR5LCBkZiRwb3V0Y29tZSksIG5yb3cpDQpuYW1lcyhwcmV2aW91c19vdXRjb21lKSA8LSBjKCJUZXJtIGRlcG9zaXQiLCAiUHJldmlvdXMgb3V0Y29tZSIsICJGcmVxIikNCnByZXZpb3VzX291dGNvbWUNCmBgYA0KRnJvbSB0aGlzIHRhYmxlIHdlIGNhbiBzZWUgcHJldmlvdXMgc3VjY2VzcyBpbmRlZWQgbGVhZCB0byBjdXJyZW50IHN1Y2Nlc3MuDQoNCmBgYHtyfQ0KZ2dwbG90KGRmLCBhZXMoZmlsbD15LCB4PXBvdXRjb21lKSkgKyBnZW9tX2Jhcihwb3NpdGlvbiA9ICJkb2RnZSIgKSArIGxhYnModGl0bGUgPSAnUHJldmlvdXMgY2FtcGFpZ24gb3V0Y29tZSB2cyBUZXJtIGRlcG9zaXQgc3Vic2NyaXB0aW9uJywgDQogICAgeCA9ICdQcmV2aW91cyBjYW1wYWlnbiBvdXRjb21lJywgeSA9ICdDdXN0b21lciBjb3VudCcpDQpgYGANCg0KDQpUaGUgc3VjY2VzcyBvZiBwcmV2aW91cyBjYW1wYWlnbiBoYWQgYSBoaWdoZXIgY2hhbmNlIG9mIHN1Y2Nlc3MgdG8gdGhlIGN1cnJlbnQgY2FtcGFpZ24uDQoNCg0KKipEb2VzIGhhdmluZyBjcmVkaXQgb24gZGVmYXVsdCBhZmZlY3QgdGVybSBkZXBvc2l0IHN1YnNjcmlwdGlvbj8qKg0KYGBge3J9DQpkZWZhdWx0X2NvdW50IDwtIGRkcGx5KGRmLCAuKGRmJHksIGRmJGRlZmF1bHQpLCBucm93KQ0KbmFtZXMoZGVmYXVsdF9jb3VudCkgPC0gYygidGVybSBkZXBvc2l0IiwgIkNyZWRpdCBieSBEZWZhdWx0IiwgIkZyZXEiKQ0KZGVmYXVsdF9jb3VudA0KYGBgDQoNCmBgYHtyfQ0KZ2dwbG90KGRmLCBhZXMoZmlsbD15LCB4PWRlZmF1bHQpKSArIGdlb21fYmFyKHBvc2l0aW9uID0gImRvZGdlIiApICsgbGFicyh0aXRsZSA9ICdDdXN0b21lcnMgZGVmYXVsdCBjcmVkaXQgc3RhdHVzIHZzIFRlcm0gZGVwb3NpdCBzdWJzY3JpcHRpb24nLCANCiAgICB4ID0gJ0N1c3RvbWVycyBkZWZhdWx0IGNyZWRpdCBzdGF0dXMnLCB5ID0gJ0N1c3RvbWVyIGNvdW50JykNCmBgYA0KDQoNCg0KVGhlIGdyYXBoIGFuZCB0YWJsZSBhYm92ZSBzaG93cyBoYXZpbmcgYSBjcmVkaXQgb24gZGVmYXVsdCBkb2Vzbid0IGxlYWQgdG8gdGVybSBkZXBvc2l0IHN1YnNjcmlwdGlvbi4gDQoNCg0KKipKb2IgdHlwZSB2cyBUZXJtIGRlcG9zaXQgc3Vic2NyaXB0aW9uKioNCmBgYHtyfQ0Kam9iX2NvdW50IDwtIGRkcGx5KGRmLCAuKGRmJGpvYiwgZGYkeSksIG5yb3cpDQpuYW1lcyhqb2JfY291bnQpIDwtIGMoIkpvYiB0eXBlIiwgInRlcm0gZGVwb3NpdCIsICJGcmVxIikNCmpvYl9jb3VudA0KYGBgDQoNClRoZSB0YWJsZSBhYm92ZSBzaG93cyB0aGF0IG1vc3QgcGVvcGxlIGluIG1hbmFnZW1lbnQgc3Vic2NyaWJlZCB0byBhIHRlcm0gZGVwb3NpdCwgZm9sbG93ZWQgYnkgYmx1ZSBjb2xsYXIgYW5kIGFkbWluaXN0cmF0aXZlLg0KDQoNCioqTWFyaXRhbCBzdGF0dXMgdnMgVGVybSBkZXBvc2l0IHN1YnNjcmlwdGlvbioqDQpgYGB7cn0NCm1hcml0YWxzdGF0dXMgPC0gZGRwbHkoZGYsIC4oZGYkbWFyaXRhbCwgZGYkeSksIG5yb3cpDQpuYW1lcyhtYXJpdGFsc3RhdHVzKSA8LSBjKCJtYXJpdGFsc3RhdHVzIiwgIlRlcm0gRGVwb3NpdCIsICJGcmVxIikNCm1hcml0YWxzdGF0dXMNCmBgYA0KDQpgYGB7cn0NCmdncGxvdChkZiwgYWVzKGZpbGw9eSwgeD1tYXJpdGFsKSkgKyBnZW9tX2Jhcihwb3NpdGlvbiA9ICJkb2RnZSIgKSArIGxhYnModGl0bGUgPSAnQ3VzdG9tZXJzIG1hcml0YWwgc3RhdHVzIHZzIFRlcm0gZGVwb3NpdCBzdWJzY3JpcHRpb24nLCANCiAgICB4ID0gJ0N1c3RvbWVycyBNYXJpdGFsIHN0YXR1cycsIHkgPSAnQ3VzdG9tZXIgY291bnQnKQ0KYGBgDQoNCg0KTW9zdCBtYXJyaWVkIHBlb3BsZSBzdWJzY3JpYmVkIHRvIHRlcm0gZGVwb3NpdCwgaG93ZXZlciwgdGhleSB3ZXJlIGFsc28gdGhlIG1ham9yaXR5IGluIHRoZSBjYW1wYWlnbi4gDQoNCg0KKipFZHVjYXRpb24gTGV2ZWwgdnMgVGVybSBkZXBvc2l0IHN1YnNjcmlwdGlvbioqDQpgYGB7cn0NCmVkdV9jb3VudDwtIGRkcGx5KGRmLCAuKGRmJGVkdWNhdGlvbiwgZGYkeSksIG5yb3cpDQpuYW1lcyhlZHVfY291bnQpIDwtIGMoIkVkdWNhdGlvbiBsZXZlbCIsICJ0ZXJtIGRlcG9zaXQiLCAiRnJlcSIpDQplZHVfY291bnQNCmBgYA0KDQoNCmBgYHtyfQ0KZ2dwbG90KGRmLCBhZXMoZmlsbD15LCB4PWVkdWNhdGlvbikpICsgZ2VvbV9iYXIocG9zaXRpb24gPSAiZG9kZ2UiICkgK2xhYnModGl0bGUgPSAnQ3VzdG9tZXJzIGVkdWNhdGlvbiBsZXZlbCB2cyBUZXJtIGRlcG9zaXQgc3Vic2NyaXB0aW9uJywgDQogICAgeCA9ICdDdXN0b21lcnMgZWR1Y2F0aW9uIGxldmVsJywgeSA9ICdDdXN0b21lciBjb3VudCcpDQpgYGANClRoZSBncmFwaCBhYm92ZSBzaG93cyBtb3N0IGN1c3RvbWVycyBhcyBwcmV2aW91c2x5IG9ic2VydmVkIGhhZCBzb21lIGxldmVsIG9mIHNlY29uZGFyeSBlZHVjYXRpb24uIEhvd2V2ZXIsIHByb3BvcnRpb25hbGx5IG1vc3QgdGVydGlhcnkgZWR1Y2F0aW9uYWwgaG9sZGVyIGFjdHVhbGx5IHN1YnNjcmliZWQgdG8gdGVybSBkZXBvc2l0IGNvbXBhcmVkIHRvIG90aGVyIGxldmVscyBvZiBlZHVjYXRpb24uDQoNCg0KKipNdWx0aXBsZSBjYWxscyhjYW1wYWlnbikgY29udGFjdCBsZWQgdG8gYSB0ZXJtIGRlcG9zaXQgb3Igbm90PyoqDQpgYGB7cn0NCmNhbXBhaWduX2NvdW50PC0gZGRwbHkoZGYsIC4oZGYkY2FtcGFpZ24sIGRmJHkpLCBucm93KQ0KbmFtZXMoY2FtcGFpZ25fY291bnQpIDwtIGMoIkNhbXBhaWduIiwgInRlcm0gZGVwb3NpdCIsICJGcmVxIikNCmNhbXBhaWduX2NvdW50DQpgYGANClRoZSB0YWJsZSBhYm92ZSBzaG93cyB0aGF0IG11bHRpcGxlIGNvbnRhY3QgZHVyaW5nIHRoZSBjYW1wYWlnbiBkaWQgbm90IHJlc3VsdCB0byBzdWJzY3JpcHRpb24uIE1vc3QgdGhlIHBlb3BsZSB3aG8gYWN0dWFsbHkgc3Vic2NyaWJlZCB0byB0ZXJtIGRlcG9zaXQgd2VyZSBvbmx5IGNvbnRhY3RlZCBvbmNlLg0KDQoNCg0KIyMgKiozLiBNdWx0aXZhcmlhdGUgQW5hbHlzaXMqKg0KDQoNCkdldHRpbmcgYSBzdW1tYXJ5IG9mIHRoZSB2YXJpYWJsZXMNCmBgYHtyfQ0Kc3VtbWFyeShkZikNCmBgYA0KVGhlIHN1bW1hcnkgYWJvdmUgc2hvd3MgdGhlIGZvbGxvd2luZzoNCg0KICAgICAgKiBUaGUgbWluaW11bSBhZ2Ugd2FzIDE4IHdoaWxlIHRoZSBtYXhpbXVtIHdhcyA5NSB5ZWFycyB3aGlsZSB0aGUgbWVhbiB3YXMgNDAuDQogICAgICANCiAgICAgICogVGhlIG1pbmltdW0gY3VzdG9tZXIncyBhdmVyYWdlIHllYXJseSBiYWxhbmNlIHdhcyAtODAxOSwgdGhlIG1heGltdW0gd2FzIDEwMjEyNyB3aGlsZSB0aGUgbWVhbiB3YXMgMTM2OC4NCiAgICAgIA0KICAgICAgKiBUaGUgbWluaW11bSBudW1iZXIgb2YgZGF5cyB0aGF0IHBhc3NlZCBieSBhZnRlciB0aGUgY2xpZW50IHdhcyBsYXN0IGNvbnRhY3RlZCBmcm9tIGEgcHJldmlvdXMgY2FtcGFpZ24gd2FzIDEgZGF5LCB0aGUgbWF4aW11bSB3YXMgMzEgZGF5cyB3aGlsZSB0aGUgbWVhbiB3YXMgMTUgZGF5cy4NCiAgICAgIA0KICAgICAgKiBUaGUgbWluaW11bSBudW1iZXIgb2YgY29udGFjdHMgcGVyZm9ybWVkIGR1cmluZyB0aGlzIGNhbXBhaWduIGFuZCBmb3IgYSBwYXJ0aWN1bGFyIGNsaWVudCB3YXMgMSwgdGhlIG1heGltdW0gd2FzIDYzIHdoaWxlIHRoZSBtZWFuIHdhcyAyLiANCiAgICAgIA0KICAgICAgDQogICAgICANCg0KKipDaGVja2luZyBmb3IgY29ycmVsYXRpb24qKg0KYGBge3IgbWVzc2FnZT1GQUxTRX0NCmxpYnJhcnkoY29ycnBsb3QpDQpgYGANCg0KDQpgYGB7cn0NCm51bWVyaWMgPC0gZGYgJT4lDQogIHNlbGVjdF9pZihpcy5udW1lcmljKSAlPiUNCiAgc2VsZWN0KCJhZ2UiLCAiYmFsYW5jZSIsICJkdXJhdGlvbiIsICJkYXkiLCAiY2FtcGFpZ24iLCAicGRheXMiLCAicHJldmlvdXMiKQ0KYGBgDQoNCg0KYGBge3J9DQpjb3JycGxvdChjb3IobnVtZXJpYykpDQpgYGANClRoZXJlIGlzIG5vIGNvcnJlbGF0aW9uIGFtb25nIHRoZSBudW1lcmljIGNvbHVtbnMgb2JzZXJ2ZWQNCg0KDQoNCiMgKipNb2RlbGluZyoqDQoNCldpbGwgYmUgcGVyZm9ybWluZyBvdXIgbW9kZWxpbmcgdXNpbmcgc3VwZXJ2aXNlZCBtZXRob2QgdGhlbiBjaGFsbGVuZ2Ugd2l0aCB1bnN1cGVydmlzZWQgbGVhcm5pbmcuDQoNCkxvYWRpbmcgaW1wb3J0YW50IGxpYnJhcmllcw0KYGBge3IgbWVzc2FnZT1GQUxTRX0NCmxpYnJhcnkoY2FUb29scykNCmxpYnJhcnkocGFydHkpDQpsaWJyYXJ5KGRwbHlyKQ0KbGlicmFyeShtYWdyaXR0cikNCmxpYnJhcnkocmFuZG9tRm9yZXN0KQ0KbGlicmFyeShlMTA3MSkNCmxpYnJhcnkoY2FUb29scykNCmxpYnJhcnkoY2xhc3MpDQpsaWJyYXJ5KHJwYXJ0KQ0KbGlicmFyeShycGFydC5wbG90KQ0KbGlicmFyeShjYXJldCkNCmxpYnJhcnkoY2FyZXRFbnNlbWJsZSkNCmxpYnJhcnkocHN5Y2gpDQpsaWJyYXJ5KEFtZWxpYSkNCmxpYnJhcnkobWljZSkNCmxpYnJhcnkoR0dhbGx5KQ0KYGBgDQoNCg0KIyMgKipBLiBQcmUtcHJvY2Vzc2luZyoqDQoNClByZXZpZXdpbmcgb3VyIHRyYWluIGRhdGEgc2V0Lg0KYGBge3J9DQpoZWFkKGRmKQ0KYGBgDQoNCg0KU2VsZWN0aW5nIG51bWVyaWMgY29sdW1ucw0KYGBge3J9DQpudW0gPC0gZGZbLCBjKDEsNiwxMCwxMjoxNSldDQpoZWFkKG51bSkNCmBgYA0KDQoNClNlbGVjdGluZyBjYXRlZ29yaWNhbCBjb2x1bW5zDQpgYGB7cn0NCmNhdCA8LSBkZlssIGMoMjo1LDc6OSwxMSwxNiwxNyldDQpoZWFkKGNhdCkNCmBgYA0KDQpMYWJlbCBlbmNvZGluZyBvdXIgY2F0ZWdvcmljYWwgY29sdW1ucw0KYGBge3IgbWVzc2FnZT1GQUxTRX0NCmxpYnJhcnkoc3VwZXJtbCkNCmBgYA0KDQpgYGB7cn0NCmxhYmVsIDwtIExhYmVsRW5jb2RlciRuZXcoKQ0KY2F0JGpvYiA8LSBsYWJlbCRmaXRfdHJhbnNmb3JtKGNhdCRqb2IpDQpjYXQkbWFyaXRhbCA8LSBsYWJlbCRmaXRfdHJhbnNmb3JtKGNhdCRtYXJpdGFsKQ0KY2F0JGVkdWNhdGlvbiA8LSBsYWJlbCRmaXRfdHJhbnNmb3JtKGNhdCRlZHVjYXRpb24pDQpjYXQkZGVmYXVsdCA8LSBsYWJlbCRmaXRfdHJhbnNmb3JtKGNhdCRkZWZhdWx0KQ0KY2F0JGhvdXNpbmcgPC0gbGFiZWwkZml0X3RyYW5zZm9ybShjYXQkaG91c2luZykNCmNhdCRsb2FuIDwtIGxhYmVsJGZpdF90cmFuc2Zvcm0oY2F0JGxvYW4pDQpjYXQkY29udGFjdCA8LSBsYWJlbCRmaXRfdHJhbnNmb3JtKGNhdCRjb250YWN0KQ0KY2F0JG1vbnRoIDwtIGxhYmVsJGZpdF90cmFuc2Zvcm0oY2F0JG1vbnRoKQ0KY2F0JHBvdXRjb21lIDwtIGxhYmVsJGZpdF90cmFuc2Zvcm0oY2F0JHBvdXRjb21lKQ0KY2F0JHkgPC0gbGFiZWwkZml0X3RyYW5zZm9ybShjYXQkeSkNCmhlYWQoY2F0KQ0KYGBgDQoNCmpvaW5pbmcgbm93IGNhdGVnb3JpY2FsIGFuZCBudW1lcmljIGRhdGENCmBgYHtyfQ0KZGF0YSA8LWNiaW5kKG51bSwgY2F0KQ0KaGVhZChkYXRhKQ0KYGBgDQoNCiMjICoqQi4gRmVhdHVyZSBzZWxlY3Rpb24qKg0KDQpXaWxsIGFsc28gcGVyZm9ybSBmZWF0dXJlIHNlbGVjdGlvbiB0byByZW1vdmUgcmVkdW5kYW50IGZlYXR1cmUgaW4gb3VyIGRhdGEgc2V0Lg0KDQphLiBHZXR0aW5nIGEgY29ycmVsYXRpb24gTWF0cml4DQpgYGB7cn0NCmNvcnJlbGF0aW9uTWF0cml4IDwtIGNvcihkYXRhKQ0KYGBgDQoNCmIuIENob29zaW5nIHRoZSBoaWdobHkgY29ycmVsYXRlZCBmZWF0dXJlcw0KYGBge3J9DQpoaWdobHlDb3JyZWxhdGVkIDwtIGZpbmRDb3JyZWxhdGlvbihjb3JyZWxhdGlvbk1hdHJpeCwgY3V0b2ZmPTAuNzApDQpgYGANCg0KDQpjLiBSZW1vdmluZyB0aGUgcmVkdW5kYW50IChoaWdobHkgY29ycmVsYXRlZCkgZmVhdHVyZXMNCmBgYHtyfQ0KRGF0YXNldDI8LWRhdGFbLWhpZ2hseUNvcnJlbGF0ZWRdDQpgYGANCg0KDQpkLiBQcmV2aWV3cyB0aGUgY29ycmVsYXRpb24gbWF0cml4DQpgYGB7cn0NCnBhcihtZnJvdyA9IGMoMSwgMikpDQpjb3JycGxvdChjb3JyZWxhdGlvbk1hdHJpeCwgb3JkZXIgPSAiaGNsdXN0IikNCmNvcnJwbG90KGNvcihEYXRhc2V0MiksIG9yZGVyID0gImhjbHVzdCIpDQpgYGANCg0KDQpXZSBjYW4gc2VlIGZyb20gdGhlIGdyYXBocyBhYm92ZSB3ZSBkb24ndCBoYXZlIGhpZ2hseSBjb3JyZWxhdGVkIGZlYXR1cmUgc28gbm9uZSB3YXMgcmVtb3ZlZC4NCg0KDQoNCiMjICoqQy4gRGVhbGluZyB3aXRoIGNsYXNzIEltYmFsYW5jZSoqDQoNClByZXZpZXdpbmcgb3VyIGNsYXNzZXMNCmBgYHtyfQ0KaGVhZChkYXRhKQ0KYGBgDQoNCmBgYHtyfQ0KY2xhc3M8LSAoZGF0YSR5KQ0KY2xhc3MuZnJlcXVlbmN5IDwtIHRhYmxlKGNsYXNzKQ0KY2xhc3MuZnJlcXVlbmN5DQpgYGANCg0KDQpGcm9tIHRoaXMgZnJlcXVlbmN5IHRhYmxlIHdlIGhhdmUgYSBodWdlIGNsYXNzIGltYmFsYW5jZSBhbmQgd2lsbCBkZWFsIHdpdGggdGhlbSBiZWZvcmUgbW92aW5nIGZvcndhcmQuDQoNCmBgYHtyfQ0KbGlicmFyeShpbWJhbGFuY2UpDQpgYGANCg0KU2VsZWN0aW5nIHRoZSB0d28gY2xhc3MgaW4gdGhlIGRhdGEgc2V0DQpgYGB7cn0NCmRmX3AgPC0gd2hpY2goZGF0YSR5ID09ICIwIikNCmRmX24gPC0gd2hpY2goZGF0YSR5ID09ICIxIikNCmBgYA0KDQoNCioqVW5kZXIgc2FtcGxpbmcgdGhlIG1ham9yaXR5IGNsYXNzLioqIA0KYGBge3J9DQpuc2FtcGxlIDwtIDU4MTANCnBpY2tfbmVnYXRpdmUgPC0gc2FtcGxlKGRmX3AsIG5zYW1wbGUpDQoNCnVuZGVyc2FtcGxlX2RmMSA8LSBkYXRhW2MoZGZfbiwgcGlja19uZWdhdGl2ZSksIF0NCg0KZGltKHVuZGVyc2FtcGxlX2RmMSkNCmBgYA0KVGhlIGZpbmFsIHByb2R1Y3Qgd2UgaGF2ZSBhIG5ldyBkYXRhIHNldCB3aXRoIDExNjIwIHJvd3MgYW5kIDE3IGNvbHVtbnMNCg0KUHJldmlld2luZyBvdXIgcmVzcG9uc2UgdmFyaWFibGUgY2xhc3MNCmBgYHtyfQ0KdGFibGUodW5kZXJzYW1wbGVfZGYxJHkpDQpgYGANCg0KTm93IHdpbGwgZ28gYWhlYWQgYW5kIHNwbGl0IG91ciBkYXRhIGludG8gdHJhaW4gYW5kIHRlc3QgZGF0YSBzZXQNCg0KYGBge3J9DQp0cmFpbi5zaXplID0gZmxvb3IoMC43NSpucm93KHVuZGVyc2FtcGxlX2RmMSkpDQp0cmFpbi5pbmRleCA9IHNhbXBsZSgxOm5yb3codW5kZXJzYW1wbGVfZGYxKSwgdHJhaW4uc2l6ZSkNCnRyYWluLnNldCA9IHVuZGVyc2FtcGxlX2RmMVt0cmFpbi5pbmRleCxdDQp0ZXN0LnNldCA9IHVuZGVyc2FtcGxlX2RmMVstdHJhaW4uaW5kZXgsXQ0KeC50cmFpbiA9IHRyYWluLnNldFssLTE3XSANCngudGVzdCA9IHRlc3Quc2V0WywtMTddIA0KeS50cmFpbiA9IHRyYWluLnNldFssMTddIA0KeS50ZXN0ID0gdGVzdC5zZXRbLDE3XQ0KYGBgDQoNCg0KDQojIyAqKktOTiBDbGFzc2lmaWVyIE1vZGVsKioNCg0KDQoNCkZpdHRpbmcgS05OIG1vZGVsDQpgYGB7cn0NCmtubi4zIDwtIGtubih0cmFpbiA9IHgudHJhaW4sIHRlc3QgPSB4LnRlc3QsIGNsID0geS50cmFpbiAsIGsgPSA1KQ0KYGBgDQoNCg0KYGBge3J9DQpkZWYgPSB0YWJsZShwcmVkaWN0ZWQgPSBrbm4uMywgdHJ1ZSA9IHkudGVzdCkNCmRlZg0KYGBgDQoNCmBgYHtyfQ0KY29uZnVzaW9uTWF0cml4KGRlZikNCmBgYA0KVGhlIG1vZGVsIGdpdmVzIHVzIGEgYmFsYW5jZWQgYWNjdXJhY3kgb2YgNzYuMDggYmVmb3JlIGFueSBoeXBlciBwYXJhbWV0ZXIgdHVuaW5nIGlzIHBlcmZvcm1lZC4NCg0KKipQYXJhbWV0ZXIgdHVuaW5nKioNCg0KY3JlYXRpbmcgU3RhbmRhcmRpemF0aW9uIGZ1bmN0aW9uDQpgYGB7cn0NCnN0YW5kYXJkaXplID0gZnVuY3Rpb24oeCl7DQogIHogPC0gKHggLSBtZWFuKHgpKSAvIHNkKHgpDQogIHJldHVybiggeikNCn0NCmBgYA0KDQphcHBseWluZyB0aGUgZnVuY3Rpb24gdG8gdGhlIGRhdGEgc2V0DQpgYGB7cn0NCnVuZGVyc2FtcGxlX2RmMiA8LQ0KICBhcHBseSh1bmRlcnNhbXBsZV9kZjEsIDIsIHN0YW5kYXJkaXplKQ0KaGVhZCh1bmRlcnNhbXBsZV9kZjIpDQpgYGANCmBgYHtyfQ0KdHJhaW4xLnNpemUgPSBmbG9vcigwLjc1Km5yb3codW5kZXJzYW1wbGVfZGYyKSkNCnRyYWluMS5pbmRleCA9IHNhbXBsZSgxOm5yb3codW5kZXJzYW1wbGVfZGYxKSwgdHJhaW4xLnNpemUpDQp0cmFpbjEuc2V0ID0gdW5kZXJzYW1wbGVfZGYyW3RyYWluMS5pbmRleCxdDQp0ZXN0MS5zZXQgPSB1bmRlcnNhbXBsZV9kZjJbLXRyYWluMS5pbmRleCxdDQp4LnRyYWluMSA9IHRyYWluMS5zZXRbLC0xN10gDQp4LnRlc3QxID0gdGVzdDEuc2V0WywtMTddIA0KeS50cmFpbjEgPSB0cmFpbjEuc2V0WywxN10gDQp5LnRlc3QxID0gdGVzdDEuc2V0WywxN10NCmBgYA0KDQoNCmBgYHtyfQ0Ka25uNSA8LSBrbm4odHJhaW4gPSB4LnRyYWluMSwgdGVzdCA9IHgudGVzdDEsIGNsID0geS50cmFpbjEgLCBrID0gNSkNCmBgYA0KDQoNCmBgYHtyfQ0KZGVmcCA9IHRhYmxlKHByZWRpY3RlZCA9IGtubjUsIHRydWUgPSB5LnRlc3QxKQ0KYGBgDQoNCmBgYHtyfQ0KY29uZnVzaW9uTWF0cml4KGRlZnApDQpgYGANCkFmdGVyIGh5cGVyIHBhcmFtZXRlciB0dW5pbmcgb3VyIG1vZGVsIGltcHJvdmVkIHRvIDc4LjQ5JSBiYWxhbmNlZCBhY2N1cmFjeS4NCg0KDQoNCiMjICoqTmFpdmUgQmF5ZXMqKg0KDQpGaXR0aW5nIE5haXZlIEJheWVzIE1vZGVsDQpgYGB7cn0NCnNldC5zZWVkKDEyMCkgIA0KY2xhc3NpZmllcl9jbCA8LSBuYWl2ZUJheWVzKHkudHJhaW4gfiAuLCBkYXRhID0geC50cmFpbikNCmBgYA0KDQoNClByZWRpY3Rpbmcgb24gdGVzdCBkYXRhJw0KYGBge3J9DQp5X3ByZWQgPC0gcHJlZGljdChjbGFzc2lmaWVyX2NsLCBuZXdkYXRhID0geC50ZXN0KQ0KYGBgDQoNCg0KQ29uZnVzaW9uIE1hdHJpeA0KYGBge3J9DQpjbSA8LSB0YWJsZSh5LnRlc3QsIHlfcHJlZCkNCmNtDQpgYGANCg0KTW9kZWwgRXZhbHVhdGlvbg0KYGBge3J9DQpjb25mdXNpb25NYXRyaXgoY20pDQpgYGANClRoZSBtb2RlbCBoYWQgYSBiYWxhbmNlZCBhY2N1cmFjeSBvZiA3NC43OSUgd2hpY2ggd2FzIGxvd2VyIHRoYW4ga25uIGFuZCBhbHNvIGJlbG93IG91ciBtZXRyaWNzIG9mIHN1Y2Nlc3MNCg0KDQoNCg0KDQoNCiMjICoqU1ZNKioNCg0KRml0dGluZyBTVk0gdG8gdGhlIFRyYWluaW5nIHNldA0KYGBge3J9DQpjbGFzc2lmaWVyID0gc3ZtKGZvcm11bGEgPSB5LnRyYWluIH4gLiwNCiAgICAgICAgICAgICAgICAgZGF0YSA9IHgudHJhaW4sDQogICAgICAgICAgICAgICAgIHR5cGUgPSAnQy1jbGFzc2lmaWNhdGlvbicsDQogICAgICAgICAgICAgICAgIGtlcm5lbCA9ICdsaW5lYXInKQ0KYGBgDQoNCg0KUHJlZGljdGluZyB0aGUgVGVzdCBzZXQgcmVzdWx0cw0KYGBge3J9DQp5X3ByZWQgPSBwcmVkaWN0KGNsYXNzaWZpZXIsIG5ld2RhdGEgPSB4LnRlc3QpDQpgYGANCg0KDQpNYWtpbmcgdGhlIENvbmZ1c2lvbiBNYXRyaXgNCmBgYHtyfQ0KY20gPSB0YWJsZSh5LnRlc3QsIHlfcHJlZCkNCmNtDQpgYGANCg0KDQoNCmBgYHtyfQ0KY29uZnVzaW9uTWF0cml4KGNtKQ0KYGBgDQoNClRoZSBTVk0gbW9kZWwgaGFkIGEgYmFsYW5jZWQgYWNjdXJhY3kgb2YgODEuMTQlIG1ha2luZyB0aGUgYmVzdCBtb2RlbCBjb21wYXJlZCB0byB0aGUgcHJldmlvdXMgdHdvLCBhbmQgYWxzbyBxdWFsaWZpZXMgd2l0aCBvdXIgbWV0cmljIG9mIHN1Y2Nlc3MuDQoNCg0KDQoNCiMjICoqVW5zdXBlcnZpc2VkIExlYXJuaW5nIHVzaW5nIEstTWVhbiBDbHVzdGVyaW5nIE1ldGhvZCoqDQoNCg0KYGBge3J9DQpoZWFkKHVuZGVyc2FtcGxlX2RmMSkNCmBgYA0KDQpTZWxlY3RpbmcgdGhlIHByZWRpY3RvciBjb2x1bW5zDQpgYGB7cn0NCnByZWRpY3RvcmNvbCA8LSB1bmRlcnNhbXBsZV9kZjFbLCAtMTddDQoNCmxhYmVsIDwtIHVuZGVyc2FtcGxlX2RmMVssIDE3XQ0KYGBgDQoNCg0KRml0dGluZyB0aGUgSy1tZWFuIENsdXN0ZXJpbmcgbW9kZWwgdXNpbmcgaz0yDQpgYGB7cn0NCmttZWFucy5yZSA8LSBrbWVhbnMocHJlZGljdG9yY29sLCBjZW50ZXJzID0gMiwgbnN0YXJ0ID0gMjApDQpgYGANCg0KDQoNCg0KQ29uZnVzaW9uIE1hdHJpeA0KYGBge3J9DQprbWVhbmNtIDwtIHRhYmxlKGxhYmVsLCBrbWVhbnMucmUkY2x1c3RlcikNCmttZWFuY20NCmBgYA0KDQogDQoNClRoZSB0YWJsZSBzaG93cyBkZXNwaXRlIGJlaW5nIHRvIG1ha2UgY29ycmVjdCBwcmVkaWN0aW9uIG9mIHRoZSB0d28gY2xhc3NlcyAsIHRoZXJlIHdhcyBhbHNvIGEgY2FzZSBvZiBoaWdoIG1pcy1wcmVkaWN0aW9uIGZvciBib3RoIGNsYXNzZXMgbWFraW5nIHRoaXMgbW9kZWwgdW5zdWl0YWJsZQ0KDQoNCg0KIyAqKkNvbmNsdXNpb24qKg0KDQogICogRnJvbSBvdXIgbW9kZWxzIGFib3ZlIHdlIGFyZSBhYmxlIHRvIHNlZSB0aGV5IHBlcmZvcm1lZCBkaWZmZXJlbnRseSBzdW1tYXJpemVkIGJlbG93Og0KICAgICogS05OIG1vZGVsID0gNzguNDklIEJhbGFuY2VkIGFjY3VyYWN5DQogICAgKiBOYWl2ZSBCYXllcyA9IDc0Ljc5JSBCYWxhbmNlZCBhY2N1cmFjeQ0KICAgICogU1ZNID0gODEuMTQlIEJhbGFuY2VkIGFjY3VyYWN5DQogICAgDQogICogT3ZlcmFsbCB0aGUgYmVzdCBtb2RlbCB0byBkZXRlcm1pbmUgaXMgYSBjdXN0b21lciBzdWJzY3JpYmUgdG8gdGVybSBkZXBvc2l0IG9yIG5vdCBpcyBTVk0uIEl0J3MgYWxzbyBpbXBvcnRhbnQgdG8gbm90ZSB1bnN1cGVydmlzZWQgdGVjaG5pcXVlcyBhcmUgbm90IHN1aXRhYmxlIGZvciB0aGlzIHByb2plY3QuDQoNCiAgKiBNb3N0IEN1c3RvbWVycyB3aG8gd2lsbCBzdWJzY3JpYmUgdG8gdGVybSBkZXBvc2l0IGFyZSB0aG9zZSB3aXRob3V0IGxvYW4gKGhvdXNpbmcgYW5kIHBlcnNvbmFsIExvYW4pLg0KDQogICogTWFraW5nIG11bHRpcGxlIGNhbXBhaWduIGNhbGxzIHRvIHRoZSBzYW1lIGN1c3RvbWVyIGRvZXNuJ3QgcmVzdWx0IGluIHRoZW0gc3Vic2NyaWJpbmcgdG8gdGVybSBkZXBvc2l0Lg0KDQogICogSGF2aW5nIGNyZWRpdCBvbiBkZWZhdWx0IGRvZXNuJ3QgZXF1YXRlIHRlcm0gZGVwb3NpdCBzdWJzY3JpcHRpb24uDQoNCiMjICoqUmVjb21tZW5kYXRpb24qKg0KDQpGb3IgZWZmZWN0aXZlbmVzcyBvZiB0aGUgY2FtcGFpZ25zIHRoZSBtYXJrZXRpbmcgdGVhbSB3b3VsZDoNCg0KICAgKiBEb24ndCBjYWxsIG9uZSBjdXN0b21lciBtdWx0aXBsZSB0aW1lcyAobW9yZSB0aGFuIDIgdGltZXMpIGluc3RlYWQgc3ByZWFkIHRoYXQgdGltZSB0byBvdGhlciBjdXN0b21lcnMuDQogICAgICANCiAgICogQmUgYXdhcmUgcGVvcGxlIHdpdGggcHJldmlvdXMgbG9hbiAoYW55IGZvcm0pIG1pZ2h0IG5vdCBiZSB3aWxsaW5nIHRvIHN1YnNjcmliZSB0byBhIHRlcm0gZGVwb3NpdC4NCiAgICAgIA0KICAgKiBGb2N1c2luZyBvbiBjdXN0b21lcnMgd2hvIHdpdGhvdXQgY3JlZGl0IGRlZmF1bHQgaXMgd2lzZSBzaW5jZSB0aGV5IHdpbGwgYmUgbGlrZWx5IHRvIHN1YnNjcmliZSB0byB0ZXJtIGRlcG9zaXQuDQo=