Get Started with R

  • Install R and RStudio on your computer.
  • Create a directory for all your R work for PSL. In the future, you can save all PSL-related R script/data in this folder
  • Open RStudio, use the command setwd to enter this directory, e.g., I type the following:
setwd("~/mydoc/Course/PSL/Rcode/")

Four Examples

Next we’ll go through the 4 examples mentioned in chap 1 of ESL. Data can be found here.

1. Prostate Cancer Data

We divide the samples into training and test data, and fit a linear regression model on the training set to predict the outcome lpsa. Then use the fitted regression model to do prediction on both the training and test data. The performance is measured by the mean squared error.

The command predict takes the fitted regression model (the output from lm) and a new dataset as input and returns the prediction on this new dataset. R “remembers” features/covariates through their names, not their location in the data matrix. So to form prediction you need to provide the new dataset as a dataframe with the same column names (for the features, i.e., x’s) as the dataframe used in the command lm.

url = "https://hastie.su.domains/ElemStatLearn/datasets/prostate.data"
prostate = read.table(url)
names(prostate)  
## "lpsa" the 2nd last var is the response variable
## "train" is the indicator for training data
prostate$train
table(prostate$train)  # 67 training vs 30 testing

## Fit a linear regression model to predict lpsa
traindata = prostate[prostate$train==TRUE,]
testdata = prostate[prostate$train==FALSE,]

## Remove the "train" indicator column
traindata = within(traindata, rm(train))
testdata = within(testdata, rm(train))

myfit = lm(lpsa ~ . , data=traindata)
myfit  ## display the estimated LS coefficients
summary(myfit)  ## more output

mypredict = predict(myfit, newdata=testdata)

## mean squared error on the training and test sets. 
sum((traindata$lpsa - myfit$fitted)^2)/nrow(traindata)  
sum((testdata$lpsa - mypredict)^2)/nrow(testdata)       

2. Spam Email

The column names for the spam data is not informative, except the last column. We will assign meaningful column names for the spam data via file spam_name.txt.

We will also rename the last column (the response variable) to be Y: Y = 1 if spam, and Y = 0 if non-spam.


spam = read.table("https://liangfgithub.github.io/Data/spam.txt")
dim(spam)
names(spam)

spam.name=read.table("https://liangfgithub.github.io/Data/spam_name.txt")
for(i in 1:(ncol(spam)-1)) names(spam)[i]=as.character(spam.name[i,]);

names(spam)[ncol(spam)]="Y"

head(spam) # display the first 5 obs/rows.
spam[1:2,] # display the first 2 obs/rows

attach(spam)
table(Y)  # counts for Y=1(spam) and 0 (email)

The distribution of each feature differs between spam an email?

To answer this question, we can compute summary statistics (e.g., mean, sd, median) within spam and within email for each feature.

spam.sum = matrix(0, 57, 6)
colnames(spam.sum) = c("spam.mean", "email.mean", "spam.sd", "email.sd", "spam.med", "email.med")
rownames(spam.sum) = colnames(spam)[1:57]

for(i in 1:57){
  spam.sum[i,1] = mean(spam[Y==1,i])
  spam.sum[i,2] = mean(spam[Y==0,i])

  spam.sum[i,3] = sd(spam[Y==1,i])
  spam.sum[i,4] = sd(spam[Y==0,i])

  spam.sum[i,5] = median(spam[Y==1,i])
  spam.sum[i,6] = median(spam[Y==0,i])  
}

spam.sum  ## too many digits after the decimal point
round(spam.sum, dig=2)

spam.sum[1,]
library(ggplot2)
tmp = as.data.frame(spam[, c(1,58)])
ggplot(tmp, aes(x=Wmake, fill=as.factor(Y))) + 
  geom_density(alpha=0.2)

Next we use linear regression to classy emails as spam or non-spam: if the prediction is bigger than 0.5, classify this email as 1, and 0 otherwise.

