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.


Files & Resources

All Files for Lesson 3.411

References

No references.

Errata

None collected yet. Let us know.

LS0tCnRpdGxlOiAiU2ltcGxlIEltcGxlbWVudGF0aW9uIG9mIGtOTiBpbiBSIgpwYXJhbXM6CiAgY2F0ZWdvcnk6IDMKICBzdGFja3M6IDAKICBudW1iZXI6IDQxMQogIHRpbWU6IDMwCiAgbGV2ZWw6IGJlZ2lubmVyCiAgdGFnczoga25uLG1hY2hpbmUgbGVhcm5pbmcsY2xhc3NpZmljYXRpb24KICBkZXNjcmlwdGlvbjogIlByZXNlbnRzIGEgc2ltcGxlIGltcGxlbWVudGF0aW9uIG9mIGtOTiBmb3IgY2xhc3NpZmljYXRpb24KICAgICAgICAgICAgICAgIGFuZCBhbm90aGVyIGltcGxlbWVudGF0aW9uIGZvciByZWdyZXNzaW9uLCBib3RoIGluIFIuIgpkYXRlOiAiPHNtYWxsPmByIFN5cy5EYXRlKClgPC9zbWFsbD4iCmF1dGhvcjogIjxzbWFsbD5NYXJ0aW4gU2NoZWRsYmF1ZXI8L3NtYWxsPiIKZW1haWw6ICJtLnNjaGVkbGJhdWVyQG5ldS5lZHUiCmFmZmlsaXRhdGlvbjogIk5vcnRoZWFzdGVybiBVbml2ZXJzaXR5IgpvdXRwdXQ6IAogIGJvb2tkb3duOjpodG1sX2RvY3VtZW50MjoKICAgIHRvYzogdHJ1ZQogICAgdG9jX2Zsb2F0OiB0cnVlCiAgICBjb2xsYXBzZWQ6IGZhbHNlCiAgICBudW1iZXJfc2VjdGlvbnM6IGZhbHNlCiAgICBjb2RlX2Rvd25sb2FkOiB0cnVlCiAgICB0aGVtZTogc3BhY2VsYWIKICAgIGhpZ2hsaWdodDogdGFuZ28KLS0tCgotLS0KdGl0bGU6ICI8c21hbGw+YHIgcGFyYW1zJGNhdGVnb3J5YC5gciBwYXJhbXMkbnVtYmVyYDwvc21hbGw+PGJyLz48c3BhbiBzdHlsZT0nY29sb3I6ICMyRTQwNTM7IGZvbnQtc2l6ZTogMC45ZW0nPmByIHJtYXJrZG93bjo6bWV0YWRhdGEkdGl0bGVgPC9zcGFuPiIKLS0tCgpgYGB7ciBjb2RlPXhmdW46OnJlYWRfdXRmOChwYXN0ZTAoaGVyZTo6aGVyZSgpLCcvUi9faW5zZXJ0MkRCLlInKSksIGluY2x1ZGUgPSBGQUxTRX0KYGBgCgotLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KCiMjIE9iamVjdGl2ZXMKClVwb24gY29tcGxldGlvbiBvZiB0aGlzIGxlc3NvbiwgeW91IHdpbGwgYmUgYWJsZSB0bzoKCi0gICBpbXBsZW1lbnQgKmtOTiogaW4gUiBmb3IgY2xhc3NpZmljYXRpb24gYW5kIHJlZ3Jlc3Npb24KCi0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQoKIyMgTW90aXZhdGlvbgoKVGhpcyBpcyBhIHNpbXBsZSBhbmQgaWxsdXN0cmF0aXZlIGltcGxlbWVudGF0aW9uIG9mIHRoZSAqay1OZWFyZXN0IE5laWdoYm9ycyogYWxnb3JpdGhtIGZvciBjbGFzc2lmaWNhdGlvbiBhbmQgcmVncmVzc2lvbi4gSXQgaXMgbmVpdGhlciBlZmZpY2llbnQgbm90IHNjYWxhYmxlLCBidXQgbWVhbnQgdG8gYmUgaWxsdXN0cmF0aXZlIC0tIGl0IHNob3VsZCBub3QgYmUgdXNlZCBpbiBwcm9kdWN0aW9uLgoKIyMgRGVmaW5pdGlvbnMgb2YgRnVuY3Rpb25zCgoqa05OTU9ERSogLSBpZGVudGlmaWVzIHRoZSBtb3N0IGZyZXF1ZW50bHkgb2NjdXJyaW5nIHZhbHVlIGluIGEgdmVjdG9yIG9mIG5vbWluYWwgdmFsdWVzCgpgYGB7cn0Ka05OTU9ERSA8LSBmdW5jdGlvbih4KSAKewogIHV4IDwtIHVuaXF1ZSh4KQogIHJldHVybiAodXhbd2hpY2gubWF4KHRhYnVsYXRlKG1hdGNoKHgsIHV4KSkpXSkKfQpgYGAKCiprTk5BVkcqIC0gY2FsY3VsYXRlcyB0aGUgYXZlcmFnZSB2YWx1ZSBpbiBhIHZlY3RvciBvZiBub21pbmFsIHZhbHVlcwoKYGBge3J9CmtOTkFWRyA8LSBmdW5jdGlvbih4KSAKewogIHJldHVybiAobWVhbih4KSkKfQpgYGAKCipkaXN0KiAtIGNhbGN1bGF0ZXMgdGhlIEV1Y2xpZGVhbiBkaXN0YW5jZSBiZXR3ZWVuIHR3byB2ZWN0b3JzIG9mIGVxdWFsIHNpemUgY29udGFpbmluZyBudW1lcmljIGVsZW1lbnRzLgoKYGBge3J9CmtOTkRJU1QgPC0gZnVuY3Rpb24ocCwgcSkKewogIGQgPC0gMAogIGZvciAoaSBpbiAxOmxlbmd0aChwKSkgewogICAgZCA8LSBkICsgKHBbaV0gLSBxW2ldKV4yCiAgfQogIAogIHJldHVybihzcXJ0KGQpKQp9CmBgYAoKKm5laWdoYm9ycyogLSByZXR1cm5zIGEgdmVjdG9yIG9mIGRpc3RhbmNlcyBiZXR3ZWVuIGFuIG9iamVjdCAqdSogYW5kIGEgZGF0YSBmcmFtZSBvZiBmZWF0dXJlczsgYWxsIGZlYXR1cmVzIG11c3QgYmUgbnVtZXJpYwoKYGBge3J9CmtOTk5laWdoYm9ycyA8LSBmdW5jdGlvbiAodHJhaW4sIHUpCnsKICAgbSA8LSBucm93KHRyYWluKQogICBkcyA8LSBudW1lcmljKG0pCiAgIGZvciAoaSBpbiAxOm0pIHsKICAgICBwIDwtIHRyYWluW2ksXQogICAgIGRzW2ldIDwtIHVubGlzdChrTk5ESVNUKHAsdSkpCiAgIH0KICAgCiAgIHJldHVybihkcykKfQpgYGAKCiprLmNsb3Nlc3QqIC0gZmluZHMgdGhlIHNtYWxsZXN0ICprKiB2YWx1ZXMgaW4gYSB2ZWN0b3Igb2YgdmFsdWVzCgpgYGB7cn0Kay5jbG9zZXN0IDwtIGZ1bmN0aW9uKG5laWdoYm9ycyxrKQp7CiAgIyB1c2VzIHRoZSBvcmRlciBmdW5jdGlvbiBmcm9tIFIgdG8gc29ydCB0aGUgdmVjdG9yIAogICMgb2YgbmVpZ2hib3JzIGJ5IGRpc3RhbmNlCiAgb3JkZXJlZC5uZWlnaGJvcnMgPC0gb3JkZXIobmVpZ2hib3JzKQogIAogICMgZXh0cmFjdHMgb25seSB0aGUgdG9wIGsgbmVpZ2hib3JzCiAgIyByZXR1cm5zIHRoZSBpbmRleGVzIG9mIHRob3NlIGNsb3Nlc3QgbmVpZ2hib3JzCiAgay5jbG9zZXN0IDwtIG9yZGVyZWQubmVpZ2hib3JzWzE6a10KfQpgYGAKCipLTk4uQ0xBU1NJRklDQVRJT04qIC0gZmluZHMgdGhlIG1vc3QgbGlrZWx5IGNsYXNzIHRoYXQgYW4gdW5rbm93biBvYmplY3QgKnUqIGJlbG9uZ3MgdG8gYmFzZWQgb24gYSB0cmFpbmluZyBkYXRhIGZyYW1lIG9mIGZlYXR1cmVzLCBhIGNvcnJlc3BvbmRpbmcgdmVjdG9yIG9mIGxhYmVscywgYW5kIGEgcHJvdmlkZWQgKmsqLiBUaGUgbmFtZSBpcyBwdXJwb3NlbHkgY2FwaXRhbGl6ZWQgdG8gYXZvaWQgY29uZmxpY3RzIHdpdGggb3RoZXIgaW1wbGVtZW50YXRpb25zIG9mIGtOTiBmcm9tIHBhY2thZ2VzLgoKYGBge3J9CktOTi5DTEFTU0lGSUNBVElPTiA8LSBmdW5jdGlvbiAodHJhaW4sIGxhYmVscywgdSwgaykKewogIG5iIDwtIGtOTk5laWdoYm9ycyh0cmFpbix1KQogIGYgPC0gay5jbG9zZXN0KG5iLGspCiAgS05OIDwtIGtOTk1PREUobGFiZWxzW2ZdKQp9CmBgYAoKKktOTi5SRUdSRVNTSU9OKiAtIGZpbmRzIHRoZSBtb3N0IGxpa2VseSB0YXJnZXQgdmFsdWUgdGhhdCBhbiB1bmtub3duIG9iamVjdCAqdSogYmFzZWQgb24gYSB0cmFpbmluZyBkYXRhIGZyYW1lIG9mIGZlYXR1cmVzLCBhIGNvcnJlc3BvbmRpbmcgdmVjdG9yIG9mIHRhcmdldCB2YWx1ZXMsIGFuZCBhIHByb3ZpZGVkICprKi4gVGhlIG5hbWUgaXMgcHVycG9zZWx5IGNhcGl0YWxpemVkIHRvIGF2b2lkIGNvbmZsaWN0cyB3aXRoIG90aGVyIGltcGxlbWVudGF0aW9ucyBvZiBrTk4gZnJvbSBwYWNrYWdlcy4KCmBgYHtyfQpLTk4uUkVHUkVTU0lPTiA8LSBmdW5jdGlvbiAodHJhaW4sIHRhcmdldCwgdSwgaykKewogIG5iIDwtIGtOTk5laWdoYm9ycyh0cmFpbix1KQogIGYgPC0gay5jbG9zZXN0KG5iLGspCiAgS05OIDwtIGtOTkFWRyh0YXJnZXRbZl0pCn0KYGBgCgojIyBVc2Ugb2YgQWxnb3JpdGhtOiBDbGFzc2lmaWNhdGlvbgoKTGV0J3MgYXBwbHkgdGhlIGFsZ29yaXRobSB0byBjbGFzc2lmeSBmb29kIGl0ZW1zIGJhc2VkIG9uIHRocmVlIGZlYXR1cmVzOiAqc3dlZXRuZXNzKiwgKmNydW5jaGluZXNzKiwgYW5kICpzYWx0aW5lc3MqLgoKYGBge3J9CmZvb2RzLnRyYWluaW5nIDwtIHJlYWQuY3N2KCJmb29kcy5jc3YiKQoKaGVhZChmb29kcy50cmFpbmluZykKYGBgCgpgYGB7cn0KIyB1bmtub3duIGNhc2UgCiMgKG5ldyBmb29kIGl0ZW1zIHdpdGggYSBtZWFzdXJlZCBzd2VldG5lc3MsIGNydW5jaGluZXNzLCBhbmQgc2FsdGluZXNzKQp1IDwtIGMoMywgMSwgMikKdyA8LSBjKDEwLCA4LCAyKQoKIyBzZXBhcmF0ZSB0aGUgbGFiZWwgYW5kIGZlYXR1cmVzCmxhYmVscyA8LSBmb29kcy50cmFpbmluZyR0eXBlCgojIG9ubHkgY29uc2lkZXIgbnVtZXJpYyBmZWF0dXJlcyAob3IgZW5jb2RlZCBjYXRlZ29yaWNhbCBmZWF0dXJlcykKdHJhaW5pbmcuZmVhdHVyZXMgPC0gZm9vZHMudHJhaW5pbmdbLDI6NF0KCiMgY2xhc3NpZnkgdGhlIG5ldyBpdGVtIHVzaW5nIG91ciBuZXcga25uIGFsZ29yaXRobQpubiA8LSBLTk4uQ0xBU1NJRklDQVRJT04odHJhaW5pbmcuZmVhdHVyZXMsIGxhYmVscywgdSwgayA9IDQpCnByaW50KHBhc3RlMCgiZm9vZCB0eXBlIGlzICciLG5uLCInIikpCgpubiA8LSBLTk4uQ0xBU1NJRklDQVRJT04odHJhaW5pbmcuZmVhdHVyZXMsIGxhYmVscywgdywgayA9IDQpCnByaW50KHBhc3RlMCgiZm9vZCB0eXBlIGlzICciLG5uLCInIikpCmBgYAoKIyMgVXNlIG9mIEFsZ29yaXRobTogUmVncmVzc2lvbgoKTGV0J3MgYXBwbHkgdGhlIGFsZ29yaXRobSB0byBjYWxjdWxhdGUgdGhlIGNvc3Qgb2YgYSBmb29kIGl0ZW0gYmFzZWQgb24gdGhyZWUgbnVtZXJpYyBhbmQgb25lIGNhdGVnb3JpY2FsIGZlYXR1cmU6ICpzd2VldG5lc3MqLCAqY3J1bmNoaW5lc3MqLCBhbmQgKnNhbHRpbmVzcyouCgpgYGB7cn0KIyB1bmtub3duIGNhc2UgCiMgKG5ldyBmb29kIGl0ZW1zIHdpdGggYSBtZWFzdXJlZCBzd2VldG5lc3MsIGNydW5jaGluZXNzLCBzYWx0aW5lc3MpCnUgPC0gYygzLCAxLCAyKQp3IDwtIGMoMTAsIDgsIDIpCgojIHNlcGFyYXRlIHRoZSB0YXJnZXQgZmVhdHVyZSBhbmQgdGhlIHByZWRpY3RpdmUgZmVhdHVyZXMKdGFyZ2V0IDwtIGZvb2RzLnRyYWluaW5nJGNvc3QKCiMgb25seSBjb25zaWRlciBudW1lcmljIGZlYXR1cmVzCnRyYWluaW5nLmZlYXR1cmVzIDwtIGZvb2RzLnRyYWluaW5nWywyOjRdCgojIGNsYXNzaWZ5IHRoZSBuZXcgaXRlbSB1c2luZyBvdXIgbmV3IGtubiBhbGdvcml0aG0KcHIgPC0gS05OLlJFR1JFU1NJT04odHJhaW5pbmcuZmVhdHVyZXMsIHRhcmdldCwgdSwgayA9IDQpCgojIHByZWRpY3RlZCBmb29kIHByaWNlIGJhc2VkIG9uIGZlYXR1cmVzCnByaW50KHBhc3RlMCgiZm9vZCBwcmljZSBpcyAnIixwciwiJyIpKQpgYGAKCkxldCdzIGFsc28gY29uc2lkZXIgdGhlIGNhdGVnb3JpY2FsIGZlYXR1cmUgKnR5cGUqLiBPZiBjb3Vyc2UsIHdlIHdpbGwgZmlyc3QgaGF2ZSB0byBjb252ZXJ0IHRoZSBmZWF0dXJlIHRvIGEgbnVtZXJpYyBvbmUgdXNpbmcgYW4gZW5jb2Rpbmcgc2NoZW1lLiBXZSB3aWxsIHVzZSBmcmVxdWVuY3kgZW5jb2RpbmcuCgpgYGB7cn0KIyB1bmtub3duIGNhc2UgCiMgKG5ldyBmb29kIGl0ZW1zIHdpdGggYSBtZWFzdXJlZCBzd2VldG5lc3MsIGNydW5jaGluZXNzLCBzYWx0aW5lc3MpCnUgPC0gYygzLCAxLCAyLCAwKQp3IDwtIGMoMTAsIDgsIDIsIDApCgojIHNlcGFyYXRlIHRoZSB0YXJnZXQgZmVhdHVyZSBhbmQgdGhlIHByZWRpY3RpdmUgZmVhdHVyZXMKdGFyZ2V0IDwtIGZvb2RzLnRyYWluaW5nJGNvc3QKCiMgb25seSBjb25zaWRlciBudW1lcmljIGZlYXR1cmVzCnRyYWluaW5nLmZlYXR1cmVzIDwtIGZvb2RzLnRyYWluaW5nWywyOjRdCnRyYWluaW5nLmZlYXR1cmVzJHR5cGUgPC0gMAoKIyBjbGFzc2lmeSB0aGUgbmV3IGl0ZW0gdXNpbmcgb3VyIG5ldyBrbm4gYWxnb3JpdGhtCnByIDwtIEtOTi5SRUdSRVNTSU9OKHRyYWluaW5nLmZlYXR1cmVzLCB0YXJnZXQsIHUsIGsgPSA0KQoKIyBwcmVkaWN0ZWQgZm9vZCBwcmljZSBiYXNlZCBvbiBmZWF0dXJlcwpwcmludChwYXN0ZTAoImZvb2QgcHJpY2UgaXMgJyIscHIsIiciKSkKYGBgCgpNYWtlIHN1cmUgeW91IHN0YW5kYXJkaXplIGFueSBuZXcgZGF0YSB2YWx1ZXMgdGhlIHNhbWUgd2F5IGFzIHlvdSBzdGFuZGFyZGl6ZWQgdGhlIHRyYWluaW5nIGRhdGEgb3IgZGlzdGFuY2UgY2FsY3VsYXRpb25zIHdpbGwgbm90IGJlIG1lYW5pbmdmdWwuCgotLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KCiMjIEZpbGVzICYgUmVzb3VyY2VzCgpgYGB7ciB6aXBGaWxlcywgZWNobz1GQUxTRX0KemlwTmFtZSA9IHNwcmludGYoIkxlc3NvbkZpbGVzLSVzLSVzLnppcCIsIAogICAgICAgICAgICAgICAgIHBhcmFtcyRjYXRlZ29yeSwKICAgICAgICAgICAgICAgICBwYXJhbXMkbnVtYmVyKQoKdGV4dEFMaW5rID0gcGFzdGUwKCJBbGwgRmlsZXMgZm9yIExlc3NvbiAiLCAKICAgICAgICAgICAgICAgcGFyYW1zJGNhdGVnb3J5LCIuIixwYXJhbXMkbnVtYmVyKQoKIyBkb3dubG9hZEZpbGVzTGluaygpIGlzIGluY2x1ZGVkIGZyb20gX2luc2VydDJEQi5SCmtuaXRyOjpyYXdfaHRtbChkb3dubG9hZEZpbGVzTGluaygiLiIsIHppcE5hbWUsIHRleHRBTGluaykpCmBgYAoKLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCgojIyBSZWZlcmVuY2VzCgpObyByZWZlcmVuY2VzLgoKIyMgRXJyYXRhCgpOb25lIGNvbGxlY3RlZCB5ZXQuIExldCB1cyBrbm93LgoKYGBgez1odG1sfQo8c2NyaXB0IHNyYz0iaHR0cHM6Ly9mb3JtLmpvdGZvcm0uY29tL3N0YXRpYy9mZWVkYmFjazIuanMiIHR5cGU9InRleHQvamF2YXNjcmlwdCI+CiAgbmV3IEpvdGZvcm1GZWVkYmFjayh7CiAgICBmb3JtSWQ6ICIyMTIxODcwNzI3ODQxNTciLAogICAgYnV0dG9uVGV4dDogIkZlZWRiYWNrIiwKICAgIGJhc2U6ICJodHRwczovL2Zvcm0uam90Zm9ybS5jb20vIiwKICAgIGJhY2tncm91bmQ6ICIjRjU5MjAyIiwKICAgIGZvbnRDb2xvcjogIiNGRkZGRkYiLAogICAgYnV0dG9uU2lkZTogImxlZnQiLAogICAgYnV0dG9uQWxpZ246ICJjZW50ZXIiLAogICAgdHlwZTogZmFsc2UsCiAgICB3aWR0aDogNzAwLAogICAgaGVpZ2h0OiA1MDAsCiAgICBpc0NhcmRGb3JtOiBmYWxzZQogIH0pOwo8L3NjcmlwdD4KYGBgCmBgYHtyIGNvZGU9eGZ1bjo6cmVhZF91dGY4KHBhc3RlMChoZXJlOjpoZXJlKCksJy9SL19kZXBsb3lLbml0LlInKSksIGluY2x1ZGUgPSBGQUxTRX0KYGBgCg==