Material comes from : https://github.com/StatQuest/roc_and_auc_demo/blob/master/roc_and_auc_demo.R

Load helper library

library(pROC) # install with install.packages("pROC")
library(randomForest) # install with install.packages("randomForest")

Generate weight and obesity datasets

set.seed(420) # this will make my results match yours
num.samples <- 100
## genereate 100 values from a normal distribution with
## mean 172 and standard deviation 29, then sort them
weight <- sort(rnorm(n=num.samples, mean=172, sd=29))

## Now we will decide if a sample is obese or not. 
## NOTE: This method for classifying a sample as obese or not
## was made up just for this example.
## rank(weight) returns 1 for the lightest, 2 for the second lightest, ...
##              ... and it returns 100 for the heaviest.

So what we do is generate a random number between 0 and 1. Then we see if that number is less than \(rank/100\). So, for the lightest sample, \(rank = 1\). This sample will be classified “obese” if we get a random number less than \(1/100\). For the second lightest sample, \(rank = 2\), we get another random number between 0 and 1 and classify this sample “obese” if that random number is < \(2/100\). We repeat that process for all 100 samples.

obese <- ifelse(test=(runif(n=num.samples) < (rank(weight)/num.samples)), 
  yes=1, no=0)

## plot the data
plot(x=weight, y=obese)

## fit a logistic regression to the data...
glm.fit=glm(obese ~ weight, family=binomial)
lines(weight, glm.fit$fitted.values)

draw ROC and AUC using pROC

NOTE: By default, the graphs come out looking terrible The problem is that ROC graphs should be square, since the x and y axes both go from 0 to 1. However, the window in which I draw them isn’t square so extra white space is added to pad the sides.

roc(obese, glm.fit$fitted.values, plot=TRUE)

Call:
roc.default(response = obese, predictor = glm.fit$fitted.values,     plot = TRUE)

Data: glm.fit$fitted.values in 45 controls (obese 0) < 55 cases (obese 1).
Area under the curve: 0.8291

Now let’s configure R so that it prints the graph as a square.

par(pty = "s") ## pty sets the aspect ratio of the plot region. Two options:
##                "s" - creates a square plotting region
##                "m" - (the default) creates a maximal plotting region
roc(obese, glm.fit$fitted.values, plot=TRUE)

Call:
roc.default(response = obese, predictor = glm.fit$fitted.values,     plot = TRUE)

Data: glm.fit$fitted.values in 45 controls (obese 0) < 55 cases (obese 1).
Area under the curve: 0.8291

NOTE: By default, roc() uses specificity on the x-axis and the values range from 1 to 0. This makes the graph look like what we would expect, but the x-axis itself might induce a headache. To use 1-specificity (i.e. the False Positive Rate) on the x-axis, set "legacy.axes" to TRUE.

roc(obese, glm.fit$fitted.values, plot=TRUE, legacy.axes=TRUE)

Call:
roc.default(response = obese, predictor = glm.fit$fitted.values,     plot = TRUE, legacy.axes = TRUE)

Data: glm.fit$fitted.values in 45 controls (obese 0) < 55 cases (obese 1).
Area under the curve: 0.8291

If you want to rename the x and y axes…

roc(obese, glm.fit$fitted.values, plot=TRUE, legacy.axes=TRUE, percent=TRUE, xlab="False Positive Percentage", ylab="True Postive Percentage")

Call:
roc.default(response = obese, predictor = glm.fit$fitted.values,     percent = TRUE, plot = TRUE, legacy.axes = TRUE, xlab = "False Positive Percentage",     ylab = "True Postive Percentage")

Data: glm.fit$fitted.values in 45 controls (obese 0) < 55 cases (obese 1).
Area under the curve: 82.91%

We can also change the color of the ROC line, and make it wider…

roc(obese, glm.fit$fitted.values, plot=TRUE, legacy.axes=TRUE, percent=TRUE, xlab="False Positive Percentage", ylab="True Postive Percentage", col="#377eb8", lwd=4)