## Extract a subset from the training data to form a test set.
## If you want to make the sampling step reproducible, 
## use set.seed function. 

set.seed(150)  
n=nrow(spam)
test.id=rep(0,n); 
test.id[sample(1:n, 500)]=1;

## I would ignore the p-values/t-stat/F-stat since
## the response Y apparently doesn't follow a normal dist

mylm=lm(Y~., data=spam[test.id !=1,]);
summary(mylm)

pred.train=ifelse(mylm$fitted>0.5,1,0)
sum(pred.train == Y[test.id==0])
train.err = 1- sum(pred.train == Y[test.id==0])/(n-500)
train.err

pred.test=predict(mylm, spam[test.id==1,]);
pred.test=ifelse(pred.test>0.5,1,0)
test.err = 1- sum(pred.test == Y[test.id==1])/500
test.err

3. Handwritten Digits

zip.train = read.table("https://liangfgithub.github.io/Data/zip.train")
zip.train = as.matrix(zip.train)
dim(zip.train)

## The first column indicate the digit
table(zip.train[,1])

Show a random sample of 12 images (arranged in a 3x4 table).

n=nrow(zip.train)
par(mfrow=c(3,4), pty='s', mar=c(1,1,1,1), xaxt='n', yaxt='n')
for(i in 1:12){
  tmp=zip.train[sample(1:n, 1), -1]; 
  tmp = tmp/max(tmp)
  tmp=matrix(tmp, 16, 16)
  image(tmp[, 16:1])
}

Make the images black-and-white. Define custom color using RColorBrewer1.

install.packages("RColorBrewer")
library(RColorBrewer)

##Color ramp def.
colors = c('white','black')
cus_col = colorRampPalette(colors=colors)

par(mfrow=c(3,4), pty='s', mar=c(1,1,1,1), xaxt='n', yaxt='n')
for(i in 1:12){
  tmp=zip.train[sample(1:n, 1), -1]; 
  tmp = tmp/max(tmp)*255
  tmp=matrix(tmp, 16, 16)
  image(tmp[, 16:1], col=cus_col(256))
}

Show the averaged image for each digit.

par(mfrow=c(3,4), pty='s', mar=c(1,1,1,1), xaxt='n', yaxt='n')
for(i in 0:9){
  tmp=zip.train[zip.train[,1]==i, -1]; 
  
  ## calculate the average image and rescale the value in each
  ## pixel to be between 0 to 255
  tmp=apply(tmp, 2, mean)
  tmp = tmp/max(tmp)*255
  
  tmp=matrix(tmp, 16, 16)
  image(tmp[, 16:1], col=cus_col(256))
}

Try “k-nearest-neighbor” in the library class to classify images in the test set.

library(class)  
?knn   ## check the help file for command "knn"

zip.test = read.table("https://liangfgithub.github.io/Data/zip.test")
zip.test = as.matrix(zip.test)

## For knn, no need to spend extra time on trianing.
## Training is done on the fly when a test point is given
## The command below may take some time to run
pred=knn(zip.train[,-1], zip.test[,-1], zip.train[,1])  

## display the classification result in a 10x10 table known as the confusion table
table(zip.test[,1], pred)  

## Compute the 0/1 test error
mytable = table(zip.test[,1], pred)
1-sum(diag(mytable))/nrow(zip.test)

4. NCI Microarray Data

http://genome-www.stanford.edu/nci60/

url = "https://hastie.su.domains/ElemStatLearn/datasets/nci.data.csv"
nci = read.table(url, sep=",",row.names=1,header=TRUE)
dim(nci)

## Create a custom color palette from green to red
cus_col = colorRampPalette(colors=c("green", "black", "red"))

