Objectives
Upon completion of this lesson, you will be able to:
- implement kNN in R for classification and regression
Motivation
This is a simple and illustrative implementation of the k-Nearest Neighbors algorithm for classification and regression. It is neither efficient not scalable, but meant to be illustrative – it should not be used in production.
Definitions of Functions
kNNMODE - identifies the most frequently occurring value in a vector of nominal values
kNNMODE <- function(x)
{
ux <- unique(x)
return (ux[which.max(tabulate(match(x, ux)))])
}
kNNAVG - calculates the average value in a vector of nominal values
kNNAVG <- function(x)
{
return (mean(x))
}
dist - calculates the Euclidean distance between two vectors of equal size containing numeric elements.
kNNDIST <- function(p, q)
{
d <- 0
for (i in 1:length(p)) {
d <- d + (p[i] - q[i])^2
}
return(sqrt(d))
}
neighbors - returns a vector of distances between an object u and a data frame of features; all features must be numeric
kNNNeighbors <- function (train, u)
{
m <- nrow(train)
ds <- numeric(m)
for (i in 1:m) {
p <- train[i,]
ds[i] <- unlist(kNNDIST(p,u))
}
return(ds)
}
k.closest - finds the smallest k values in a vector of values
k.closest <- function(neighbors,k)
{
# uses the order function from R to sort the vector
# of neighbors by distance
ordered.neighbors <- order(neighbors)
# extracts only the top k neighbors
# returns the indexes of those closest neighbors
k.closest <- ordered.neighbors[1:k]
}
KNN.CLASSIFICATION - finds the most likely class that an unknown object u belongs to based on a training data frame of features, a corresponding vector of labels, and a provided k. The name is purposely capitalized to avoid conflicts with other implementations of kNN from packages.
KNN.CLASSIFICATION <- function (train, labels, u, k)
{
nb <- kNNNeighbors(train,u)
f <- k.closest(nb,k)
KNN <- kNNMODE(labels[f])
}
KNN.REGRESSION - finds the most likely target value that an unknown object u based on a training data frame of features, a corresponding vector of target values, and a provided k. The name is purposely capitalized to avoid conflicts with other implementations of kNN from packages.
KNN.REGRESSION <- function (train, target, u, k)
{
nb <- kNNNeighbors(train,u)
f <- k.closest(nb,k)
KNN <- kNNAVG(target[f])
}
Use of Algorithm: Classification
Let’s apply the algorithm to classify food items based on three features: sweetness, crunchiness, and saltiness.
foods.training <- read.csv("foods.csv")
head(foods.training)
## ingredient sweetness crunchiness saltiness type cost
## 1 apple 10 9 0 fruit 2.3
## 2 bacon 1 4 8 protein 1.8
## 3 banana 10 1 0 fruit 2.8
## 4 carrot 7 10 0 vegetable 2.1
## 5 celery 3 10 0 vegetable 1.2
## 6 cheese 1 1 5 protein 3.2
# unknown case
# (new food items with a measured sweetness, crunchiness, and saltiness)
u <- c(3, 1, 2)
w <- c(10, 8, 2)
# separate the label and features
labels <- foods.training$type
# only consider numeric features (or encoded categorical features)
training.features <- foods.training[,2:4]
# classify the new item using our new knn algorithm
nn <- KNN.CLASSIFICATION(training.features, labels, u, k = 4)
print(paste0("food type is '",nn,"'"))
## [1] "food type is 'protein'"
nn <- KNN.CLASSIFICATION(training.features, labels, w, k = 4)
print(paste0("food type is '",nn,"'"))
## [1] "food type is 'fruit'"
Use of Algorithm: Regression
Let’s apply the algorithm to calculate the cost of a food item based on three numeric and one categorical feature: sweetness, crunchiness, and saltiness.
# unknown case
# (new food items with a measured sweetness, crunchiness, saltiness)
u <- c(3, 1, 2)
w <- c(10, 8, 2)
# separate the target feature and the predictive features
target <- foods.training$cost
# only consider numeric features
training.features <- foods.training[,2:4]
# classify the new item using our new knn algorithm
pr <- KNN.REGRESSION(training.features, target, u, k = 4)
# predicted food price based on features
print(paste0("food price is '",pr,"'"))
## [1] "food price is '4.95'"
Let’s also consider the categorical feature type. Of course, we will first have to convert the feature to a numeric one using an encoding scheme. We will use frequency encoding.
# unknown case
# (new food items with a measured sweetness, crunchiness, saltiness)
u <- c(3, 1, 2, 0)
w <- c(10, 8, 2, 0)
# separate the target feature and the predictive features
target <- foods.training$cost
# only consider numeric features
training.features <- foods.training[,2:4]
training.features$type <- 0
# classify the new item using our new knn algorithm
pr <- KNN.REGRESSION(training.features, target, u, k = 4)
# predicted food price based on features
print(paste0("food price is '",pr,"'"))
## [1] "food price is '4.95'"
Make sure you standardize any new data values the same way as you standardized the training data or distance calculations will not be meaningful.
References
No references.
Errata
None collected yet. Let us know.
LS0tCnRpdGxlOiAiU2ltcGxlIEltcGxlbWVudGF0aW9uIG9mIGtOTiBpbiBSIgpwYXJhbXM6CiAgY2F0ZWdvcnk6IDMKICBzdGFja3M6IDAKICBudW1iZXI6IDQxMQogIHRpbWU6IDMwCiAgbGV2ZWw6IGJlZ2lubmVyCiAgdGFnczoga25uLG1hY2hpbmUgbGVhcm5pbmcsY2xhc3NpZmljYXRpb24KICBkZXNjcmlwdGlvbjogIlByZXNlbnRzIGEgc2ltcGxlIGltcGxlbWVudGF0aW9uIG9mIGtOTiBmb3IgY2xhc3NpZmljYXRpb24KICAgICAgICAgICAgICAgIGFuZCBhbm90aGVyIGltcGxlbWVudGF0aW9uIGZvciByZWdyZXNzaW9uLCBib3RoIGluIFIuIgpkYXRlOiAiPHNtYWxsPmByIFN5cy5EYXRlKClgPC9zbWFsbD4iCmF1dGhvcjogIjxzbWFsbD5NYXJ0aW4gU2NoZWRsYmF1ZXI8L3NtYWxsPiIKZW1haWw6ICJtLnNjaGVkbGJhdWVyQG5ldS5lZHUiCmFmZmlsaXRhdGlvbjogIk5vcnRoZWFzdGVybiBVbml2ZXJzaXR5IgpvdXRwdXQ6IAogIGJvb2tkb3duOjpodG1sX2RvY3VtZW50MjoKICAgIHRvYzogdHJ1ZQogICAgdG9jX2Zsb2F0OiB0cnVlCiAgICBjb2xsYXBzZWQ6IGZhbHNlCiAgICBudW1iZXJfc2VjdGlvbnM6IGZhbHNlCiAgICBjb2RlX2Rvd25sb2FkOiB0cnVlCiAgICB0aGVtZTogc3BhY2VsYWIKICAgIGhpZ2hsaWdodDogdGFuZ28KLS0tCgotLS0KdGl0bGU6ICI8c21hbGw+YHIgcGFyYW1zJGNhdGVnb3J5YC5gciBwYXJhbXMkbnVtYmVyYDwvc21hbGw+PGJyLz48c3BhbiBzdHlsZT0nY29sb3I6ICMyRTQwNTM7IGZvbnQtc2l6ZTogMC45ZW0nPmByIHJtYXJrZG93bjo6bWV0YWRhdGEkdGl0bGVgPC9zcGFuPiIKLS0tCgpgYGB7ciBjb2RlPXhmdW46OnJlYWRfdXRmOChwYXN0ZTAoaGVyZTo6aGVyZSgpLCcvUi9faW5zZXJ0MkRCLlInKSksIGluY2x1ZGUgPSBGQUxTRX0KYGBgCgotLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KCiMjIE9iamVjdGl2ZXMKClVwb24gY29tcGxldGlvbiBvZiB0aGlzIGxlc3NvbiwgeW91IHdpbGwgYmUgYWJsZSB0bzoKCi0gICBpbXBsZW1lbnQgKmtOTiogaW4gUiBmb3IgY2xhc3NpZmljYXRpb24gYW5kIHJlZ3Jlc3Npb24KCi0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQoKIyMgTW90aXZhdGlvbgoKVGhpcyBpcyBhIHNpbXBsZSBhbmQgaWxsdXN0cmF0aXZlIGltcGxlbWVudGF0aW9uIG9mIHRoZSAqay1OZWFyZXN0IE5laWdoYm9ycyogYWxnb3JpdGhtIGZvciBjbGFzc2lmaWNhdGlvbiBhbmQgcmVncmVzc2lvbi4gSXQgaXMgbmVpdGhlciBlZmZpY2llbnQgbm90IHNjYWxhYmxlLCBidXQgbWVhbnQgdG8gYmUgaWxsdXN0cmF0aXZlIC0tIGl0IHNob3VsZCBub3QgYmUgdXNlZCBpbiBwcm9kdWN0aW9uLgoKIyMgRGVmaW5pdGlvbnMgb2YgRnVuY3Rpb25zCgoqa05OTU9ERSogLSBpZGVudGlmaWVzIHRoZSBtb3N0IGZyZXF1ZW50bHkgb2NjdXJyaW5nIHZhbHVlIGluIGEgdmVjdG9yIG9mIG5vbWluYWwgdmFsdWVzCgpgYGB7cn0Ka05OTU9ERSA8LSBmdW5jdGlvbih4KSAKewogIHV4IDwtIHVuaXF1ZSh4KQogIHJldHVybiAodXhbd2hpY2gubWF4KHRhYnVsYXRlKG1hdGNoKHgsIHV4KSkpXSkKfQpgYGAKCiprTk5BVkcqIC0gY2FsY3VsYXRlcyB0aGUgYXZlcmFnZSB2YWx1ZSBpbiBhIHZlY3RvciBvZiBub21pbmFsIHZhbHVlcwoKYGBge3J9CmtOTkFWRyA8LSBmdW5jdGlvbih4KSAKewogIHJldHVybiAobWVhbih4KSkKfQpgYGAKCipkaXN0KiAtIGNhbGN1bGF0ZXMgdGhlIEV1Y2xpZGVhbiBkaXN0YW5jZSBiZXR3ZWVuIHR3byB2ZWN0b3JzIG9mIGVxdWFsIHNpemUgY29udGFpbmluZyBudW1lcmljIGVsZW1lbnRzLgoKYGBge3J9CmtOTkRJU1QgPC0gZnVuY3Rpb24ocCwgcSkKewogIGQgPC0gMAogIGZvciAoaSBpbiAxOmxlbmd0aChwKSkgewogICAgZCA8LSBkICsgKHBbaV0gLSBxW2ldKV4yCiAgfQogIAogIHJldHVybihzcXJ0KGQpKQp9CmBgYAoKKm5laWdoYm9ycyogLSByZXR1cm5zIGEgdmVjdG9yIG9mIGRpc3RhbmNlcyBiZXR3ZWVuIGFuIG9iamVjdCAqdSogYW5kIGEgZGF0YSBmcmFtZSBvZiBmZWF0dXJlczsgYWxsIGZlYXR1cmVzIG11c3QgYmUgbnVtZXJpYwoKYGBge3J9CmtOTk5laWdoYm9ycyA8LSBmdW5jdGlvbiAodHJhaW4sIHUpCnsKICAgbSA8LSBucm93KHRyYWluKQogICBkcyA8LSBudW1lcmljKG0pCiAgIGZvciAoaSBpbiAxOm0pIHsKICAgICBwIDwtIHRyYWluW2ksXQogICAgIGRzW2ldIDwtIHVubGlzdChrTk5ESVNUKHAsdSkpCiAgIH0KICAgCiAgIHJldHVybihkcykKfQpgYGAKCiprLmNsb3Nlc3QqIC0gZmluZHMgdGhlIHNtYWxsZXN0ICprKiB2YWx1ZXMgaW4gYSB2ZWN0b3Igb2YgdmFsdWVzCgpgYGB7cn0Kay5jbG9zZXN0IDwtIGZ1bmN0aW9uKG5laWdoYm9ycyxrKQp7CiAgIyB1c2VzIHRoZSBvcmRlciBmdW5jdGlvbiBmcm9tIFIgdG8gc29ydCB0aGUgdmVjdG9yIAogICMgb2YgbmVpZ2hib3JzIGJ5IGRpc3RhbmNlCiAgb3JkZXJlZC5uZWlnaGJvcnMgPC0gb3JkZXIobmVpZ2hib3JzKQogIAogICMgZXh0cmFjdHMgb25seSB0aGUgdG9wIGsgbmVpZ2hib3JzCiAgIyByZXR1cm5zIHRoZSBpbmRleGVzIG9mIHRob3NlIGNsb3Nlc3QgbmVpZ2hib3JzCiAgay5jbG9zZXN0IDwtIG9yZGVyZWQubmVpZ2hib3JzWzE6a10KfQpgYGAKCipLTk4uQ0xBU1NJRklDQVRJT04qIC0gZmluZHMgdGhlIG1vc3QgbGlrZWx5IGNsYXNzIHRoYXQgYW4gdW5rbm93biBvYmplY3QgKnUqIGJlbG9uZ3MgdG8gYmFzZWQgb24gYSB0cmFpbmluZyBkYXRhIGZyYW1lIG9mIGZlYXR1cmVzLCBhIGNvcnJlc3BvbmRpbmcgdmVjdG9yIG9mIGxhYmVscywgYW5kIGEgcHJvdmlkZWQgKmsqLiBUaGUgbmFtZSBpcyBwdXJwb3NlbHkgY2FwaXRhbGl6ZWQgdG8gYXZvaWQgY29uZmxpY3RzIHdpdGggb3RoZXIgaW1wbGVtZW50YXRpb25zIG9mIGtOTiBmcm9tIHBhY2thZ2VzLgoKYGBge3J9CktOTi5DTEFTU0lGSUNBVElPTiA8LSBmdW5jdGlvbiAodHJhaW4sIGxhYmVscywgdSwgaykKewogIG5iIDwtIGtOTk5laWdoYm9ycyh0cmFpbix1KQogIGYgPC0gay5jbG9zZXN0KG5iLGspCiAgS05OIDwtIGtOTk1PREUobGFiZWxzW2ZdKQp9CmBgYAoKKktOTi5SRUdSRVNTSU9OKiAtIGZpbmRzIHRoZSBtb3N0IGxpa2VseSB0YXJnZXQgdmFsdWUgdGhhdCBhbiB1bmtub3duIG9iamVjdCAqdSogYmFzZWQgb24gYSB0cmFpbmluZyBkYXRhIGZyYW1lIG9mIGZlYXR1cmVzLCBhIGNvcnJlc3BvbmRpbmcgdmVjdG9yIG9mIHRhcmdldCB2YWx1ZXMsIGFuZCBhIHByb3ZpZGVkICprKi4gVGhlIG5hbWUgaXMgcHVycG9zZWx5IGNhcGl0YWxpemVkIHRvIGF2b2lkIGNvbmZsaWN0cyB3aXRoIG90aGVyIGltcGxlbWVudGF0aW9ucyBvZiBrTk4gZnJvbSBwYWNrYWdlcy4KCmBgYHtyfQpLTk4uUkVHUkVTU0lPTiA8LSBmdW5jdGlvbiAodHJhaW4sIHRhcmdldCwgdSwgaykKewogIG5iIDwtIGtOTk5laWdoYm9ycyh0cmFpbix1KQogIGYgPC0gay5jbG9zZXN0KG5iLGspCiAgS05OIDwtIGtOTkFWRyh0YXJnZXRbZl0pCn0KYGBgCgojIyBVc2Ugb2YgQWxnb3JpdGhtOiBDbGFzc2lmaWNhdGlvbgoKTGV0J3MgYXBwbHkgdGhlIGFsZ29yaXRobSB0byBjbGFzc2lmeSBmb29kIGl0ZW1zIGJhc2VkIG9uIHRocmVlIGZlYXR1cmVzOiAqc3dlZXRuZXNzKiwgKmNydW5jaGluZXNzKiwgYW5kICpzYWx0aW5lc3MqLgoKYGBge3J9CmZvb2RzLnRyYWluaW5nIDwtIHJlYWQuY3N2KCJmb29kcy5jc3YiKQoKaGVhZChmb29kcy50cmFpbmluZykKYGBgCgpgYGB7cn0KIyB1bmtub3duIGNhc2UgCiMgKG5ldyBmb29kIGl0ZW1zIHdpdGggYSBtZWFzdXJlZCBzd2VldG5lc3MsIGNydW5jaGluZXNzLCBhbmQgc2FsdGluZXNzKQp1IDwtIGMoMywgMSwgMikKdyA8LSBjKDEwLCA4LCAyKQoKIyBzZXBhcmF0ZSB0aGUgbGFiZWwgYW5kIGZlYXR1cmVzCmxhYmVscyA8LSBmb29kcy50cmFpbmluZyR0eXBlCgojIG9ubHkgY29uc2lkZXIgbnVtZXJpYyBmZWF0dXJlcyAob3IgZW5jb2RlZCBjYXRlZ29yaWNhbCBmZWF0dXJlcykKdHJhaW5pbmcuZmVhdHVyZXMgPC0gZm9vZHMudHJhaW5pbmdbLDI6NF0KCiMgY2xhc3NpZnkgdGhlIG5ldyBpdGVtIHVzaW5nIG91ciBuZXcga25uIGFsZ29yaXRobQpubiA8LSBLTk4uQ0xBU1NJRklDQVRJT04odHJhaW5pbmcuZmVhdHVyZXMsIGxhYmVscywgdSwgayA9IDQpCnByaW50KHBhc3RlMCgiZm9vZCB0eXBlIGlzICciLG5uLCInIikpCgpubiA8LSBLTk4uQ0xBU1NJRklDQVRJT04odHJhaW5pbmcuZmVhdHVyZXMsIGxhYmVscywgdywgayA9IDQpCnByaW50KHBhc3RlMCgiZm9vZCB0eXBlIGlzICciLG5uLCInIikpCmBgYAoKIyMgVXNlIG9mIEFsZ29yaXRobTogUmVncmVzc2lvbgoKTGV0J3MgYXBwbHkgdGhlIGFsZ29yaXRobSB0byBjYWxjdWxhdGUgdGhlIGNvc3Qgb2YgYSBmb29kIGl0ZW0gYmFzZWQgb24gdGhyZWUgbnVtZXJpYyBhbmQgb25lIGNhdGVnb3JpY2FsIGZlYXR1cmU6ICpzd2VldG5lc3MqLCAqY3J1bmNoaW5lc3MqLCBhbmQgKnNhbHRpbmVzcyouCgpgYGB7cn0KIyB1bmtub3duIGNhc2UgCiMgKG5ldyBmb29kIGl0ZW1zIHdpdGggYSBtZWFzdXJlZCBzd2VldG5lc3MsIGNydW5jaGluZXNzLCBzYWx0aW5lc3MpCnUgPC0gYygzLCAxLCAyKQp3IDwtIGMoMTAsIDgsIDIpCgojIHNlcGFyYXRlIHRoZSB0YXJnZXQgZmVhdHVyZSBhbmQgdGhlIHByZWRpY3RpdmUgZmVhdHVyZXMKdGFyZ2V0IDwtIGZvb2RzLnRyYWluaW5nJGNvc3QKCiMgb25seSBjb25zaWRlciBudW1lcmljIGZlYXR1cmVzCnRyYWluaW5nLmZlYXR1cmVzIDwtIGZvb2RzLnRyYWluaW5nWywyOjRdCgojIGNsYXNzaWZ5IHRoZSBuZXcgaXRlbSB1c2luZyBvdXIgbmV3IGtubiBhbGdvcml0aG0KcHIgPC0gS05OLlJFR1JFU1NJT04odHJhaW5pbmcuZmVhdHVyZXMsIHRhcmdldCwgdSwgayA9IDQpCgojIHByZWRpY3RlZCBmb29kIHByaWNlIGJhc2VkIG9uIGZlYXR1cmVzCnByaW50KHBhc3RlMCgiZm9vZCBwcmljZSBpcyAnIixwciwiJyIpKQpgYGAKCkxldCdzIGFsc28gY29uc2lkZXIgdGhlIGNhdGVnb3JpY2FsIGZlYXR1cmUgKnR5cGUqLiBPZiBjb3Vyc2UsIHdlIHdpbGwgZmlyc3QgaGF2ZSB0byBjb252ZXJ0IHRoZSBmZWF0dXJlIHRvIGEgbnVtZXJpYyBvbmUgdXNpbmcgYW4gZW5jb2Rpbmcgc2NoZW1lLiBXZSB3aWxsIHVzZSBmcmVxdWVuY3kgZW5jb2RpbmcuCgpgYGB7cn0KIyB1bmtub3duIGNhc2UgCiMgKG5ldyBmb29kIGl0ZW1zIHdpdGggYSBtZWFzdXJlZCBzd2VldG5lc3MsIGNydW5jaGluZXNzLCBzYWx0aW5lc3MpCnUgPC0gYygzLCAxLCAyLCAwKQp3IDwtIGMoMTAsIDgsIDIsIDApCgojIHNlcGFyYXRlIHRoZSB0YXJnZXQgZmVhdHVyZSBhbmQgdGhlIHByZWRpY3RpdmUgZmVhdHVyZXMKdGFyZ2V0IDwtIGZvb2RzLnRyYWluaW5nJGNvc3QKCiMgb25seSBjb25zaWRlciBudW1lcmljIGZlYXR1cmVzCnRyYWluaW5nLmZlYXR1cmVzIDwtIGZvb2RzLnRyYWluaW5nWywyOjRdCnRyYWluaW5nLmZlYXR1cmVzJHR5cGUgPC0gMAoKIyBjbGFzc2lmeSB0aGUgbmV3IGl0ZW0gdXNpbmcgb3VyIG5ldyBrbm4gYWxnb3JpdGhtCnByIDwtIEtOTi5SRUdSRVNTSU9OKHRyYWluaW5nLmZlYXR1cmVzLCB0YXJnZXQsIHUsIGsgPSA0KQoKIyBwcmVkaWN0ZWQgZm9vZCBwcmljZSBiYXNlZCBvbiBmZWF0dXJlcwpwcmludChwYXN0ZTAoImZvb2QgcHJpY2UgaXMgJyIscHIsIiciKSkKYGBgCgpNYWtlIHN1cmUgeW91IHN0YW5kYXJkaXplIGFueSBuZXcgZGF0YSB2YWx1ZXMgdGhlIHNhbWUgd2F5IGFzIHlvdSBzdGFuZGFyZGl6ZWQgdGhlIHRyYWluaW5nIGRhdGEgb3IgZGlzdGFuY2UgY2FsY3VsYXRpb25zIHdpbGwgbm90IGJlIG1lYW5pbmdmdWwuCgotLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KCiMjIEZpbGVzICYgUmVzb3VyY2VzCgpgYGB7ciB6aXBGaWxlcywgZWNobz1GQUxTRX0KemlwTmFtZSA9IHNwcmludGYoIkxlc3NvbkZpbGVzLSVzLSVzLnppcCIsIAogICAgICAgICAgICAgICAgIHBhcmFtcyRjYXRlZ29yeSwKICAgICAgICAgICAgICAgICBwYXJhbXMkbnVtYmVyKQoKdGV4dEFMaW5rID0gcGFzdGUwKCJBbGwgRmlsZXMgZm9yIExlc3NvbiAiLCAKICAgICAgICAgICAgICAgcGFyYW1zJGNhdGVnb3J5LCIuIixwYXJhbXMkbnVtYmVyKQoKIyBkb3dubG9hZEZpbGVzTGluaygpIGlzIGluY2x1ZGVkIGZyb20gX2luc2VydDJEQi5SCmtuaXRyOjpyYXdfaHRtbChkb3dubG9hZEZpbGVzTGluaygiLiIsIHppcE5hbWUsIHRleHRBTGluaykpCmBgYAoKLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCgojIyBSZWZlcmVuY2VzCgpObyByZWZlcmVuY2VzLgoKIyMgRXJyYXRhCgpOb25lIGNvbGxlY3RlZCB5ZXQuIExldCB1cyBrbm93LgoKYGBgez1odG1sfQo8c2NyaXB0IHNyYz0iaHR0cHM6Ly9mb3JtLmpvdGZvcm0uY29tL3N0YXRpYy9mZWVkYmFjazIuanMiIHR5cGU9InRleHQvamF2YXNjcmlwdCI+CiAgbmV3IEpvdGZvcm1GZWVkYmFjayh7CiAgICBmb3JtSWQ6ICIyMTIxODcwNzI3ODQxNTciLAogICAgYnV0dG9uVGV4dDogIkZlZWRiYWNrIiwKICAgIGJhc2U6ICJodHRwczovL2Zvcm0uam90Zm9ybS5jb20vIiwKICAgIGJhY2tncm91bmQ6ICIjRjU5MjAyIiwKICAgIGZvbnRDb2xvcjogIiNGRkZGRkYiLAogICAgYnV0dG9uU2lkZTogImxlZnQiLAogICAgYnV0dG9uQWxpZ246ICJjZW50ZXIiLAogICAgdHlwZTogZmFsc2UsCiAgICB3aWR0aDogNzAwLAogICAgaGVpZ2h0OiA1MDAsCiAgICBpc0NhcmRGb3JtOiBmYWxzZQogIH0pOwo8L3NjcmlwdD4KYGBgCmBgYHtyIGNvZGU9eGZ1bjo6cmVhZF91dGY4KHBhc3RlMChoZXJlOjpoZXJlKCksJy9SL19kZXBsb3lLbml0LlInKSksIGluY2x1ZGUgPSBGQUxTRX0KYGBgCg==