Machine Learning Series Intro

All posts from this series can be found at here.

During this time at home that most of us have been forced into I’ve decided start retooling my arsenal as a statistician. After getting my Master’s in Applied Statistics, I’ve spent the past 7 years working in marketing analytics. I’ve kept some aspects of these skills sharp, in particular linear regression and coding in R (building shiny apps and learning/developing tidyverse toolbox). But it’s time to both refresh and learn some new statistical techniques.

I decided to dive into machine learning and do that in R. It’s a topic where I have little exposure, but have a lot to learn. After coming across the book Machine Learning with R, the tidyverse, and mlr by Hefin Rhys I decided to buy it and make the leap. I’m not far into the book, but so far it’s been really easy to follow along and does a great job going over the tidyverse.

My goal is to go through the entire book chapter by chapter, and as I’m a big baseball nerd, create small projects with baseball data for each algorithm.


kNN Project: Predicting which MiLB hitters from A+ in 2013 made it to MLB


After going through the entire chapter on k-nearest neighbors (kNN), I decided to use kNN to build a model to predict which minor league players from high-A (A+) in 2013 ended up making a major league debut. I used a 100 plate appearance (PA) cutoff to be included in the data.

I settled on 4 predictors, one being the seasonal age of the player (Age) and the others being three rate statistics in strikeout percentage (K%), walk percentage (BB%) and isolated slugging percentage (ISO). The performance stats are typically decent indicators of player performance, while age plays a big factor in projecting talent. Additionally, the three performance metrics shouldn’t have much covariance between them.

I pulled the data from fangraphs and the leaderboard can be accessed here.

library("tidyverse")
library("mlr")
MiLB2013 <- read_csv("data/MiLB_HighA_2013.csv")
# Remove the player names from the data
MiLB2013knn <- select(MiLB2013,-Name)
summary(MiLB2013knn)
      Age              BB                 K                ISO               MLB           
 Min.   :19.00   Min.   :0.009901   Min.   :0.05991   Min.   :0.009615   Length:461        
 1st Qu.:22.00   1st Qu.:0.065089   1st Qu.:0.15353   1st Qu.:0.085308   Class :character  
 Median :23.00   Median :0.085389   Median :0.19312   Median :0.125767   Mode  :character  
 Mean   :22.88   Mean   :0.087832   Mean   :0.19784   Mean   :0.130792                     
 3rd Qu.:24.00   3rd Qu.:0.107784   3rd Qu.:0.23709   3rd Qu.:0.174545                     
 Max.   :29.00   Max.   :0.204409   Max.   :0.40415   Max.   :0.325243                     
MiLB2013knn %>% 
  group_by(MLB) %>%
  summarise(n = n()) %>%
  mutate(freq = n / sum(n))

To figure out what k value is optimal for the kNN model, I did hyperparameter tuning for k. I expected k to be a bit lower initially and started across the set of k = 1:10. When k=10 was the best performing, I expanded it out to k=25. Again, k=25 came back as the best performing and then opened it up to 60 as it shows below.

# Task
MiLBTask <- makeClassifTask(data = MiLB2013knn, target = "MLB")
Provided data is not a pure data.frame but from class tbl_df, hence it will be converted.
# hyperarameter tuning
knnParamSpace <- makeParamSet(makeDiscreteParam("k", values = 1:60))
gridSearch <- makeTuneControlGrid()
cvForTuning <- makeResampleDesc("RepCV", folds = 10, reps = 20)
tunedMiLB <- tuneParams("classif.knn", task = MiLBTask,
                     resampling = cvForTuning,
                     par.set = knnParamSpace, control = gridSearch)
tunedMiLB
Tune result:
Op. pars: k=35
mmce.test.mean=0.1906707
tunedMiLB$x
$k
[1] 35
knnTuningData <- generateHyperParsEffectData(tunedMiLB)
plotHyperParsEffect(knnTuningData, x = "k", y = "mmce.test.mean",
                    plot.type = "line") +
  theme_bw()

The value of k that minimizes the mean mmce is 33. The graph above displays the mmce mean for all values of k between 1 and 60. While 33 is the minimum value on the graph, a potential range of k between 28 to 35 could work and in another iteration, k=35 had the lowest value.

I expected a lower optimum value for k because the data only has 461 observations.

tunedKnnMiLB <- setHyperPars(makeLearner("classif.knn"),
                         par.vals = tunedMiLB$x)

tunedKnnModelMiLB <- train(tunedKnnMiLB, MiLBTask)



Model Performance

To evaluate performance I used K-fold cross validation with 5 folds and 50 repetitions and started with the tuned k=33.

knn <- makeLearner("classif.knn", par.vals = list("k"=33))
knnModel <- train(knn, MiLBTask)
kFold <- makeResampleDesc(method = "RepCV", folds = 5, reps = 50,
                          stratify = TRUE)
kFoldCV <- resample(learner = knn, task = MiLBTask,
                    resampling = kFold, measures = list(mmce, acc), show.info = FALSE)
kFoldCV$aggr
mmce.test.mean  acc.test.mean 
     0.1923622      0.8076378 
calculateConfusionMatrix(kFoldCV$pred, relative = TRUE)
Relative confusion matrix (normalized by row/column):
        predicted