Call:
roc.default(response = obese, predictor = glm.fit$fitted.values,     percent = TRUE, plot = TRUE, legacy.axes = TRUE, xlab = "False Positive Percentage",     ylab = "True Postive Percentage", col = "#377eb8", lwd = 4)

Data: glm.fit$fitted.values in 45 controls (obese 0) < 55 cases (obese 1).
Area under the curve: 82.91%

Calculate AUC

If we want to find out the optimal threshold we can store the data used to make the ROC graph in a variable…

roc.info <- roc(obese, glm.fit$fitted.values, legacy.axes=TRUE)
str(roc.info)
List of 15
 $ percent           : logi FALSE
 $ sensitivities     : num [1:101] 1 1 1 1 1 ...
 $ specificities     : num [1:101] 0 0.0222 0.0444 0.0667 0.0889 ...
 $ thresholds        : num [1:101] -Inf 0.0135 0.0325 0.0525 0.0702 ...
 $ direction         : chr "<"
 $ cases             : Named num [1:55] 0.128 0.133 0.159 0.25 0.278 ...
  ..- attr(*, "names")= chr [1:55] "9" "10" "12" "23" ...
 $ controls          : Named num [1:45] 0.0129 0.0141 0.0508 0.0542 0.0862 ...
  ..- attr(*, "names")= chr [1:45] "1" "2" "3" "4" ...
 $ fun.sesp          :function (thresholds, controls, cases, direction)  
 $ auc               : 'auc' num 0.829
  ..- attr(*, "partial.auc")= logi FALSE
  ..- attr(*, "percent")= logi FALSE
  ..- attr(*, "roc")=List of 15
  .. ..$ percent           : logi FALSE
  .. ..$ sensitivities     : num [1:101] 1 1 1 1 1 ...
  .. ..$ specificities     : num [1:101] 0 0.0222 0.0444 0.0667 0.0889 ...
  .. ..$ thresholds        : num [1:101] -Inf 0.0135 0.0325 0.0525 0.0702 ...
  .. ..$ direction         : chr "<"
  .. ..$ cases             : Named num [1:55] 0.128 0.133 0.159 0.25 0.278 ...
  .. .. ..- attr(*, "names")= chr [1:55] "9" "10" "12" "23" ...
  .. ..$ controls          : Named num [1:45] 0.0129 0.0141 0.0508 0.0542 0.0862 ...
  .. .. ..- attr(*, "names")= chr [1:45] "1" "2" "3" "4" ...
  .. ..$ fun.sesp          :function (thresholds, controls, cases, direction)  
  .. ..$ auc               : 'auc' num 0.829
  .. .. ..- attr(*, "partial.auc")= logi FALSE
  .. .. ..- attr(*, "percent")= logi FALSE
  .. .. ..- attr(*, "roc")=List of 8
  .. .. .. ..$ percent      : logi FALSE
  .. .. .. ..$ sensitivities: num [1:101] 1 1 1 1 1 ...
  .. .. .. ..$ specificities: num [1:101] 0 0.0222 0.0444 0.0667 0.0889 ...
  .. .. .. ..$ thresholds   : num [1:101] -Inf 0.0135 0.0325 0.0525 0.0702 ...
  .. .. .. ..$ direction    : chr "<"
  .. .. .. ..$ cases        : Named num [1:55] 0.128 0.133 0.159 0.25 0.278 ...
  .. .. .. .. ..- attr(*, "names")= chr [1:55] "9" "10" "12" "23" ...
  .. .. .. ..$ controls     : Named num [1:45] 0.0129 0.0141 0.0508 0.0542 0.0862 ...
  .. .. .. .. ..- attr(*, "names")= chr [1:45] "1" "2" "3" "4" ...
  .. .. .. ..$ fun.sesp     :function (thresholds, controls, cases, direction)  
  .. .. .. ..- attr(*, "class")= chr "roc"
  .. ..$ call              : language roc.default(response = obese, predictor = glm.fit$fitted.values, legacy.axes = TRUE)
  .. ..$ original.predictor: Named num [1:100] 0.0129 0.0141 0.0508 0.0542 0.0862 ...
  .. .. ..- attr(*, "names")= chr [1:100] "1" "2" "3" "4" ...
  .. ..$ original.response : num [1:100] 0 0 0 0 0 0 0 0 1 1 ...
  .. ..$ predictor         : Named num [1:100] 0.0129 0.0141 0.0508 0.0542 0.0862 ...
  .. .. ..- attr(*, "names")= chr [1:100] "1" "2" "3" "4" ...
  .. ..$ response          : num [1:100] 0 0 0 0 0 0 0 0 1 1 ...
  .. ..$ levels            : chr [1:2] "0" "1"
  .. ..- attr(*, "class")= chr "roc"
 $ call              : language roc.default(response = obese, predictor = glm.fit$fitted.values, legacy.axes = TRUE)
 $ original.predictor: Named num [1:100] 0.0129 0.0141 0.0508 0.0542 0.0862 ...
  ..- attr(*, "names")= chr [1:100] "1" "2" "3" "4" ...
 $ original.response : num [1:100] 0 0 0 0 0 0 0 0 1 1 ...
 $ predictor         : Named num [1:100] 0.0129 0.0141 0.0508 0.0542 0.0862 ...
  ..- attr(*, "names")= chr [1:100] "1" "2" "3" "4" ...
 $ response          : num [1:100] 0 0 0 0 0 0 0 0 1 1 ...
 $ levels            : chr [1:2] "0" "1"
 - attr(*, "class")= chr "roc"

