Setting the data

First, install/activate the packages…

if(!require(FNN)){install.packages("FNN")}
library(tidyverse)
library(FNN)

… and load the data. (don’t forget to set your working directory).

load("songs.RData")

Next, we fix two important things: the target (around which to find neighbors) and the variables on which the distances will be computed. The dataset is songs, we choose the variables that have the same scale (from 0 to 1): danceability, energy, speechiness and valence.

target <- filter(songs, song_name == "In The End")
var_dist <- c("danceability", "energy", "speechiness", "valence")

Performing k-NN

Thanks to the FNN package, we are ready to go! The syntax is very simple

neighbors <- get.knnx(data = songs %>% select(all_of(var_dist)),    # Data source: be careful with the columns!
                      query = target %>% select(all_of(var_dist)),  # Target (with the right columns)
                      k = 50)                                       # Nb of neighbors

There are two outputs in neighbors: the index of the neighbors and the distance to the target.

neighbors$nn.index  # Index = row n° in the dataset
     [,1]  [,2] [,3] [,4]  [,5] [,6] [,7] [,8] [,9] [,10] [,11] [,12] [,13] [,14] [,15] [,16] [,17] [,18]
[1,]    2 11021 7568 8644 12451 6735 7966 2198 8300  5331  5857  5233  2252  5897  5397  3566  7392  9990
     [,19] [,20] [,21] [,22] [,23] [,24] [,25] [,26] [,27] [,28] [,29] [,30] [,31] [,32] [,33] [,34] [,35]
[1,]  7009  5457  9786  6640 10545  7593    56  5952  6688 12669 11228    10  4769 12947  1052  1686  9084
     [,36] [,37] [,38] [,39] [,40] [,41] [,42] [,43] [,44] [,45] [,46] [,47] [,48] [,49] [,50]
[1,]  7865 12838  6650  7606  1072  5333  3632  5671  5860  3964  7822  9370  9953 11098  9362
neighbors$nn.dist  # The corresponding distances (in decreasing order naturally)
     [,1]        [,2]      [,3]       [,4]       [,5]       [,6]      [,7]       [,8]       [,9]      [,10]
[1,]    0 0.008312039 0.0163282 0.01940515 0.02977734 0.03017018 0.0304959 0.03237731 0.03330165 0.03451087
          [,11]      [,12]      [,13]      [,14]      [,15]      [,16]      [,17]      [,18]     [,19]
[1,] 0.03494582 0.03611149 0.03660546 0.03860622 0.04202428 0.04255585 0.04272657 0.04509157 0.0451792
          [,20]      [,21]      [,22]      [,23]      [,24]      [,25]      [,26]      [,27]      [,28]
[1,] 0.04560493 0.04614109 0.04777196 0.05024301 0.05125466 0.05132251 0.05133469 0.05149214 0.05176765
          [,29]      [,30]      [,31]    [,32]      [,33]      [,34]      [,35]      [,36]      [,37]
[1,] 0.05177152 0.05232246 0.05259553 0.054128 0.05453082 0.05459157 0.05581657 0.05683309 0.05685499
          [,38]      [,39]      [,40]      [,41]      [,42]      [,43]      [,44]      [,45]      [,46]
[1,] 0.05758481 0.05765319 0.05815677 0.05896575 0.05984279 0.06119028 0.06146064 0.06162475 0.06335803
          [,47]      [,48]      [,49]      [,50]
[1,] 0.06396007 0.06427161 0.06449031 0.06458181

Let’s see the corresponding songs.

songs[as.numeric(neighbors$nn.index),]              # Applying the indices to the dataset
neighbor_names <- songs[as.numeric(neighbors$nn.index),] %>% pull(song_name) # Same + keeping the names only

Visualizing the result

knn_data <- songs %>%                                   # Extracting data in tody format!
    select(all_of(c(var_dist, "song_name"))) %>%
    pivot_longer(names_to = "attribute", values_to = "value", -song_name)
knn_data %>%                                            # Plotting
    ggplot(aes(x = attribute, y = value)) + geom_jitter(size = 0.5) +
    geom_jitter(data = knn_data %>% filter(song_name %in% neighbor_names), color = "yellow", size = 2) +
    geom_jitter(data = knn_data %>% filter(song_name == target_name), color = "red", size = 3) 

The job in incredibly well done!