true     No        Yes       -err.-   
  No     0.96/0.82 0.04/0.28 0.04     
  Yes    0.66/0.18 0.34/0.72 0.66     
  -err.-      0.18      0.28 0.19     


Absolute confusion matrix:
        predicted
true        No  Yes -err.-
  No     16720  730    730
  Yes     3704 1896   3704
  -err.-  3704  730   4434

The confusion matrix above shows that “No” MLB predictions were correct 82% of the time and “Yes” MLB predictions were correct 72% of the time.

However, of the players that did make it to MLB, it was wrong on their classification 66% of the time.

So the model seems to be conservative when predicting that a player will make it to MLB. The data had 24% of players make it to MLB and in the cross validation, it only predicted “Yes” 11% of the time.

Ultimately the model does a pretty good job of identifying which players won’t make MLB but a horrible job identifying who ultimately does make MLB.

Because the “Yes” error was so high, I wondered if a lower k value, might trade off some of the overall model error for a bit better “Yes” MLB result. Looking back at the hyperparameters plot, there was a local minimum under 20, so I tested out the model with k=17.

knn2 <- makeLearner("classif.knn", par.vals = list("k"=17))
knnModel2 <- train(knn2, MiLBTask)
# Use same CV as above
# kFold <- makeResampleDesc(method = "RepCV", folds = 5, reps = 50,
#                          stratify = TRUE)
kFoldCV2 <- resample(learner = knn2, task = MiLBTask,
                    resampling = kFold, measures = list(mmce, acc), show.info = FALSE)
kFoldCV2$aggr
mmce.test.mean  acc.test.mean 
     0.1995227      0.8004773 
calculateConfusionMatrix(kFoldCV2$pred, relative = TRUE)
Relative confusion matrix (normalized by row/column):
        predicted
true     No        Yes       -err.-   
  No     0.93/0.83 0.07/0.35 0.07     
  Yes    0.61/0.17 0.39/0.65 0.61     
  -err.-      0.17      0.35 0.20     


Absolute confusion matrix:
        predicted
true        No  Yes -err.-
  No     16242 1208   1208
  Yes     3391 2209   3391
  -err.-  3391 1208   4599

The result here is mixed. When the model predicted that a player made MLB, it was only correct 64% of the time, down from 72% from the first model. However, looking at the players that did make it to MLB, the model correctly classified 40%, up from 34% in the first model.

The number of total players that this model predicted would make MLB is 15%, up from 11% from the first model. This is still well short of the 24% mark that did make MLB.

Which model is best depends on your goals. I’m leaning toward the second model because it’s making a closer attempt at predicting how many players actually made it, even if it’s wrong more often. Model 1 predicted correctly that 38 players would make MLB on average, while model 2 correctly predicted 44 players on average. Again, considering 112 players from 2013 made it up, that’s not a great model!



Predictions

Even though neither model did a great job in cross validation and the models use all 2013 data, I wanted to explore predicting some players from 2014 using our models.

I selected players that were ranked 26-30 in wRC+ in 2014. These players were Carlos Correa, Derrick Chung, Tyler White, Kyle Waldrop and Daniel Carbonell. We have a major league regular that debuted at a young age in Correa, a quad-A hitter in Tyler White, a cup of coffee MLB player in Waldrop and two other players that never made it up to the bigs.

# data for Carlos Correa, Derrick Chung, Tyler White, Kyle Waldrop, Daniel Carbonell
newMiLBdata <- tibble(Age = c(19, 26, 23, 22, 23),
                      BB = c(.123, .095, .151, .076, .06),
                      K = c(.154, .1, .145, .194, .19),
                      ISO = c(.185, .118, .260, .156, .194))
# Using k=33
newMiLBPred1 <- predict(knnModel, newdata = newMiLBdata)
Provided data for prediction is not a pure data.frame but from class tbl_df, hence it will be converted.
getPredictionResponse(newMiLBPred1)
[1] Yes No  No  No  No 
Levels: No Yes
# Using k=17
newMiLBPred2 <- predict(knnModel2, newdata = newMiLBdata)
Provided data for prediction is not a pure data.frame but from class tbl_df, hence it will be converted.
getPredictionResponse(newMiLBPred2)
[1] Yes No  Yes Yes No 
Levels: No Yes

The first kNN model with k=33 only predicted Carlos Correa to make MLB out of the 5 players. In past results it also correctly selected Kyle Waldrop to make it to MLB. The conservative nature of handing out “yes” MLB predictions is present in our small sample set. In both iterations, the model was wrong on Tyler White. Given that his stats were all superior to Waldrop (who was predicted correctly in a past iteration) - it points to age potentially having a large impact in the kNN model.

The second kNN model with k=17 correctly predicted the playing futures of all 5 players. Given that the model on average was only correctly predicting MLB players 62% of the time, the results here are either (a) lucky or (b) biased based on my selection of high performing 2014 players (ranked 26-30 in wRC+).



Closing Thoughts

While kNN was easy to implement and fun to test out on this question, it may not be the best algorithm for predicting which players would make MLB. It’s easy to identify who won’t make it, but it’s a lot harder to identify who does! It would also be nice to have information about the variables used in the model - like which one is most closely related to making it to the big leagues, which is something kNN doesn’t offer.

I also wanted to note that a decent amount of error may have been introduced given the sample of players needing only 100 plate appearances to be included in the data. It’s well known that any player can get “hot” or “cold” for 100+ PA - see our 2014 example of Derrick Chung! Ideally we’d be looking at more data than just A+ performance to gauge their full abilities.

