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 used behind the scenes
library(knitr)
library(kableExtra)

Prerequisites

For this chapter we’ll use the following packages:

# Helper packages
library(dplyr)    # for data manipulation
library(ggplot2)  # for data visualization

# Modeling packages
library(mclust)   # for fitting clustering algorithms

To illustrate the main concepts of model-based clustering we’ll use the geyser data provided by the MASS package along with the my_basket data:

data(geyser, package = 'MASS')

url <- "https://koalaverse.github.io/homlr/data/my_basket.csv"
my_basket <- readr::read_csv(url)

Measuring probability and uncertainty

Figure 22.1:

ggplot(geyser, aes(waiting, duration)) +
  geom_point(size = 0.75, alpha = 0.5) +
  geom_density2d(aes(alpha = ..level..), show.legend = FALSE) +
  scale_x_continuous("X") +
  scale_y_continuous("Y") +
  theme(
    axis.ticks = element_blank(),
    axis.text = element_blank()
  )

# Apply GMM model with 3 components
geyser_mc <- Mclust(geyser, G = 3, verbose = FALSE)

# Plot results
plot(geyser_mc, what = "density")

plot(geyser_mc, what = "uncertainty")

# Observations with high uncertainty
sort(geyser_mc$uncertainty, decreasing = TRUE) %>% head()
      187       211        85       285        28       206 
0.4689087 0.4542588 0.4355496 0.4355496 0.4312406 0.4168440 

Covariance types

Table 22.1:

readr::read_csv('data/GMM-covariances.csv') %>%
  knitr::kable(caption = 'Parameterizations of the covariance matrix',
                align = c('c', 'c', 'c', 'c', 'c', 'c')) %>%
  kable_styling(bootstrap_options = "striped", full_width = FALSE)
Parameterizations of the covariance matrix
Model Family Volume Shape Orientation Identifier
1 Spherical Equal Equal NA EII
2 Spherical Variable Equal NA VII
3 Diagonal Equal Equal Axes EEI
4 Diagonal Variable Equal Axes VEI
5 Diagonal Equal Variable Axes EVI
6 Diagonal Variable Variable Axes VVI
7 General Equal Equal Equal EEE
8 General Equal Variable Equal EVE
9 General Variable Equal Equal VEE
10 General Variable Variable Equal VVE
11 General Equal Equal Variable EEV
12 General Variable Equal Variable VEV
13 General Equal Variable Variable EVV
14 General Variable Variable Variable VVV

Figure 22.3:

par(
  mfrow = c(2, 5), mar = c(0.5, 0.5, 2.5, 0.5),
  cex = .4,
  pch = 19,
  col = alpha('black', 0.25),
  xaxt = 'n', yaxt = 'n', ann = FALSE, cex.main = 3
  )

# EII
set.seed(111)
obj <- mlbench::mlbench.hypercube(n = 400)
df <- data.frame(
    x = obj$x[, 1],
    y = obj$x[, 2]
)
m <- Mclust(df, verbose = FALSE)
plot(m, what = "density", type = "hdr")
points(df)
title(main = m$modelName)

# VII
df <- data_frame(
    x1 = c(rnorm(100, sd = .75), rnorm(100, sd = .25) + 5),
    x2 = c(rnorm(100, sd = .75), rnorm(100, sd = .25))
)
m <- Mclust(df, G = 2, verbose = FALSE)
plot(m, what = "density", type = "hdr")
points(df)
title(main = m$modelName)

# EEI
df <- data_frame(
    x1 = c(rnorm(100, sd = .75), rnorm(100, sd = .75)),
    x2 = c(rnorm(100, sd = .75), rnorm(100, sd = .75) - 5)
)
m <- Mclust(df, G = 2, modelNames = "EEI", verbose = FALSE)
plot(m, what = "density", type = "hdr")
points(df)
title(main = m$modelName)

# VVI
set.seed(111)
obj <- mlbench::mlbench.cuboids(300)
df <- data.frame(
    x = obj$x[, 1],
    y = obj$x[, 2]
)
m <- Mclust(df, verbose = FALSE)
plot(m, what = "density", type = "hdr")
points(df)
title(main = m$modelName)

# VVE
m <- Mclust(faithful, G = 2, verbose = FALSE)
plot(m, what = "density", type = "hdr")
points(faithful)
title(main = m$modelName)

