Note: Some results may differ from the hard copy book due to the changing of sampling procedures introduced in R 3.6.0. See http://bit.ly/35D1SW7 for more details. Access and run the source code for this notebook here.
Hidden chapter requirements used in the book to set the plotting theme and load packages used in hidden code chunks:
knitr::opts_chunk$set(
message = FALSE,
warning = FALSE,
cache = FALSE
)
# Set the graphical theme
ggplot2::theme_set(ggplot2::theme_light())
# packages for hidden code chunks
library(kableExtra)
library(pca3d)
Prerequisites
This chapter leverages the following packages:
library(dplyr) # basic data manipulation and plotting
library(ggplot2) # data visualization
library(h2o) # performing dimension reduction
To illustrate dimension reduction techniques, we’ll use the my_basket
data set:
url <- "https://koalaverse.github.io/homlr/data/my_basket.csv"
my_basket <- readr::read_csv(url)
Parsed with column specification:
cols(
.default = col_double()
)
See spec(...) for full column specifications.
dim(my_basket)
[1] 2000 42
The idea
Table 17.1:
# compute feature correlation
m <- cor(my_basket)
# plot features with highest correlations
data.frame(
row = rownames(m)[row(m)[upper.tri(m)]],
col = colnames(m)[col(m)[upper.tri(m)]],
corr = m[upper.tri(m)],
stringsAsFactors = FALSE
) %>%
filter(corr < 1 & corr > .25) %>%
rename(`Item 1` = row, `Item 2` = col, Correlation = corr) %>%
mutate(Correlation = round(Correlation, 3)) %>%
arrange(desc(Correlation)) %>%
kable(caption = "Various items in our my basket data that are correlated.") %>%
kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Various items in our my basket data that are correlated.
Item 1 |
Item 2 |
Correlation |
cheese |
mayonnaise |
0.345 |
bulmers |
fosters |
0.335 |
cheese |
bread |
0.320 |
lasagna |
pizza |
0.316 |
pepsi |
coke |
0.309 |
red.wine |
fosters |
0.308 |
milk |
muesli |
0.302 |
mars |
twix |
0.301 |
red.wine |
bulmers |
0.298 |
bulmers |
kronenbourg |
0.289 |
milk |
tea |
0.288 |
red.wine |
kronenbourg |
0.286 |
7up |
coke |
0.282 |
spinach |
broccoli |
0.282 |
mayonnaise |
bread |
0.278 |
peas |
potatoes |
0.271 |
peas |
carrots |
0.270 |
tea |
instant.coffee |
0.270 |
milk |
instant.coffee |
0.267 |
bread |
lettuce |
0.264 |
twix |
kitkat |
0.259 |
mars |
kitkat |
0.255 |
muesli |
instant.coffee |
0.251 |
Finding principal components
Figure 17.1:
df <- AmesHousing::make_ames() %>%
select(var1 = First_Flr_SF, var2 = Gr_Liv_Area) %>%
filter(var1 != var2) %>%
mutate_all(log) %>%
scale() %>%
data.frame() %>%
filter(var1 < 4)
ggplot(df, aes(var1, var2)) +
geom_jitter(alpha = .2, size = 1, color = "dodgerblue") +
geom_segment(
aes(x = 0, xend = 1.5 , y = 0, yend = 1.5),
arrow = arrow(length = unit(0.25,"cm")), size = 0.75, color = "black"
) +
annotate("text", x = 1, y = .2, label = "First principal component", size = 2.5, hjust = 0) +
annotate("text", x = -3, y = .8, label = "Second principal component", size = 2.5, hjust = 0) +
geom_segment(
aes(x = 0, xend = -0.27 , y = 0, yend = .65),
arrow = arrow(length = unit(0.25,"cm")), size = 0.75, color = "black"
) +
xlab("Feature 2") +
ylab("Feature 1") +
theme_bw()
Creates Figure 17.2. Uncomment snapshotPCA3d(file = "3D-PCA.png")
to save image to desired location.
df <- AmesHousing::make_ames() %>%
select(var1 = First_Flr_SF, var2 = Gr_Liv_Area, var3 = TotRms_AbvGrd) %>%
filter(var1 != var2) %>%
mutate_at(vars(var1, var2), log)
pca <- prcomp(df, scale = FALSE)
pca3d(pca)
[1] 0.135693940 0.022140269 0.009712809
Creating new device
#snapshotPCA3d(file="3D-PCA.png")
Imports Figure 17.2:
knitr::include_graphics("images/3D-PCA.png")
Selecting the number of principal components
Eigenvalue criterion
# Compute eigenvalues
eigen <- my_pca@model$importance["Standard deviation", ] %>%
as.vector() %>%
.^2
# Sum of all eigenvalues equals number of variables
sum(eigen)
[1] 42
# Find PCs where the sum of eigenvalues is greater than or equal to 1
which(eigen >= 1)
[1] 1 2 3 4 5 6 7 8 9 10
Figure 17.5:
data.frame(
PC = seq_along(eigen),
Eigenvalue = unlist(eigen)
) %>%
ggplot(aes(PC, Eigenvalue)) +
geom_point() +
geom_hline(yintercept = 1, lty = "dashed", color = "red") +
scale_y_continuous(breaks = 0:6) +
xlab("PC") +
annotate("text", x = 15, y = 1, label = "eigenvalue criteria cutoff", color = "red", size = 5, hjust = 0, vjust = -1)
Proportion of variance explained criterion
# Extract and plot PVE and CVE
data.frame(
PC = my_pca@model$importance %>% seq_along(),
PVE = my_pca@model$importance %>% .[2,] %>% unlist(),
CVE = my_pca@model$importance %>% .[3,] %>% unlist()
) %>%
tidyr::gather(metric, variance_explained, -PC) %>%
ggplot(aes(PC, variance_explained)) +
geom_point() +
facet_wrap(~ metric, ncol = 1, scales = "free")
ve <- data.frame(
PC = my_pca@model$importance %>% names(),
PVE = my_pca@model$importance %>% .[2,] %>% unlist(),
CVE = my_pca@model$importance %>% .[3,] %>% unlist()
)
# How many PCs required to explain at least 75% of total variability
min(which(ve$CVE >= 0.75))
[1] 27
Scree plot criterion
data.frame(
PC = my_pca@model$importance %>% seq_along,
PVE = my_pca@model$importance %>% .[2,] %>% unlist()
) %>%
ggplot(aes(PC, PVE, group = 1, label = PC)) +
geom_point() +
geom_line() +
geom_text(nudge_y = -.002)
h2o.shutdown(prompt = FALSE)
LS0tCnRpdGxlOiAiQ2hhcHRlciAxNzogUHJpbmNpcGFsIENvbXBvbmVudHMgQW5hbHlzaXMiCm91dHB1dDogaHRtbF9ub3RlYm9vawotLS0KCl9fTm90ZV9fOiBTb21lIHJlc3VsdHMgbWF5IGRpZmZlciBmcm9tIHRoZSBoYXJkIGNvcHkgYm9vayBkdWUgdG8gdGhlIGNoYW5naW5nIG9mIHNhbXBsaW5nIHByb2NlZHVyZXMgaW50cm9kdWNlZCBpbiBSIDMuNi4wLiBTZWUgaHR0cDovL2JpdC5seS8zNUQxU1c3IGZvciBtb3JlIGRldGFpbHMuIEFjY2VzcyBhbmQgcnVuIHRoZSBzb3VyY2UgY29kZSBmb3IgdGhpcyBub3RlYm9vayBbaGVyZV0oaHR0cHM6Ly9yc3R1ZGlvLmNsb3VkL3Byb2plY3QvODAxMTg1KS4KCkhpZGRlbiBjaGFwdGVyIHJlcXVpcmVtZW50cyB1c2VkIGluIHRoZSBib29rIHRvIHNldCB0aGUgcGxvdHRpbmcgdGhlbWUgYW5kIGxvYWQgcGFja2FnZXMgdXNlZCBpbiBoaWRkZW4gY29kZSBjaHVua3M6CgpgYGB7ciBzZXR1cH0Ka25pdHI6Om9wdHNfY2h1bmskc2V0KAogIG1lc3NhZ2UgPSBGQUxTRSwgCiAgd2FybmluZyA9IEZBTFNFLCAKICBjYWNoZSA9IEZBTFNFCikKCiMgU2V0IHRoZSBncmFwaGljYWwgdGhlbWUKZ2dwbG90Mjo6dGhlbWVfc2V0KGdncGxvdDI6OnRoZW1lX2xpZ2h0KCkpCgojIHBhY2thZ2VzIGZvciBoaWRkZW4gY29kZSBjaHVua3MKbGlicmFyeShrYWJsZUV4dHJhKQpsaWJyYXJ5KHBjYTNkKQpgYGAKCiMjIFByZXJlcXVpc2l0ZXMKClRoaXMgY2hhcHRlciBsZXZlcmFnZXMgdGhlIGZvbGxvd2luZyBwYWNrYWdlczoKCmBgYHtyIHBjYS1wa2ctcHJlcmVxfQpsaWJyYXJ5KGRwbHlyKSAgICAgICAjIGJhc2ljIGRhdGEgbWFuaXB1bGF0aW9uIGFuZCBwbG90dGluZwpsaWJyYXJ5KGdncGxvdDIpICAgICAjIGRhdGEgdmlzdWFsaXphdGlvbgpsaWJyYXJ5KGgybykgICAgICAgICAjIHBlcmZvcm1pbmcgZGltZW5zaW9uIHJlZHVjdGlvbgpgYGAKClRvIGlsbHVzdHJhdGUgZGltZW5zaW9uIHJlZHVjdGlvbiB0ZWNobmlxdWVzLCB3ZSdsbCB1c2UgdGhlIGBteV9iYXNrZXRgIGRhdGEgc2V0OgoKYGBge3IgcGNhLWRhdGEtcHJlcmVxfQp1cmwgPC0gImh0dHBzOi8va29hbGF2ZXJzZS5naXRodWIuaW8vaG9tbHIvZGF0YS9teV9iYXNrZXQuY3N2IgpteV9iYXNrZXQgPC0gcmVhZHI6OnJlYWRfY3N2KHVybCkKZGltKG15X2Jhc2tldCkgIApgYGAKCiMjIFRoZSBpZGVhCgpUYWJsZSAxNy4xOgoKYGBge3IgdW5zdXBlcnZpc2VkLWNvcnJlbGF0aW9uLCBmaWcuaGVpZ2h0PTUsIGZpZy5jYXA9IlRvcCAxMCB2YXJpYWJsZXMgY29udGFpbmluZyB0aGUgc3Ryb25nZXN0IGNvcnJlbGF0aW9uIHdpdGggYXQgbGVhc3Qgb25lIG90aGVyIGZlYXR1cmUuIn0KIyBjb21wdXRlIGZlYXR1cmUgY29ycmVsYXRpb24KbSA8LSBjb3IobXlfYmFza2V0KQoKIyBwbG90IGZlYXR1cmVzIHdpdGggaGlnaGVzdCBjb3JyZWxhdGlvbnMKZGF0YS5mcmFtZSgKICByb3cgPSByb3duYW1lcyhtKVtyb3cobSlbdXBwZXIudHJpKG0pXV0sCiAgY29sID0gY29sbmFtZXMobSlbY29sKG0pW3VwcGVyLnRyaShtKV1dLAogIGNvcnIgPSBtW3VwcGVyLnRyaShtKV0sCiAgc3RyaW5nc0FzRmFjdG9ycyA9IEZBTFNFCiAgKSAlPiUKICBmaWx0ZXIoY29yciA8IDEgJiBjb3JyID4gLjI1KSAlPiUKICByZW5hbWUoYEl0ZW0gMWAgPSByb3csIGBJdGVtIDJgID0gY29sLCBDb3JyZWxhdGlvbiA9IGNvcnIpICU+JQogIG11dGF0ZShDb3JyZWxhdGlvbiA9IHJvdW5kKENvcnJlbGF0aW9uLCAzKSkgJT4lCiAgYXJyYW5nZShkZXNjKENvcnJlbGF0aW9uKSkgJT4lCiAga2FibGUoY2FwdGlvbiA9ICJWYXJpb3VzIGl0ZW1zIGluIG91ciBteSBiYXNrZXQgZGF0YSB0aGF0IGFyZSBjb3JyZWxhdGVkLiIpICU+JQogIGthYmxlX3N0eWxpbmcoYm9vdHN0cmFwX29wdGlvbnMgPSBjKCJzdHJpcGVkIiwgImhvdmVyIiksIGZ1bGxfd2lkdGggPSBGQUxTRSkKYGBgCgojIyBGaW5kaW5nIHByaW5jaXBhbCBjb21wb25lbnRzCgpGaWd1cmUgMTcuMToKCmBgYHtyIGNyZWF0ZS1wY2EtaW1hZ2UsIGZpZy53aWR0aD0zLjc1LCBmaWcuaGVpZ2h0PTIuNzUsIGZpZy5jYXA9IlByaW5jaXBhbCBjb21wb25lbnRzIG9mIHR3byBmZWF0dXJlcyB0aGF0IGhhdmUgMC41NiBjb3JyZWxhdGlvbi4ifQpkZiA8LSBBbWVzSG91c2luZzo6bWFrZV9hbWVzKCkgJT4lCiAgc2VsZWN0KHZhcjEgPSBGaXJzdF9GbHJfU0YsIHZhcjIgPSBHcl9MaXZfQXJlYSkgJT4lCiAgZmlsdGVyKHZhcjEgIT0gdmFyMikgJT4lCiAgbXV0YXRlX2FsbChsb2cpICU+JQogIHNjYWxlKCkgJT4lIAogIGRhdGEuZnJhbWUoKSAlPiUKICBmaWx0ZXIodmFyMSA8IDQpCmdncGxvdChkZiwgYWVzKHZhcjEsIHZhcjIpKSArCiAgZ2VvbV9qaXR0ZXIoYWxwaGEgPSAuMiwgc2l6ZSA9IDEsIGNvbG9yID0gImRvZGdlcmJsdWUiKSArCiAgZ2VvbV9zZWdtZW50KAogICAgYWVzKHggPSAwLCB4ZW5kID0gMS41ICwgeSA9IDAsIHllbmQgPSAxLjUpLAogICAgYXJyb3cgPSBhcnJvdyhsZW5ndGggPSB1bml0KDAuMjUsImNtIikpLCBzaXplID0gMC43NSwgY29sb3IgPSAiYmxhY2siCiAgICApICsKICBhbm5vdGF0ZSgidGV4dCIsIHggPSAxLCB5ID0gLjIsIGxhYmVsID0gIkZpcnN0IHByaW5jaXBhbCBjb21wb25lbnQiLCBzaXplID0gMi41LCBoanVzdCA9IDApICsKICBhbm5vdGF0ZSgidGV4dCIsIHggPSAtMywgeSA9IC44LCBsYWJlbCA9ICJTZWNvbmQgcHJpbmNpcGFsIGNvbXBvbmVudCIsIHNpemUgPSAyLjUsIGhqdXN0ID0gMCkgKwogIGdlb21fc2VnbWVudCgKICAgIGFlcyh4ID0gMCwgeGVuZCA9IC0wLjI3ICwgeSA9IDAsIHllbmQgPSAuNjUpLAogICAgYXJyb3cgPSBhcnJvdyhsZW5ndGggPSB1bml0KDAuMjUsImNtIikpLCBzaXplID0gMC43NSwgY29sb3IgPSAiYmxhY2siCiAgICApICsKICB4bGFiKCJGZWF0dXJlIDIiKSArCiAgeWxhYigiRmVhdHVyZSAxIikgKwogIHRoZW1lX2J3KCkKYGBgCgpDcmVhdGVzIEZpZ3VyZSAxNy4yLiBVbmNvbW1lbnQgYHNuYXBzaG90UENBM2QoZmlsZSA9ICIzRC1QQ0EucG5nIilgIHRvIHNhdmUgaW1hZ2UKdG8gZGVzaXJlZCBsb2NhdGlvbi4KCmBgYHtyIGNyZWF0ZS0zRC1wY2EtaW1hZ2UsIHdhcm5pbmc9RkFMU0V9CmRmIDwtIEFtZXNIb3VzaW5nOjptYWtlX2FtZXMoKSAlPiUKICBzZWxlY3QodmFyMSA9IEZpcnN0X0Zscl9TRiwgdmFyMiA9IEdyX0xpdl9BcmVhLCB2YXIzID0gVG90Um1zX0FidkdyZCkgJT4lCiAgZmlsdGVyKHZhcjEgIT0gdmFyMikgJT4lCiAgbXV0YXRlX2F0KHZhcnModmFyMSwgdmFyMiksIGxvZykKCnBjYSA8LSBwcmNvbXAoZGYsIHNjYWxlID0gRkFMU0UpCnBjYTNkKHBjYSkKI3NuYXBzaG90UENBM2QoZmlsZT0iM0QtUENBLnBuZyIpCmBgYAoKSW1wb3J0cyBGaWd1cmUgMTcuMjoKCmBgYHtyIHBjYS0zZC1wbG90LCBmaWcuY2FwPSJQcmluY2lwYWwgY29tcG9uZW50cyBvZiB0aHJlZSBmZWF0dXJlcy4iLCBvdXQuaGVpZ2h0PSI3NSUiLCBvdXQud2lkdGg9Ijc1JSJ9CmtuaXRyOjppbmNsdWRlX2dyYXBoaWNzKCJpbWFnZXMvM0QtUENBLnBuZyIpCmBgYAoKIyMgUGVyZm9ybWluZyBQQ0EgaW4gUgoKYGBge3IgcGNhLWgyby1pbml0fQpoMm8ubm9fcHJvZ3Jlc3MoKSAgIyB0dXJuIG9mZiBwcm9ncmVzcyBiYXJzIGZvciBicmV2aXR5Cmgyby5pbml0KG1heF9tZW1fc2l6ZSA9ICI1ZyIpICAjIGNvbm5lY3QgdG8gSDJPIGluc3RhbmNlCmBgYAoKYGBge3IgcGNhLW1vZGVsLTF9CiMgY29udmVydCBkYXRhIHRvIGgybyBvYmplY3QKbXlfYmFza2V0LmgybyA8LSBhcy5oMm8obXlfYmFza2V0KQoKIyBydW4gUENBCm15X3BjYSA8LSBoMm8ucHJjb21wKAogIHRyYWluaW5nX2ZyYW1lID0gbXlfYmFza2V0LmgybywKICBwY2FfbWV0aG9kID0gIkdyYW1TVkQiLAogIGsgPSBuY29sKG15X2Jhc2tldC5oMm8pLCAKICB0cmFuc2Zvcm0gPSAiU1RBTkRBUkRJWkUiLCAKICBpbXB1dGVfbWlzc2luZyA9IFRSVUUsCiAgbWF4X3J1bnRpbWVfc2VjcyA9IDEwMDAKKQpgYGAKCmBgYHtyIHBjYS1yZXN1bHRzfQpteV9wY2EKYGBgCgpgYGB7ciBwY2EtY29udHJpYnV0aW9ucywgZmlnLmhlaWdodD01LCBmaWcuY2FwPSJGZWF0dXJlIGxvYWRpbmdzIGlsbHVzdHJhdGluZyB0aGUgaW5mbHVlbmNlIHRoYXQgZWFjaCB2YXJpYWJsZSBoYXMgb24gdGhlIGZpcnN0IHByaW5jaXBhbCBjb21wb25lbnQuIn0KbXlfcGNhQG1vZGVsJGVpZ2VudmVjdG9ycyAlPiUgCiAgYXMuZGF0YS5mcmFtZSgpICU+JSAKICBtdXRhdGUoZmVhdHVyZSA9IHJvdy5uYW1lcyguKSkgJT4lCiAgZ2dwbG90KGFlcyhwYzEsIHJlb3JkZXIoZmVhdHVyZSwgcGMxKSkpICsKICBnZW9tX3BvaW50KCkKYGBgCgpgYGB7ciBwYzEtcGMyLWNvbnRyaWJ1dGlvbnMsIGZpZy5oZWlnaHQ9NCwgZmlnLmNhcD0iRmVhdHVyZSBjb250cmlidXRpb24gZm9yIHByaW5jaXBhbCBjb21wb25lbnRzIG9uZSBhbmQgdHdvLiJ9Cm15X3BjYUBtb2RlbCRlaWdlbnZlY3RvcnMgJT4lIAogIGFzLmRhdGEuZnJhbWUoKSAlPiUgCiAgbXV0YXRlKGZlYXR1cmUgPSByb3cubmFtZXMoLikpICU+JQogIGdncGxvdChhZXMocGMxLCBwYzIsIGxhYmVsID0gZmVhdHVyZSkpICsKICBnZW9tX3RleHQoKQpgYGAKCiMjIFNlbGVjdGluZyB0aGUgbnVtYmVyIG9mIHByaW5jaXBhbCBjb21wb25lbnRzCgojIyMgRWlnZW52YWx1ZSBjcml0ZXJpb24KCmBgYHtyIGVpZ2VuLWNyaXRlcmlvbn0KIyBDb21wdXRlIGVpZ2VudmFsdWVzCmVpZ2VuIDwtIG15X3BjYUBtb2RlbCRpbXBvcnRhbmNlWyJTdGFuZGFyZCBkZXZpYXRpb24iLCBdICU+JQogIGFzLnZlY3RvcigpICU+JQogIC5eMgogIAojIFN1bSBvZiBhbGwgZWlnZW52YWx1ZXMgZXF1YWxzIG51bWJlciBvZiB2YXJpYWJsZXMKc3VtKGVpZ2VuKQoKIyBGaW5kIFBDcyB3aGVyZSB0aGUgc3VtIG9mIGVpZ2VudmFsdWVzIGlzIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAxCndoaWNoKGVpZ2VuID49IDEpCmBgYAoKRmlndXJlIDE3LjU6CgpgYGB7ciBlaWdlbi1jcml0ZXJpb24tcGxvdCwgZmlnLmNhcD0iRWlnZW52YWx1ZSBjcml0ZXJpb24ga2VlcHMgYWxsIHByaW5jaXBhbCBjb21wb25lbnRzIHdoZXJlIHRoZSBzdW0gb2YgdGhlIGVpZ2VudmFsdWVzIGFyZSBhYm92ZSBvciBlcXVhbCB0byBhIHZhbHVlIG9mIG9uZS4iLCBmaWcuaGVpZ2h0PTMuNSwgZmlnLndpZHRoPTl9CmRhdGEuZnJhbWUoCiAgUEMgPSBzZXFfYWxvbmcoZWlnZW4pLAogIEVpZ2VudmFsdWUgPSB1bmxpc3QoZWlnZW4pCikgJT4lCiAgZ2dwbG90KGFlcyhQQywgRWlnZW52YWx1ZSkpICsKICBnZW9tX3BvaW50KCkgKwogIGdlb21faGxpbmUoeWludGVyY2VwdCA9IDEsIGx0eSA9ICJkYXNoZWQiLCBjb2xvciA9ICJyZWQiKSArCiAgc2NhbGVfeV9jb250aW51b3VzKGJyZWFrcyA9IDA6NikgKwogIHhsYWIoIlBDIikgKwogIGFubm90YXRlKCJ0ZXh0IiwgeCA9IDE1LCB5ID0gMSwgbGFiZWwgPSAiZWlnZW52YWx1ZSBjcml0ZXJpYSBjdXRvZmYiLCBjb2xvciA9ICJyZWQiLCBzaXplID0gNSwgaGp1c3QgPSAwLCB2anVzdCA9IC0xKSAKYGBgCgoKIyMjIFByb3BvcnRpb24gb2YgdmFyaWFuY2UgZXhwbGFpbmVkIGNyaXRlcmlvbgoKYGBge3IgcHZlLWN2ZS1wbG90LCBmaWcuY2FwPSJQVkUgY3JpdGVyaW9uIGtlZXBzIGFsbCBwcmluY2lwYWwgY29tcG9uZW50cyB0aGF0IGFyZSBhYm92ZSBvciBlcXVhbCB0byBhIHByZS1zcGVjaWZpZWQgdGhyZXNob2xkIG9mIHRvdGFsIHZhcmlhYmlsaXR5IGV4cGxhaW5lZC4iLCBmaWcuaGVpZ2h0PTUsIGZpZy53aWR0aD05fQojIEV4dHJhY3QgYW5kIHBsb3QgUFZFIGFuZCBDVkUKZGF0YS5mcmFtZSgKICBQQyAgPSBteV9wY2FAbW9kZWwkaW1wb3J0YW5jZSAlPiUgc2VxX2Fsb25nKCksCiAgUFZFID0gbXlfcGNhQG1vZGVsJGltcG9ydGFuY2UgJT4lIC5bMixdICU+JSB1bmxpc3QoKSwKICBDVkUgPSBteV9wY2FAbW9kZWwkaW1wb3J0YW5jZSAlPiUgLlszLF0gJT4lIHVubGlzdCgpCikgJT4lCiAgdGlkeXI6OmdhdGhlcihtZXRyaWMsIHZhcmlhbmNlX2V4cGxhaW5lZCwgLVBDKSAlPiUKICBnZ3Bsb3QoYWVzKFBDLCB2YXJpYW5jZV9leHBsYWluZWQpKSArCiAgZ2VvbV9wb2ludCgpICsKICBmYWNldF93cmFwKH4gbWV0cmljLCBuY29sID0gMSwgc2NhbGVzID0gImZyZWUiKQpgYGAKCmBgYHtyIGNvbXB1dGUtcHZlLXNpbGVudGx5fQp2ZSA8LSBkYXRhLmZyYW1lKAogIFBDICA9IG15X3BjYUBtb2RlbCRpbXBvcnRhbmNlICU+JSBuYW1lcygpLAogIFBWRSA9IG15X3BjYUBtb2RlbCRpbXBvcnRhbmNlICU+JSAuWzIsXSAlPiUgdW5saXN0KCksCiAgQ1ZFID0gbXlfcGNhQG1vZGVsJGltcG9ydGFuY2UgJT4lIC5bMyxdICU+JSB1bmxpc3QoKQopCmBgYAoKYGBge3J9CiMgSG93IG1hbnkgUENzIHJlcXVpcmVkIHRvIGV4cGxhaW4gYXQgbGVhc3QgNzUlIG9mIHRvdGFsIHZhcmlhYmlsaXR5Cm1pbih3aGljaCh2ZSRDVkUgPj0gMC43NSkpCmBgYAoKIyMjIFNjcmVlIHBsb3QgY3JpdGVyaW9uCgpgYGB7ciBwY2Etc2NyZWUtcGxvdC1jcml0ZXJpb24sIGZpZy5jYXA9IlNjcmVlIHBsb3QgY3JpdGVyaW9uIGxvb2tzIGZvciB0aGUgJ2VsYm93JyBpbiB0aGUgY3VydmUgYW5kIGtlZXBzIGFsbCBwcmluY2lwYWwgY29tcG9uZW50cyBiZWZvcmUgdGhlIGxpbmUgZmxhdHRlbnMgb3V0LiIsIGZpZy5oZWlnaHQ9My41LCBmaWcud2lkdGg9OX0KZGF0YS5mcmFtZSgKICBQQyAgPSBteV9wY2FAbW9kZWwkaW1wb3J0YW5jZSAlPiUgc2VxX2Fsb25nLAogIFBWRSA9IG15X3BjYUBtb2RlbCRpbXBvcnRhbmNlICU+JSAuWzIsXSAlPiUgdW5saXN0KCkKKSAlPiUKICBnZ3Bsb3QoYWVzKFBDLCBQVkUsIGdyb3VwID0gMSwgbGFiZWwgPSBQQykpICsKICBnZW9tX3BvaW50KCkgKwogIGdlb21fbGluZSgpICsKICBnZW9tX3RleHQobnVkZ2VfeSA9IC0uMDAyKQpgYGAKCmBgYHtyfQpoMm8uc2h1dGRvd24ocHJvbXB0ID0gRkFMU0UpCmBgYAoK