I’ll be looking to use this data in future algorithms - including in the next chapter on logistic regression!



LS0tCnRpdGxlOiAiUG9zdCAxOiBrLU5lYXJlc3QgTmVpZ2hib3JzIEFsZ29yaXRobSIKZGF0ZTogImByIGZvcm1hdChTeXMudGltZSgpLCAnJWQgJUIgJVknKWAiCm91dHB1dDogaHRtbF9ub3RlYm9vawotLS0KPGJyLz48YnIvPgoqX19NYWNoaW5lIExlYXJuaW5nIFNlcmllcyBJbnRyb19fKgoKKkFsbCBwb3N0cyBmcm9tIHRoaXMgc2VyaWVzIGNhbiBiZSBmb3VuZCBhdCBbaGVyZV0oaHR0cHM6Ly9zbWFkYXBsYXlzZmFudGFzeS5jb20vbWxyLykuKgoKKkR1cmluZyB0aGlzIHRpbWUgYXQgaG9tZSB0aGF0IG1vc3Qgb2YgdXMgaGF2ZSBiZWVuIGZvcmNlZCBpbnRvIEkndmUgZGVjaWRlZCBzdGFydCByZXRvb2xpbmcgbXkgYXJzZW5hbCBhcyBhIHN0YXRpc3RpY2lhbi4gQWZ0ZXIgZ2V0dGluZyBteSBNYXN0ZXIncyBpbiBBcHBsaWVkIFN0YXRpc3RpY3MsIEkndmUgc3BlbnQgdGhlIHBhc3QgNyB5ZWFycyB3b3JraW5nIGluIG1hcmtldGluZyBhbmFseXRpY3MuIEkndmUga2VwdCBzb21lIGFzcGVjdHMgb2YgdGhlc2Ugc2tpbGxzIHNoYXJwLCBpbiBwYXJ0aWN1bGFyIGxpbmVhciByZWdyZXNzaW9uIGFuZCBjb2RpbmcgaW4gUiAoYnVpbGRpbmcgc2hpbnkgYXBwcyBhbmQgbGVhcm5pbmcvZGV2ZWxvcGluZyB0aWR5dmVyc2UgdG9vbGJveCkuIEJ1dCBpdCdzIHRpbWUgdG8gYm90aCByZWZyZXNoIGFuZCBsZWFybiBzb21lIG5ldyBzdGF0aXN0aWNhbCB0ZWNobmlxdWVzLiogCgoqSSBkZWNpZGVkIHRvIGRpdmUgaW50byBtYWNoaW5lIGxlYXJuaW5nIGFuZCBkbyB0aGF0IGluIFIuIEl0J3MgYSB0b3BpYyB3aGVyZSBJIGhhdmUgbGl0dGxlIGV4cG9zdXJlLCBidXQgaGF2ZSBhIGxvdCB0byBsZWFybi4gQWZ0ZXIgY29taW5nIGFjcm9zcyB0aGUgYm9vayBbTWFjaGluZSBMZWFybmluZyB3aXRoIFIsIHRoZSB0aWR5dmVyc2UsIGFuZCBtbHJdKGh0dHBzOi8vd3d3Lm1hbm5pbmcuY29tL2Jvb2tzL21hY2hpbmUtbGVhcm5pbmctd2l0aC1yLXRoZS10aWR5dmVyc2UtYW5kLW1scikgYnkgW0hlZmluIFJoeXNdKGh0dHBzOi8vdHdpdHRlci5jb20vSFJKMjEpIEkgZGVjaWRlZCB0byBidXkgaXQgYW5kIG1ha2UgdGhlIGxlYXAuIEknbSBub3QgZmFyIGludG8gdGhlIGJvb2ssIGJ1dCBzbyBmYXIgaXQncyBiZWVuIHJlYWxseSBlYXN5IHRvIGZvbGxvdyBhbG9uZyBhbmQgZG9lcyBhIGdyZWF0IGpvYiBnb2luZyBvdmVyIHRoZSB0aWR5dmVyc2UuKgoKKk15IGdvYWwgaXMgdG8gZ28gdGhyb3VnaCB0aGUgZW50aXJlIGJvb2sgY2hhcHRlciBieSBjaGFwdGVyLCBhbmQgYXMgSSdtIGEgYmlnIGJhc2ViYWxsIG5lcmQsIGNyZWF0ZSBzbWFsbCBwcm9qZWN0cyB3aXRoIGJhc2ViYWxsIGRhdGEgZm9yIGVhY2ggYWxnb3JpdGhtLioKCjxici8+CgojIyMga05OIFByb2plY3Q6IFByZWRpY3Rpbmcgd2hpY2ggTWlMQiBoaXR0ZXJzIGZyb20gQSsgaW4gMjAxMyBtYWRlIGl0IHRvIE1MQgo8YnIvPgpBZnRlciBnb2luZyB0aHJvdWdoIHRoZSBlbnRpcmUgY2hhcHRlciBvbiBrLW5lYXJlc3QgbmVpZ2hib3JzIChrTk4pLCBJIGRlY2lkZWQgdG8gdXNlIGtOTiB0byBidWlsZCBhIG1vZGVsIHRvIHByZWRpY3Qgd2hpY2ggbWlub3IgbGVhZ3VlIHBsYXllcnMgZnJvbSBoaWdoLUEgKEErKSBpbiAyMDEzIGVuZGVkIHVwIG1ha2luZyBhIG1ham9yIGxlYWd1ZSBkZWJ1dC4gSSB1c2VkIGEgMTAwIHBsYXRlIGFwcGVhcmFuY2UgKFBBKSBjdXRvZmYgdG8gYmUgaW5jbHVkZWQgaW4gdGhlIGRhdGEuCgpJIHNldHRsZWQgb24gNCBwcmVkaWN0b3JzLCBvbmUgYmVpbmcgdGhlIHNlYXNvbmFsIGFnZSBvZiB0aGUgcGxheWVyIChBZ2UpIGFuZCB0aGUgb3RoZXJzIGJlaW5nIHRocmVlIHJhdGUgc3RhdGlzdGljcyBpbiBzdHJpa2VvdXQgcGVyY2VudGFnZSAoSyUpLCB3YWxrIHBlcmNlbnRhZ2UgKEJCJSkgYW5kIGlzb2xhdGVkIHNsdWdnaW5nIHBlcmNlbnRhZ2UgKElTTykuIFRoZSBwZXJmb3JtYW5jZSBzdGF0cyBhcmUgdHlwaWNhbGx5IGRlY2VudCBpbmRpY2F0b3JzIG9mIHBsYXllciBwZXJmb3JtYW5jZSwgd2hpbGUgYWdlIHBsYXlzIGEgYmlnIGZhY3RvciBpbiBwcm9qZWN0aW5nIHRhbGVudC4gQWRkaXRpb25hbGx5LCB0aGUgdGhyZWUgcGVyZm9ybWFuY2UgbWV0cmljcyBzaG91bGRuJ3QgaGF2ZSBtdWNoIGNvdmFyaWFuY2UgYmV0d2VlbiB0aGVtLgoKSSBwdWxsZWQgdGhlIGRhdGEgZnJvbSBmYW5ncmFwaHMgYW5kIHRoZSBsZWFkZXJib2FyZCBjYW4gYmUgYWNjZXNzZWQgW2hlcmVdKGh0dHBzOi8vd3d3LmZhbmdyYXBocy5jb20vbGVhZGVycy9taW5vci1sZWFndWU/cG9zPWFsbCZsZz04LDksMTAmc3RhdHM9YmF0JnF1YWw9MTAwJnR5cGU9MSZ0ZWFtPSZzZWFzb249MjAxMyZzZWFzb25FbmQ9MjAxMyZvcmc9JmluZD0wJnNwbGl0VGVhbT1mYWxzZSZwbGF5ZXJzPSZzb3J0PTE4LDEpLgoKYGBge3IgbWVzc2FnZT1GQUxTRX0KbGlicmFyeSgidGlkeXZlcnNlIikKbGlicmFyeSgibWxyIikKCk1pTEIyMDEzIDwtIHJlYWRfY3N2KCJkYXRhL01pTEJfSGlnaEFfMjAxMy5jc3YiKQoKIyBSZW1vdmUgdGhlIHBsYXllciBuYW1lcyBmcm9tIHRoZSBkYXRhCk1pTEIyMDEza25uIDwtIHNlbGVjdChNaUxCMjAxMywtTmFtZSkKCnN1bW1hcnkoTWlMQjIwMTNrbm4pCgpNaUxCMjAxM2tubiAlPiUgCiAgZ3JvdXBfYnkoTUxCKSAlPiUKICBzdW1tYXJpc2UobiA9IG4oKSkgJT4lCiAgbXV0YXRlKGZyZXEgPSBuIC8gc3VtKG4pKQpgYGAKClRvIGZpZ3VyZSBvdXQgd2hhdCBrIHZhbHVlIGlzIG9wdGltYWwgZm9yIHRoZSBrTk4gbW9kZWwsIEkgZGlkIGh5cGVycGFyYW1ldGVyIHR1bmluZyBmb3Igay4gSSBleHBlY3RlZCBrIHRvIGJlIGEgYml0IGxvd2VyIGluaXRpYWxseSBhbmQgc3RhcnRlZCBhY3Jvc3MgdGhlIHNldCBvZiBrID0gMToxMC4gV2hlbiBrPTEwIHdhcyB0aGUgYmVzdCBwZXJmb3JtaW5nLCBJIGV4cGFuZGVkIGl0IG91dCB0byBrPTI1LiBBZ2Fpbiwgaz0yNSBjYW1lIGJhY2sgYXMgdGhlIGJlc3QgcGVyZm9ybWluZyBhbmQgdGhlbiBvcGVuZWQgaXQgdXAgdG8gNjAgYXMgaXQgc2hvd3MgYmVsb3cuCgpgYGB7ciBtZXNzYWdlPUZBTFNFfQojIFRhc2sKTWlMQlRhc2sgPC0gbWFrZUNsYXNzaWZUYXNrKGRhdGEgPSBNaUxCMjAxM2tubiwgdGFyZ2V0ID0gIk1MQiIpCgojIGh5cGVyYXJhbWV0ZXIgdHVuaW5nCmtublBhcmFtU3BhY2UgPC0gbWFrZVBhcmFtU2V0KG1ha2VEaXNjcmV0ZVBhcmFtKCJrIiwgdmFsdWVzID0gMTo2MCkpCmdyaWRTZWFyY2ggPC0gbWFrZVR1bmVDb250cm9sR3JpZCgpCmN2Rm9yVHVuaW5nIDwtIG1ha2VSZXNhbXBsZURlc2MoIlJlcENWIiwgZm9sZHMgPSAxMCwgcmVwcyA9IDIwKQp0dW5lZE1pTEIgPC0gdHVuZVBhcmFtcygiY2xhc3NpZi5rbm4iLCB0YXNrID0gTWlMQlRhc2ssCiAgICAgICAgICAgICAgICAgICAgIHJlc2FtcGxpbmcgPSBjdkZvclR1bmluZywKICAgICAgICAgICAgICAgICAgICAgcGFyLnNldCA9IGtublBhcmFtU3BhY2UsIGNvbnRyb2wgPSBncmlkU2VhcmNoKQp0dW5lZE1pTEIKdHVuZWRNaUxCJHgKCmtublR1bmluZ0RhdGEgPC0gZ2VuZXJhdGVIeXBlclBhcnNFZmZlY3REYXRhKHR1bmVkTWlMQikKCnBsb3RIeXBlclBhcnNFZmZlY3Qoa25uVHVuaW5nRGF0YSwgeCA9ICJrIiwgeSA9ICJtbWNlLnRlc3QubWVhbiIsCiAgICAgICAgICAgICAgICAgICAgcGxvdC50eXBlID0gImxpbmUiKSArCiAgdGhlbWVfYncoKQoKYGBgCgpUaGUgdmFsdWUgb2YgayB0aGF0IG1pbmltaXplcyB0aGUgbWVhbiBtbWNlIGlzIDMzLiBUaGUgZ3JhcGggYWJvdmUgZGlzcGxheXMgdGhlIG1tY2UgbWVhbiBmb3IgYWxsIHZhbHVlcyBvZiBrIGJldHdlZW4gMSBhbmQgNjAuIFdoaWxlIDMzIGlzIHRoZSBtaW5pbXVtIHZhbHVlIG9uIHRoZSBncmFwaCwgYSBwb3RlbnRpYWwgcmFuZ2Ugb2YgayBiZXR3ZWVuIDI4IHRvIDM1IGNvdWxkIHdvcmsgYW5kIGluIGFub3RoZXIgaXRlcmF0aW9uLCBrPTM1IGhhZCB0aGUgbG93ZXN0IHZhbHVlLgoKSSBleHBlY3RlZCBhIGxvd2VyIG9wdGltdW0gdmFsdWUgZm9yIGsgYmVjYXVzZSB0aGUgZGF0YSBvbmx5IGhhcyA0NjEgb2JzZXJ2YXRpb25zLgoKYGBge3IgbWVzc2FnZT1GQUxTRX0KdHVuZWRLbm5NaUxCIDwtIHNldEh5cGVyUGFycyhtYWtlTGVhcm5lcigiY2xhc3NpZi5rbm4iKSwKICAgICAgICAgICAgICAgICAgICAgICAgIHBhci52YWxzID0gdHVuZWRNaUxCJHgpCgp0dW5lZEtubk1vZGVsTWlMQiA8LSB0cmFpbih0dW5lZEtubk1pTEIsIE1pTEJUYXNrKQpgYGAKCjxici8+PGJyLz4KCl9fTW9kZWwgUGVyZm9ybWFuY2VfXyAKClRvIGV2YWx1YXRlIHBlcmZvcm1hbmNlIEkgdXNlZCBLLWZvbGQgY3Jvc3MgdmFsaWRhdGlvbiB3aXRoIDUgZm9sZHMgYW5kIDUwIHJlcGV0aXRpb25zIGFuZCBzdGFydGVkIHdpdGggdGhlIHR1bmVkIGs9MzMuCgpgYGB7ciBtZXNzYWdlPUZBTFNFfQoKa25uIDwtIG1ha2VMZWFybmVyKCJjbGFzc2lmLmtubiIsIHBhci52YWxzID0gbGlzdCgiayI9MzMpKQprbm5Nb2RlbCA8LSB0cmFpbihrbm4sIE1pTEJUYXNrKQoKa0ZvbGQgPC0gbWFrZVJlc2FtcGxlRGVzYyhtZXRob2QgPSAiUmVwQ1YiLCBmb2xkcyA9IDUsIHJlcHMgPSA1MCwKICAgICAgICAgICAgICAgICAgICAgICAgICBzdHJhdGlmeSA9IFRSVUUpCgprRm9sZENWIDwtIHJlc2FtcGxlKGxlYXJuZXIgPSBrbm4sIHRhc2sgPSBNaUxCVGFzaywKICAgICAgICAgICAgICAgICAgICByZXNhbXBsaW5nID0ga0ZvbGQsIG1lYXN1cmVzID0gbGlzdChtbWNlLCBhY2MpLCBzaG93LmluZm8gPSBGQUxTRSkKCmtGb2xkQ1YkYWdncgoKY2FsY3VsYXRlQ29uZnVzaW9uTWF0cml4KGtGb2xkQ1YkcHJlZCwgcmVsYXRpdmUgPSBUUlVFKQoKYGBgCgpUaGUgY29uZnVzaW9uIG1hdHJpeCBhYm92ZSBzaG93cyB0aGF0ICJObyIgTUxCIHByZWRpY3Rpb25zIHdlcmUgY29ycmVjdCA4MiUgb2YgdGhlIHRpbWUgYW5kICJZZXMiIE1MQiBwcmVkaWN0aW9ucyB3ZXJlIGNvcnJlY3QgNzIlIG9mIHRoZSB0aW1lLgoKSG93ZXZlciwgb2YgdGhlIHBsYXllcnMgdGhhdCBkaWQgbWFrZSBpdCB0byBNTEIsIGl0IHdhcyB3cm9uZyBvbiB0aGVpciBjbGFzc2lmaWNhdGlvbiA2NiUgb2YgdGhlIHRpbWUuCgpTbyB0aGUgbW9kZWwgc2VlbXMgdG8gYmUgY29uc2VydmF0aXZlIHdoZW4gcHJlZGljdGluZyB0aGF0IGEgcGxheWVyIHdpbGwgbWFrZSBpdCB0byBNTEIuIFRoZSBkYXRhIGhhZCAyNCUgb2YgcGxheWVycyBtYWtlIGl0IHRvIE1MQiBhbmQgaW4gdGhlIGNyb3NzIHZhbGlkYXRpb24sIGl0IG9ubHkgcHJlZGljdGVkICJZZXMiIDExJSBvZiB0aGUgdGltZS4KClVsdGltYXRlbHkgdGhlIG1vZGVsIGRvZXMgYSBwcmV0dHkgZ29vZCBqb2Igb2YgaWRlbnRpZnlpbmcgd2hpY2ggcGxheWVycyB3b24ndCBtYWtlIE1MQiBidXQgYSBob3JyaWJsZSBqb2IgaWRlbnRpZnlpbmcgd2hvIHVsdGltYXRlbHkgZG9lcyBtYWtlIE1MQi4KCkJlY2F1c2UgdGhlICJZZXMiIGVycm9yIHdhcyBzbyBoaWdoLCBJIHdvbmRlcmVkIGlmIGEgbG93ZXIgayB2YWx1ZSwgbWlnaHQgdHJhZGUgb2ZmIHNvbWUgb2YgdGhlIG92ZXJhbGwgbW9kZWwgZXJyb3IgZm9yIGEgYml0IGJldHRlciAiWWVzIiBNTEIgcmVzdWx0LiBMb29raW5nIGJhY2sgYXQgdGhlIGh5cGVycGFyYW1ldGVycyBwbG90LCB0aGVyZSB3YXMgYSBsb2NhbCBtaW5pbXVtIHVuZGVyIDIwLCBzbyBJIHRlc3RlZCBvdXQgdGhlIG1vZGVsIHdpdGggaz0xNy4KCmBgYHtyfQprbm4yIDwtIG1ha2VMZWFybmVyKCJjbGFzc2lmLmtubiIsIHBhci52YWxzID0gbGlzdCgiayI9MTcpKQprbm5Nb2RlbDIgPC0gdHJhaW4oa25uMiwgTWlMQlRhc2spCgojIFVzZSBzYW1lIENWIGFzIGFib3ZlCiMga0ZvbGQgPC0gbWFrZVJlc2FtcGxlRGVzYyhtZXRob2QgPSAiUmVwQ1YiLCBmb2xkcyA9IDUsIHJlcHMgPSA1MCwKIyAgICAgICAgICAgICAgICAgICAgICAgICAgc3RyYXRpZnkgPSBUUlVFKQoKa0ZvbGRDVjIgPC0gcmVzYW1wbGUobGVhcm5lciA9IGtubjIsIHRhc2sgPSBNaUxCVGFzaywKICAgICAgICAgICAgICAgICAgICByZXNhbXBsaW5nID0ga0ZvbGQsIG1lYXN1cmVzID0gbGlzdChtbWNlLCBhY2MpLCBzaG93LmluZm8gPSBGQUxTRSkKCmtGb2xkQ1YyJGFnZ3IKCmNhbGN1bGF0ZUNvbmZ1c2lvbk1hdHJpeChrRm9sZENWMiRwcmVkLCByZWxhdGl2ZSA9IFRSVUUpCmBgYAoKVGhlIHJlc3VsdCBoZXJlIGlzIG1peGVkLiBXaGVuIHRoZSBtb2RlbCBwcmVkaWN0ZWQgdGhhdCBhIHBsYXllciBtYWRlIE1MQiwgaXQgd2FzIG9ubHkgY29ycmVjdCA2NCUgb2YgdGhlIHRpbWUsIGRvd24gZnJvbSA3MiUgZnJvbSB0aGUgZmlyc3QgbW9kZWwuIEhvd2V2ZXIsIGxvb2tpbmcgYXQgdGhlIHBsYXllcnMgdGhhdCBkaWQgbWFrZSBpdCB0byBNTEIsIHRoZSBtb2RlbCBjb3JyZWN0bHkgY2xhc3NpZmllZCA0MCUsIHVwIGZyb20gMzQlIGluIHRoZSBmaXJzdCBtb2RlbC4KClRoZSBudW1iZXIgb2YgdG90YWwgcGxheWVycyB0aGF0IHRoaXMgbW9kZWwgcHJlZGljdGVkIHdvdWxkIG1ha2UgTUxCIGlzIDE1JSwgdXAgZnJvbSAxMSUgZnJvbSB0aGUgZmlyc3QgbW9kZWwuIFRoaXMgaXMgc3RpbGwgd2VsbCBzaG9ydCBvZiB0aGUgMjQlIG1hcmsgdGhhdCBkaWQgbWFrZSBNTEIuCgpXaGljaCBtb2RlbCBpcyBiZXN0IGRlcGVuZHMgb24geW91ciBnb2Fscy4gSSdtIGxlYW5pbmcgdG93YXJkIHRoZSBzZWNvbmQgbW9kZWwgYmVjYXVzZSBpdCdzIG1ha2luZyBhIGNsb3NlciBhdHRlbXB0IGF0IHByZWRpY3RpbmcgaG93IG1hbnkgcGxheWVycyBhY3R1YWxseSBtYWRlIGl0LCBldmVuIGlmIGl0J3Mgd3JvbmcgbW9yZSBvZnRlbi4gTW9kZWwgMSBwcmVkaWN0ZWQgY29ycmVjdGx5IHRoYXQgMzggcGxheWVycyB3b3VsZCBtYWtlIE1MQiBvbiBhdmVyYWdlLCB3aGlsZSBtb2RlbCAyIGNvcnJlY3RseSBwcmVkaWN0ZWQgNDQgcGxheWVycyBvbiBhdmVyYWdlLiBBZ2FpbiwgY29uc2lkZXJpbmcgMTEyIHBsYXllcnMgZnJvbSAyMDEzIG1hZGUgaXQgdXAsIHRoYXQncyBub3QgYSBncmVhdCBtb2RlbCEKCjxici8+PGJyLz4KX19QcmVkaWN0aW9uc19fCgpFdmVuIHRob3VnaCBuZWl0aGVyIG1vZGVsIGRpZCBhIGdyZWF0IGpvYiBpbiBjcm9zcyB2YWxpZGF0aW9uIGFuZCB0aGUgbW9kZWxzIHVzZSBhbGwgMjAxMyBkYXRhLCBJIHdhbnRlZCB0byBleHBsb3JlIHByZWRpY3Rpbmcgc29tZSBwbGF5ZXJzIGZyb20gMjAxNCB1c2luZyBvdXIgbW9kZWxzLiAKCkkgc2VsZWN0ZWQgcGxheWVycyB0aGF0IHdlcmUgcmFua2VkIDI2LTMwIGluIHdSQysgaW4gMjAxNC4gVGhlc2UgcGxheWVycyB3ZXJlIENhcmxvcyBDb3JyZWEsIERlcnJpY2sgQ2h1bmcsIFR5bGVyIFdoaXRlLCBLeWxlIFdhbGRyb3AgYW5kIERhbmllbCBDYXJib25lbGwuIFdlIGhhdmUgYSBtYWpvciBsZWFndWUgcmVndWxhciB0aGF0IGRlYnV0ZWQgYXQgYSB5b3VuZyBhZ2UgaW4gQ29ycmVhLCBhIHF1YWQtQSBoaXR0ZXIgaW4gVHlsZXIgV2hpdGUsIGEgY3VwIG9mIGNvZmZlZSBNTEIgcGxheWVyIGluIFdhbGRyb3AgYW5kIHR3byBvdGhlciBwbGF5ZXJzIHRoYXQgbmV2ZXIgbWFkZSBpdCB1cCB0byB0aGUgYmlncy4KCmBgYHtyfQojIGRhdGEgZm9yIENhcmxvcyBDb3JyZWEsIERlcnJpY2sgQ2h1bmcsIFR5bGVyIFdoaXRlLCBLeWxlIFdhbGRyb3AsIERhbmllbCBDYXJib25lbGwKbmV3TWlMQmRhdGEgPC0gdGliYmxlKEFnZSA9IGMoMTksIDI2LCAyMywgMjIsIDIzKSwKICAgICAgICAgICAgICAgICAgICAgIEJCID0gYyguMTIzLCAuMDk1LCAuMTUxLCAuMDc2LCAuMDYpLAogICAgICAgICAgICAgICAgICAgICAgSyA9IGMoLjE1NCwgLjEsIC4xNDUsIC4xOTQsIC4xOSksCiAgICAgICAgICAgICAgICAgICAgICBJU08gPSBjKC4xODUsIC4xMTgsIC4yNjAsIC4xNTYsIC4xOTQpKQoKIyBVc2luZyBrPTMzCm5ld01pTEJQcmVkMSA8LSBwcmVkaWN0KGtubk1vZGVsLCBuZXdkYXRhID0gbmV3TWlMQmRhdGEpCgpnZXRQcmVkaWN0aW9uUmVzcG9uc2UobmV3TWlMQlByZWQxKQoKIyBVc2luZyBrPTE3Cm5ld01pTEJQcmVkMiA8LSBwcmVkaWN0KGtubk1vZGVsMiwgbmV3ZGF0YSA9IG5ld01pTEJkYXRhKQoKZ2V0UHJlZGljdGlvblJlc3BvbnNlKG5ld01pTEJQcmVkMikKYGBgCgpUaGUgZmlyc3Qga05OIG1vZGVsIHdpdGggaz0zMyBvbmx5IHByZWRpY3RlZCBDYXJsb3MgQ29ycmVhIHRvIG1ha2UgTUxCIG91dCBvZiB0aGUgNSBwbGF5ZXJzLiBJbiBwYXN0IHJlc3VsdHMgaXQgYWxzbyBjb3JyZWN0bHkgc2VsZWN0ZWQgS3lsZSBXYWxkcm9wIHRvIG1ha2UgaXQgdG8gTUxCLiBUaGUgY29uc2VydmF0aXZlIG5hdHVyZSBvZiBoYW5kaW5nIG91dCAieWVzIiBNTEIgcHJlZGljdGlvbnMgaXMgcHJlc2VudCBpbiBvdXIgc21hbGwgc2FtcGxlIHNldC4gSW4gYm90aCBpdGVyYXRpb25zLCB0aGUgbW9kZWwgd2FzIHdyb25nIG9uIFR5bGVyIFdoaXRlLiBHaXZlbiB0aGF0IGhpcyBzdGF0cyB3ZXJlIGFsbCBzdXBlcmlvciB0byBXYWxkcm9wICh3aG8gd2FzIHByZWRpY3RlZCBjb3JyZWN0bHkgaW4gYSBwYXN0IGl0ZXJhdGlvbikgLSBpdCBwb2ludHMgdG8gYWdlIHBvdGVudGlhbGx5IGhhdmluZyBhIGxhcmdlIGltcGFjdCBpbiB0aGUga05OIG1vZGVsLiAKClRoZSBzZWNvbmQga05OIG1vZGVsIHdpdGggaz0xNyBjb3JyZWN0bHkgcHJlZGljdGVkIHRoZSBwbGF5aW5nIGZ1dHVyZXMgb2YgYWxsIDUgcGxheWVycy4gR2l2ZW4gdGhhdCB0aGUgbW9kZWwgb24gYXZlcmFnZSB3YXMgb25seSBjb3JyZWN0bHkgcHJlZGljdGluZyBNTEIgcGxheWVycyA2MiUgb2YgdGhlIHRpbWUsIHRoZSByZXN1bHRzIGhlcmUgYXJlIGVpdGhlciAoYSkgbHVja3kgb3IgKGIpIGJpYXNlZCBiYXNlZCBvbiBteSBzZWxlY3Rpb24gb2YgaGlnaCBwZXJmb3JtaW5nIDIwMTQgcGxheWVycyAocmFua2VkIDI2LTMwIGluIHdSQyspLiAKCjxici8+PGJyLz4KX19DbG9zaW5nIFRob3VnaHRzX18KCldoaWxlIGtOTiB3YXMgZWFzeSB0byBpbXBsZW1lbnQgYW5kIGZ1biB0byB0ZXN0IG91dCBvbiB0aGlzIHF1ZXN0aW9uLCBpdCBtYXkgbm90IGJlIHRoZSBiZXN0IGFsZ29yaXRobSBmb3IgcHJlZGljdGluZyB3aGljaCBwbGF5ZXJzIHdvdWxkIG1ha2UgTUxCLiBJdCdzIGVhc3kgdG8gaWRlbnRpZnkgd2hvIHdvbid0IG1ha2UgaXQsIGJ1dCBpdCdzIGEgbG90IGhhcmRlciB0byBpZGVudGlmeSB3aG8gZG9lcyEgSXQgd291bGQgYWxzbyBiZSBuaWNlIHRvIGhhdmUgaW5mb3JtYXRpb24gYWJvdXQgdGhlIHZhcmlhYmxlcyB1c2VkIGluIHRoZSBtb2RlbCAtIGxpa2Ugd2hpY2ggb25lIGlzIG1vc3QgY2xvc2VseSByZWxhdGVkIHRvIG1ha2luZyBpdCB0byB0aGUgYmlnIGxlYWd1ZXMsIHdoaWNoIGlzIHNvbWV0aGluZyBrTk4gZG9lc24ndCBvZmZlci4gCgpJIGFsc28gd2FudGVkIHRvIG5vdGUgdGhhdCBhIGRlY2VudCBhbW91bnQgb2YgZXJyb3IgbWF5IGhhdmUgYmVlbiBpbnRyb2R1Y2VkIGdpdmVuIHRoZSBzYW1wbGUgb2YgcGxheWVycyBuZWVkaW5nIG9ubHkgMTAwIHBsYXRlIGFwcGVhcmFuY2VzIHRvIGJlIGluY2x1ZGVkIGluIHRoZSBkYXRhLiBJdCdzIHdlbGwga25vd24gdGhhdCBhbnkgcGxheWVyIGNhbiBnZXQgImhvdCIgb3IgImNvbGQiIGZvciAxMDArIFBBIC0gc2VlIG91ciAyMDE0IGV4YW1wbGUgb2YgRGVycmljayBDaHVuZyEgSWRlYWxseSB3ZSdkIGJlIGxvb2tpbmcgYXQgbW9yZSBkYXRhIHRoYW4ganVzdCBBKyBwZXJmb3JtYW5jZSB0byBnYXVnZSB0aGVpciBmdWxsIGFiaWxpdGllcy4KCkknbGwgYmUgbG9va2luZyB0byB1c2UgdGhpcyBkYXRhIGluIGZ1dHVyZSBhbGdvcml0aG1zIC0gaW5jbHVkaW5nIGluIHRoZSBuZXh0IGNoYXB0ZXIgb24gbG9naXN0aWMgcmVncmVzc2lvbiEKCjxici8+PGJyLz4K