Data Science Stream
Cluster Analysis
Purpose
No answer required.
Clustering Techniques
No answer required.
Preparations
# Specify required packages
cluster_packages <- c("cluster", "factoextra", "ggfortify", "palmerpenguins")
# Install missing packages
install.packages(setdiff(cluster_packages, rownames(installed.packages())))
# Load all packages
lapply(cluster_packages, library, character.only = TRUE)
Penguin Data
No answer required.
plot(penguins)

For several of the continuous random variable scatter plots, there does appear to be visible clustering occurring - e.g. flipper_length_mm
vs bill_depth_mm
suggests that there are two distinct clusters. Therefore applying a clustering technique does seem to be warranted.
Preparing data for Cluster Analysis
No answer required.
penguins <- na.omit(penguins)
penguins_subset <- penguins[, 3:6]
penguins_scaled <- scale(penguins_subset)
head(penguins_scaled)
## bill_length_mm bill_depth_mm flipper_length_mm body_mass_g
## [1,] -0.8946955 0.7795590 -1.4246077 -0.5676206
## [2,] -0.8215515 0.1194043 -1.0678666 -0.5055254
## [3,] -0.6752636 0.4240910 -0.4257325 -1.1885721
## [4,] -1.3335592 1.0842457 -0.5684290 -0.9401915
## [5,] -0.8581235 1.7444004 -0.7824736 -0.6918109
## [6,] -0.9312674 0.3225288 -1.4246077 -0.7228585
\(k\)-means Clustering
set.seed(1) # included for reproducibility purposes
kmeans_fit3 <- kmeans(penguins_scaled, 3)
We can check the cluster membership as follows:
kmeans_fit3$cluster
## [1] 1 1 1 1 1 1 1 1 1 1 1 1 2 1 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [38] 1 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 1 1 1 1 1 1
## [75] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 1 2 1 1 1 1 1
## [112] 1 1 1 1 1 1 1 1 1 1 1 1 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 3 3
## [149] 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3
## [186] 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3
## [223] 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3
## [260] 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 2 1 2 2 2 2 2 2 2 1
## [297] 2 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 2 2 2 2 2 2 2 2 2 2 2 2 2
The between_ss /total_ss
value is 72.1%.
Visualising our results
plot(penguins[, c(1,3:6)], col = kmeans_fit3$cluster)

As we can see, the clusters are actually doing a pretty good job of separating penguins by species!
plot(as.numeric(penguins$species), col = kmeans_fit3$cluster,
xlab = "penguin ID",
ylab = "(Adelie = 1, Chinstrap = 2, Gentoo = 3", las = 1)

Based on these plots, it seems that the \(k\)-means clustering has done a great job of clustering the penguins into clusters of different species. If you look carefully, there is some error when distinguishing between Chinstrap and Adelie penguins. If we also include more data such as sex
or island
, we might be able to more clearly distinguish between these two species.
Two-way Table Summary
💻
two_way_table <- table(as.numeric(penguins$species), kmeans_fit3$cluster)
dimnames(two_way_table) <- list(levels(penguins$species),
c("Cluster 1", "Cluster 2", "Cluster 3")
)
two_way_table
## Cluster 1 Cluster 2 Cluster 3
## Adelie 22 0 124
## Chinstrap 63 0 5
## Gentoo 0 119 0
The two-way table highlights the accuracy of the clustering - only a few Chinstrap penguins and some Adelie penguins have been misclassified.
Cluster plots
fviz_cluster(kmeans_fit3, data = penguins_scaled)

kmeans_fit2 <- kmeans(penguins_scaled, 2)
kmeans_fit4 <- kmeans(penguins_scaled, 4)
The between_ss /total_ss
results for these new cluster fits are:
- 58.5% for \(k=2\).
- 77.1% for \(k=4\).
The between_ss /total_ss
value actually increases as we increase the number of clusters, which isn’t necessarily accurate - we know e.g. that there are not four species of penguin in our data (what happens if we use \(k=10\)?!).
Diagnostics
No answer required.
The silhouette method
kmeans_sil <- fviz_nbclust(penguins_scaled, kmeans, method = "silhouette")
kmeans_sil

Based on this plot, it seems that the optimal number of clusters is two.
The gap statistic method
kmeans_gap <- fviz_nbclust(penguins_scaled, kmeans, method = "gap")
kmeans_gap

Based on this plot, it seems that the optimal number of clusters is four.
fviz_cluster(kmeans_fit2, data = penguins_scaled)

fviz_cluster(kmeans_fit4, data = penguins_scaled)

Answers may vary here. Three or four clusters seem reasonable.
Extension 1: PAM Clustering
pam_fit2 <- pam(penguins_scaled, 2)
pam_fit3 <- pam(penguins_scaled, 3)
pam_fit4 <- pam(penguins_scaled, 4)
pam_sil <- fviz_nbclust(penguins_scaled, pam, method = "silhouette")
pam_sil

These results suggest that two clusters are optimal.
The R code below shows results for a cluster choice of three.
fviz_cluster(pam_fit3, data = penguins_scaled)

plot(as.numeric(penguins$species), col = pam_fit3$clustering)

