1.0 Introduction

This is a dataset made available by Spain’s Ministry of Education and Science in its KEEL (Knowledge Extraction and Evolutionary Learning) repository. I think it would be terrific if we could leverage machine learning to improve predictions in healthcare. It could potentially spare patients from a lot of anxiety as well as save a lot of money and effort.

2.0 Getting ahold of the data

2.1 Downloading the data

The data can be downloaded here. The data glossary is here.

2.2 Reading the data

Having downloaded the data, we read it into R. We skip the first 26 lines of the file which contain information about the file but no actual data.

thyroid <- read.csv("thyroid.dat", header = FALSE, skip = 26) # skipping the first 26 lines
colnames(thyroid) <- c("age", "sex", "on_thyroxine", "query_on_thyroxine", "antithyroid_medication", "sick", "pregnant", "thyroid_surgery", "I131_treatment", "query_hypothyroid", "query_hyperthyroid", "lithium", "goitre", "tumor", "hypopituitary", "psych", "TSH", "T3", "TT4", "T4U", "FTI","class")
str(thyroid)
'data.frame':   7200 obs. of  22 variables:
 $ age                   : num  0.73 0.24 0.47 0.64 0.23 0.69 0.85 0.48 0.67 0.62 ...
 $ sex                   : int  0 0 0 1 0 1 1 1 0 0 ...
 $ on_thyroxine          : int  1 0 0 0 0 0 0 0 1 0 ...
 $ query_on_thyroxine    : int  0 0 0 0 0 0 0 0 0 0 ...
 $ antithyroid_medication: int  0 0 0 0 0 0 0 0 0 0 ...
 $ sick                  : int  0 0 0 0 0 0 0 0 0 1 ...
 $ pregnant              : int  0 0 0 0 0 0 0 0 0 0 ...
 $ thyroid_surgery       : int  0 0 0 0 0 0 0 0 0 0 ...
 $ I131_treatment        : int  1 0 0 0 0 0 0 0 0 0 ...
 $ query_hypothyroid     : int  0 0 0 0 0 0 0 0 0 0 ...
 $ query_hyperthyroid    : int  0 0 0 0 0 0 0 0 0 0 ...
 $ lithium               : int  0 0 0 0 0 0 0 0 0 0 ...
 $ goitre                : int  0 0 0 0 0 0 0 0 0 0 ...
 $ tumor                 : int  0 0 0 0 0 0 0 0 0 0 ...
 $ hypopituitary         : int  0 0 0 0 0 0 0 0 0 0 ...
 $ psych                 : int  0 0 0 0 0 0 0 0 0 0 ...
 $ TSH                   : num  0.0006 0.00025 0.0019 0.0009 0.00025 0.00025 0.00025 0.00208 0.0013 0.011 ...
 $ T3                    : num  0.015 0.03 0.024 0.017 0.026 0.016 0.023 0.02 0.024 0.008 ...
 $ TT4                   : num  0.12 0.143 0.102 0.077 0.139 0.086 0.128 0.086 0.087 0.073 ...
 $ T4U                   : num  0.082 0.133 0.131 0.09 0.09 0.07 0.104 0.078 0.109 0.074 ...
 $ FTI                   : num  0.146 0.108 0.078 0.085 0.153 0.123 0.121 0.11 0.08 0.098 ...
 $ class                 : int  3 3 3 3 3 3 3 3 3 2 ...


There are \(7200\) observations with \(21\) predictors and \(1\) response variable, \(class\).

3.0 Data exploration and munging

3.1 Data exploration

There are \(21\) predictors. \(age\) is given as number between \(0\) and \(1\). \(sex\) is a Boolean variable. It’s not clear whether \(1\) means “male” or “female”. There are many Boolean variables: \(on\_thyroxine\) tells us whether the patient is on Levothyroxine, \(query\_on\_thyroxine\) whether the patient was asked if he was taking Levothyroxine, \(antithyroid\_medication\) whether the patient is on any medication to treat a thyroid condition, and so on. Some that aren’t too obvious, or at least not to me: \(I131\_treatment\) tells us if the patient is under I131 treatment, \(lithium\) if the patient is taking lithium for a different condition, \(goitre\) whether the patient exhibits goiter, and \(hypopituitary\) whether the patient has hypopituitarism. I am not sure what \(psych\) stands for. The variables \(TSH\), \(T3\), \(TT4\), \(T4U\), and \(FTI\) represent the results of various blood tests used to screen for thyroid problems.

The response variable is \(class\), which can take \(1\) of \(3\) values: \(1\) is normal, \(2\) is hyperthyroidism, and \(3\) hypothyroidism. Over \(90\%\) of the observations are classified as hypothyroidism.

3.2 Data munging

We can start by changing \(age\) to numbers we are familiar with, and converting the response to a factor variable. We will also change the response from type integer to factor.

# Changle "class" to factor
thyroid$class <- factor(thyroid$class)
# Multiply "age" by 100
thyroid$age <- 100 * thyroid$age


There is no missing data in the dataset.

3.4 Splitting into training and testing datasets

Let’s split the data into training and testing data sets.

library(caTools)
set.seed(1000) # reproducibility
split <- sample.split(thyroid$class, SplitRatio = 0.7)
thyroid_train <- subset(thyroid, split==TRUE)
thyroid_test <- subset(thyroid, split==FALSE)


4.0 Decision trees

Loading the required libraries

library(rpart)
library(rpart.plot)