## Show just the first 500 genes
image(as.matrix(nci[1:500,]), col=cus_col(64))
LS0tCnRpdGxlOiAiKFBTTCkgV2VlayAxOiBFeGFtcGxlcyBmcm9tIEVTTCIKZGF0ZTogIkZhbGwgMjAyMyIKb3V0cHV0OgogIGh0bWxfbm90ZWJvb2s6CiAgICB0aGVtZTogcmVhZGFibGUKICAgIHRvYzogVFJVRQogICAgdG9jX2Zsb2F0OiBUUlVFCi0tLQoKIyMgR2V0IFN0YXJ0ZWQgd2l0aCBSCgoqIEluc3RhbGwgW1JdKGh0dHBzOi8vY3Jhbi5yLXByb2plY3Qub3JnLykgYW5kIFtSU3R1ZGlvXShodHRwczovL3d3dy5yc3R1ZGlvLmNvbS8pIG9uIHlvdXIgY29tcHV0ZXIuCiogQ3JlYXRlIGEgZGlyZWN0b3J5IGZvciBhbGwgeW91ciBSIHdvcmsgZm9yIFBTTC4gSW4gdGhlIGZ1dHVyZSwgeW91IGNhbiBzYXZlIGFsbCBQU0wtcmVsYXRlZCBSIHNjcmlwdC9kYXRhIGluIHRoaXMgZm9sZGVyCiogT3BlbiBSU3R1ZGlvLCB1c2UgdGhlIGNvbW1hbmQgYHNldHdkYCB0byBlbnRlciB0aGlzIGRpcmVjdG9yeSwgZS5nLiwgSSB0eXBlIHRoZSBmb2xsb3dpbmc6CgpgYGB7ciwgZXZhbD1GQUxTRX0Kc2V0d2QoIn4vbXlkb2MvQ291cnNlL1BTTC9SY29kZS8iKQpgYGAKCiMjIEZvdXIgRXhhbXBsZXMKCk5leHQgd2UnbGwgZ28gdGhyb3VnaCB0aGUgNCBleGFtcGxlcyBtZW50aW9uZWQgaW4gY2hhcCAxIG9mIFtFU0xdKGh0dHA6Ly93d3ctc3RhdC5zdGFuZm9yZC5lZHUvfnRpYnMvRWxlbVN0YXRMZWFybi8pLiBEYXRhIGNhbiBiZSBmb3VuZCBbaGVyZV0oaHR0cHM6Ly9oYXN0aWUuc3UuZG9tYWlucy9FbGVtU3RhdExlYXJuL2RhdGEuaHRtbCkuIAoKIyMjIDEuIFByb3N0YXRlIENhbmNlciBEYXRhCgoKV2UgZGl2aWRlIHRoZSBzYW1wbGVzIGludG8gdHJhaW5pbmcgYW5kIHRlc3QgZGF0YSwgYW5kIGZpdCBhIGxpbmVhciByZWdyZXNzaW9uIG1vZGVsIG9uIHRoZSB0cmFpbmluZyBzZXQgdG8gcHJlZGljdCB0aGUgb3V0Y29tZSBgbHBzYWAuIFRoZW4gdXNlIHRoZSBmaXR0ZWQgcmVncmVzc2lvbiBtb2RlbCB0byBkbyBwcmVkaWN0aW9uIG9uIGJvdGggdGhlIHRyYWluaW5nIGFuZCB0ZXN0IGRhdGEuIFRoZSBwZXJmb3JtYW5jZSBpcyBtZWFzdXJlZCBieSB0aGUgbWVhbiBzcXVhcmVkIGVycm9yLiAKClRoZSBjb21tYW5kIGBwcmVkaWN0YCB0YWtlcyB0aGUgZml0dGVkIHJlZ3Jlc3Npb24gbW9kZWwgKHRoZSBvdXRwdXQgZnJvbSBgbG1gKSBhbmQgYSBuZXcgZGF0YXNldCBhcyBpbnB1dCBhbmQgcmV0dXJucyB0aGUgcHJlZGljdGlvbiBvbiB0aGlzIG5ldyBkYXRhc2V0LiBSICJyZW1lbWJlcnMiICBmZWF0dXJlcy9jb3ZhcmlhdGVzIHRocm91Z2ggdGhlaXIgbmFtZXMsIG5vdCB0aGVpciBsb2NhdGlvbiBpbiB0aGUgZGF0YSBtYXRyaXguIFNvIHRvIGZvcm0gcHJlZGljdGlvbiB5b3UgbmVlZCB0byBwcm92aWRlIHRoZSBuZXcgZGF0YXNldCBhcyBhIGRhdGFmcmFtZSB3aXRoIHRoZSBzYW1lIGNvbHVtbiBuYW1lcyAoZm9yIHRoZSBmZWF0dXJlcywgaS5lLiwgeCdzKSBhcyB0aGUgZGF0YWZyYW1lIHVzZWQgaW4gdGhlIGNvbW1hbmQgYGxtYC4KCmBgYHtyLCBldmFsPUZBTFNFfQp1cmwgPSAiaHR0cHM6Ly9oYXN0aWUuc3UuZG9tYWlucy9FbGVtU3RhdExlYXJuL2RhdGFzZXRzL3Byb3N0YXRlLmRhdGEiCnByb3N0YXRlID0gcmVhZC50YWJsZSh1cmwpCm5hbWVzKHByb3N0YXRlKSAgCiMjICJscHNhIiB0aGUgMm5kIGxhc3QgdmFyIGlzIHRoZSByZXNwb25zZSB2YXJpYWJsZQojIyAidHJhaW4iIGlzIHRoZSBpbmRpY2F0b3IgZm9yIHRyYWluaW5nIGRhdGEKcHJvc3RhdGUkdHJhaW4KdGFibGUocHJvc3RhdGUkdHJhaW4pICAjIDY3IHRyYWluaW5nIHZzIDMwIHRlc3RpbmcKCiMjIEZpdCBhIGxpbmVhciByZWdyZXNzaW9uIG1vZGVsIHRvIHByZWRpY3QgbHBzYQp0cmFpbmRhdGEgPSBwcm9zdGF0ZVtwcm9zdGF0ZSR0cmFpbj09VFJVRSxdCnRlc3RkYXRhID0gcHJvc3RhdGVbcHJvc3RhdGUkdHJhaW49PUZBTFNFLF0KCiMjIFJlbW92ZSB0aGUgInRyYWluIiBpbmRpY2F0b3IgY29sdW1uCnRyYWluZGF0YSA9IHdpdGhpbih0cmFpbmRhdGEsIHJtKHRyYWluKSkKdGVzdGRhdGEgPSB3aXRoaW4odGVzdGRhdGEsIHJtKHRyYWluKSkKCm15Zml0ID0gbG0obHBzYSB+IC4gLCBkYXRhPXRyYWluZGF0YSkKbXlmaXQgICMjIGRpc3BsYXkgdGhlIGVzdGltYXRlZCBMUyBjb2VmZmljaWVudHMKc3VtbWFyeShteWZpdCkgICMjIG1vcmUgb3V0cHV0CgpteXByZWRpY3QgPSBwcmVkaWN0KG15Zml0LCBuZXdkYXRhPXRlc3RkYXRhKQoKIyMgbWVhbiBzcXVhcmVkIGVycm9yIG9uIHRoZSB0cmFpbmluZyBhbmQgdGVzdCBzZXRzLiAKc3VtKCh0cmFpbmRhdGEkbHBzYSAtIG15Zml0JGZpdHRlZCleMikvbnJvdyh0cmFpbmRhdGEpICAKc3VtKCh0ZXN0ZGF0YSRscHNhIC0gbXlwcmVkaWN0KV4yKS9ucm93KHRlc3RkYXRhKSAgICAgICAKYGBgCgojIyMgMi4gU3BhbSBFbWFpbAoKClRoZSBjb2x1bW4gbmFtZXMgZm9yIHRoZSBzcGFtIGRhdGEgaXMgbm90IGluZm9ybWF0aXZlLCBleGNlcHQgdGhlIGxhc3QgY29sdW1uLiBXZSB3aWxsIGFzc2lnbiBtZWFuaW5nZnVsIGNvbHVtbiBuYW1lcyBmb3IgdGhlIHNwYW0gZGF0YSB2aWEgZmlsZSBgc3BhbV9uYW1lLnR4dGAuCgpXZSB3aWxsIGFsc28gcmVuYW1lIHRoZSBsYXN0IGNvbHVtbiAgKHRoZSByZXNwb25zZSB2YXJpYWJsZSkgdG8gYmUgYFlgOiAgWSA9IDEgaWYgc3BhbSwgYW5kIFkgPSAwIGlmIG5vbi1zcGFtLiAKCmBgYHtyfQoKc3BhbSA9IHJlYWQudGFibGUoImh0dHBzOi8vbGlhbmdmZ2l0aHViLmdpdGh1Yi5pby9EYXRhL3NwYW0udHh0IikKZGltKHNwYW0pCm5hbWVzKHNwYW0pCgpzcGFtLm5hbWU9cmVhZC50YWJsZSgiaHR0cHM6Ly9saWFuZ2ZnaXRodWIuZ2l0aHViLmlvL0RhdGEvc3BhbV9uYW1lLnR4dCIpCmZvcihpIGluIDE6KG5jb2woc3BhbSktMSkpIG5hbWVzKHNwYW0pW2ldPWFzLmNoYXJhY3RlcihzcGFtLm5hbWVbaSxdKTsKCm5hbWVzKHNwYW0pW25jb2woc3BhbSldPSJZIgoKaGVhZChzcGFtKSAjIGRpc3BsYXkgdGhlIGZpcnN0IDUgb2JzL3Jvd3MuCnNwYW1bMToyLF0gIyBkaXNwbGF5IHRoZSBmaXJzdCAyIG9icy9yb3dzCgphdHRhY2goc3BhbSkKdGFibGUoWSkgICMgY291bnRzIGZvciBZPTEoc3BhbSkgYW5kIDAgKGVtYWlsKQpgYGAKCgo+IFRoZSBkaXN0cmlidXRpb24gb2YgZWFjaCBmZWF0dXJlIGRpZmZlcnMgYmV0d2VlbiBzcGFtIGFuIGVtYWlsPyAKClRvIGFuc3dlciB0aGlzIHF1ZXN0aW9uLCB3ZSBjYW4gY29tcHV0ZSBzdW1tYXJ5IHN0YXRpc3RpY3MgKGUuZy4sIG1lYW4sIHNkLCBtZWRpYW4pIHdpdGhpbiBzcGFtIGFuZCB3aXRoaW4gZW1haWwgZm9yIGVhY2ggZmVhdHVyZS4gCgpgYGB7cn0Kc3BhbS5zdW0gPSBtYXRyaXgoMCwgNTcsIDYpCmNvbG5hbWVzKHNwYW0uc3VtKSA9IGMoInNwYW0ubWVhbiIsICJlbWFpbC5tZWFuIiwgInNwYW0uc2QiLCAiZW1haWwuc2QiLCAic3BhbS5tZWQiLCAiZW1haWwubWVkIikKcm93bmFtZXMoc3BhbS5zdW0pID0gY29sbmFtZXMoc3BhbSlbMTo1N10KCmZvcihpIGluIDE6NTcpewogIHNwYW0uc3VtW2ksMV0gPSBtZWFuKHNwYW1bWT09MSxpXSkKICBzcGFtLnN1bVtpLDJdID0gbWVhbihzcGFtW1k9PTAsaV0pCgogIHNwYW0uc3VtW2ksM10gPSBzZChzcGFtW1k9PTEsaV0pCiAgc3BhbS5zdW1baSw0XSA9IHNkKHNwYW1bWT09MCxpXSkKCiAgc3BhbS5zdW1baSw1XSA9IG1lZGlhbihzcGFtW1k9PTEsaV0pCiAgc3BhbS5zdW1baSw2XSA9IG1lZGlhbihzcGFtW1k9PTAsaV0pICAKfQoKc3BhbS5zdW0gICMjIHRvbyBtYW55IGRpZ2l0cyBhZnRlciB0aGUgZGVjaW1hbCBwb2ludApyb3VuZChzcGFtLnN1bSwgZGlnPTIpCgpzcGFtLnN1bVsxLF0KbGlicmFyeShnZ3Bsb3QyKQp0bXAgPSBhcy5kYXRhLmZyYW1lKHNwYW1bLCBjKDEsNTgpXSkKZ2dwbG90KHRtcCwgYWVzKHg9V21ha2UsIGZpbGw9YXMuZmFjdG9yKFkpKSkgKyAKICBnZW9tX2RlbnNpdHkoYWxwaGE9MC4yKQpgYGAKCk5leHQgd2UgdXNlIGxpbmVhciByZWdyZXNzaW9uIHRvIGNsYXNzeSBlbWFpbHMgYXMgc3BhbSBvciBub24tc3BhbTogaWYgdGhlIHByZWRpY3Rpb24gaXMgYmlnZ2VyIHRoYW4gMC41LCBjbGFzc2lmeSB0aGlzIGVtYWlsIGFzIDEsIGFuZCAwIG90aGVyd2lzZS4gCgpgYGB7cn0KIyMgRXh0cmFjdCBhIHN1YnNldCBmcm9tIHRoZSB0cmFpbmluZyBkYXRhIHRvIGZvcm0gYSB0ZXN0IHNldC4KIyMgSWYgeW91IHdhbnQgdG8gbWFrZSB0aGUgc2FtcGxpbmcgc3RlcCByZXByb2R1Y2libGUsIAojIyB1c2Ugc2V0LnNlZWQgZnVuY3Rpb24uIAoKc2V0LnNlZWQoMTUwKSAgCm49bnJvdyhzcGFtKQp0ZXN0LmlkPXJlcCgwLG4pOyAKdGVzdC5pZFtzYW1wbGUoMTpuLCA1MDApXT0xOwoKIyMgSSB3b3VsZCBpZ25vcmUgdGhlIHAtdmFsdWVzL3Qtc3RhdC9GLXN0YXQgc2luY2UKIyMgdGhlIHJlc3BvbnNlIFkgYXBwYXJlbnRseSBkb2Vzbid0IGZvbGxvdyBhIG5vcm1hbCBkaXN0CgpteWxtPWxtKFl+LiwgZGF0YT1zcGFtW3Rlc3QuaWQgIT0xLF0pOwpzdW1tYXJ5KG15bG0pCgpwcmVkLnRyYWluPWlmZWxzZShteWxtJGZpdHRlZD4wLjUsMSwwKQpzdW0ocHJlZC50cmFpbiA9PSBZW3Rlc3QuaWQ9PTBdKQp0cmFpbi5lcnIgPSAxLSBzdW0ocHJlZC50cmFpbiA9PSBZW3Rlc3QuaWQ9PTBdKS8obi01MDApCnRyYWluLmVycgoKcHJlZC50ZXN0PXByZWRpY3QobXlsbSwgc3BhbVt0ZXN0LmlkPT0xLF0pOwpwcmVkLnRlc3Q9aWZlbHNlKHByZWQudGVzdD4wLjUsMSwwKQp0ZXN0LmVyciA9IDEtIHN1bShwcmVkLnRlc3QgPT0gWVt0ZXN0LmlkPT0xXSkvNTAwCnRlc3QuZXJyCmBgYAoKCiMjIyAzLiBIYW5kd3JpdHRlbiBEaWdpdHMKCgpgYGB7cn0KemlwLnRyYWluID0gcmVhZC50YWJsZSgiaHR0cHM6Ly9saWFuZ2ZnaXRodWIuZ2l0aHViLmlvL0RhdGEvemlwLnRyYWluIikKemlwLnRyYWluID0gYXMubWF0cml4KHppcC50cmFpbikKZGltKHppcC50cmFpbikKCiMjIFRoZSBmaXJzdCBjb2x1bW4gaW5kaWNhdGUgdGhlIGRpZ2l0CnRhYmxlKHppcC50cmFpblssMV0pCmBgYAoKU2hvdyBhIHJhbmRvbSBzYW1wbGUgb2YgMTIgaW1hZ2VzIChhcnJhbmdlZCBpbiBhIDN4NCB0YWJsZSkuCgpgYGB7cn0Kbj1ucm93KHppcC50cmFpbikKcGFyKG1mcm93PWMoMyw0KSwgcHR5PSdzJywgbWFyPWMoMSwxLDEsMSksIHhheHQ9J24nLCB5YXh0PSduJykKZm9yKGkgaW4gMToxMil7CiAgdG1wPXppcC50cmFpbltzYW1wbGUoMTpuLCAxKSwgLTFdOyAKICB0bXAgPSB0bXAvbWF4KHRtcCkKICB0bXA9bWF0cml4KHRtcCwgMTYsIDE2KQogIGltYWdlKHRtcFssIDE2OjFdKQp9CmBgYAoKTWFrZSB0aGUgaW1hZ2VzIGJsYWNrLWFuZC13aGl0ZS4gRGVmaW5lIGN1c3RvbSBjb2xvciB1c2luZyBgUkNvbG9yQnJld2VyMWAuCgpgYGB7cn0KaW5zdGFsbC5wYWNrYWdlcygiUkNvbG9yQnJld2VyIikKbGlicmFyeShSQ29sb3JCcmV3ZXIpCgojI0NvbG9yIHJhbXAgZGVmLgpjb2xvcnMgPSBjKCd3aGl0ZScsJ2JsYWNrJykKY3VzX2NvbCA9IGNvbG9yUmFtcFBhbGV0dGUoY29sb3JzPWNvbG9ycykKCnBhcihtZnJvdz1jKDMsNCksIHB0eT0ncycsIG1hcj1jKDEsMSwxLDEpLCB4YXh0PSduJywgeWF4dD0nbicpCmZvcihpIGluIDE6MTIpewogIHRtcD16aXAudHJhaW5bc2FtcGxlKDE6biwgMSksIC0xXTsgCiAgdG1wID0gdG1wL21heCh0bXApKjI1NQogIHRtcD1tYXRyaXgodG1wLCAxNiwgMTYpCiAgaW1hZ2UodG1wWywgMTY6MV0sIGNvbD1jdXNfY29sKDI1NikpCn0KYGBgCgpTaG93IHRoZSBhdmVyYWdlZCBpbWFnZSBmb3IgZWFjaCBkaWdpdC4gCgpgYGB7cn0KcGFyKG1mcm93PWMoMyw0KSwgcHR5PSdzJywgbWFyPWMoMSwxLDEsMSksIHhheHQ9J24nLCB5YXh0PSduJykKZm9yKGkgaW4gMDo5KXsKICB0bXA9emlwLnRyYWluW3ppcC50cmFpblssMV09PWksIC0xXTsgCiAgCiAgIyMgY2FsY3VsYXRlIHRoZSBhdmVyYWdlIGltYWdlIGFuZCByZXNjYWxlIHRoZSB2YWx1ZSBpbiBlYWNoCiAgIyMgcGl4ZWwgdG8gYmUgYmV0d2VlbiAwIHRvIDI1NQogIHRtcD1hcHBseSh0bXAsIDIsIG1lYW4pCiAgdG1wID0gdG1wL21heCh0bXApKjI1NQogIAogIHRtcD1tYXRyaXgodG1wLCAxNiwgMTYpCiAgaW1hZ2UodG1wWywgMTY6MV0sIGNvbD1jdXNfY29sKDI1NikpCn0KYGBgCgpUcnkgImstbmVhcmVzdC1uZWlnaGJvciIgIGluIHRoZSBsaWJyYXJ5IGBjbGFzc2AgdG8gY2xhc3NpZnkgaW1hZ2VzIGluIHRoZSB0ZXN0IHNldC4gCgpgYGB7cn0KbGlicmFyeShjbGFzcykgIAo/a25uICAgIyMgY2hlY2sgdGhlIGhlbHAgZmlsZSBmb3IgY29tbWFuZCAia25uIgoKemlwLnRlc3QgPSByZWFkLnRhYmxlKCJodHRwczovL2xpYW5nZmdpdGh1Yi5naXRodWIuaW8vRGF0YS96aXAudGVzdCIpCnppcC50ZXN0ID0gYXMubWF0cml4KHppcC50ZXN0KQoKIyMgRm9yIGtubiwgbm8gbmVlZCB0byBzcGVuZCBleHRyYSB0aW1lIG9uIHRyaWFuaW5nLgojIyBUcmFpbmluZyBpcyBkb25lIG9uIHRoZSBmbHkgd2hlbiBhIHRlc3QgcG9pbnQgaXMgZ2l2ZW4KIyMgVGhlIGNvbW1hbmQgYmVsb3cgbWF5IHRha2Ugc29tZSB0aW1lIHRvIHJ1bgpwcmVkPWtubih6aXAudHJhaW5bLC0xXSwgemlwLnRlc3RbLC0xXSwgemlwLnRyYWluWywxXSkgIAoKIyMgZGlzcGxheSB0aGUgY2xhc3NpZmljYXRpb24gcmVzdWx0IGluIGEgMTB4MTAgdGFibGUga25vd24gYXMgdGhlIGNvbmZ1c2lvbiB0YWJsZQp0YWJsZSh6aXAudGVzdFssMV0sIHByZWQpICAKCiMjIENvbXB1dGUgdGhlIDAvMSB0ZXN0IGVycm9yCm15dGFibGUgPSB0YWJsZSh6aXAudGVzdFssMV0sIHByZWQpCjEtc3VtKGRpYWcobXl0YWJsZSkpL25yb3coemlwLnRlc3QpCmBgYAoKCgojIyMgNC4gTkNJIE1pY3JvYXJyYXkgRGF0YQoKCltodHRwOi8vZ2Vub21lLXd3dy5zdGFuZm9yZC5lZHUvbmNpNjAvXShbaHR0cDovL2dlbm9tZS13d3cuc3RhbmZvcmQuZWR1L25jaTYwL10pCgoKYGBge3J9CnVybCA9ICJodHRwczovL2hhc3RpZS5zdS5kb21haW5zL0VsZW1TdGF0TGVhcm4vZGF0YXNldHMvbmNpLmRhdGEuY3N2IgpuY2kgPSByZWFkLnRhYmxlKHVybCwgc2VwPSIsIixyb3cubmFtZXM9MSxoZWFkZXI9VFJVRSkKZGltKG5jaSkKCiMjIENyZWF0ZSBhIGN1c3RvbSBjb2xvciBwYWxldHRlIGZyb20gZ3JlZW4gdG8gcmVkCmN1c19jb2wgPSBjb2xvclJhbXBQYWxldHRlKGNvbG9ycz1jKCJncmVlbiIsICJibGFjayIsICJyZWQiKSkKCiMjIFNob3cganVzdCB0aGUgZmlyc3QgNTAwIGdlbmVzCmltYWdlKGFzLm1hdHJpeChuY2lbMTo1MDAsXSksIGNvbD1jdXNfY29sKDY0KSkKYGBgCg==