We can see from these plots that the PAM clustering does a very good job of clustering penguins into species (although there is still some trouble distinguishing Adelie and Chinstrap penguins). For the larger numbers of clusters, the algorithm may also be distinguishing between male and female penguins - try checking if this is true.
Extension 2: Hierarchical Clustering
hier_fit <- agnes(penguins_scaled)
plot(hier_fit, which = 2)

hier_fit <- agnes(penguins_scaled)
hier_fit_single <- agnes(penguins_scaled, method = "single")
hier_fit_ward <- agnes(penguins_scaled, method = "ward")
plot(hier_fit_single, which = 2)

plot(hier_fit_ward, which = 2)

The dendrograms all look quite different. The ward
method one looks perhaps the cleanest and easiest to assess.
The code shown below produces a coloured dendrogram with three clusters, for the hierarchical clustering algorithm using the “ward” measurement method.
penguin_dendro <- hcut(penguins_scaled,
k = 3,
hc_func = "agnes",
hc_method = "ward",
hc_metric = "euclidean")
fviz_dend(penguin_dendro)

Extension 3: Fuzzy Clustering
The R code below shows fits for the different cluster number options.
fuzzy_fit2 <- fanny(penguins_scaled, 2)
fuzzy_fit3 <- fanny(penguins_scaled, 3)
fuzzy_fit4 <- fanny(penguins_scaled, 4)
Please note that output for these fits is omitted due to the length of the results.
fuzzy_sil <- fviz_nbclust(penguins_scaled, fanny, method = "silhouette")
fuzzy_sil

We obtain the same results as for the \(k\)-means and PAM clustering here, i.e. we have a result of two clusters.
The R code below shows results for a cluster choice of two.
fviz_cluster(fuzzy_fit2, data = penguins_scaled)

# This code assumes you are assessing a result stored in the object fuzzy_fit2
plot(as.numeric(penguins$species), col = fuzzy_fit2$cluster)