Now let’s build the tree and plot it. We will use all the predictors to build the tree. The parameter minbucket allows us to select how the minimum number of observations that are placed in one of the tree’s “leaves” or “buckets”.

# Building a tree with a minimum of 50 observations on each leaf
thyroid_tree <- rpart(class ~ ., data = thyroid_train, control=rpart.control(minbucket=50))
prp(thyroid_tree)


The tree splits on 3 of the variables. If we were to change the minbucket parameter, we would get more or fewer splits. Sticking with this tree for now, let’s get its accuracy on the training set itself.

# Generate predictions on training set
PredictCART_train = predict(thyroid_tree, type = "class")
# Confusion matrix of training set
conf_matrix_train <- table(thyroid_train$class, PredictCART_train)
conf_matrix_train
   PredictCART_train
       1    2    3
  1  116    0    0
  2    0  258    0
  3   10   32 4624


The rows are the ground truth whereas the columns are the predictions generated by the tree. The tree perfectly predicts the normal and hyperthyroidism cases on the training set. We can compute its overall accuracy as the ratio of the correct predictions to the size of the dataset.

\[ Accuracy=\frac{Number\ of\ normal,\ hyperthyroidism,\ and\ hypothyroidism\ cases\ correctly\ predicted}{Total\ number\ of\ cases} \]

The number of the correctly predicted cases is the sum of the diagonals of the matrix.

sum(diag(conf_matrix_train)) / sum(conf_matrix_train)
[1] 0.9916667


So the accuracy is over \(99\%\). What is the performance of this tree on the test set?

PredictCART_test = predict(thyroid_tree, newdata = thyroid_test, type = "class")
# Confusion matrix of test set
conf_matrix_test <- table(thyroid_test$class, PredictCART_test)
conf_matrix_test
   PredictCART_test
       1    2    3
  1   50    0    0
  2    0  110    0
  3    6   18 1976


The accuracy of this tree on the testing set is:

sum(diag(conf_matrix_test)) / sum(conf_matrix_test)
[1] 0.9888889


The performance of the tree on the test set is just as strong as on the training set. Can we do even better?

4.1 Cross-validation

We can use cross-validation to obtain the tree that maximizes accuracy. There is a parameter called cp, or complexity parameter, that allows us, indirectly, to specify how many observations will be placed on each leaf. We can do 10-fold cross-validation for each cp value, compute the average accuracy of each cp value, and then take the best cp value and build the final tree with it.

Let’s load the libraries we will need to perform the cross-validation:

library(caret)
library(e1071)


Now define the parameters of the cross-validation. We will try values of \(cp\) from \(0.01\) to \(0.50\), in steps of \(0.01\).

# Setting cross-validation to be 10-fold
fitControl = trainControl( method = "cv", number = 10 )
# Setting cp to .01, .02, ..., 0.5
cartGrid = expand.grid( .cp = (1:50)*0.01) 


Perform the cross-validation to determine the best cp parameter. The train function will, for each value of \(cp\), perform 10-fold cross-validation, and report the average accuracy of the \(10\) runs for that value of \(cp\). Since there are \(50\) values of \(cp\) and we are performing 10-fold cross-validation for each of them, train() builds \(500\) trees and computes the accuracy of each. So it takes a little while to run.

train(class ~ ., data = thyroid_train, method = "rpart", trControl = fitControl, tuneGrid = cartGrid)
CART 

5040 samples
  21 predictor
   3 classes: '1', '2', '3' 

No pre-processing
Resampling: Cross-Validated (10 fold) 
Summary of sample sizes: 4538, 4536, 4535, 4535, 4536, 4535, ... 
Resampling results across tuning parameters:

  cp    Accuracy   Kappa    
  0.01  0.9938468  0.9574844
  0.02  0.9942440  0.9604630
  0.03  0.9942440  0.9604630
  0.04  0.9920623  0.9460961
  0.05  0.9916655  0.9435706
  0.06  0.9916655  0.9435706
  0.07  0.9916655  0.9435706
  0.08  0.9916655  0.9435706
  0.09  0.9916655  0.9435706
  0.10  0.9916655  0.9435706
  0.11  0.9916655  0.9435706
  0.12  0.9916655  0.9435706
  0.13  0.9916655  0.9435706
  0.14  0.9916655  0.9435706
  0.15  0.9916655  0.9435706
  0.16  0.9916655  0.9435706
  0.17  0.9916655  0.9435706
  0.18  0.9916655  0.9435706
  0.19  0.9916655  0.9435706
  0.20  0.9916655  0.9435706
  0.21  0.9851229  0.9062083
  0.22  0.9801654  0.8766649
  0.23  0.9757963  0.8504515
  0.24  0.9757963  0.8504515
  0.25  0.9757963  0.8504515
  0.26  0.9757963  0.8504515
  0.27  0.9757963  0.8504515
  0.28  0.9757963  0.8504515
  0.29  0.9757963  0.8504515
  0.30  0.9757963  0.8504515
  0.31  0.9615248  0.7625517
  0.32  0.9527816  0.7061530
  0.33  0.9527816  0.7061530
  0.34  0.9527816  0.7061530
  0.35  0.9527816  0.7061530
  0.36  0.9428676  0.4861434
  0.37  0.9295582  0.1334101
  0.38  0.9257958  0.0000000
  0.39  0.9257958  0.0000000
  0.40  0.9257958  0.0000000
  0.41  0.9257958  0.0000000
  0.42  0.9257958  0.0000000
  0.43  0.9257958  0.0000000
  0.44  0.9257958  0.0000000
  0.45  0.9257958  0.0000000
  0.46  0.9257958  0.0000000
  0.47  0.9257958  0.0000000
  0.48  0.9257958  0.0000000
  0.49  0.9257958  0.0000000
  0.50  0.9257958  0.0000000