and then extract just the information that we want from that variable.

roc.df <- data.frame(
  tpp=roc.info$sensitivities*100, ## tpp = true positive percentage
  fpp=(1 - roc.info$specificities)*100, ## fpp = false positive precentage
  thresholds=roc.info$thresholds)

head(roc.df) ## head() will show us the values for the upper right-hand corner
             ## of the ROC graph, when the threshold is so low 
             ## (negative infinity) that every single sample is called "obese".
             ## Thus TPP = 100% and FPP = 100%

now let’s look at the thresholds between TPP 60% and 80%… We can calculate the area under the curve…

roc.df[roc.df$tpp > 60 & roc.df$tpp < 80,]
roc(obese, glm.fit$fitted.values, plot=TRUE, legacy.axes=TRUE, percent=TRUE, xlab="False Positive Percentage", ylab="True Postive Percentage", col="#377eb8", lwd=4, print.auc=TRUE)

Call:
roc.default(response = obese, predictor = glm.fit$fitted.values,     percent = TRUE, plot = TRUE, legacy.axes = TRUE, xlab = "False Positive Percentage",     ylab = "True Postive Percentage", col = "#377eb8", lwd = 4,     print.auc = TRUE)

Data: glm.fit$fitted.values in 45 controls (obese 0) < 55 cases (obese 1).
Area under the curve: 82.91%

or partial area under the curve.

roc(obese, glm.fit$fitted.values, plot=TRUE, legacy.axes=TRUE, percent=TRUE, xlab="False Positive Percentage", ylab="True Postive Percentage", col="#377eb8", lwd=4, print.auc=TRUE, print.auc.x=45, partial.auc=c(100, 90), auc.polygon = TRUE, auc.polygon.col = "#377eb822")

Call:
roc.default(response = obese, predictor = glm.fit$fitted.values,     percent = TRUE, plot = TRUE, legacy.axes = TRUE, xlab = "False Positive Percentage",     ylab = "True Postive Percentage", col = "#377eb8", lwd = 4,     print.auc = TRUE, print.auc.x = 45, partial.auc = c(100,         90), auc.polygon = TRUE, auc.polygon.col = "#377eb822")

Data: glm.fit$fitted.values in 45 controls (obese 0) < 55 cases (obese 1).
Partial area under the curve (specificity 100%-90%): 4.727%

Try random forest model

rf.model <- randomForest(factor(obese) ~ weight)