Ok, but what if we want to include another variable, like tempo? Easy: add it to the list of variables and scale it via mutate()!

var_dist2 <- c("danceability", "energy", "speechiness", "valence", "tempo")    # Adding the tempo
songs2 <- songs %>% mutate(tempo = tempo / 250)                                # Scaling the tempo
target2 <- target %>% mutate(tempo = tempo / 250)
head(songs2) %>% select(song_name, artist, duration, danceability, tempo)      # Check scale

Ok, we are ready for a second round of k-NN.

neighbors2 <- get.knnx(data = songs2 %>% select(var_dist2),                              # New data source!
Warning messages:
1: Unknown or uninitialised column: `dist`. 
2: Unknown or uninitialised column: `dist`. 
                      query = target2 %>% select(var_dist2),                             # Target
                      k = 10,                                                            # Nb of neighbors
                      algorithm = "brute")                                               # Algo type             
neighbor_names2 <- songs2[as.numeric(neighbors2$nn.index),] %>% pull(song_name)

knn_data2 <- songs2 %>% 
    select(var_dist2, "song_name") %>%             
    pivot_longer(-song_name, names_to = "attribute", values_to = "value")

knn_data2 %>% 
    ggplot(aes(x = attribute, y = value)) + geom_jitter(size = 0.5) +
    geom_jitter(data = knn_data2 %>% filter(song_name %in% neighbor_names2), color = "yellow", size = 2) +
    geom_jitter(data = knn_data2 %>% filter(song_name == target_name), color = "red", size = 3) 

Predictive k-NN

Nearest neighbors can be used for prediction purposes. We have used 5 variables to detect proximity. Let’s see if they can help predict the popularity of the song. Let’s compute the average popularity of the target’s neighbors.

# The number we are trying to predict:
songs %>% filter(song_name == target_name) %>% pull(popularity)
[1] 66
songs[as.numeric(neighbors2$nn.index),] %>% 
    pull(popularity) %>%
    mean()
[1] 50

Does weighting help improve the forecast?

library(magrittr)
Warning message:
Unknown or uninitialised column: `dist`. 
songs[as.numeric(neighbors2$nn.index),] %>% 
    pull(popularity) %>%
    multiply_by(exp(-neighbors2$nn.dist)/mean(exp(-neighbors2$nn.dist))) %>%  # Pipe multiplication!
    mean()
[1] 50.08815

Not really.
What this means is that the target is much less/more popular than songs that have very similar characteristics.

neighbors2 <- get.knnx(data = songs2 %>% select(var_dist2),                              # New data source!
Warning messages:
1: Unknown or uninitialised column: `dist`. 
2: Unknown or uninitialised column: `dist`. 
                      query = target2 %>% select(var_dist2),                             # Target
                      k = 13070,                                                            # Nb of neighbors
                      algorithm = "brute")                                               # Algo type             
neighbor_names2 <- songs2[as.numeric(neighbors2$nn.index),] %>% pull(song_name)

knn_data2 <- songs2 
knn_data2$dist[neighbors2$nn.index] <- neighbors2$nn.dist
Unknown or uninitialised column: `dist`.
knn_data2 <- knn_data2 %>%
    select(var_dist2, "song_name", "dist") %>%             
    pivot_longer(-c("song_name", "dist"), names_to = "attribute", values_to = "value") 

knn_data2_f <- knn_data2 %>% 
    arrange(dist) %>%
    head(1000)

knn_data2 %>% 
    ggplot(aes(x = attribute, y = value)) + geom_jitter(size = 0.5) +
    geom_jitter(data = knn_data2_f %>% 
                    filter(song_name %in% neighbor_names2, song_name != "Poker Face"), 
                aes(color = dist), size = 1) +
    geom_jitter(data = knn_data2_f %>% filter(song_name == "Poker Face"), color = "red", size = 3) +
    scale_colour_distiller(palette = "Spectral", direction = +1, values = c(0, 0.8, 1))