Accuracy was used to select the optimal model using  the largest value.
The final value used for the model was cp = 0.03. 


The train() function from the caret package selects the best value of cp: \(0.03\). We can then use this value to build the final tree:

thyroid_tree_cv <- rpart(class ~ ., data = thyroid_train, control=rpart.control(cp=0.03))
prp(thyroid_tree_cv)


Now we can compute the accuracy on the training set again.

# Generate predictions on training set using cross-validated tree
PredictCART_train_cv = predict(thyroid_tree_cv, type = "class")
# Confusion matrix of training set
conf_matrix_train_cv <- table(thyroid_train$class, PredictCART_train_cv)
conf_matrix_train_cv
   PredictCART_train_cv
       1    2    3
  1  116    0    0
  2    0  258    0
  3   10   17 4639


The accuracy on the training set is even higher now:

sum(diag(conf_matrix_train_cv)) / sum(conf_matrix_train_cv)
[1] 0.9946429


Finally, what about the accuracy of the tree on the test set?

# Generate predictions on test set using cross-validated set
PredictCART_test_cv = predict(thyroid_tree_cv, newdat = thyroid_test, type = "class")
# Confusion matrix of training set
conf_matrix_test_cv <- table(thyroid_test$class, PredictCART_test_cv)
conf_matrix_test_cv
   PredictCART_test_cv
       1    2    3
  1   50    0    0
  2    0  110    0
  3    6    8 1986


sum(diag(conf_matrix_test_cv)) / sum(conf_matrix_test_cv)
[1] 0.9935185


Out of more than \(2100\) cases in the test set, only \(14\) were misclassified by the model. How big an improvement is this over not bothering to use any model at all and merely predicting each case will be hypothyroidism, i.e., \(class=3\)? The vast majority of the cases are indeed hypothyroidism.

hypothyroidism_num <- nrow(thyroid_test[thyroid_test$class == "3",])
total_cases <- nrow(thyroid_test)
baseline_accuracy <- hypothyroidism_num / total_cases
baseline_accuracy
[1] 0.9259259


The no-model-at-all baseline accuracy of \(92.6\%\) is \(6.8\%\) worse than that of the optimized tree we built.

5.0 References

  1. Bertsimas, D., O’Hair, A. The Analytics Edge. Spring 2014. edX.org.