## ROC for random forest
roc(obese, rf.model$votes[,1], plot=TRUE, legacy.axes=TRUE, percent=TRUE, xlab="False Positive Percentage", ylab="True Postive Percentage", col="#4daf4a", lwd=4, print.auc=TRUE)

Call:
roc.default(response = obese, predictor = rf.model$votes[, 1],     percent = TRUE, plot = TRUE, legacy.axes = TRUE, xlab = "False Positive Percentage",     ylab = "True Postive Percentage", col = "#4daf4a", lwd = 4,     print.auc = TRUE)

Data: rf.model$votes[, 1] in 45 controls (obese 0) > 55 cases (obese 1).
Area under the curve: 79.78%

Compare two model

par(pty = "s")
roc(obese, glm.fit$fitted.values, plot=TRUE, legacy.axes=TRUE, percent=TRUE, xlab="False Positive Percentage", ylab="True Postive Percentage", col="#377eb8", lwd=4, print.auc=TRUE)

Call:
roc.default(response = obese, predictor = glm.fit$fitted.values,     percent = TRUE, plot = TRUE, legacy.axes = TRUE, xlab = "False Positive Percentage",     ylab = "True Postive Percentage", col = "#377eb8", lwd = 4,     print.auc = TRUE)

Data: glm.fit$fitted.values in 45 controls (obese 0) < 55 cases (obese 1).
Area under the curve: 82.91%
plot.roc(obese, rf.model$votes[,1], percent=TRUE, col="#4daf4a", lwd=4, print.auc=TRUE, add=TRUE, print.auc.y=40)
legend("bottomright", legend=c("Logisitic Regression", "Random Forest"), col=c("#377eb8", "#4daf4a"), lwd=4)