# EEE
set.seed(111)
obj <- mlbench::mlbench.cassini(200)
df <- data.frame(
    x = obj$x[, 1],
    y = obj$x[, 2]
)
m <- Mclust(df, G = 20, verbose = FALSE)
plot(m, what = "density", type = "hdr")
points(df)
title(main = m$modelName)

# EEV
set.seed(111)
obj <- mlbench::mlbench.spirals(200, 1, 0.025)
df <- data.frame(
    x = obj$x[, 1],
    y = obj$x[, 2]
)
m <- Mclust(df, G = 20, verbose = FALSE)
plot(m, what = "density", type = "hdr")
points(df)
title(main = m$modelName)

# VEV
m <- Mclust(mtcars[, c('mpg', 'wt')], G = 2, verbose = FALSE)
plot(m, what = "density", type = "hdr")
points(mtcars[, c('mpg', 'wt')])
title(main = m$modelName)

# EEV
set.seed(111)
obj <- mlbench::mlbench.smiley()
df <- data.frame(
    x = obj$x[, 1],
    y = obj$x[, 2]
)
m <- Mclust(df, G = 20, verbose = FALSE)
plot(m, what = "density", type = "hdr")
points(df)
title(main = m$modelName)

# EVE
set.seed(111)
obj <- mlbench::mlbench.1spiral(300, sd = .15)
df <- data.frame(
    x = obj[, 1],
    y = obj[, 2]
)
m <- Mclust(df, verbose = FALSE)
plot(m, what = "density", type = "hdr")
points(df)
title(main = m$modelName)

Model selection

summary(geyser_mc)
---------------------------------------------------- 
Gaussian finite mixture model fitted by EM algorithm 
---------------------------------------------------- 

Mclust EEI (diagonal, equal volume and shape) model with 3 components: 

Clustering table:
  1   2   3 
 91 107 101 
geyser_optimal_mc <- Mclust(geyser, verbose = FALSE)

summary(geyser_optimal_mc)
---------------------------------------------------- 
Gaussian finite mixture model fitted by EM algorithm 
---------------------------------------------------- 

Mclust VVI (diagonal, varying volume and shape) model with 4 components: 

Clustering table:
 1  2  3  4 
90 17 98 94 
legend_args <- list(x = "bottomright", ncol = 5)
plot(geyser_optimal_mc, what = 'BIC', legendArgs = legend_args)

plot(geyser_optimal_mc, what = 'classification')

plot(geyser_optimal_mc, what = 'uncertainty')

My basket example

my_basket_mc <- Mclust(my_basket, 1:20, verbose = FALSE)

summary(my_basket_mc)
---------------------------------------------------- 
Gaussian finite mixture model fitted by EM algorithm 
---------------------------------------------------- 

Mclust EEV (ellipsoidal, equal volume and shape) model with 6 components: 

Clustering table:
  1   2   3   4   5   6 
391 403  75 315 365 451 
plot(my_basket_mc, what = 'BIC', 
     legendArgs = list(x = "bottomright", ncol = 5))

probabilities <- my_basket_mc$z 
colnames(probabilities) <- paste0('C', 1:6)

probabilities <- probabilities %>%
  as.data.frame() %>%
  mutate(id = row_number()) %>%
  tidyr::gather(cluster, probability, -id)

ggplot(probabilities, aes(probability)) +
  geom_histogram() +
  facet_wrap(~ cluster, nrow = 2)

uncertainty <- data.frame(
  id = 1:nrow(my_basket),
  cluster = my_basket_mc$classification,
  uncertainty = my_basket_mc$uncertainty
)

uncertainty %>%
  group_by(cluster) %>%
  filter(uncertainty > 0.25) %>%
  ggplot(aes(uncertainty, reorder(id, uncertainty))) +
  geom_point() +
  facet_wrap(~ cluster, scales = 'free_y', nrow = 1)

cluster2 <- my_basket %>%
  scale() %>%
  as.data.frame() %>%
  mutate(cluster = my_basket_mc$classification) %>%
  filter(cluster == 2) %>%
  select(-cluster)