LS0tDQp0aXRsZTogIlByZWRpY3RpbmcgdGh5cm9pZCBkaWFnbm9zZXMgd2l0aCBkZWNpc2lvbiB0cmVlcyINCm91dHB1dDogaHRtbF9ub3RlYm9vaw0KLS0tDQoNCiMjIDEuMCBJbnRyb2R1Y3Rpb24NCg0KVGhpcyBpcyBhIGRhdGFzZXQgbWFkZSBhdmFpbGFibGUgYnkgU3BhaW4ncyBNaW5pc3RyeSBvZiBFZHVjYXRpb24gYW5kIFNjaWVuY2UgaW4gaXRzIEtFRUwgKEtub3dsZWRnZSBFeHRyYWN0aW9uIGFuZCBFdm9sdXRpb25hcnkgTGVhcm5pbmcpIFtyZXBvc2l0b3J5XShodHRwOi8vc2NpMnMudWdyLmVzL2tlZWwvZGF0YXNldHMucGhwKS4gSSB0aGluayBpdCB3b3VsZCBiZSB0ZXJyaWZpYyBpZiB3ZSBjb3VsZCBsZXZlcmFnZSBtYWNoaW5lIGxlYXJuaW5nIHRvIGltcHJvdmUgcHJlZGljdGlvbnMgaW4gaGVhbHRoY2FyZS4gSXQgY291bGQgcG90ZW50aWFsbHkgc3BhcmUgcGF0aWVudHMgZnJvbSBhIGxvdCBvZiBhbnhpZXR5IGFzIHdlbGwgYXMgc2F2ZSBhIGxvdCBvZiBtb25leSBhbmQgZWZmb3J0Lg0KPGJyPg0KDQojIyAyLjAgR2V0dGluZyBhaG9sZCBvZiB0aGUgZGF0YQ0KDQojIyMgMi4xIERvd25sb2FkaW5nIHRoZSBkYXRhDQoNClRoZSBkYXRhIGNhbiBiZSBkb3dubG9hZGVkIFtoZXJlXShodHRwOi8vc2NpMnMudWdyLmVzL2tlZWwvZGF0YXNldC9kYXRhL2NsYXNzaWZpY2F0aW9uL3RoeXJvaWQuemlwKS4gVGhlIGRhdGEgZ2xvc3NhcnkgaXMgW2hlcmVdKGh0dHA6Ly9zY2kycy51Z3IuZXMva2VlbC9kYXRhc2V0L2RhdGEvY2xhc3NpZmljYXRpb24vdGh5cm9pZC1uYW1lcy50eHQpLiANCg0KIyMjIDIuMiBSZWFkaW5nIHRoZSBkYXRhDQpIYXZpbmcgZG93bmxvYWRlZCB0aGUgZGF0YSwgd2UgcmVhZCBpdCBpbnRvIFIuIFdlIHNraXAgdGhlIGZpcnN0IDI2IGxpbmVzIG9mIHRoZSBmaWxlIHdoaWNoIGNvbnRhaW4gaW5mb3JtYXRpb24gYWJvdXQgdGhlIGZpbGUgYnV0IG5vIGFjdHVhbCBkYXRhLg0KYGBge3J9DQp0aHlyb2lkIDwtIHJlYWQuY3N2KCJ0aHlyb2lkLmRhdCIsIGhlYWRlciA9IEZBTFNFLCBza2lwID0gMjYpICMgc2tpcHBpbmcgdGhlIGZpcnN0IDI2IGxpbmVzDQpjb2xuYW1lcyh0aHlyb2lkKSA8LSBjKCJhZ2UiLCAic2V4IiwgIm9uX3RoeXJveGluZSIsICJxdWVyeV9vbl90aHlyb3hpbmUiLCAiYW50aXRoeXJvaWRfbWVkaWNhdGlvbiIsICJzaWNrIiwgInByZWduYW50IiwgInRoeXJvaWRfc3VyZ2VyeSIsICJJMTMxX3RyZWF0bWVudCIsICJxdWVyeV9oeXBvdGh5cm9pZCIsICJxdWVyeV9oeXBlcnRoeXJvaWQiLCAibGl0aGl1bSIsICJnb2l0cmUiLCAidHVtb3IiLCAiaHlwb3BpdHVpdGFyeSIsICJwc3ljaCIsICJUU0giLCAiVDMiLCAiVFQ0IiwgIlQ0VSIsICJGVEkiLCJjbGFzcyIpDQpzdHIodGh5cm9pZCkNCmBgYA0KPGJyPg0KDQpUaGVyZSBhcmUgJDcyMDAkIG9ic2VydmF0aW9ucyB3aXRoICQyMSQgcHJlZGljdG9ycyBhbmQgJDEkIHJlc3BvbnNlIHZhcmlhYmxlLCAkY2xhc3MkLg0KDQojIyAzLjAgRGF0YSBleHBsb3JhdGlvbiBhbmQgbXVuZ2luZw0KDQojIyMgMy4xIERhdGEgZXhwbG9yYXRpb24NCg0KVGhlcmUgYXJlICQyMSQgcHJlZGljdG9ycy4gJGFnZSQgaXMgZ2l2ZW4gYXMgbnVtYmVyIGJldHdlZW4gJDAkIGFuZCAkMSQuICRzZXgkIGlzIGEgQm9vbGVhbiB2YXJpYWJsZS4gSXQncyBub3QgY2xlYXIgd2hldGhlciAkMSQgbWVhbnMgIm1hbGUiIG9yICJmZW1hbGUiLiBUaGVyZSBhcmUgbWFueSBCb29sZWFuIHZhcmlhYmxlczogJG9uXF90aHlyb3hpbmUkIHRlbGxzIHVzIHdoZXRoZXIgdGhlIHBhdGllbnQgaXMgb24gW0xldm90aHlyb3hpbmVdKGh0dHA6Ly93d3cud2VibWQuY29tL2RydWdzLzIvZHJ1Zy0xMzgyNi9sLXRoeXJveGluZS1vcmFsL2RldGFpbHMpLCAkcXVlcnlcX29uXF90aHlyb3hpbmUkIHdoZXRoZXIgdGhlIHBhdGllbnQgd2FzIGFza2VkIGlmIGhlIHdhcyB0YWtpbmcgTGV2b3RoeXJveGluZSwgJGFudGl0aHlyb2lkXF9tZWRpY2F0aW9uJCB3aGV0aGVyIHRoZSBwYXRpZW50IGlzIG9uIGFueSBtZWRpY2F0aW9uIHRvIHRyZWF0IGEgdGh5cm9pZCBjb25kaXRpb24sIGFuZCBzbyBvbi4gU29tZSB0aGF0IGFyZW4ndCB0b28gb2J2aW91cywgb3IgYXQgbGVhc3Qgbm90IHRvIG1lOiAkSTEzMVxfdHJlYXRtZW50JCB0ZWxscyB1cyBpZiB0aGUgcGF0aWVudCBpcyB1bmRlciBbSTEzMSB0cmVhdG1lbnRdKGh0dHBzOi8vd3d3LnJhZGlvbG9neWluZm8ub3JnL2VuL2luZm8uY2ZtP3BnPXJhZGlvaW9kaW5lKSwgJGxpdGhpdW0kIGlmIHRoZSBwYXRpZW50IGlzIHRha2luZyBbbGl0aGl1bV0oaHR0cDovL3BzeWNoZWR1Y2F0aW9uLm9yZy90cmVhdG1lbnQvbW9vZC1zdGFiaWxpemVycy90aGUtYmlnLXRocmVlLWZvci1iaXBvbGFyLWRlcHJlc3Npb24vbGl0aGl1bS9saXRoaXVtLXJpc2tzLykgZm9yIGEgZGlmZmVyZW50IGNvbmRpdGlvbiwgJGdvaXRyZSQgd2hldGhlciB0aGUgcGF0aWVudCBleGhpYml0cyBbZ29pdGVyXShodHRwczovL2VuLndpa2lwZWRpYS5vcmcvd2lraS9Hb2l0cmUpLCBhbmQgJGh5cG9waXR1aXRhcnkkIHdoZXRoZXIgdGhlIHBhdGllbnQgaGFzIFtoeXBvcGl0dWl0YXJpc21dKGh0dHBzOi8vcGl0dWl0YXJ5c29jaWV0eS5vcmcvcGF0aWVudC1lZHVjYXRpb24vcGl0dWl0YXJ5LWRpc29yZGVycy9oeXBvcGl0dWl0YXJpc20vd2hhdC1hcmUtdGhlLXN5bXB0b21zLW9mLWh5cG9waXR1aXRhcmlzbSkuIEkgYW0gbm90IHN1cmUgd2hhdCAkcHN5Y2gkIHN0YW5kcyBmb3IuIFRoZSB2YXJpYWJsZXMgJFRTSCQsICRUMyQsICRUVDQkLCAkVDRVJCwgYW5kICRGVEkkIHJlcHJlc2VudCB0aGUgcmVzdWx0cyBvZiB2YXJpb3VzIGJsb29kIHRlc3RzIHVzZWQgdG8gc2NyZWVuIGZvciB0aHlyb2lkIHByb2JsZW1zLg0KPGJyPg0KDQpUaGUgcmVzcG9uc2UgdmFyaWFibGUgaXMgJGNsYXNzJCwgd2hpY2ggY2FuIHRha2UgJDEkIG9mICQzJCB2YWx1ZXM6ICQxJCBpcyBub3JtYWwsICQyJCBpcyBbaHlwZXJ0aHlyb2lkaXNtXShodHRwczovL2VuLndpa2lwZWRpYS5vcmcvd2lraS9IeXBlcnRoeXJvaWRpc20pLCBhbmQgJDMkIFtoeXBvdGh5cm9pZGlzbV0oaHR0cHM6Ly93d3cudGh5cm9pZC5vcmcvaHlwb3RoeXJvaWRpc20vKS4gT3ZlciAkOTBcJSQgb2YgdGhlIG9ic2VydmF0aW9ucyBhcmUgY2xhc3NpZmllZCBhcyBoeXBvdGh5cm9pZGlzbS4NCjxicj4NCg0KIyMjIDMuMiBEYXRhIG11bmdpbmcNCg0KV2UgY2FuIHN0YXJ0IGJ5IGNoYW5naW5nICRhZ2UkIHRvIG51bWJlcnMgd2UgYXJlIGZhbWlsaWFyIHdpdGgsIGFuZCBjb252ZXJ0aW5nIHRoZSByZXNwb25zZSB0byBhIGZhY3RvciB2YXJpYWJsZS4gV2Ugd2lsbCBhbHNvIGNoYW5nZSB0aGUgcmVzcG9uc2UgZnJvbSB0eXBlIGludGVnZXIgdG8gZmFjdG9yLg0KYGBge3J9DQojIENoYW5nbGUgImNsYXNzIiB0byBmYWN0b3INCnRoeXJvaWQkY2xhc3MgPC0gZmFjdG9yKHRoeXJvaWQkY2xhc3MpDQojIE11bHRpcGx5ICJhZ2UiIGJ5IDEwMA0KdGh5cm9pZCRhZ2UgPC0gMTAwICogdGh5cm9pZCRhZ2UNCmBgYA0KPGJyPg0KDQpUaGVyZSBpcyBubyBtaXNzaW5nIGRhdGEgaW4gdGhlIGRhdGFzZXQuDQoNCiMjIyAzLjQgU3BsaXR0aW5nIGludG8gdHJhaW5pbmcgYW5kIHRlc3RpbmcgZGF0YXNldHMNCg0KTGV0J3Mgc3BsaXQgdGhlIGRhdGEgaW50byB0cmFpbmluZyBhbmQgdGVzdGluZyBkYXRhIHNldHMuDQpgYGB7cn0NCmxpYnJhcnkoY2FUb29scykNCnNldC5zZWVkKDEwMDApICMgcmVwcm9kdWNpYmlsaXR5DQpzcGxpdCA8LSBzYW1wbGUuc3BsaXQodGh5cm9pZCRjbGFzcywgU3BsaXRSYXRpbyA9IDAuNykNCnRoeXJvaWRfdHJhaW4gPC0gc3Vic2V0KHRoeXJvaWQsIHNwbGl0PT1UUlVFKQ0KdGh5cm9pZF90ZXN0IDwtIHN1YnNldCh0aHlyb2lkLCBzcGxpdD09RkFMU0UpDQpgYGANCjxicj4NCg0KIyMgNC4wIERlY2lzaW9uIHRyZWVzDQoNCkxvYWRpbmcgdGhlIHJlcXVpcmVkIGxpYnJhcmllcw0KYGBge3J9DQpsaWJyYXJ5KHJwYXJ0KQ0KbGlicmFyeShycGFydC5wbG90KQ0KYGBgDQo8YnI+DQoNCk5vdyBsZXQncyBidWlsZCB0aGUgdHJlZSBhbmQgcGxvdCBpdC4gV2Ugd2lsbCB1c2UgYWxsIHRoZSBwcmVkaWN0b3JzIHRvIGJ1aWxkIHRoZSB0cmVlLiBUaGUgcGFyYW1ldGVyICptaW5idWNrZXQqIGFsbG93cyB1cyB0byBzZWxlY3QgaG93IHRoZSBtaW5pbXVtIG51bWJlciBvZiBvYnNlcnZhdGlvbnMgdGhhdCBhcmUgcGxhY2VkIGluIG9uZSBvZiB0aGUgdHJlZSdzICJsZWF2ZXMiIG9yICJidWNrZXRzIi4NCmBgYHtyfQ0KIyBCdWlsZGluZyBhIHRyZWUgd2l0aCBhIG1pbmltdW0gb2YgNTAgb2JzZXJ2YXRpb25zIG9uIGVhY2ggbGVhZg0KdGh5cm9pZF90cmVlIDwtIHJwYXJ0KGNsYXNzIH4gLiwgZGF0YSA9IHRoeXJvaWRfdHJhaW4sIGNvbnRyb2w9cnBhcnQuY29udHJvbChtaW5idWNrZXQ9NTApKQ0KcHJwKHRoeXJvaWRfdHJlZSkNCmBgYA0KPGJyPg0KDQpUaGUgdHJlZSBzcGxpdHMgb24gMyBvZiB0aGUgdmFyaWFibGVzLiBJZiB3ZSB3ZXJlIHRvIGNoYW5nZSB0aGUgKm1pbmJ1Y2tldCogcGFyYW1ldGVyLCB3ZSB3b3VsZCBnZXQgbW9yZSBvciBmZXdlciBzcGxpdHMuIFN0aWNraW5nIHdpdGggdGhpcyB0cmVlIGZvciBub3csIGxldCdzIGdldCBpdHMgYWNjdXJhY3kgb24gdGhlIHRyYWluaW5nIHNldCBpdHNlbGYuDQpgYGB7cn0NCiMgR2VuZXJhdGUgcHJlZGljdGlvbnMgb24gdHJhaW5pbmcgc2V0DQpQcmVkaWN0Q0FSVF90cmFpbiA9IHByZWRpY3QodGh5cm9pZF90cmVlLCB0eXBlID0gImNsYXNzIikNCiMgQ29uZnVzaW9uIG1hdHJpeCBvZiB0cmFpbmluZyBzZXQNCmNvbmZfbWF0cml4X3RyYWluIDwtIHRhYmxlKHRoeXJvaWRfdHJhaW4kY2xhc3MsIFByZWRpY3RDQVJUX3RyYWluKQ0KY29uZl9tYXRyaXhfdHJhaW4NCmBgYA0KPGJyPg0KDQpUaGUgcm93cyBhcmUgdGhlIGdyb3VuZCB0cnV0aCB3aGVyZWFzIHRoZSBjb2x1bW5zIGFyZSB0aGUgcHJlZGljdGlvbnMgZ2VuZXJhdGVkIGJ5IHRoZSB0cmVlLiBUaGUgdHJlZSBwZXJmZWN0bHkgcHJlZGljdHMgdGhlIG5vcm1hbCBhbmQgaHlwZXJ0aHlyb2lkaXNtIGNhc2VzIG9uIHRoZSB0cmFpbmluZyBzZXQuIFdlIGNhbiBjb21wdXRlIGl0cyBvdmVyYWxsIGFjY3VyYWN5IGFzIHRoZSByYXRpbyBvZiB0aGUgY29ycmVjdCBwcmVkaWN0aW9ucyB0byB0aGUgc2l6ZSBvZiB0aGUgZGF0YXNldC4NCjxicj4NCg0KJCQNCkFjY3VyYWN5PVxmcmFje051bWJlclwgb2ZcIG5vcm1hbCxcIGh5cGVydGh5cm9pZGlzbSxcIGFuZFwgaHlwb3RoeXJvaWRpc21cIGNhc2VzXCBjb3JyZWN0bHlcIHByZWRpY3RlZH17VG90YWxcIG51bWJlclwgb2ZcIGNhc2VzfQ0KJCQNCjxicj4NCg0KVGhlIG51bWJlciBvZiB0aGUgY29ycmVjdGx5IHByZWRpY3RlZCBjYXNlcyBpcyB0aGUgc3VtIG9mIHRoZSBkaWFnb25hbHMgb2YgdGhlIG1hdHJpeC4NCmBgYHtyfQ0Kc3VtKGRpYWcoY29uZl9tYXRyaXhfdHJhaW4pKSAvIHN1bShjb25mX21hdHJpeF90cmFpbikNCmBgYA0KPGJyPg0KDQpTbyB0aGUgYWNjdXJhY3kgaXMgb3ZlciAkOTlcJSQuIFdoYXQgaXMgdGhlIHBlcmZvcm1hbmNlIG9mIHRoaXMgdHJlZSBvbiB0aGUgdGVzdCBzZXQ/DQpgYGB7cn0NClByZWRpY3RDQVJUX3Rlc3QgPSBwcmVkaWN0KHRoeXJvaWRfdHJlZSwgbmV3ZGF0YSA9IHRoeXJvaWRfdGVzdCwgdHlwZSA9ICJjbGFzcyIpDQojIENvbmZ1c2lvbiBtYXRyaXggb2YgdGVzdCBzZXQNCmNvbmZfbWF0cml4X3Rlc3QgPC0gdGFibGUodGh5cm9pZF90ZXN0JGNsYXNzLCBQcmVkaWN0Q0FSVF90ZXN0KQ0KY29uZl9tYXRyaXhfdGVzdA0KYGBgDQo8YnI+DQoNClRoZSBhY2N1cmFjeSBvZiB0aGlzIHRyZWUgb24gdGhlIHRlc3Rpbmcgc2V0IGlzOg0KYGBge3J9DQpzdW0oZGlhZyhjb25mX21hdHJpeF90ZXN0KSkgLyBzdW0oY29uZl9tYXRyaXhfdGVzdCkNCmBgYA0KPGJyPg0KDQpUaGUgcGVyZm9ybWFuY2Ugb2YgdGhlIHRyZWUgb24gdGhlIHRlc3Qgc2V0IGlzIGp1c3QgYXMgc3Ryb25nIGFzIG9uIHRoZSB0cmFpbmluZyBzZXQuIENhbiB3ZSBkbyBldmVuIGJldHRlcj8NCg0KIyMjIDQuMSBDcm9zcy12YWxpZGF0aW9uDQoNCldlIGNhbiB1c2UgY3Jvc3MtdmFsaWRhdGlvbiB0byBvYnRhaW4gdGhlIHRyZWUgdGhhdCBtYXhpbWl6ZXMgYWNjdXJhY3kuIFRoZXJlIGlzIGEgcGFyYW1ldGVyIGNhbGxlZCAqY3AqLCBvciBjb21wbGV4aXR5IHBhcmFtZXRlciwgdGhhdCBhbGxvd3MgdXMsIGluZGlyZWN0bHksIHRvIHNwZWNpZnkgaG93IG1hbnkgb2JzZXJ2YXRpb25zIHdpbGwgYmUgcGxhY2VkIG9uIGVhY2ggbGVhZi4gV2UgY2FuIGRvIDEwLWZvbGQgY3Jvc3MtdmFsaWRhdGlvbiBmb3IgZWFjaCAqY3AqIHZhbHVlLCBjb21wdXRlIHRoZSBhdmVyYWdlIGFjY3VyYWN5IG9mIGVhY2ggKmNwKiB2YWx1ZSwgYW5kIHRoZW4gdGFrZSB0aGUgYmVzdCAqY3AqIHZhbHVlIGFuZCBidWlsZCB0aGUgZmluYWwgdHJlZSB3aXRoIGl0Lg0KPGJyPg0KDQpMZXQncyBsb2FkIHRoZSBsaWJyYXJpZXMgd2Ugd2lsbCBuZWVkIHRvIHBlcmZvcm0gdGhlIGNyb3NzLXZhbGlkYXRpb246DQpgYGB7cn0NCmxpYnJhcnkoY2FyZXQpDQpsaWJyYXJ5KGUxMDcxKQ0KYGBgDQo8YnI+DQoNCk5vdyBkZWZpbmUgdGhlIHBhcmFtZXRlcnMgb2YgdGhlIGNyb3NzLXZhbGlkYXRpb24uIFdlIHdpbGwgdHJ5IHZhbHVlcyBvZiAkY3AkIGZyb20gJDAuMDEkIHRvICQwLjUwJCwgaW4gc3RlcHMgb2YgJDAuMDEkLg0KYGBge3J9DQojIFNldHRpbmcgY3Jvc3MtdmFsaWRhdGlvbiB0byBiZSAxMC1mb2xkDQpmaXRDb250cm9sID0gdHJhaW5Db250cm9sKCBtZXRob2QgPSAiY3YiLCBudW1iZXIgPSAxMCApDQojIFNldHRpbmcgY3AgdG8gLjAxLCAuMDIsIC4uLiwgMC41DQpjYXJ0R3JpZCA9IGV4cGFuZC5ncmlkKCAuY3AgPSAoMTo1MCkqMC4wMSkgDQpgYGANCjxicj4NCg0KUGVyZm9ybSB0aGUgY3Jvc3MtdmFsaWRhdGlvbiB0byBkZXRlcm1pbmUgdGhlIGJlc3QgKmNwKiBwYXJhbWV0ZXIuIFRoZSBgdHJhaW5gIGZ1bmN0aW9uIHdpbGwsIGZvciBlYWNoIHZhbHVlIG9mICRjcCQsIHBlcmZvcm0gMTAtZm9sZCBjcm9zcy12YWxpZGF0aW9uLCBhbmQgcmVwb3J0IHRoZSBhdmVyYWdlIGFjY3VyYWN5IG9mIHRoZSAkMTAkIHJ1bnMgZm9yIHRoYXQgdmFsdWUgb2YgJGNwJC4gU2luY2UgdGhlcmUgYXJlICQ1MCQgdmFsdWVzIG9mICRjcCQgYW5kIHdlIGFyZSBwZXJmb3JtaW5nIDEwLWZvbGQgY3Jvc3MtdmFsaWRhdGlvbiBmb3IgZWFjaCBvZiB0aGVtLCBgdHJhaW4oKWAgYnVpbGRzICQ1MDAkIHRyZWVzIGFuZCBjb21wdXRlcyB0aGUgYWNjdXJhY3kgb2YgZWFjaC4gU28gaXQgdGFrZXMgYSBsaXR0bGUgd2hpbGUgdG8gcnVuLg0KYGBge3J9DQp0cmFpbihjbGFzcyB+IC4sIGRhdGEgPSB0aHlyb2lkX3RyYWluLCBtZXRob2QgPSAicnBhcnQiLCB0ckNvbnRyb2wgPSBmaXRDb250cm9sLCB0dW5lR3JpZCA9IGNhcnRHcmlkKQ0KYGBgDQo8YnI+DQoNClRoZSBgdHJhaW4oKWAgZnVuY3Rpb24gZnJvbSB0aGUgKipjYXJldCoqIHBhY2thZ2Ugc2VsZWN0cyB0aGUgYmVzdCB2YWx1ZSBvZiAqY3AqOiAkMC4wMyQuIFdlIGNhbiB0aGVuIHVzZSB0aGlzIHZhbHVlIHRvIGJ1aWxkIHRoZSBmaW5hbCB0cmVlOg0KYGBge3J9DQp0aHlyb2lkX3RyZWVfY3YgPC0gcnBhcnQoY2xhc3MgfiAuLCBkYXRhID0gdGh5cm9pZF90cmFpbiwgY29udHJvbD1ycGFydC5jb250cm9sKGNwPTAuMDMpKQ0KcHJwKHRoeXJvaWRfdHJlZV9jdikNCmBgYA0KPGJyPg0KDQpOb3cgd2UgY2FuIGNvbXB1dGUgdGhlIGFjY3VyYWN5IG9uIHRoZSB0cmFpbmluZyBzZXQgYWdhaW4uDQpgYGB7cn0NCiMgR2VuZXJhdGUgcHJlZGljdGlvbnMgb24gdHJhaW5pbmcgc2V0IHVzaW5nIGNyb3NzLXZhbGlkYXRlZCB0cmVlDQpQcmVkaWN0Q0FSVF90cmFpbl9jdiA9IHByZWRpY3QodGh5cm9pZF90cmVlX2N2LCB0eXBlID0gImNsYXNzIikNCiMgQ29uZnVzaW9uIG1hdHJpeCBvZiB0cmFpbmluZyBzZXQNCmNvbmZfbWF0cml4X3RyYWluX2N2IDwtIHRhYmxlKHRoeXJvaWRfdHJhaW4kY2xhc3MsIFByZWRpY3RDQVJUX3RyYWluX2N2KQ0KY29uZl9tYXRyaXhfdHJhaW5fY3YNCmBgYA0KPGJyPg0KDQpUaGUgYWNjdXJhY3kgb24gdGhlIHRyYWluaW5nIHNldCBpcyBldmVuIGhpZ2hlciBub3c6DQpgYGB7cn0NCnN1bShkaWFnKGNvbmZfbWF0cml4X3RyYWluX2N2KSkgLyBzdW0oY29uZl9tYXRyaXhfdHJhaW5fY3YpDQpgYGANCjxicj4NCg0KRmluYWxseSwgd2hhdCBhYm91dCB0aGUgYWNjdXJhY3kgb2YgdGhlIHRyZWUgb24gdGhlIHRlc3Qgc2V0Pw0KYGBge3J9DQojIEdlbmVyYXRlIHByZWRpY3Rpb25zIG9uIHRlc3Qgc2V0IHVzaW5nIGNyb3NzLXZhbGlkYXRlZCBzZXQNClByZWRpY3RDQVJUX3Rlc3RfY3YgPSBwcmVkaWN0KHRoeXJvaWRfdHJlZV9jdiwgbmV3ZGF0ID0gdGh5cm9pZF90ZXN0LCB0eXBlID0gImNsYXNzIikNCiMgQ29uZnVzaW9uIG1hdHJpeCBvZiB0cmFpbmluZyBzZXQNCmNvbmZfbWF0cml4X3Rlc3RfY3YgPC0gdGFibGUodGh5cm9pZF90ZXN0JGNsYXNzLCBQcmVkaWN0Q0FSVF90ZXN0X2N2KQ0KY29uZl9tYXRyaXhfdGVzdF9jdg0KYGBgDQo8YnI+DQoNCmBgYHtyfQ0Kc3VtKGRpYWcoY29uZl9tYXRyaXhfdGVzdF9jdikpIC8gc3VtKGNvbmZfbWF0cml4X3Rlc3RfY3YpDQpgYGANCjxicj4NCg0KT3V0IG9mIG1vcmUgdGhhbiAkMjEwMCQgY2FzZXMgaW4gdGhlIHRlc3Qgc2V0LCBvbmx5ICQxNCQgd2VyZSBtaXNjbGFzc2lmaWVkIGJ5IHRoZSBtb2RlbC4gSG93IGJpZyBhbiBpbXByb3ZlbWVudCBpcyB0aGlzIG92ZXIgbm90IGJvdGhlcmluZyB0byB1c2UgYW55IG1vZGVsIGF0IGFsbCBhbmQgbWVyZWx5IHByZWRpY3RpbmcgZWFjaCBjYXNlIHdpbGwgYmUgaHlwb3RoeXJvaWRpc20sIGkuZS4sICRjbGFzcz0zJD8gVGhlIHZhc3QgbWFqb3JpdHkgb2YgdGhlIGNhc2VzIGFyZSBpbmRlZWQgaHlwb3RoeXJvaWRpc20uDQpgYGB7cn0NCmh5cG90aHlyb2lkaXNtX251bSA8LSBucm93KHRoeXJvaWRfdGVzdFt0aHlyb2lkX3Rlc3QkY2xhc3MgPT0gIjMiLF0pDQp0b3RhbF9jYXNlcyA8LSBucm93KHRoeXJvaWRfdGVzdCkNCmJhc2VsaW5lX2FjY3VyYWN5IDwtIGh5cG90aHlyb2lkaXNtX251bSAvIHRvdGFsX2Nhc2VzDQpiYXNlbGluZV9hY2N1cmFjeQ0KYGBgDQo8YnI+DQoNClRoZSBuby1tb2RlbC1hdC1hbGwgYmFzZWxpbmUgYWNjdXJhY3kgb2YgJDkyLjZcJSQgaXMgJDYuOFwlJCB3b3JzZSB0aGFuIHRoYXQgb2YgdGhlIG9wdGltaXplZCB0cmVlIHdlIGJ1aWx0Lg0KDQojIyA1LjAgUmVmZXJlbmNlcw0KDQoxLiBCZXJ0c2ltYXMsIEQuLCBPJ0hhaXIsIEEuICoqKlRoZSBBbmFseXRpY3MgRWRnZSoqKi4gU3ByaW5nIDIwMTQuIFtlZFgub3JnXSh3d3cuZWRYLm9yZyku