LS0tCnRpdGxlOiAiUzg6IGsgTmVhcmVzdCBOZWlnaGJvcnMiCm91dHB1dDogaHRtbF9ub3RlYm9vawotLS0KCiMgU2V0dGluZyB0aGUgZGF0YQoKRmlyc3QsIGluc3RhbGwvYWN0aXZhdGUgdGhlIHBhY2thZ2VzLi4uIAoKYGBge3IsIG1lc3NhZ2UgPSBGQUxTRSwgd2FybmluZyA9IEZBTFNFfQppZighcmVxdWlyZShGTk4pKXtpbnN0YWxsLnBhY2thZ2VzKCJGTk4iKX0KbGlicmFyeSh0aWR5dmVyc2UpCmxpYnJhcnkoRk5OKQpgYGAKCi4uLiBhbmQgbG9hZCB0aGUgZGF0YS4gKCpkb24ndCBmb3JnZXQgdG8gc2V0IHlvdXIgd29ya2luZyBkaXJlY3RvcnkqKS4KCmBgYHtyfQpsb2FkKCJzb25ncy5SRGF0YSIpCmBgYAoKTmV4dCwgd2UgZml4IHR3byBpbXBvcnRhbnQgdGhpbmdzOiB0aGUgKip0YXJnZXQqKiAoYXJvdW5kIHdoaWNoIHRvIGZpbmQgbmVpZ2hib3JzKSBhbmQgdGhlICoqdmFyaWFibGVzKiogb24gd2hpY2ggdGhlIGRpc3RhbmNlcyB3aWxsIGJlIGNvbXB1dGVkLiAKVGhlIGRhdGFzZXQgaXMgc29uZ3MsIHdlIGNob29zZSB0aGUgdmFyaWFibGVzIHRoYXQgaGF2ZSB0aGUgc2FtZSBzY2FsZSAoZnJvbSAwIHRvIDEpOiBkYW5jZWFiaWxpdHksIGVuZXJneSwgc3BlZWNoaW5lc3MgYW5kIHZhbGVuY2UuCgpgYGB7cn0KdGFyZ2V0X25hbWUgPC0gIkluIFRoZSBFbmQiCnRhcmdldCA8LSBmaWx0ZXIoc29uZ3MsIHNvbmdfbmFtZSA9PSB0YXJnZXRfbmFtZSkKdmFyX2Rpc3QgPC0gYygiZGFuY2VhYmlsaXR5IiwgImVuZXJneSIsICJzcGVlY2hpbmVzcyIsICJ2YWxlbmNlIikKYGBgCgojIFBlcmZvcm1pbmcgay1OTgoKVGhhbmtzIHRvIHRoZSBGTk4gcGFja2FnZSwgd2UgYXJlIHJlYWR5IHRvIGdvISBUaGUgc3ludGF4IGlzIHZlcnkgc2ltcGxlCgpgYGB7cn0KbmVpZ2hib3JzIDwtIGdldC5rbm54KGRhdGEgPSBzb25ncyAlPiUgc2VsZWN0KGFsbF9vZih2YXJfZGlzdCkpLCAgICAjIERhdGEgc291cmNlOiBiZSBjYXJlZnVsIHdpdGggdGhlIGNvbHVtbnMhCiAgICAgICAgICAgICAgICAgICAgICBxdWVyeSA9IHRhcmdldCAlPiUgc2VsZWN0KGFsbF9vZih2YXJfZGlzdCkpLCAgIyBUYXJnZXQgKHdpdGggdGhlIHJpZ2h0IGNvbHVtbnMpCiAgICAgICAgICAgICAgICAgICAgICBrID0gMjApICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIyBOYiBvZiBuZWlnaGJvcnMKYGBgCgpUaGVyZSBhcmUgdHdvIG91dHB1dHMgaW4gKipuZWlnaGJvcnMqKjogdGhlIGluZGV4IG9mIHRoZSBuZWlnaGJvcnMgYW5kIHRoZSBkaXN0YW5jZSB0byB0aGUgdGFyZ2V0LgoKYGBge3J9Cm5laWdoYm9ycyRubi5pbmRleCAgIyBJbmRleCA9IHJvdyBuwrAgaW4gdGhlIGRhdGFzZXQKYGBgCgpgYGB7cn0KbmVpZ2hib3JzJG5uLmRpc3QgICMgVGhlIGNvcnJlc3BvbmRpbmcgZGlzdGFuY2VzIChpbiBkZWNyZWFzaW5nIG9yZGVyIG5hdHVyYWxseSkKYGBgCgpMZXQncyBzZWUgdGhlIGNvcnJlc3BvbmRpbmcgc29uZ3MuCgpgYGB7cn0Kc29uZ3NbYXMubnVtZXJpYyhuZWlnaGJvcnMkbm4uaW5kZXgpLF0gICAgICAgICAgICAgICMgQXBwbHlpbmcgdGhlIGluZGljZXMgdG8gdGhlIGRhdGFzZXQKbmVpZ2hib3JfbmFtZXMgPC0gc29uZ3NbYXMubnVtZXJpYyhuZWlnaGJvcnMkbm4uaW5kZXgpLF0gJT4lIHB1bGwoc29uZ19uYW1lKSAjIFNhbWUgKyBrZWVwaW5nIHRoZSBuYW1lcyBvbmx5CmBgYAoKCiMgVmlzdWFsaXppbmcgdGhlIHJlc3VsdAoKYGBge3J9Cmtubl9kYXRhIDwtIHNvbmdzICU+JSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIyBFeHRyYWN0aW5nIGRhdGEgaW4gdG9keSBmb3JtYXQhCiAgICBzZWxlY3QoYWxsX29mKGModmFyX2Rpc3QsICJzb25nX25hbWUiKSkpICU+JQogICAgcGl2b3RfbG9uZ2VyKG5hbWVzX3RvID0gImF0dHJpYnV0ZSIsIHZhbHVlc190byA9ICJ2YWx1ZSIsIC1zb25nX25hbWUpCmtubl9kYXRhICU+JSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIyBQbG90dGluZwogICAgZ2dwbG90KGFlcyh4ID0gYXR0cmlidXRlLCB5ID0gdmFsdWUpKSArIGdlb21faml0dGVyKHNpemUgPSAwLjUpICsKICAgIGdlb21faml0dGVyKGRhdGEgPSBrbm5fZGF0YSAlPiUgZmlsdGVyKHNvbmdfbmFtZSAlaW4lIG5laWdoYm9yX25hbWVzKSwgY29sb3IgPSAieWVsbG93Iiwgc2l6ZSA9IDIpICsKICAgIGdlb21faml0dGVyKGRhdGEgPSBrbm5fZGF0YSAlPiUgZmlsdGVyKHNvbmdfbmFtZSA9PSB0YXJnZXRfbmFtZSksIGNvbG9yID0gInJlZCIsIHNpemUgPSAzKSAKYGBgCgpUaGUgam9iIGluIGluY3JlZGlibHkgd2VsbCBkb25lIQoKT2ssIGJ1dCB3aGF0IGlmIHdlIHdhbnQgdG8gaW5jbHVkZSBhbm90aGVyIHZhcmlhYmxlLCBsaWtlICoqdGVtcG8qKj8gRWFzeTogYWRkIGl0IHRvIHRoZSBsaXN0IG9mIHZhcmlhYmxlcyBhbmQgc2NhbGUgaXQgdmlhICoqbXV0YXRlKiooKSEKCmBgYHtyfQp2YXJfZGlzdDIgPC0gYygiZGFuY2VhYmlsaXR5IiwgImVuZXJneSIsICJzcGVlY2hpbmVzcyIsICJ2YWxlbmNlIiwgInRlbXBvIikgICAgIyBBZGRpbmcgdGhlIHRlbXBvCnNvbmdzMiA8LSBzb25ncyAlPiUgbXV0YXRlKHRlbXBvID0gdGVtcG8gLyAyNTApICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAjIFNjYWxpbmcgdGhlIHRlbXBvCnRhcmdldDIgPC0gdGFyZ2V0ICU+JSBtdXRhdGUodGVtcG8gPSB0ZW1wbyAvIDI1MCkKaGVhZChzb25nczIpICU+JSBzZWxlY3Qoc29uZ19uYW1lLCBhcnRpc3QsIGR1cmF0aW9uLCBkYW5jZWFiaWxpdHksIHRlbXBvKSAgICAgICMgQ2hlY2sgc2NhbGUKYGBgCgpPaywgd2UgYXJlIHJlYWR5IGZvciBhIHNlY29uZCByb3VuZCBvZiAqayotTk4uCgpgYGB7cn0KbmVpZ2hib3JzMiA8LSBnZXQua25ueChkYXRhID0gc29uZ3MyICU+JSBzZWxlY3QodmFyX2Rpc3QyKSwgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAjIE5ldyBkYXRhIHNvdXJjZSEKICAgICAgICAgICAgICAgICAgICAgIHF1ZXJ5ID0gdGFyZ2V0MiAlPiUgc2VsZWN0KHZhcl9kaXN0MiksICAgICAgICAgICAgICAgICAgICAgICAgICAgICAjIFRhcmdldAogICAgICAgICAgICAgICAgICAgICAgayA9IDEwLCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICMgTmIgb2YgbmVpZ2hib3JzCiAgICAgICAgICAgICAgICAgICAgICBhbGdvcml0aG0gPSAiYnJ1dGUiKSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIyBBbGdvIHR5cGUgICAgICAgICAgICAgCm5laWdoYm9yX25hbWVzMiA8LSBzb25nczJbYXMubnVtZXJpYyhuZWlnaGJvcnMyJG5uLmluZGV4KSxdICU+JSBwdWxsKHNvbmdfbmFtZSkKCmtubl9kYXRhMiA8LSBzb25nczIgJT4lIAogICAgc2VsZWN0KHZhcl9kaXN0MiwgInNvbmdfbmFtZSIpICU+JSAgICAgICAgICAgICAKICAgIHBpdm90X2xvbmdlcigtc29uZ19uYW1lLCBuYW1lc190byA9ICJhdHRyaWJ1dGUiLCB2YWx1ZXNfdG8gPSAidmFsdWUiKQoKa25uX2RhdGEyICU+JSAKICAgIGdncGxvdChhZXMoeCA9IGF0dHJpYnV0ZSwgeSA9IHZhbHVlKSkgKyBnZW9tX2ppdHRlcihzaXplID0gMC41KSArCiAgICBnZW9tX2ppdHRlcihkYXRhID0ga25uX2RhdGEyICU+JSBmaWx0ZXIoc29uZ19uYW1lICVpbiUgbmVpZ2hib3JfbmFtZXMyKSwgY29sb3IgPSAieWVsbG93Iiwgc2l6ZSA9IDIpICsKICAgIGdlb21faml0dGVyKGRhdGEgPSBrbm5fZGF0YTIgJT4lIGZpbHRlcihzb25nX25hbWUgPT0gdGFyZ2V0X25hbWUpLCBjb2xvciA9ICJyZWQiLCBzaXplID0gMykgCmBgYAoKCiMgUHJlZGljdGl2ZSBrLU5OCgpOZWFyZXN0IG5laWdoYm9ycyBjYW4gYmUgdXNlZCBmb3IgcHJlZGljdGlvbiBwdXJwb3Nlcy4gV2UgaGF2ZSB1c2VkIDUgdmFyaWFibGVzIHRvIGRldGVjdCBwcm94aW1pdHkuIExldCdzIHNlZSBpZiB0aGV5IGNhbiBoZWxwIHByZWRpY3QgdGhlIHBvcHVsYXJpdHkgb2YgdGhlIHNvbmcuIExldCdzIGNvbXB1dGUgdGhlIGF2ZXJhZ2UgcG9wdWxhcml0eSBvZiB0aGUgKnRhcmdldConcyBuZWlnaGJvcnMuCgpgYGB7ciwgd2FybmluZyA9IEZ9CiMgVGhlIG51bWJlciB3ZSBhcmUgdHJ5aW5nIHRvIHByZWRpY3Q6CnNvbmdzICU+JSBmaWx0ZXIoc29uZ19uYW1lID09IHRhcmdldF9uYW1lKSAlPiUgcHVsbChwb3B1bGFyaXR5KQpzb25nc1thcy5udW1lcmljKG5laWdoYm9yczIkbm4uaW5kZXgpLF0gJT4lIAogICAgcHVsbChwb3B1bGFyaXR5KSAlPiUKICAgIG1lYW4oKQpgYGAKCkRvZXMgd2VpZ2h0aW5nIGhlbHAgaW1wcm92ZSB0aGUgZm9yZWNhc3Q/CgpgYGB7ciwgd2FybmluZyA9IEZBTFNFLCBtZXNzYWdlID0gRkFMU0V9CmxpYnJhcnkobWFncml0dHIpCnNvbmdzW2FzLm51bWVyaWMobmVpZ2hib3JzMiRubi5pbmRleCksXSAlPiUgCiAgICBwdWxsKHBvcHVsYXJpdHkpICU+JQogICAgbXVsdGlwbHlfYnkoZXhwKC1uZWlnaGJvcnMyJG5uLmRpc3QpL21lYW4oZXhwKC1uZWlnaGJvcnMyJG5uLmRpc3QpKSkgJT4lICAjIFBpcGUgbXVsdGlwbGljYXRpb24hCiAgICBtZWFuKCkKYGBgCgpOb3QgcmVhbGx5LiAgCldoYXQgdGhpcyBtZWFucyBpcyB0aGF0IHRoZSB0YXJnZXQgaXMgbXVjaCBsZXNzL21vcmUgcG9wdWxhciB0aGFuIHNvbmdzIHRoYXQgaGF2ZSB2ZXJ5IHNpbWlsYXIgY2hhcmFjdGVyaXN0aWNzLiAKCmBgYHtyfQoKYGBgCgoKCmBgYHtyfQpuZWlnaGJvcnMyIDwtIGdldC5rbm54KGRhdGEgPSBzb25nczIgJT4lIHNlbGVjdCh2YXJfZGlzdDIpLCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICMgTmV3IGRhdGEgc291cmNlIQogICAgICAgICAgICAgICAgICAgICAgcXVlcnkgPSB0YXJnZXQyICU+JSBzZWxlY3QodmFyX2Rpc3QyKSwgICAgICAgICAgICAgICAgICAgICAgICAgICAgICMgVGFyZ2V0CiAgICAgICAgICAgICAgICAgICAgICBrID0gMTMwNzAsICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIyBOYiBvZiBuZWlnaGJvcnMKICAgICAgICAgICAgICAgICAgICAgIGFsZ29yaXRobSA9ICJicnV0ZSIpICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAjIEFsZ28gdHlwZSAgICAgICAgICAgICAKbmVpZ2hib3JfbmFtZXMyIDwtIHNvbmdzMlthcy5udW1lcmljKG5laWdoYm9yczIkbm4uaW5kZXgpLF0gJT4lIHB1bGwoc29uZ19uYW1lKQoKa25uX2RhdGEyIDwtIHNvbmdzMiAKa25uX2RhdGEyJGRpc3RbbmVpZ2hib3JzMiRubi5pbmRleF0gPC0gbmVpZ2hib3JzMiRubi5kaXN0Cmtubl9kYXRhMiA8LSBrbm5fZGF0YTIgJT4lCiAgICBzZWxlY3QodmFyX2Rpc3QyLCAic29uZ19uYW1lIiwgImRpc3QiKSAlPiUgICAgICAgICAgICAgCiAgICBwaXZvdF9sb25nZXIoLWMoInNvbmdfbmFtZSIsICJkaXN0IiksIG5hbWVzX3RvID0gImF0dHJpYnV0ZSIsIHZhbHVlc190byA9ICJ2YWx1ZSIpIAoKa25uX2RhdGEyX2YgPC0ga25uX2RhdGEyICU+JSAKICAgIGFycmFuZ2UoZGlzdCkgJT4lCiAgICBoZWFkKDEwMDApCgprbm5fZGF0YTIgJT4lIAogICAgZ2dwbG90KGFlcyh4ID0gYXR0cmlidXRlLCB5ID0gdmFsdWUpKSArIGdlb21faml0dGVyKHNpemUgPSAwLjUpICsKICAgIGdlb21faml0dGVyKGRhdGEgPSBrbm5fZGF0YTJfZiAlPiUgCiAgICAgICAgICAgICAgICAgICAgZmlsdGVyKHNvbmdfbmFtZSAlaW4lIG5laWdoYm9yX25hbWVzMiwgc29uZ19uYW1lICE9ICJQb2tlciBGYWNlIiksIAogICAgICAgICAgICAgICAgYWVzKGNvbG9yID0gZGlzdCksIHNpemUgPSAxKSArCiAgICBnZW9tX2ppdHRlcihkYXRhID0ga25uX2RhdGEyX2YgJT4lIGZpbHRlcihzb25nX25hbWUgPT0gIlBva2VyIEZhY2UiKSwgY29sb3IgPSAicmVkIiwgc2l6ZSA9IDMpICsKICAgIHNjYWxlX2NvbG91cl9kaXN0aWxsZXIocGFsZXR0ZSA9ICJTcGVjdHJhbCIsIGRpcmVjdGlvbiA9ICsxLCB2YWx1ZXMgPSBjKDAsIDAuOCwgMSkpCmBgYAoKYGBge3J9CgpgYGAKCg==