cluster2 %>%
  tidyr::gather(product, std_count) %>%
  group_by(product) %>%
  summarize(avg = mean(std_count)) %>%
  ggplot(aes(avg, reorder(product, avg))) +
  geom_point() +
  labs(x = "Average standardized consumption", y = NULL)

LS0tCnRpdGxlOiAiQ2hhcHRlciAyMTogTW9kZWwtYmFzZWQgQ2x1c3RlcmluZyIKb3V0cHV0OiBodG1sX25vdGVib29rCi0tLQoKX19Ob3RlX186IFNvbWUgcmVzdWx0cyBtYXkgZGlmZmVyIGZyb20gdGhlIGhhcmQgY29weSBib29rIGR1ZSB0byB0aGUgY2hhbmdpbmcgb2YKc2FtcGxpbmcgcHJvY2VkdXJlcyBpbnRyb2R1Y2VkIGluIFIgMy42LjAuIFNlZSBodHRwOi8vYml0Lmx5LzM1RDFTVzcgZm9yIG1vcmUKZGV0YWlscy4gQWNjZXNzIGFuZCBydW4gdGhlIHNvdXJjZSBjb2RlIGZvciB0aGlzIG5vdGVib29rIFtoZXJlXShodHRwczovL3JzdHVkaW8uY2xvdWQvcHJvamVjdC84MDExODUpLgoKSGlkZGVuIGNoYXB0ZXIgcmVxdWlyZW1lbnRzIHVzZWQgaW4gdGhlIGJvb2sgdG8gc2V0IHRoZSBwbG90dGluZyB0aGVtZSBhbmQgbG9hZApwYWNrYWdlcyB1c2VkIGluIGhpZGRlbiBjb2RlIGNodW5rczoKCmBgYHtyIHNldHVwfQprbml0cjo6b3B0c19jaHVuayRzZXQoCiAgbWVzc2FnZSA9IEZBTFNFLCAKICB3YXJuaW5nID0gRkFMU0UsIAogIGNhY2hlID0gRkFMU0UKKQoKIyBTZXQgdGhlIGdyYXBoaWNhbCB0aGVtZQpnZ3Bsb3QyOjp0aGVtZV9zZXQoZ2dwbG90Mjo6dGhlbWVfbGlnaHQoKSkKCiMgcGFja2FnZXMgdXNlZCBiZWhpbmQgdGhlIHNjZW5lcwpsaWJyYXJ5KGtuaXRyKQpsaWJyYXJ5KGthYmxlRXh0cmEpCmBgYAoKIyMgUHJlcmVxdWlzaXRlcwoKRm9yIHRoaXMgY2hhcHRlciB3ZSdsbCB1c2UgdGhlIGZvbGxvd2luZyBwYWNrYWdlczoKCmBgYHtyIG1vZGVsLWNsdXN0ZXJpbmctcGtnc30KIyBIZWxwZXIgcGFja2FnZXMKbGlicmFyeShkcGx5cikgICAgIyBmb3IgZGF0YSBtYW5pcHVsYXRpb24KbGlicmFyeShnZ3Bsb3QyKSAgIyBmb3IgZGF0YSB2aXN1YWxpemF0aW9uCgojIE1vZGVsaW5nIHBhY2thZ2VzCmxpYnJhcnkobWNsdXN0KSAgICMgZm9yIGZpdHRpbmcgY2x1c3RlcmluZyBhbGdvcml0aG1zCmBgYAoKVG8gaWxsdXN0cmF0ZSB0aGUgbWFpbiBjb25jZXB0cyBvZiBtb2RlbC1iYXNlZCBjbHVzdGVyaW5nIHdlJ2xsIHVzZSB0aGUgYGdleXNlcmAgZGF0YSBwcm92aWRlZCBieSB0aGUgX19NQVNTX18gcGFja2FnZSBhbG9uZyB3aXRoIHRoZSBgbXlfYmFza2V0YCBkYXRhOgoKYGBge3IgbW9kZWwtY2x1c3RlcmluZy1kYXRhfQpkYXRhKGdleXNlciwgcGFja2FnZSA9ICdNQVNTJykKCnVybCA8LSAiaHR0cHM6Ly9rb2FsYXZlcnNlLmdpdGh1Yi5pby9ob21sci9kYXRhL215X2Jhc2tldC5jc3YiCm15X2Jhc2tldCA8LSByZWFkcjo6cmVhZF9jc3YodXJsKQpgYGAKCgojIyBNZWFzdXJpbmcgcHJvYmFiaWxpdHkgYW5kIHVuY2VydGFpbnR5CgpGaWd1cmUgMjIuMToKCmBgYHtyIG11bHRpdmFyaWF0ZS1kZW5zaXR5LXBsb3QsIGZpZy5jYXA9J0RhdGEgcG9pbnRzIGFjcm9zcyB0d28gZmVhdHVyZXMgKFggYW5kIFkpIGFwcGVhciB0byBjb21lIGZyb20gdGhyZWUgbXVsdGl2YXJpYXRlIG5vcm1hbCBkaXN0cmlidXRpb25zLicsIGZpZy5oZWlnaHQ9MywgZmlnLndpZHRoPTMuNX0KZ2dwbG90KGdleXNlciwgYWVzKHdhaXRpbmcsIGR1cmF0aW9uKSkgKwogIGdlb21fcG9pbnQoc2l6ZSA9IDAuNzUsIGFscGhhID0gMC41KSArCiAgZ2VvbV9kZW5zaXR5MmQoYWVzKGFscGhhID0gLi5sZXZlbC4uKSwgc2hvdy5sZWdlbmQgPSBGQUxTRSkgKwogIHNjYWxlX3hfY29udGludW91cygiWCIpICsKICBzY2FsZV95X2NvbnRpbnVvdXMoIlkiKSArCiAgdGhlbWUoCiAgICBheGlzLnRpY2tzID0gZWxlbWVudF9ibGFuaygpLAogICAgYXhpcy50ZXh0ID0gZWxlbWVudF9ibGFuaygpCiAgKQpgYGAKCmBgYHtyIGdleXNlci1tYzEtcGxvdCwgb3V0LndpZHRoPSI0OCUiLCBmaWcuYXNwPTEsIGZpZy5zaG93PSdob2xkJywgZmlnLmNhcD0nTXVsdGl2YXJpYXRlIGRlbnNpdHkgcGxvdCAobGVmdCkgaGlnaGxpZ2h0aW5nIHRocmVlIGNsdXN0ZXJzIGluIHRoZSBgZ2V5c2VyYCBkYXRhIGFuZCBhbiB1bmNlcnRhaW50eSBwbG90IChyaWdodCkgaGlnaGxpZ2h0aW5nIG9ic2VydmF0aW9ucyB3aXRoIGhpZ2ggdW5jZXJ0YWludHkgb2Ygd2hpY2ggY2x1c3RlciB0aGV5IGFyZSBhIG1lbWJlciBvZi4nfQojIEFwcGx5IEdNTSBtb2RlbCB3aXRoIDMgY29tcG9uZW50cwpnZXlzZXJfbWMgPC0gTWNsdXN0KGdleXNlciwgRyA9IDMsIHZlcmJvc2UgPSBGQUxTRSkKCiMgUGxvdCByZXN1bHRzCnBsb3QoZ2V5c2VyX21jLCB3aGF0ID0gImRlbnNpdHkiKQpwbG90KGdleXNlcl9tYywgd2hhdCA9ICJ1bmNlcnRhaW50eSIpCmBgYAoKYGBge3IgZ2V5c2VyLW1jLWhpZ2gtdW5jZXJ0YWludHktb2JzfQojIE9ic2VydmF0aW9ucyB3aXRoIGhpZ2ggdW5jZXJ0YWludHkKc29ydChnZXlzZXJfbWMkdW5jZXJ0YWludHksIGRlY3JlYXNpbmcgPSBUUlVFKSAlPiUgaGVhZCgpCmBgYAoKIyMgQ292YXJpYW5jZSB0eXBlcwoKVGFibGUgMjIuMToKCmBgYHtyIGNvdmFyaWFuY2UtcGFyYW1ldGVyaXphdGlvbiwgZmlnLmNhcD0nUGFyYW1ldGVyaXphdGlvbnMgb2YgdGhlIGNvdmFyaWFuY2UgbWF0cml4Lid9CnJlYWRyOjpyZWFkX2NzdignZGF0YS9HTU0tY292YXJpYW5jZXMuY3N2JykgJT4lCiAga25pdHI6OmthYmxlKGNhcHRpb24gPSAnUGFyYW1ldGVyaXphdGlvbnMgb2YgdGhlIGNvdmFyaWFuY2UgbWF0cml4JywKICAgICAgICAgICAgICAgIGFsaWduID0gYygnYycsICdjJywgJ2MnLCAnYycsICdjJywgJ2MnKSkgJT4lCiAga2FibGVfc3R5bGluZyhib290c3RyYXBfb3B0aW9ucyA9ICJzdHJpcGVkIiwgZnVsbF93aWR0aCA9IEZBTFNFKQpgYGAKCkZpZ3VyZSAyMi4zOgoKYGBge3IgdmlzdWFsaXplLWRpZmZlcmVudC1jb3ZhcmlhbmNlLW1vZGVscywgZmlnLmNhcD0nR3JhcGhpY2FsIHJlcHJlc2VudGF0aW9uIG9mIGhvdyBkaWZmZXJlbnQgY292YXJpYW5jZSBtb2RlbHMgYWxsb3cgR01NcyB0byBjYXB0dXJlIGRpZmZlcmVudCBjbHVzdGVyIHN0cnVjdHVyZXMuJywgZmlnLmhlaWdodD00LjUsIGZpZy53aWR0aD0xMH0KcGFyKAogIG1mcm93ID0gYygyLCA1KSwgbWFyID0gYygwLjUsIDAuNSwgMi41LCAwLjUpLAogIGNleCA9IC40LAogIHBjaCA9IDE5LAogIGNvbCA9IGFscGhhKCdibGFjaycsIDAuMjUpLAogIHhheHQgPSAnbicsIHlheHQgPSAnbicsIGFubiA9IEZBTFNFLCBjZXgubWFpbiA9IDMKICApCgojIEVJSQpzZXQuc2VlZCgxMTEpCm9iaiA8LSBtbGJlbmNoOjptbGJlbmNoLmh5cGVyY3ViZShuID0gNDAwKQpkZiA8LSBkYXRhLmZyYW1lKAogICAgeCA9IG9iaiR4WywgMV0sCiAgICB5ID0gb2JqJHhbLCAyXQopCm0gPC0gTWNsdXN0KGRmLCB2ZXJib3NlID0gRkFMU0UpCnBsb3QobSwgd2hhdCA9ICJkZW5zaXR5IiwgdHlwZSA9ICJoZHIiKQpwb2ludHMoZGYpCnRpdGxlKG1haW4gPSBtJG1vZGVsTmFtZSkKCiMgVklJCmRmIDwtIGRhdGFfZnJhbWUoCiAgICB4MSA9IGMocm5vcm0oMTAwLCBzZCA9IC43NSksIHJub3JtKDEwMCwgc2QgPSAuMjUpICsgNSksCiAgICB4MiA9IGMocm5vcm0oMTAwLCBzZCA9IC43NSksIHJub3JtKDEwMCwgc2QgPSAuMjUpKQopCm0gPC0gTWNsdXN0KGRmLCBHID0gMiwgdmVyYm9zZSA9IEZBTFNFKQpwbG90KG0sIHdoYXQgPSAiZGVuc2l0eSIsIHR5cGUgPSAiaGRyIikKcG9pbnRzKGRmKQp0aXRsZShtYWluID0gbSRtb2RlbE5hbWUpCgojIEVFSQpkZiA8LSBkYXRhX2ZyYW1lKAogICAgeDEgPSBjKHJub3JtKDEwMCwgc2QgPSAuNzUpLCBybm9ybSgxMDAsIHNkID0gLjc1KSksCiAgICB4MiA9IGMocm5vcm0oMTAwLCBzZCA9IC43NSksIHJub3JtKDEwMCwgc2QgPSAuNzUpIC0gNSkKKQptIDwtIE1jbHVzdChkZiwgRyA9IDIsIG1vZGVsTmFtZXMgPSAiRUVJIiwgdmVyYm9zZSA9IEZBTFNFKQpwbG90KG0sIHdoYXQgPSAiZGVuc2l0eSIsIHR5cGUgPSAiaGRyIikKcG9pbnRzKGRmKQp0aXRsZShtYWluID0gbSRtb2RlbE5hbWUpCgojIFZWSQpzZXQuc2VlZCgxMTEpCm9iaiA8LSBtbGJlbmNoOjptbGJlbmNoLmN1Ym9pZHMoMzAwKQpkZiA8LSBkYXRhLmZyYW1lKAogICAgeCA9IG9iaiR4WywgMV0sCiAgICB5ID0gb2JqJHhbLCAyXQopCm0gPC0gTWNsdXN0KGRmLCB2ZXJib3NlID0gRkFMU0UpCnBsb3QobSwgd2hhdCA9ICJkZW5zaXR5IiwgdHlwZSA9ICJoZHIiKQpwb2ludHMoZGYpCnRpdGxlKG1haW4gPSBtJG1vZGVsTmFtZSkKCiMgVlZFCm0gPC0gTWNsdXN0KGZhaXRoZnVsLCBHID0gMiwgdmVyYm9zZSA9IEZBTFNFKQpwbG90KG0sIHdoYXQgPSAiZGVuc2l0eSIsIHR5cGUgPSAiaGRyIikKcG9pbnRzKGZhaXRoZnVsKQp0aXRsZShtYWluID0gbSRtb2RlbE5hbWUpCgojIEVFRQpzZXQuc2VlZCgxMTEpCm9iaiA8LSBtbGJlbmNoOjptbGJlbmNoLmNhc3NpbmkoMjAwKQpkZiA8LSBkYXRhLmZyYW1lKAogICAgeCA9IG9iaiR4WywgMV0sCiAgICB5ID0gb2JqJHhbLCAyXQopCm0gPC0gTWNsdXN0KGRmLCBHID0gMjAsIHZlcmJvc2UgPSBGQUxTRSkKcGxvdChtLCB3aGF0ID0gImRlbnNpdHkiLCB0eXBlID0gImhkciIpCnBvaW50cyhkZikKdGl0bGUobWFpbiA9IG0kbW9kZWxOYW1lKQoKIyBFRVYKc2V0LnNlZWQoMTExKQpvYmogPC0gbWxiZW5jaDo6bWxiZW5jaC5zcGlyYWxzKDIwMCwgMSwgMC4wMjUpCmRmIDwtIGRhdGEuZnJhbWUoCiAgICB4ID0gb2JqJHhbLCAxXSwKICAgIHkgPSBvYmokeFssIDJdCikKbSA8LSBNY2x1c3QoZGYsIEcgPSAyMCwgdmVyYm9zZSA9IEZBTFNFKQpwbG90KG0sIHdoYXQgPSAiZGVuc2l0eSIsIHR5cGUgPSAiaGRyIikKcG9pbnRzKGRmKQp0aXRsZShtYWluID0gbSRtb2RlbE5hbWUpCgojIFZFVgptIDwtIE1jbHVzdChtdGNhcnNbLCBjKCdtcGcnLCAnd3QnKV0sIEcgPSAyLCB2ZXJib3NlID0gRkFMU0UpCnBsb3QobSwgd2hhdCA9ICJkZW5zaXR5IiwgdHlwZSA9ICJoZHIiKQpwb2ludHMobXRjYXJzWywgYygnbXBnJywgJ3d0JyldKQp0aXRsZShtYWluID0gbSRtb2RlbE5hbWUpCgojIEVFVgpzZXQuc2VlZCgxMTEpCm9iaiA8LSBtbGJlbmNoOjptbGJlbmNoLnNtaWxleSgpCmRmIDwtIGRhdGEuZnJhbWUoCiAgICB4ID0gb2JqJHhbLCAxXSwKICAgIHkgPSBvYmokeFssIDJdCikKbSA8LSBNY2x1c3QoZGYsIEcgPSAyMCwgdmVyYm9zZSA9IEZBTFNFKQpwbG90KG0sIHdoYXQgPSAiZGVuc2l0eSIsIHR5cGUgPSAiaGRyIikKcG9pbnRzKGRmKQp0aXRsZShtYWluID0gbSRtb2RlbE5hbWUpCgojIEVWRQpzZXQuc2VlZCgxMTEpCm9iaiA8LSBtbGJlbmNoOjptbGJlbmNoLjFzcGlyYWwoMzAwLCBzZCA9IC4xNSkKZGYgPC0gZGF0YS5mcmFtZSgKICAgIHggPSBvYmpbLCAxXSwKICAgIHkgPSBvYmpbLCAyXQopCm0gPC0gTWNsdXN0KGRmLCB2ZXJib3NlID0gRkFMU0UpCnBsb3QobSwgd2hhdCA9ICJkZW5zaXR5IiwgdHlwZSA9ICJoZHIiKQpwb2ludHMoZGYpCnRpdGxlKG1haW4gPSBtJG1vZGVsTmFtZSkKYGBgCgojIyBNb2RlbCBzZWxlY3Rpb24KCmBgYHtyIGdleXNlci1tb2RlbC1mYW1pbHksIGxpbmV3aWR0aCA9IDcwfQpzdW1tYXJ5KGdleXNlcl9tYykKYGBgCgpgYGB7ciBnZXlzZXItbWMyLXBsb3QsIG91dC53aWR0aD0iMzIlIiwgZmlnLmFzcD0xLCBmaWcuc2hvdz0naG9sZCcsIGZpZy5jYXA9J0lkZW50aWZ5aW5nIHRoZSBvcHRpbWFsIEdNTSBtb2RlbCBhbmQgbnVtYmVyIG9mIGNsdXN0ZXJzIGZvciB0aGUgYGdleXNlcmAgZGF0YSAobGVmdCkuIFRoZSBjbGFzc2lmaWNhdGlvbiAoY2VudGVyKSBhbmQgdW5jZXJ0YWludHkgKHJpZ2h0KSBwbG90cyBpbGx1c3RyYXRlIHdoaWNoIG9ic2VydmF0aW9ucyBhcmUgYXNzaWduZWQgdG8gZWFjaCBjbHVzdGVyIGFuZCB0aGVpciBsZXZlbCBvZiBhc3NpZ25tZW50IHVuY2VydGFpbnR5LicsIGxpbmV3aWR0aCA9IDcwfQpnZXlzZXJfb3B0aW1hbF9tYyA8LSBNY2x1c3QoZ2V5c2VyLCB2ZXJib3NlID0gRkFMU0UpCgpzdW1tYXJ5KGdleXNlcl9vcHRpbWFsX21jKQoKbGVnZW5kX2FyZ3MgPC0gbGlzdCh4ID0gImJvdHRvbXJpZ2h0IiwgbmNvbCA9IDUpCnBsb3QoZ2V5c2VyX29wdGltYWxfbWMsIHdoYXQgPSAnQklDJywgbGVnZW5kQXJncyA9IGxlZ2VuZF9hcmdzKQpwbG90KGdleXNlcl9vcHRpbWFsX21jLCB3aGF0ID0gJ2NsYXNzaWZpY2F0aW9uJykKcGxvdChnZXlzZXJfb3B0aW1hbF9tYywgd2hhdCA9ICd1bmNlcnRhaW50eScpCmBgYAoKCiMjIE15IGJhc2tldCBleGFtcGxlCgpgYGB7ciBteS1iYXNrZXQtQklDLCBmaWcuY2FwPSdCSUMgc2NvcmVzIGZvciBjbHVzdGVycyAoY29tcG9uZW50cykgcmFuZ2luZyBmcm9tIDEtMjAnLCBmaWcuaGVpZ2h0PTQuNSwgbGluZXdpZHRoID0gNzB9Cm15X2Jhc2tldF9tYyA8LSBNY2x1c3QobXlfYmFza2V0LCAxOjIwLCB2ZXJib3NlID0gRkFMU0UpCgpzdW1tYXJ5KG15X2Jhc2tldF9tYykKCnBsb3QobXlfYmFza2V0X21jLCB3aGF0ID0gJ0JJQycsIAogICAgIGxlZ2VuZEFyZ3MgPSBsaXN0KHggPSAiYm90dG9tcmlnaHQiLCBuY29sID0gNSkpCmBgYAoKYGBge3IgbXktYmFza2V0LXByb2JhYmlsaXRpZXMsIGZpZy5jYXA9J0Rpc3RyaWJ1dGlvbiBvZiBwcm9iYWJpbGl0aWVzIGZvciBhbGwgb2JzZXJ2YXRpb25zIGFsaWduaW5nIHRvIGVhY2ggb2YgdGhlIHNpeCBjbHVzdGVycy4nLCBmaWcuaGVpZ2h0PTR9CnByb2JhYmlsaXRpZXMgPC0gbXlfYmFza2V0X21jJHogCmNvbG5hbWVzKHByb2JhYmlsaXRpZXMpIDwtIHBhc3RlMCgnQycsIDE6NikKCnByb2JhYmlsaXRpZXMgPC0gcHJvYmFiaWxpdGllcyAlPiUKICBhcy5kYXRhLmZyYW1lKCkgJT4lCiAgbXV0YXRlKGlkID0gcm93X251bWJlcigpKSAlPiUKICB0aWR5cjo6Z2F0aGVyKGNsdXN0ZXIsIHByb2JhYmlsaXR5LCAtaWQpCgpnZ3Bsb3QocHJvYmFiaWxpdGllcywgYWVzKHByb2JhYmlsaXR5KSkgKwogIGdlb21faGlzdG9ncmFtKCkgKwogIGZhY2V0X3dyYXAofiBjbHVzdGVyLCBucm93ID0gMikKYGBgCgpgYGB7ciBjbHVzdGVyLXVuY2VydGFpbnR5LCBmaWcud2lkdGg9MTAsIGZpZy5jYXA9J09ic2VydmF0aW9ucyB0aGF0IGFyZSBhbGlnbmVkIHRvIGVhY2ggY2x1c3RlciBidXQgdGhlaXIgdW5jZXJ0YWludHkgb2YgbWVtYmVyc2hpcCBpcyBncmVhdGVyIHRoYW4gMC4yNS4nfQp1bmNlcnRhaW50eSA8LSBkYXRhLmZyYW1lKAogIGlkID0gMTpucm93KG15X2Jhc2tldCksCiAgY2x1c3RlciA9IG15X2Jhc2tldF9tYyRjbGFzc2lmaWNhdGlvbiwKICB1bmNlcnRhaW50eSA9IG15X2Jhc2tldF9tYyR1bmNlcnRhaW50eQopCgp1bmNlcnRhaW50eSAlPiUKICBncm91cF9ieShjbHVzdGVyKSAlPiUKICBmaWx0ZXIodW5jZXJ0YWludHkgPiAwLjI1KSAlPiUKICBnZ3Bsb3QoYWVzKHVuY2VydGFpbnR5LCByZW9yZGVyKGlkLCB1bmNlcnRhaW50eSkpKSArCiAgZ2VvbV9wb2ludCgpICsKICBmYWNldF93cmFwKH4gY2x1c3Rlciwgc2NhbGVzID0gJ2ZyZWVfeScsIG5yb3cgPSAxKQpgYGAKCmBgYHtyIGNsdXN0ZXIyLWNvbnN1bXB0aW9uLCBmaWcuaGVpZ2h0PTUsIGZpZy5jYXA9J0F2ZXJhZ2Ugc3RhbmRhcmRpemVkIGNvbnN1bXB0aW9uIGZvciBjbHVzdGVyIDIgb2JzZXJ2YXRpb25zIGNvbXBhcmVkIHRvIGFsbCBvYnNlcnZhdGlvbnMuJ30KY2x1c3RlcjIgPC0gbXlfYmFza2V0ICU+JQogIHNjYWxlKCkgJT4lCiAgYXMuZGF0YS5mcmFtZSgpICU+JQogIG11dGF0ZShjbHVzdGVyID0gbXlfYmFza2V0X21jJGNsYXNzaWZpY2F0aW9uKSAlPiUKICBmaWx0ZXIoY2x1c3RlciA9PSAyKSAlPiUKICBzZWxlY3QoLWNsdXN0ZXIpCgpjbHVzdGVyMiAlPiUKICB0aWR5cjo6Z2F0aGVyKHByb2R1Y3QsIHN0ZF9jb3VudCkgJT4lCiAgZ3JvdXBfYnkocHJvZHVjdCkgJT4lCiAgc3VtbWFyaXplKGF2ZyA9IG1lYW4oc3RkX2NvdW50KSkgJT4lCiAgZ2dwbG90KGFlcyhhdmcsIHJlb3JkZXIocHJvZHVjdCwgYXZnKSkpICsKICBnZW9tX3BvaW50KCkgKwogIGxhYnMoeCA9ICJBdmVyYWdlIHN0YW5kYXJkaXplZCBjb25zdW1wdGlvbiIsIHkgPSBOVUxMKQpgYGA=