From these plots we can see that the fuzzy clustering performs similarly to the previous methods.
That’s all the content covered.
References
Horikoshi, M., Y. Tang, A. Dickey, M. Grenie, R. Thompson, L. Selzer, D. Strbenac, K. Voronin, and D. Pulatov. 2021.
ggfortify: Data Visualization Tools for Statistical Analysis Results.
https://github.com/sinhrks/ggfortify.
Horst, Allison Marie, Alison Presmanes Hill, and Kristen B Gorman. 2020.
Palmerpenguins: Palmer Archipelago (Antarctica) Penguin Data.
https://doi.org/10.5281/zenodo.3960218.
Kassambara, A., and F. Mundt. 2020.
factoextra: Extract and Visualize the Results of Multivariate Data Analyses.
http://www.sthda.com/english/rpkgs/factoextra.
R Core Team. 2021.
R: A Language and Environment for Statistical Computing. Vienna, Austria: R Foundation for Statistical Computing.
https://www.R-project.org/.
Thulin, M. 2021. Modern Statistics with R: From Wrangling and Exploring Data to Inference and Predictive Modelling.
These notes have been prepared by Rupert Kuveke. Please note that some of the content in these notes has been developed from content in Thulin (2021). The copyright for the material in these notes resides with the authors named above, with the Department of Mathematical and Physical Sciences and with La Trobe University. Copyright in this work is vested in La Trobe University including all La Trobe University branding and naming. Unless otherwise stated, material within this work is licensed under a Creative Commons Attribution-Non Commercial-Non Derivatives License
BY-NC-ND.
LS0tDQp0aXRsZTogIlNUTTEwMDE6IENvbXB1dGVyIExhYiA3QiBTb2x1dGlvbnMiDQpvdXRwdXQ6DQogIGJvb2tkb3duOjpodG1sX2RvY3VtZW50MjogDQogICAgdG9jOiB0cnVlDQogICAgdG9jX2Zsb2F0OiB0cnVlDQogICAgY29kZV9kb3dubG9hZDogdHJ1ZQ0KICAgIHRoZW1lOiByZWFkYWJsZQ0KICAgIGNvZGVfZm9sZGluZzogc2hvdw0KYmlibGlvZ3JhcGh5OiBTVE0xMDAxX0RTX0NMX3JlZmVyZW5jZXMuYmliIA0KbGluay1jaXRhdGlvbnM6IHllcw0KLS0tDQoNCjxzdHlsZT4NCiNUT0Mgew0KICBiYWNrZ3JvdW5kOiB1cmwoImh0dHBzOi8vd3d3LmxhdHJvYmUuZWR1LmF1L19tZWRpYS9sYS10cm9iZS1hcGkvdjUvaW1nL2xvZ28uc3ZnIik7DQogIGJhY2tncm91bmQtc2l6ZTogY29udGFpbjsNCiAgcGFkZGluZy10b3A6IDgwcHggIWltcG9ydGFudDsNCiAgYmFja2dyb3VuZC1yZXBlYXQ6IG5vLXJlcGVhdDsNCn0NCjwvc3R5bGU+DQoNCiMjIyBEYXRhIFNjaWVuY2UgU3RyZWFtIHstfQ0KDQoNCiMjIyBUb3BpYyA3QjogQmlnIERhdGEgSSAoQ2x1c3RlcmluZykgey19DQoNCjxicj4NCg0KRXhhbXBsZSBSIGNvZGUgc29sdXRpb25zIGZvciB0aGUgW0RhdGEgU2NpZW5jZSBDb21wdXRlciBMYWIgN10oaHR0cHM6Ly9ycHVicy5jb20vTFRVX1NUTTEwMDEvU01EU01DTDcpLCB3aGljaCB1c2VzIHBlbmd1aW4gZGF0YSBmcm9tIHRoZSBgcGFsbWVycGVuZ3VpbnNgIFIgcGFja2FnZV5bV2UgYWxzbyB1dGlsaXNlIHRoZSBwYWNrYWdlcyBgZmFjdG9leHRyYWAgW0BmYWN0b10sIGBnZ2ZvcnRpZnlgIFtAZ2ddIGFuZCBgY2x1c3RlcmBbQFJdLl0gW0BwZW5ndWluc10sIGFyZSBwcmVzZW50ZWQgYmVsb3cuDQoNCiMgQ2x1c3RlciBBbmFseXNpcw0KDQojIyBQdXJwb3NlIHstfQ0KDQpObyBhbnN3ZXIgcmVxdWlyZWQuDQoNCiMjIENsdXN0ZXJpbmcgVGVjaG5pcXVlcyB7LX0NCg0KTm8gYW5zd2VyIHJlcXVpcmVkLg0KDQojIFByZXBhcmF0aW9ucyB7I3ByZXB9DQoNCmBgYHtyIGV2YWwgPSBULCBpbmNsdWRlID0gRn0NCmxpYnJhcnkoY2x1c3RlcikNCmxpYnJhcnkoZmFjdG9leHRyYSkNCmxpYnJhcnkoZ2dmb3J0aWZ5KQ0KbGlicmFyeShwYWxtZXJwZW5ndWlucykNCmBgYA0KDQpgYGB7ciBldmFsID0gRiwgZWNobyA9IFR9DQojIFNwZWNpZnkgcmVxdWlyZWQgcGFja2FnZXMNCmNsdXN0ZXJfcGFja2FnZXMgPC0gYygiY2x1c3RlciIsICJmYWN0b2V4dHJhIiwgImdnZm9ydGlmeSIsICJwYWxtZXJwZW5ndWlucyIpDQojIEluc3RhbGwgbWlzc2luZyBwYWNrYWdlcw0KaW5zdGFsbC5wYWNrYWdlcyhzZXRkaWZmKGNsdXN0ZXJfcGFja2FnZXMsIHJvd25hbWVzKGluc3RhbGxlZC5wYWNrYWdlcygpKSkpDQojIExvYWQgYWxsIHBhY2thZ2VzDQpsYXBwbHkoY2x1c3Rlcl9wYWNrYWdlcywgbGlicmFyeSwgY2hhcmFjdGVyLm9ubHkgPSBUUlVFKQ0KYGBgDQoNCiMjIFBlbmd1aW4gRGF0YQ0KDQpObyBhbnN3ZXIgcmVxdWlyZWQuDQoNCiMjDQoNCmBgYHtyIGV2YWwgPSBULCBlY2hvID0gVCwgZmlnLmRpbSA9IGMoOCwxMCl9DQpwbG90KHBlbmd1aW5zKQ0KYGBgDQoNCiMjDQoNCkZvciBzZXZlcmFsIG9mIHRoZSBjb250aW51b3VzIHJhbmRvbSB2YXJpYWJsZSBzY2F0dGVyIHBsb3RzLCB0aGVyZSBkb2VzIGFwcGVhciB0byBiZSB2aXNpYmxlIGNsdXN0ZXJpbmcgb2NjdXJyaW5nIC0gZS5nLiBgZmxpcHBlcl9sZW5ndGhfbW1gIHZzIGBiaWxsX2RlcHRoX21tYCBzdWdnZXN0cyB0aGF0IHRoZXJlIGFyZSB0d28gZGlzdGluY3QgY2x1c3RlcnMuIFRoZXJlZm9yZSBhcHBseWluZyBhIGNsdXN0ZXJpbmcgdGVjaG5pcXVlIGRvZXMgc2VlbSB0byBiZSB3YXJyYW50ZWQuDQoNCiMjIFByZXBhcmluZyBkYXRhIGZvciBDbHVzdGVyIEFuYWx5c2lzDQoNCk5vIGFuc3dlciByZXF1aXJlZC4NCg0KIyMjDQoNCmBgYHtyIGV2YWwgPSBULCBlY2hvID0gVH0NCnBlbmd1aW5zIDwtIG5hLm9taXQocGVuZ3VpbnMpDQpwZW5ndWluc19zdWJzZXQgPC0gcGVuZ3VpbnNbLCAzOjZdDQpgYGANCg0KIyMjDQoNCk5vIGFuc3dlciByZXF1aXJlZC4NCg0KIyMjDQoNCmBgYHtyIGNsYXNzLnNvdXJjZSA9ICJmb2xkLXNob3ciLCBldmFsID0gVCwgZWNobyA9IFR9DQpwZW5ndWluc19zY2FsZWQgPC0gc2NhbGUocGVuZ3VpbnNfc3Vic2V0KQ0KaGVhZChwZW5ndWluc19zY2FsZWQpDQpgYGANCg0KIyAkayQtbWVhbnMgQ2x1c3RlcmluZw0KDQojIyB7I2ttZWFuczN9DQoNCmBgYHtyIGNsYXNzLnNvdXJjZSA9ICJmb2xkLXNob3ciLCBldmFsID0gVCwgZWNobyA9IFQsIGNhY2hlID0gVH0NCnNldC5zZWVkKDEpICMgaW5jbHVkZWQgZm9yIHJlcHJvZHVjaWJpbGl0eSBwdXJwb3Nlcw0Ka21lYW5zX2ZpdDMgPC0ga21lYW5zKHBlbmd1aW5zX3NjYWxlZCwgMykNCmBgYA0KDQojIw0KDQpXZSBjYW4gY2hlY2sgdGhlIGNsdXN0ZXIgbWVtYmVyc2hpcCBhcyBmb2xsb3dzOg0KDQpgYGB7ciBjbGFzcy5zb3VyY2UgPSAiZm9sZC1zaG93IiwgZXZhbCA9IFQsIGVjaG8gPSBULCBjYWNoZSA9IFR9DQprbWVhbnNfZml0MyRjbHVzdGVyDQpgYGANCg0KVGhlIGBiZXR3ZWVuX3NzIC90b3RhbF9zc2AgdmFsdWUgaXMgNzIuMSUuDQoNCiMjIFZpc3VhbGlzaW5nIG91ciByZXN1bHRzIHsja21lYW5zdmlzdWFsaXNlfQ0KDQpgYGB7ciBjbGFzcy5zb3VyY2UgPSAiZm9sZC1zaG93IiwgZXZhbCA9IFQsIGVjaG8gPSBULCBjYWNoZSA9IFQsIGZpZy5kaW0gPSBjKDgsIDEwKX0NCnBsb3QocGVuZ3VpbnNbLCBjKDEsMzo2KV0sIGNvbCA9IGttZWFuc19maXQzJGNsdXN0ZXIpDQpgYGANCg0KQXMgd2UgY2FuIHNlZSwgdGhlIGNsdXN0ZXJzIGFyZSBhY3R1YWxseSBkb2luZyBhIHByZXR0eSBnb29kIGpvYiBvZiBzZXBhcmF0aW5nIHBlbmd1aW5zIGJ5IHNwZWNpZXMhDQoNCiMjIyANCg0KYGBge3IgY2xhc3Muc291cmNlID0gImZvbGQtc2hvdyIsIGV2YWwgPSBULCBlY2hvID0gVCwgY2FjaGUgPSBULCBmaWcuZGltID0gYyg2LCA0KX0NCnBsb3QoYXMubnVtZXJpYyhwZW5ndWlucyRzcGVjaWVzKSwgY29sID0ga21lYW5zX2ZpdDMkY2x1c3RlciwgDQogICAgIHhsYWIgPSAicGVuZ3VpbiBJRCIsIA0KICAgICB5bGFiID0gIihBZGVsaWUgPSAxLCBDaGluc3RyYXAgPSAyLCBHZW50b28gPSAzIiwgbGFzID0gMSkNCmBgYA0KDQpCYXNlZCBvbiB0aGVzZSBwbG90cywgaXQgc2VlbXMgdGhhdCB0aGUgJGskLW1lYW5zIGNsdXN0ZXJpbmcgaGFzIGRvbmUgYSBncmVhdCBqb2Igb2YgY2x1c3RlcmluZyB0aGUgcGVuZ3VpbnMgaW50byBjbHVzdGVycyBvZiBkaWZmZXJlbnQgc3BlY2llcy4gSWYgeW91IGxvb2sgY2FyZWZ1bGx5LCB0aGVyZSBpcyBzb21lIGVycm9yIHdoZW4gZGlzdGluZ3Vpc2hpbmcgYmV0d2VlbiBDaGluc3RyYXAgYW5kIEFkZWxpZSBwZW5ndWlucy4gSWYgd2UgYWxzbyBpbmNsdWRlIG1vcmUgZGF0YSBzdWNoIGFzIGBzZXhgIG9yIGBpc2xhbmRgLCB3ZSBtaWdodCBiZSBhYmxlIHRvIG1vcmUgY2xlYXJseSBkaXN0aW5ndWlzaCBiZXR3ZWVuIHRoZXNlIHR3byBzcGVjaWVzLg0KDQojIyMgVHdvLXdheSBUYWJsZSBTdW1tYXJ5DQoNCmByIGVtbzo6amkoImNvbXB1dGVyIilgIA0KDQpgYGB7ciBjbGFzcy5zb3VyY2UgPSAiZm9sZC1zaG93IiwgZXZhbCA9IFQsIGVjaG8gPSBUfQ0KdHdvX3dheV90YWJsZSA8LSB0YWJsZShhcy5udW1lcmljKHBlbmd1aW5zJHNwZWNpZXMpLCBrbWVhbnNfZml0MyRjbHVzdGVyKQ0KZGltbmFtZXModHdvX3dheV90YWJsZSkgPC0gbGlzdChsZXZlbHMocGVuZ3VpbnMkc3BlY2llcyksDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGMoIkNsdXN0ZXIgMSIsICJDbHVzdGVyIDIiLCAiQ2x1c3RlciAzIikNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgKQ0KdHdvX3dheV90YWJsZQ0KDQpgYGANCg0KVGhlIHR3by13YXkgdGFibGUgaGlnaGxpZ2h0cyB0aGUgYWNjdXJhY3kgb2YgdGhlIGNsdXN0ZXJpbmcgLSBvbmx5IGEgZmV3IENoaW5zdHJhcCBwZW5ndWlucyBhbmQgc29tZSBBZGVsaWUgcGVuZ3VpbnMgaGF2ZSBiZWVuIG1pc2NsYXNzaWZpZWQuDQoNCiMjIyBDbHVzdGVyIHBsb3RzICB7I2ttZWFuc3Zpen0NCg0KDQpgYGB7ciBjbGFzcy5zb3VyY2UgPSAiZm9sZC1zaG93IiwgZXZhbCA9IFQsIGVjaG8gPSBUfQ0KZnZpel9jbHVzdGVyKGttZWFuc19maXQzLCBkYXRhID0gcGVuZ3VpbnNfc2NhbGVkKQ0KYGBgDQoNCiMjIHsja21lYW5zbW9yZX0NCg0KYGBge3IgY2xhc3Muc291cmNlID0gImZvbGQtc2hvdyIsIGV2YWwgPSBULCBlY2hvID0gVCwgY2FjaGUgPSBUfQ0Ka21lYW5zX2ZpdDIgPC0ga21lYW5zKHBlbmd1aW5zX3NjYWxlZCwgMikNCmttZWFuc19maXQ0IDwtIGttZWFucyhwZW5ndWluc19zY2FsZWQsIDQpDQpgYGANCg0KVGhlIGBiZXR3ZWVuX3NzIC90b3RhbF9zc2AgcmVzdWx0cyBmb3IgdGhlc2UgbmV3IGNsdXN0ZXIgZml0cyBhcmU6DQoNCiogNTguNSUgZm9yICRrPTIkLg0KKiA3Ny4xJSBmb3IgJGs9NCQuDQoNClRoZSBgYmV0d2Vlbl9zcyAvdG90YWxfc3NgIHZhbHVlIGFjdHVhbGx5IGluY3JlYXNlcyBhcyB3ZSBpbmNyZWFzZSB0aGUgbnVtYmVyIG9mIGNsdXN0ZXJzLCB3aGljaCBpc24ndCBuZWNlc3NhcmlseSBhY2N1cmF0ZSAtIHdlIGtub3cgZS5nLiB0aGF0IHRoZXJlIGFyZSBub3QgZm91ciBzcGVjaWVzIG9mIHBlbmd1aW4gaW4gb3VyIGRhdGEgKHdoYXQgaGFwcGVucyBpZiB3ZSB1c2UgJGs9MTAkPyEpLg0KDQojIyBEaWFnbm9zdGljcyB7I25iY2x1c3R9DQoNCk5vIGFuc3dlciByZXF1aXJlZC4NCg0KIyMjIFRoZSBzaWxob3VldHRlIG1ldGhvZA0KDQpgYGB7ciBjbGFzcy5zb3VyY2UgPSAiZm9sZC1zaG93IiwgZXZhbCA9IFQsIGVjaG8gPSBULCBjYWNoZSA9IFQsIGZpZy5kaW0gPSBjKDYsIDQpfQ0Ka21lYW5zX3NpbCA8LSBmdml6X25iY2x1c3QocGVuZ3VpbnNfc2NhbGVkLCBrbWVhbnMsIG1ldGhvZCA9ICJzaWxob3VldHRlIikNCmttZWFuc19zaWwNCmBgYA0KDQpCYXNlZCBvbiB0aGlzIHBsb3QsIGl0IHNlZW1zIHRoYXQgdGhlIG9wdGltYWwgbnVtYmVyIG9mIGNsdXN0ZXJzIGlzIHR3by4NCg0KIyMjIFRoZSBnYXAgc3RhdGlzdGljIG1ldGhvZA0KDQpgYGB7ciBjbGFzcy5zb3VyY2UgPSAiZm9sZC1zaG93IiwgZXZhbCA9IFQsIGVjaG8gPSBULCBjYWNoZSA9IFQsIGZpZy5kaW0gPSBjKDYsIDQpfQ0Ka21lYW5zX2dhcCA8LSBmdml6X25iY2x1c3QocGVuZ3VpbnNfc2NhbGVkLCBrbWVhbnMsIG1ldGhvZCA9ICJnYXAiKQ0Ka21lYW5zX2dhcA0KYGBgDQoNCkJhc2VkIG9uIHRoaXMgcGxvdCwgaXQgc2VlbXMgdGhhdCB0aGUgb3B0aW1hbCBudW1iZXIgb2YgY2x1c3RlcnMgaXMgZm91ci4NCg0KIyMjIA0KDQpgYGB7ciBjbGFzcy5zb3VyY2UgPSAiZm9sZC1zaG93IiwgZXZhbCA9IFQsIGVjaG8gPSBULCBjYWNoZSA9IFQsIGZpZy5kaW0gPSBjKDYsIDQpfQ0KZnZpel9jbHVzdGVyKGttZWFuc19maXQyLCBkYXRhID0gcGVuZ3VpbnNfc2NhbGVkKQ0KZnZpel9jbHVzdGVyKGttZWFuc19maXQ0LCBkYXRhID0gcGVuZ3VpbnNfc2NhbGVkKQ0KYGBgDQoNCiMjIA0KDQpBbnN3ZXJzIG1heSB2YXJ5IGhlcmUuIFRocmVlIG9yIGZvdXIgY2x1c3RlcnMgc2VlbSByZWFzb25hYmxlLg0KDQojIEV4dGVuc2lvbiAxOiBQQU0gQ2x1c3RlcmluZw0KDQojIyB7I3BhbX0NCg0KYGBge3IgY2xhc3Muc291cmNlID0gImZvbGQtc2hvdyIsIGV2YWwgPSBULCBlY2hvID0gVCwgY2FjaGUgPSBUfQ0KcGFtX2ZpdDIgPC0gcGFtKHBlbmd1aW5zX3NjYWxlZCwgMikNCnBhbV9maXQzIDwtIHBhbShwZW5ndWluc19zY2FsZWQsIDMpDQpwYW1fZml0NCA8LSBwYW0ocGVuZ3VpbnNfc2NhbGVkLCA0KQ0KYGBgDQoNCiMjIHsjcGFtYmVzdH0NCg0KYGBge3IgY2xhc3Muc291cmNlID0gImZvbGQtc2hvdyIsIGV2YWwgPSBULCBlY2hvID0gVCwgY2FjaGUgPSBULCBmaWcuZGltID0gYyg2LCA0KX0NCnBhbV9zaWwgPC0gZnZpel9uYmNsdXN0KHBlbmd1aW5zX3NjYWxlZCwgcGFtLCBtZXRob2QgPSAic2lsaG91ZXR0ZSIpDQpwYW1fc2lsDQpgYGANCg0KVGhlc2UgcmVzdWx0cyBzdWdnZXN0IHRoYXQgdHdvIGNsdXN0ZXJzIGFyZSBvcHRpbWFsLg0KDQojIw0KDQpUaGUgUiBjb2RlIGJlbG93IHNob3dzIHJlc3VsdHMgZm9yIGEgY2x1c3RlciBjaG9pY2Ugb2YgdGhyZWUuDQoNCmBgYHtyIGNsYXNzLnNvdXJjZSA9ICJmb2xkLXNob3ciLCBldmFsID0gVCwgZWNobyA9IFQsIGNhY2hlID0gVCwgZmlnLmRpbSA9IGMoNiwgNCl9DQpmdml6X2NsdXN0ZXIocGFtX2ZpdDMsIGRhdGEgPSBwZW5ndWluc19zY2FsZWQpDQpgYGANCg0KYGBge3IgY2xhc3Muc291cmNlID0gImZvbGQtc2hvdyIsIGV2YWwgPSBULCBlY2hvID0gVCwgY2FjaGUgPSBULCBmaWcuZGltID0gYyg2LCA0KX0NCnBsb3QoYXMubnVtZXJpYyhwZW5ndWlucyRzcGVjaWVzKSwgY29sID0gcGFtX2ZpdDMkY2x1c3RlcmluZykNCmBgYA0KDQpXZSBjYW4gc2VlIGZyb20gdGhlc2UgcGxvdHMgdGhhdCB0aGUgUEFNIGNsdXN0ZXJpbmcgZG9lcyBhIHZlcnkgZ29vZCBqb2Igb2YgY2x1c3RlcmluZyBwZW5ndWlucyBpbnRvIHNwZWNpZXMgKGFsdGhvdWdoIHRoZXJlIGlzIHN0aWxsIHNvbWUgdHJvdWJsZSBkaXN0aW5ndWlzaGluZyBBZGVsaWUgYW5kIENoaW5zdHJhcCBwZW5ndWlucykuIEZvciB0aGUgbGFyZ2VyIG51bWJlcnMgb2YgY2x1c3RlcnMsIHRoZSBhbGdvcml0aG0gbWF5IGFsc28gYmUgZGlzdGluZ3Vpc2hpbmcgYmV0d2VlbiBtYWxlIGFuZCBmZW1hbGUgcGVuZ3VpbnMgLSB0cnkgY2hlY2tpbmcgaWYgdGhpcyBpcyB0cnVlLg0KDQojIEV4dGVuc2lvbiAyOiBIaWVyYXJjaGljYWwgQ2x1c3RlcmluZw0KDQojIyB7I2hpZXJhcmNoaWNhbH0NCg0KYGBge3IgY2xhc3Muc291cmNlID0gImZvbGQtc2hvdyIsIGV2YWwgPSBULCBlY2hvID0gVCwgY2FjaGUgPSBUfQ0KaGllcl9maXQgPC0gYWduZXMocGVuZ3VpbnNfc2NhbGVkKQ0KYGBgDQoNCiMjDQoNCmBgYHtyIGNsYXNzLnNvdXJjZSA9ICJmb2xkLXNob3ciLCBldmFsID0gVCwgZWNobyA9IFQsIGZpZy5kaW0gPSBjKDgsIDYpLCBjYWNoZSA9IFR9DQpwbG90KGhpZXJfZml0LCB3aGljaCA9IDIpDQpgYGANCg0KIyMNCg0KYGBge3IgY2xhc3Muc291cmNlID0gImZvbGQtc2hvdyIsIGV2YWwgPSBULCBlY2hvID0gVCwgY2FjaGUgPSBUfQ0KaGllcl9maXQgPC0gYWduZXMocGVuZ3VpbnNfc2NhbGVkKQ0KaGllcl9maXRfc2luZ2xlIDwtIGFnbmVzKHBlbmd1aW5zX3NjYWxlZCwgbWV0aG9kID0gInNpbmdsZSIpDQpoaWVyX2ZpdF93YXJkIDwtIGFnbmVzKHBlbmd1aW5zX3NjYWxlZCwgbWV0aG9kID0gIndhcmQiKQ0KYGBgDQoNCmBgYHtyIGNsYXNzLnNvdXJjZSA9ICJmb2xkLXNob3ciLCBldmFsID0gVCwgZWNobyA9IFQsIGZpZy5kaW0gPSBjKDgsIDYpLCBjYWNoZSA9IFR9DQpwbG90KGhpZXJfZml0X3NpbmdsZSwgd2hpY2ggPSAyKQ0KcGxvdChoaWVyX2ZpdF93YXJkLCB3aGljaCA9IDIpDQpgYGANCg0KVGhlIGRlbmRyb2dyYW1zIGFsbCBsb29rIHF1aXRlIGRpZmZlcmVudC4gVGhlIGB3YXJkYCBtZXRob2Qgb25lIGxvb2tzIHBlcmhhcHMgdGhlIGNsZWFuZXN0IGFuZCBlYXNpZXN0IHRvIGFzc2Vzcy4NCg0KIyMgDQoNClRoZSBjb2RlIHNob3duIGJlbG93IHByb2R1Y2VzIGEgY29sb3VyZWQgZGVuZHJvZ3JhbSB3aXRoIHRocmVlIGNsdXN0ZXJzLCBmb3IgdGhlIGhpZXJhcmNoaWNhbCBjbHVzdGVyaW5nIGFsZ29yaXRobSB1c2luZyB0aGUgIndhcmQiIG1lYXN1cmVtZW50IG1ldGhvZC4NCg0KYGBge3IgY2xhc3Muc291cmNlID0gImZvbGQtc2hvdyIsIGV2YWwgPSBULCBlY2hvID0gVCwgY2FjaGUgPSBULCBmaWcuZGltID0gYyg4LCA2KSwgd2FybmluZyA9IEYsIG1lc3NhZ2UgPSBGfQ0KcGVuZ3Vpbl9kZW5kcm8gPC0gaGN1dChwZW5ndWluc19zY2FsZWQsIA0KICAgICAgICAgICAgICAgIGsgPSAzLCANCiAgICAgICAgICAgICAgICBoY19mdW5jID0gImFnbmVzIiwNCiAgICAgICAgICAgICAgICBoY19tZXRob2QgPSAid2FyZCIsDQogICAgICAgICAgICAgICAgaGNfbWV0cmljID0gImV1Y2xpZGVhbiIpIA0KDQpmdml6X2RlbmQocGVuZ3Vpbl9kZW5kcm8pDQpgYGANCg0KIyBFeHRlbnNpb24gMzogRnV6enkgQ2x1c3RlcmluZw0KDQojIw0KDQpUaGUgUiBjb2RlIGJlbG93IHNob3dzIGZpdHMgZm9yIHRoZSBkaWZmZXJlbnQgY2x1c3RlciBudW1iZXIgb3B0aW9ucy4NCg0KYGBge3IgY2xhc3Muc291cmNlID0gImZvbGQtc2hvdyIsIGV2YWwgPSBULCBlY2hvID0gVCwgY2FjaGUgPSBUfQ0KZnV6enlfZml0MiA8LSBmYW5ueShwZW5ndWluc19zY2FsZWQsIDIpDQpmdXp6eV9maXQzIDwtIGZhbm55KHBlbmd1aW5zX3NjYWxlZCwgMykNCmZ1enp5X2ZpdDQgPC0gZmFubnkocGVuZ3VpbnNfc2NhbGVkLCA0KQ0KYGBgDQoNClBsZWFzZSBub3RlIHRoYXQgb3V0cHV0IGZvciB0aGVzZSBmaXRzIGlzIG9taXR0ZWQgZHVlIHRvIHRoZSBsZW5ndGggb2YgdGhlIHJlc3VsdHMuDQoNCiMjIHsjZnV6enliZXN0fQ0KDQpgYGB7ciBjbGFzcy5zb3VyY2UgPSAiZm9sZC1zaG93IiwgZXZhbCA9IFQsIGVjaG8gPSBULCBjYWNoZSA9IFQsIGZpZy5kaW0gPSBjKDYsIDQpLCB3YXJuaW5nID0gRiwgbWVzc2FnZSA9IEZ9DQpmdXp6eV9zaWwgPC0gZnZpel9uYmNsdXN0KHBlbmd1aW5zX3NjYWxlZCwgZmFubnksIG1ldGhvZCA9ICJzaWxob3VldHRlIikNCmZ1enp5X3NpbA0KYGBgDQoNCldlIG9idGFpbiB0aGUgc2FtZSByZXN1bHRzIGFzIGZvciB0aGUgJGskLW1lYW5zIGFuZCBQQU0gY2x1c3RlcmluZyBoZXJlLCBpLmUuIHdlIGhhdmUgYSByZXN1bHQgb2YgdHdvIGNsdXN0ZXJzLg0KDQojIw0KDQpUaGUgUiBjb2RlIGJlbG93IHNob3dzIHJlc3VsdHMgZm9yIGEgY2x1c3RlciBjaG9pY2Ugb2YgdHdvLg0KDQpgYGB7ciBjbGFzcy5zb3VyY2UgPSAiZm9sZC1zaG93IiwgZXZhbCA9IFQsIGVjaG8gPSBULCBjYWNoZSA9IFQsIGZpZy5kaW0gPSBjKDYsIDQpfQ0KZnZpel9jbHVzdGVyKGZ1enp5X2ZpdDIsIGRhdGEgPSBwZW5ndWluc19zY2FsZWQpDQpgYGANCg0KYGBge3IgY2xhc3Muc291cmNlID0gImZvbGQtc2hvdyIsIGV2YWwgPSBULCBlY2hvID0gVCwgY2FjaGUgPSBULCBmaWcuZGltID0gYyg2LCA0KX0NCiMgVGhpcyBjb2RlIGFzc3VtZXMgeW91IGFyZSBhc3Nlc3NpbmcgYSByZXN1bHQgc3RvcmVkIGluIHRoZSBvYmplY3QgZnV6enlfZml0Mg0KcGxvdChhcy5udW1lcmljKHBlbmd1aW5zJHNwZWNpZXMpLCBjb2wgPSBmdXp6eV9maXQyJGNsdXN0ZXIpDQpgYGANCg0KRnJvbSB0aGVzZSBwbG90cyB3ZSBjYW4gc2VlIHRoYXQgdGhlIGZ1enp5IGNsdXN0ZXJpbmcgcGVyZm9ybXMgc2ltaWxhcmx5IHRvIHRoZSBwcmV2aW91cyBtZXRob2RzLg0KDQo8YnI+DQoNCiMjIyMgVGhhdCdzIGFsbCB0aGUgY29udGVudCBjb3ZlcmVkLiAjIyMjIHstfQ0KDQo8YnI+DQoNCiMgUmVmZXJlbmNlcyB7LSAjUmVmfQ0KPGRpdiBpZD0icmVmcyI+PC9kaXY+DQoNCjxicj4NCg0KPGZvbnQgY29sb3IgPSAiZ3JleSI+DQpUaGVzZSBub3RlcyBoYXZlIGJlZW4gcHJlcGFyZWQgYnkgUnVwZXJ0IEt1dmVrZS4gUGxlYXNlIG5vdGUgdGhhdCBzb21lIG9mIHRoZSBjb250ZW50IGluIHRoZXNlIG5vdGVzIGhhcyBiZWVuIGRldmVsb3BlZCBmcm9tIGNvbnRlbnQgaW4gQE1vZFN0YXQuIFRoZSBjb3B5cmlnaHQgZm9yIHRoZSBtYXRlcmlhbCBpbiB0aGVzZSBub3RlcyByZXNpZGVzIHdpdGggdGhlIGF1dGhvcnMgbmFtZWQgYWJvdmUsIHdpdGggdGhlIERlcGFydG1lbnQgb2YgTWF0aGVtYXRpY2FsIGFuZCBQaHlzaWNhbCBTY2llbmNlcyBhbmQgd2l0aCBMYSBUcm9iZSBVbml2ZXJzaXR5LiBDb3B5cmlnaHQgaW4gdGhpcyB3b3JrIGlzIHZlc3RlZCBpbiBMYSBUcm9iZSBVbml2ZXJzaXR5IGluY2x1ZGluZyBhbGwgTGEgVHJvYmUgVW5pdmVyc2l0eSBicmFuZGluZyBhbmQgbmFtaW5nLiBVbmxlc3Mgb3RoZXJ3aXNlIHN0YXRlZCwgbWF0ZXJpYWwgd2l0aGluIHRoaXMgd29yayBpcyBsaWNlbnNlZCB1bmRlciBhIENyZWF0aXZlIENvbW1vbnMgQXR0cmlidXRpb24tTm9uIENvbW1lcmNpYWwtTm9uIERlcml2YXRpdmVzIExpY2Vuc2UgDQo8YSBocmVmID0gImh0dHBzOi8vY3JlYXRpdmVjb21tb25zLm9yZy9saWNlbnNlcy9ieS1uYy1uZC80LjAvQ0MiIHRhcmdldD0iX2JsYW5rIj4gQlktTkMtTkQuIDwvYT4NCjwvZm9udD4=