LS0tDQp0aXRsZTogIlJPQyBBbmQgQVVDIEV4YW1wbGUiDQphdXRob3I6ICJZYWxpbiBZYW5nIg0KZGF0ZTogImByIFN5cy5EYXRlKClgIg0Kb3V0cHV0Og0KICBodG1sX25vdGVib29rOg0KICAgIHRvYzogVFJVRQ0KICAgIHRvY19mbG9hdDogVFJVRQ0KICB3b3JkX2RvY3VtZW50Og0KICAgIHRvYzogbm8NCiAgICB0b2NfZGVwdGg6ICczJw0KLS0tDQoNCk1hdGVyaWFsIGNvbWVzIGZyb20gOiBbaHR0cHM6Ly9naXRodWIuY29tL1N0YXRRdWVzdC9yb2NfYW5kX2F1Y19kZW1vL2Jsb2IvbWFzdGVyL3JvY19hbmRfYXVjX2RlbW8uUl0oaHR0cHM6Ly9naXRodWIuY29tL1N0YXRRdWVzdC9yb2NfYW5kX2F1Y19kZW1vL2Jsb2IvbWFzdGVyL3JvY19hbmRfYXVjX2RlbW8uUikNCg0KIyMgTG9hZCBoZWxwZXIgbGlicmFyeQ0KDQpgYGB7ciB3YXJuaW5nPUZBTFNFfQ0KbGlicmFyeShwUk9DKSAjIGluc3RhbGwgd2l0aCBpbnN0YWxsLnBhY2thZ2VzKCJwUk9DIikNCmxpYnJhcnkocmFuZG9tRm9yZXN0KSAjIGluc3RhbGwgd2l0aCBpbnN0YWxsLnBhY2thZ2VzKCJyYW5kb21Gb3Jlc3QiKQ0KYGBgDQoNCg0KIyMgR2VuZXJhdGUgd2VpZ2h0IGFuZCBvYmVzaXR5IGRhdGFzZXRzDQoNCmBgYHtyfQ0Kc2V0LnNlZWQoNDIwKSAjIHRoaXMgd2lsbCBtYWtlIG15IHJlc3VsdHMgbWF0Y2ggeW91cnMNCm51bS5zYW1wbGVzIDwtIDEwMA0KIyMgZ2VuZXJlYXRlIDEwMCB2YWx1ZXMgZnJvbSBhIG5vcm1hbCBkaXN0cmlidXRpb24gd2l0aA0KIyMgbWVhbiAxNzIgYW5kIHN0YW5kYXJkIGRldmlhdGlvbiAyOSwgdGhlbiBzb3J0IHRoZW0NCndlaWdodCA8LSBzb3J0KHJub3JtKG49bnVtLnNhbXBsZXMsIG1lYW49MTcyLCBzZD0yOSkpDQoNCiMjIE5vdyB3ZSB3aWxsIGRlY2lkZSBpZiBhIHNhbXBsZSBpcyBvYmVzZSBvciBub3QuIA0KIyMgTk9URTogVGhpcyBtZXRob2QgZm9yIGNsYXNzaWZ5aW5nIGEgc2FtcGxlIGFzIG9iZXNlIG9yIG5vdA0KIyMgd2FzIG1hZGUgdXAganVzdCBmb3IgdGhpcyBleGFtcGxlLg0KIyMgcmFuayh3ZWlnaHQpIHJldHVybnMgMSBmb3IgdGhlIGxpZ2h0ZXN0LCAyIGZvciB0aGUgc2Vjb25kIGxpZ2h0ZXN0LCAuLi4NCiMjICAgICAgICAgICAgICAuLi4gYW5kIGl0IHJldHVybnMgMTAwIGZvciB0aGUgaGVhdmllc3QuDQpgYGANCg0KU28gd2hhdCB3ZSBkbyBpcyBnZW5lcmF0ZSBhIHJhbmRvbSBudW1iZXIgYmV0d2VlbiAwIGFuZCAxLiANClRoZW4gd2Ugc2VlIGlmIHRoYXQgbnVtYmVyIGlzIGxlc3MgdGhhbiAkcmFuay8xMDAkLiBTbywgZm9yIHRoZSBsaWdodGVzdCBzYW1wbGUsICRyYW5rID0gMSQuIFRoaXMgc2FtcGxlIHdpbGwgYmUgY2xhc3NpZmllZCAib2Jlc2UiIGlmIHdlIGdldCBhIHJhbmRvbSBudW1iZXIgbGVzcyB0aGFuICQxLzEwMCQuIA0KRm9yIHRoZSBzZWNvbmQgbGlnaHRlc3Qgc2FtcGxlLCAkcmFuayA9IDIkLCB3ZSBnZXQgYW5vdGhlciByYW5kb20gbnVtYmVyIGJldHdlZW4gMCBhbmQgMSBhbmQgY2xhc3NpZnkgdGhpcyBzYW1wbGUgIm9iZXNlIiBpZiB0aGF0IHJhbmRvbSBudW1iZXIgaXMgPCAkMi8xMDAkLiBXZSByZXBlYXQgdGhhdCBwcm9jZXNzIGZvciBhbGwgMTAwIHNhbXBsZXMuDQoNCmBgYHtyfQ0Kb2Jlc2UgPC0gaWZlbHNlKHRlc3Q9KHJ1bmlmKG49bnVtLnNhbXBsZXMpIDwgKHJhbmsod2VpZ2h0KS9udW0uc2FtcGxlcykpLCANCiAgeWVzPTEsIG5vPTApDQoNCiMjIHBsb3QgdGhlIGRhdGENCnBsb3QoeD13ZWlnaHQsIHk9b2Jlc2UpDQoNCiMjIGZpdCBhIGxvZ2lzdGljIHJlZ3Jlc3Npb24gdG8gdGhlIGRhdGEuLi4NCmdsbS5maXQ9Z2xtKG9iZXNlIH4gd2VpZ2h0LCBmYW1pbHk9Ymlub21pYWwpDQpsaW5lcyh3ZWlnaHQsIGdsbS5maXQkZml0dGVkLnZhbHVlcykNCmBgYA0KDQojIyBkcmF3IFJPQyBhbmQgQVVDIHVzaW5nIHBST0MNCg0KTk9URTogQnkgZGVmYXVsdCwgdGhlIGdyYXBocyBjb21lIG91dCBsb29raW5nIHRlcnJpYmxlIA0KVGhlIHByb2JsZW0gaXMgdGhhdCBST0MgZ3JhcGhzIHNob3VsZCBiZSBzcXVhcmUsIHNpbmNlIHRoZSB4IGFuZCB5IGF4ZXMgYm90aCBnbyBmcm9tIDAgdG8gMS4gDQpIb3dldmVyLCB0aGUgd2luZG93IGluIHdoaWNoIEkgZHJhdyB0aGVtIGlzbid0IHNxdWFyZSBzbyBleHRyYSB3aGl0ZSBzcGFjZSBpcyBhZGRlZCB0byBwYWQgdGhlIHNpZGVzLg0KDQpgYGB7ciBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQ0Kcm9jKG9iZXNlLCBnbG0uZml0JGZpdHRlZC52YWx1ZXMsIHBsb3Q9VFJVRSkNCmBgYA0KDQpOb3cgbGV0J3MgY29uZmlndXJlIFIgc28gdGhhdCBpdCBwcmludHMgdGhlIGdyYXBoIGFzIGEgc3F1YXJlLg0KDQpgYGB7ciBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQ0KcGFyKHB0eSA9ICJzIikgIyMgcHR5IHNldHMgdGhlIGFzcGVjdCByYXRpbyBvZiB0aGUgcGxvdCByZWdpb24uIFR3byBvcHRpb25zOg0KIyMgICAgICAgICAgICAgICAgInMiIC0gY3JlYXRlcyBhIHNxdWFyZSBwbG90dGluZyByZWdpb24NCiMjICAgICAgICAgICAgICAgICJtIiAtICh0aGUgZGVmYXVsdCkgY3JlYXRlcyBhIG1heGltYWwgcGxvdHRpbmcgcmVnaW9uDQpyb2Mob2Jlc2UsIGdsbS5maXQkZml0dGVkLnZhbHVlcywgcGxvdD1UUlVFKQ0KYGBgDQpOT1RFOiBCeSBkZWZhdWx0LCByb2MoKSB1c2VzIHNwZWNpZmljaXR5IG9uIHRoZSB4LWF4aXMgYW5kIHRoZSB2YWx1ZXMgcmFuZ2UgZnJvbSAxIHRvIDAuIA0KVGhpcyBtYWtlcyB0aGUgZ3JhcGggbG9vayBsaWtlIHdoYXQgd2Ugd291bGQgZXhwZWN0LCBidXQgdGhlIHgtYXhpcyBpdHNlbGYgbWlnaHQgaW5kdWNlIGEgaGVhZGFjaGUuDQpUbyB1c2UgKioxLXNwZWNpZmljaXR5IChpLmUuIHRoZSBGYWxzZSBQb3NpdGl2ZSBSYXRlKSoqIG9uIHRoZSB4LWF4aXMsIHNldCBgImxlZ2FjeS5heGVzIiB0byBUUlVFYC4NCg0KYGBge3IgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0NCnJvYyhvYmVzZSwgZ2xtLmZpdCRmaXR0ZWQudmFsdWVzLCBwbG90PVRSVUUsIGxlZ2FjeS5heGVzPVRSVUUpDQpgYGANCklmIHlvdSB3YW50IHRvIHJlbmFtZSB0aGUgeCBhbmQgeSBheGVzLi4uDQoNCmBgYHtyIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9DQpyb2Mob2Jlc2UsIGdsbS5maXQkZml0dGVkLnZhbHVlcywgcGxvdD1UUlVFLCBsZWdhY3kuYXhlcz1UUlVFLCBwZXJjZW50PVRSVUUsIHhsYWI9IkZhbHNlIFBvc2l0aXZlIFBlcmNlbnRhZ2UiLCB5bGFiPSJUcnVlIFBvc3RpdmUgUGVyY2VudGFnZSIpDQpgYGANCg0KV2UgY2FuIGFsc28gY2hhbmdlIHRoZSBjb2xvciBvZiB0aGUgUk9DIGxpbmUsIGFuZCBtYWtlIGl0IHdpZGVyLi4uDQoNCmBgYHtyIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9DQpyb2Mob2Jlc2UsIGdsbS5maXQkZml0dGVkLnZhbHVlcywgcGxvdD1UUlVFLCBsZWdhY3kuYXhlcz1UUlVFLCBwZXJjZW50PVRSVUUsIHhsYWI9IkZhbHNlIFBvc2l0aXZlIFBlcmNlbnRhZ2UiLCB5bGFiPSJUcnVlIFBvc3RpdmUgUGVyY2VudGFnZSIsIGNvbD0iIzM3N2ViOCIsIGx3ZD00KQ0KYGBgDQoNCiMjIENhbGN1bGF0ZSBBVUMNCg0KSWYgd2Ugd2FudCB0byBmaW5kIG91dCB0aGUgb3B0aW1hbCB0aHJlc2hvbGQgd2UgY2FuIHN0b3JlIHRoZSBkYXRhIHVzZWQgdG8gbWFrZSB0aGUgUk9DIGdyYXBoIGluIGEgdmFyaWFibGUuLi4NCg0KYGBge3IgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0NCnJvYy5pbmZvIDwtIHJvYyhvYmVzZSwgZ2xtLmZpdCRmaXR0ZWQudmFsdWVzLCBsZWdhY3kuYXhlcz1UUlVFKQ0Kc3RyKHJvYy5pbmZvKQ0KYGBgDQoNCmFuZCB0aGVuIGV4dHJhY3QganVzdCB0aGUgaW5mb3JtYXRpb24gdGhhdCB3ZSB3YW50IGZyb20gdGhhdCB2YXJpYWJsZS4NCg0KYGBge3J9DQpyb2MuZGYgPC0gZGF0YS5mcmFtZSgNCiAgdHBwPXJvYy5pbmZvJHNlbnNpdGl2aXRpZXMqMTAwLCAjIyB0cHAgPSB0cnVlIHBvc2l0aXZlIHBlcmNlbnRhZ2UNCiAgZnBwPSgxIC0gcm9jLmluZm8kc3BlY2lmaWNpdGllcykqMTAwLCAjIyBmcHAgPSBmYWxzZSBwb3NpdGl2ZSBwcmVjZW50YWdlDQogIHRocmVzaG9sZHM9cm9jLmluZm8kdGhyZXNob2xkcykNCg0KaGVhZChyb2MuZGYpICMjIGhlYWQoKSB3aWxsIHNob3cgdXMgdGhlIHZhbHVlcyBmb3IgdGhlIHVwcGVyIHJpZ2h0LWhhbmQgY29ybmVyDQogICAgICAgICAgICAgIyMgb2YgdGhlIFJPQyBncmFwaCwgd2hlbiB0aGUgdGhyZXNob2xkIGlzIHNvIGxvdyANCiAgICAgICAgICAgICAjIyAobmVnYXRpdmUgaW5maW5pdHkpIHRoYXQgZXZlcnkgc2luZ2xlIHNhbXBsZSBpcyBjYWxsZWQgIm9iZXNlIi4NCiAgICAgICAgICAgICAjIyBUaHVzIFRQUCA9IDEwMCUgYW5kIEZQUCA9IDEwMCUNCmBgYA0KDQpub3cgbGV0J3MgbG9vayBhdCB0aGUgdGhyZXNob2xkcyBiZXR3ZWVuIFRQUCA2MCUgYW5kIDgwJS4uLg0KV2UgY2FuIGNhbGN1bGF0ZSB0aGUgYXJlYSB1bmRlciB0aGUgY3VydmUuLi4NCg0KYGBge3IgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0NCnJvYy5kZltyb2MuZGYkdHBwID4gNjAgJiByb2MuZGYkdHBwIDwgODAsXQ0Kcm9jKG9iZXNlLCBnbG0uZml0JGZpdHRlZC52YWx1ZXMsIHBsb3Q9VFJVRSwgbGVnYWN5LmF4ZXM9VFJVRSwgcGVyY2VudD1UUlVFLCB4bGFiPSJGYWxzZSBQb3NpdGl2ZSBQZXJjZW50YWdlIiwgeWxhYj0iVHJ1ZSBQb3N0aXZlIFBlcmNlbnRhZ2UiLCBjb2w9IiMzNzdlYjgiLCBsd2Q9NCwgcHJpbnQuYXVjPVRSVUUpDQpgYGANCm9yIHBhcnRpYWwgYXJlYSB1bmRlciB0aGUgY3VydmUuDQoNCmBgYHtyIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9DQpyb2Mob2Jlc2UsIGdsbS5maXQkZml0dGVkLnZhbHVlcywgcGxvdD1UUlVFLCBsZWdhY3kuYXhlcz1UUlVFLCBwZXJjZW50PVRSVUUsIHhsYWI9IkZhbHNlIFBvc2l0aXZlIFBlcmNlbnRhZ2UiLCB5bGFiPSJUcnVlIFBvc3RpdmUgUGVyY2VudGFnZSIsIGNvbD0iIzM3N2ViOCIsIGx3ZD00LCBwcmludC5hdWM9VFJVRSwgcHJpbnQuYXVjLng9NDUsIHBhcnRpYWwuYXVjPWMoMTAwLCA5MCksIGF1Yy5wb2x5Z29uID0gVFJVRSwgYXVjLnBvbHlnb24uY29sID0gIiMzNzdlYjgyMiIpDQpgYGANCg0KIyMgVHJ5IHJhbmRvbSBmb3Jlc3QgbW9kZWwNCg0KYGBge3IgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0NCnJmLm1vZGVsIDwtIHJhbmRvbUZvcmVzdChmYWN0b3Iob2Jlc2UpIH4gd2VpZ2h0KQ0KDQojIyBST0MgZm9yIHJhbmRvbSBmb3Jlc3QNCnJvYyhvYmVzZSwgcmYubW9kZWwkdm90ZXNbLDFdLCBwbG90PVRSVUUsIGxlZ2FjeS5heGVzPVRSVUUsIHBlcmNlbnQ9VFJVRSwgeGxhYj0iRmFsc2UgUG9zaXRpdmUgUGVyY2VudGFnZSIsIHlsYWI9IlRydWUgUG9zdGl2ZSBQZXJjZW50YWdlIiwgY29sPSIjNGRhZjRhIiwgbHdkPTQsIHByaW50LmF1Yz1UUlVFKQ0KYGBgDQoNCiMjIENvbXBhcmUgdHdvIG1vZGVsDQoNCmBgYHtyIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9DQpwYXIocHR5ID0gInMiKQ0Kcm9jKG9iZXNlLCBnbG0uZml0JGZpdHRlZC52YWx1ZXMsIHBsb3Q9VFJVRSwgbGVnYWN5LmF4ZXM9VFJVRSwgcGVyY2VudD1UUlVFLCB4bGFiPSJGYWxzZSBQb3NpdGl2ZSBQZXJjZW50YWdlIiwgeWxhYj0iVHJ1ZSBQb3N0aXZlIFBlcmNlbnRhZ2UiLCBjb2w9IiMzNzdlYjgiLCBsd2Q9NCwgcHJpbnQuYXVjPVRSVUUpDQoNCnBsb3Qucm9jKG9iZXNlLCByZi5tb2RlbCR2b3Rlc1ssMV0sIHBlcmNlbnQ9VFJVRSwgY29sPSIjNGRhZjRhIiwgbHdkPTQsIHByaW50LmF1Yz1UUlVFLCBhZGQ9VFJVRSwgcHJpbnQuYXVjLnk9NDApDQpsZWdlbmQoImJvdHRvbXJpZ2h0IiwgbGVnZW5kPWMoIkxvZ2lzaXRpYyBSZWdyZXNzaW9uIiwgIlJhbmRvbSBGb3Jlc3QiKSwgY29sPWMoIiMzNzdlYjgiLCAiIzRkYWY0YSIpLCBsd2Q9NCkNCmBgYA0KDQoNCg0KDQoNCg0K