1 Load Packages

library(tidyverse)
library(scales)
ggplot2::theme_set(theme_minimal())

2 Read Data

mall_customers <- read_csv("../data/Mall_Customers.csv")
Parsed with column specification:
cols(
  CustomerID = col_double(),
  Gender = col_character(),
  Age = col_double(),
  `Annual Income (k$)` = col_double(),
  `Spending Score (1-100)` = col_double()
)
str(mall_customers)
Classes ‘spec_tbl_df’, ‘tbl_df’, ‘tbl’ and 'data.frame':    200 obs. of  5 variables:
 $ CustomerID            : num  1 2 3 4 5 6 7 8 9 10 ...
 $ Gender                : chr  "Male" "Male" "Female" "Female" ...
 $ Age                   : num  19 21 20 23 31 22 35 23 64 30 ...
 $ Annual Income (k$)    : num  15 15 16 16 17 17 18 18 19 19 ...
 $ Spending Score (1-100): num  39 81 6 77 40 76 6 94 3 72 ...
 - attr(*, "spec")=
  .. cols(
  ..   CustomerID = col_double(),
  ..   Gender = col_character(),
  ..   Age = col_double(),
  ..   `Annual Income (k$)` = col_double(),
  ..   `Spending Score (1-100)` = col_double()
  .. )

Data yang digunakan terdiri dari 200 observasi dan 5 peubah, yaitu

  • CustomerID : ID unik masing-masing customer
  • Gender : Jenis kelamin customer
  • Age : Usia (tahun)
  • Annual Income (k$) : Nominal yang dihabiskan oleh customer dalam satu tahun di mall tersebut (dalam ribu dollar).
  • Spending Score (1-100) : Score

3 Exploration

Kita lihat terlebih dahulu beberapa baris pertama dan terakhir dari data.

mall_customers %>% head()
mall_customers %>% tail()

Apakah ada nilai yang kosong (missing value)?

sum(is.na(mall_customers))
[1] 0

Ternyata dari data tersebut semuanya terisi dan tidak ada missing value.

mall_customers %>% summary()
   CustomerID        Gender               Age        Annual Income (k$)
 Min.   :  1.00   Length:200         Min.   :18.00   Min.   : 15.00    
 1st Qu.: 50.75   Class :character   1st Qu.:28.75   1st Qu.: 41.50    
 Median :100.50   Mode  :character   Median :36.00   Median : 61.50    
 Mean   :100.50                      Mean   :38.85   Mean   : 60.56    
 3rd Qu.:150.25                      3rd Qu.:49.00   3rd Qu.: 78.00    
 Max.   :200.00                      Max.   :70.00   Max.   :137.00    
 Spending Score (1-100)
 Min.   : 1.00         
 1st Qu.:34.75         
 Median :50.00         
 Mean   :50.20         
 3rd Qu.:73.00         
 Max.   :99.00         
mall_customers %>% 
  count(Gender) %>% 
  mutate(percentage = n/sum(n)) %>% 
  ggplot(aes(x = Gender, y = percentage)) +
  geom_bar(stat = "identity", color = "white", fill = "skyblue") +
  geom_text(aes(label = n), vjust = -0.25) +
  scale_y_continuous(labels = percent, breaks = seq(0, 0.60, by = 0.2), limits = c(0, 0.60)) +
  labs(title = "Proportion of Gender",
       y = "% of Customers",
       x = "Gender")

Customer yang datang untuk berbelanja lebih banyak perempuan, meskipun tidak terlalu jauh beda jika dilihat dari persentasenya.

mall_customers %>% 
  ggplot(aes(x = Age, y = ..density..)) +
  geom_histogram(binwidth = 1, color = "skyblue", fill = "lightblue") +
  scale_x_continuous(breaks = seq(15, 75, by = 5)) +
  scale_y_continuous(labels = percent, breaks = seq(0, 0.10, by = 0.025), limits = c(0, 0.10)) +
  labs(title = "Distribution of Age",
       y = "% of Customers",
       x = "Age")

Dilihat berdasarkan usianya, sebagian besar berusia di bawah 40 tahun. Customer dengan usia 33 tahun yang paling banyak berbelanja.

mall_customers %>% 
  ggplot(aes(x = Gender, y = Age)) +
  geom_boxplot(color = "skyblue", fill = "lightblue") +
  scale_y_continuous(breaks = seq(15, 75, by = 5)) +
  coord_flip() +
  labs(title = "Summary of Age vs Gender") +
  theme(line = element_blank())

Jika dipisahkan berdasarkan jenis kelaminnya, customer perempuan sebagian besar lebih muda, yaitu 35 tahun, dibandingkan denga customer laki-laki yang sebagian besar berusia sekitar 37 tahun. Customer laki-laki juga memiliki rentang usia yang lebih besar dibandingkan customer perempuan.

mall_customers %>% 
  ggplot(aes(x = `Annual Income (k$)`, y = ..density..)) +
  geom_histogram(binwidth = 5, color = "skyblue", fill = "lightblue") +
  scale_x_continuous(breaks = seq(10, 175, by = 10)) +
  scale_y_continuous(labels = percent, limits = c(0, 0.02)) +
  labs(title = "Distribution of Annual Income",
       y = "% of Customers",
       x = "Annual Income (k$)")

Customer yang berbelanja sebagian besar mempunyai penghasilan antara $40k dan $85k per tahun. Customer yang paling banyak berbelanja adalah yang penghasilannya sebesar $60k per tahun.

mall_customers %>% 
  group_by(Gender) %>% 
  summarise(`Avg Annual Income (k$)` = mean(`Annual Income (k$)`))

mall_customers %>% 
  ggplot(aes(x = Gender, y = `Annual Income (k$)`)) +
  geom_boxplot(color = "skyblue", fill = "lightblue", outlier.color = "red") +
  scale_y_continuous(breaks = seq(10, 175, by = 10)) +
  coord_flip() +
  labs(title = "Summary of Annual Income vs Gender",
       y = "Annual Income (k$)") +
  theme(line = element_blank())

Customer laki-laki yang berbelanja rata-rata mempunyai penghasilan per tahun lebih besar dibandingkan dengan customer perempuan. Namun jika dilihat dari grafik boxplot di atas, terlihat ada customer laki-laki yang penghasilannya jauh di atas customer yang lain. Data seperti ini yang disebut sebagai pencilan atau outlier.

mall_customers %>% 
  filter(`Annual Income (k$)` < 130) %>% 
  group_by(Gender) %>% 
  summarise(`Avg Annual Income (k$)` = mean(`Annual Income (k$)`))

Setelah pencilan tersebut dikeluarkan, terlihat bahwa rata-rata penghasilan per tahun antara customer laki-laki dan perempuan tidak jauh berbeda.

mall_customers %>% 
  ggplot(aes(x = `Spending Score (1-100)`, y = ..count../sum(..count..))) +
  geom_histogram(binwidth = 5, color = "skyblue", fill = "lightblue") +
  scale_x_continuous(breaks = seq(0, 100, by = 10)) +
  scale_y_continuous(labels = percent, limits = c(0, 0.15)) +
  labs(title = "Distribution of Spending Score",
       y = "% of Customers",
       x = "Spending Score")

mall_customers %>% 
  ggplot(aes(x = Gender, y = `Spending Score (1-100)`)) +
  geom_boxplot(color = "skyblue", fill = "lightblue", outlier.color = "red") +
  scale_y_continuous(breaks = seq(0, 100, by = 10)) +
  coord_flip() +
  labs(title = "Summary of Spending Score vs Gender",
       y = "Spending Score") +
  theme(line = element_blank())

4 Clustering

mall_customers %>% head()

Pada data yang digunakan, terdapat peubah CustomerID yang merupakan ID bagi masing-masing customer dan Gender yang berup nilai kategorik. Karena algoritma yang akan digunakan untuk melakukan segmentasi ini adalah k-means, maka dua peubah tersebut tidak akan digunakan.

abt <- mall_customers %>% 
  select(-CustomerID, -Gender) %>% 
  rename(Income = `Annual Income (k$)`, Spending = `Spending Score (1-100)`)

4.1 Correlation Check

Kita lihat terlebih dahulu korelasi antar peubah yang akan digunakan untuk segmentasi.

library(corrplot)
corrmatrix <- cor(abt)
corrplot(corrmatrix, method = "number")

corrtest <- cor.mtest(corrmatrix)$p %>% as.data.frame(row.names = names(abt))
names(corrtest) <- names(abt)
corrtest

Berdasarkan plot korelasi di atas, tidak ada peubah yang saling berkorelasi cukup tinggi dan dari hasil ujinya pun tidak ada nilai p-value yang kurang dari 0.05 (tarf nyata 5%). Jika ada peubah yang saling berkorelasi signifikan, maka sebaiknya dilakukan reduksi dimensi terlebih dahulu, misalnya dengan metode Principal Component Analysis (PCA).

4.2 Standardisasi

abt %>% head()
abt <- abt %>% scale()

4.3 Number of Cluster

set.seed(2019)
k <- 15
wss <- lapply(2:k, function(x)kmeans(x = abt, centers = x, iter.max = 1000, nstart = 25)$tot.withinss)
wss <- unlist(wss)
qplot(x = 2:k, y = wss, geom = "line") +
  geom_point() +
  scale_x_continuous(breaks = 2:k) +
  labs(title = "Optimum Number of Cluster - Elbow Method",
       x = "Number of Cluster")

Dengan menggunakan metode Elbow, masih agak membingungkan antara k = 4 atau k= 5 atau bahkan k = 6. Kita gunakan bantuan lain untuk mempermudah kita dalam membandingkan hasil tersebut.

library(gridExtra)
library(factoextra)
set.seed(2019)
p1 <- fviz_nbclust(x = abt, FUNcluster = kmeans, method = "wss", k.max = 15)
p2 <- fviz_nbclust(x = abt, FUNcluster = kmeans, method = "silhouette", k.max = 15)

grid.arrange(p1, p2)

Berdasarkan nilai silhouette kita dapatkan k = 5.

set.seed(2019)
kcl <- kmeans(x = abt, centers = 5, iter.max = 1000, nstart = 25)

4.4 Cluster Visualization

fviz_cluster(object = kcl, data = abt, ggtheme = theme_minimal(), shape = 19, show.clust.cent = TRUE, geom = "point")

4.5 Profiling

mall_customers <- mall_customers %>% 
  mutate(Cluster = kcl$cluster)

mall_customers %>% 
  count(Cluster, Gender) %>% 
  group_by(Cluster) %>% 
  mutate(pct = n/sum(n)) %>% 
  ggplot(aes(x = Cluster, y = pct, fill = Gender)) +
  geom_bar(stat = "identity") +
  scale_y_continuous(labels = percent) +
  labs(title = "Proportion of Gender by Cluster",
       y = "% of Gender in Cluster")

cluster <- mall_customers %>% 
  group_by(Cluster) %>% 
  summarise(Member = n(),
            Age = mean(Age),
            Income = mean(`Annual Income (k$)`),
            Spending = mean(`Spending Score (1-100)`))  %>% 
  mutate(labels = case_when(Cluster == 1 ~ "Young & Spender",
                            Cluster == 2 ~ "Tua & Mapan",
                            Cluster == 3 ~ "Tua & Menabung",
                            Cluster == 4 ~ "Middle & Menabung",
                            Cluster == 5 ~ "Middle & Hemat",
                            TRUE ~ "Uncategorized"))
cluster
cluster %>% 
  mutate(pct = Member/sum(Member)) %>% 
  ggplot(aes(x = labels, y = pct)) +
  geom_bar(stat = "identity", color = "skyblue", fill = "lightblue") +
  geom_text(aes(label = Member), vjust = -0.25) +
  scale_y_continuous(labels = percent, limits = c(0, 0.4)) +
  labs(title = "Size of Cluster",
       x = "Cluster",
       y = "% of Customers")

LS0tDQp0aXRsZTogIk1hbGwgQ3VzdG9tZXJzIFNlZ21lbnRhdGlvbiINCmF1dGhvcjogIkFlcCBIaWRheWF0dWxvaCINCmRhdGU6ICJMYXN0IFVwZGF0ZTogYHIgZm9ybWF0KFN5cy5EYXRlKCksICclWSAlYiAlZCcpYCINCm91dHB1dDogDQogIGh0bWxfbm90ZWJvb2s6DQogICAgbnVtYmVyX3NlY3Rpb25zOiB5ZXMNCiAgICB0aGVtZTogc3BhY2VsYWINCiAgICBkZl9wcmludDogcGFnZWQNCiAgICB0b2M6IHllcw0KICAgIHRvY19kZXB0aDogNA0KICAgIHRvY19mbG9hdDogdHJ1ZQ0KLS0tDQoNCjxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI+DQoNCmJvZHl7IC8qIE5vcm1hbCAgICovDQogICAgICBmb250LXNpemU6IDEycHg7DQogIH0NCnRkIHsgIC8qIFRhYmxlICAqLw0KICBmb250LXNpemU6IDEycHg7DQp9DQpoMS50aXRsZSB7DQogIGZvbnQtc2l6ZTogMzhweDsNCiAgY29sb3I6IGxpZ2h0Ymx1ZTsNCiAgZm9udC13ZWlnaHQ6IGJvbGQ7DQp9DQpoMSB7IC8qIEhlYWRlciAxICovDQogIGZvbnQtc2l6ZTogMjRweDsNCiAgY29sb3I6IERhcmtCbHVlOw0KfQ0KaDIgeyAvKiBIZWFkZXIgMiAqLw0KICBmb250LXNpemU6IDIwcHg7DQogIGNvbG9yOiBEYXJrQmx1ZTsNCn0NCmgzIHsgLyogSGVhZGVyIDMgKi8NCiAgZm9udC1zaXplOiAxNnB4Ow0KIyAgZm9udC1mYW1pbHk6ICJUaW1lcyBOZXcgUm9tYW4iLCBUaW1lcywgc2VyaWY7DQogIGNvbG9yOiBEYXJrQmx1ZTsNCn0NCmg0IHsgLyogSGVhZGVyIDQgKi8NCiAgZm9udC1zaXplOiAxNHB4Ow0KICBjb2xvcjogRGFya0JsdWU7DQp9DQpjb2RlLnJ7IC8qIENvZGUgYmxvY2sgKi8NCiAgICBmb250LXNpemU6IDEycHg7DQp9DQpwcmUgeyAvKiBDb2RlIGJsb2NrIC0gZGV0ZXJtaW5lcyBjb2RlIHNwYWNpbmcgYmV0d2VlbiBsaW5lcyAqLw0KICAgIGZvbnQtc2l6ZTogMTJweDsNCn0NCjwvc3R5bGU+DQoNCg0KYGBge3Igc2V0dXAsIGluY2x1ZGU9RkFMU0V9DQoja25pdHI6Om9wdHNfY2h1bmskc2V0KGVjaG8gPSBUUlVFKQ0Ka25pdHI6Om9wdHNfY2h1bmskc2V0KGVjaG89VFJVRSwgcmVzdWx0cz0naG9sZCcsIHdhcm5pbmc9RkFMU0UsIGZpZy5zaG93PSdob2xkJywgbWVzc2FnZT1GQUxTRSkgDQpvcHRpb25zKHNjaXBlbiA9IDk5KQ0KYGBgDQojIExvYWQgUGFja2FnZXMNCmBgYHtyIHBrZ30NCmxpYnJhcnkodGlkeXZlcnNlKQ0KbGlicmFyeShzY2FsZXMpDQpnZ3Bsb3QyOjp0aGVtZV9zZXQodGhlbWVfbWluaW1hbCgpKQ0KYGBgDQoNCiMgUmVhZCBEYXRhDQpgYGB7cn0NCm1hbGxfY3VzdG9tZXJzIDwtIHJlYWRfY3N2KCIuLi9kYXRhL01hbGxfQ3VzdG9tZXJzLmNzdiIpDQpzdHIobWFsbF9jdXN0b21lcnMpDQpgYGANCg0KRGF0YSB5YW5nIGRpZ3VuYWthbiB0ZXJkaXJpIGRhcmkgMjAwIG9ic2VydmFzaSBkYW4gNSBwZXViYWgsIHlhaXR1DQoNCiogYEN1c3RvbWVySURgIDogSUQgdW5payBtYXNpbmctbWFzaW5nIGN1c3RvbWVyDQoqIGBHZW5kZXJgIDogSmVuaXMga2VsYW1pbiBjdXN0b21lcg0KKiBgQWdlYCA6IFVzaWEgKHRhaHVuKQ0KKiBgQW5udWFsIEluY29tZSAoayQpYCA6IE5vbWluYWwgeWFuZyBkaWhhYmlza2FuIG9sZWggY3VzdG9tZXIgZGFsYW0gc2F0dSB0YWh1biBkaSBtYWxsIHRlcnNlYnV0IChkYWxhbSByaWJ1IGRvbGxhcikuDQoqIGBTcGVuZGluZyBTY29yZSAoMS0xMDApYCA6IFNjb3JlDQoNCiMgRXhwbG9yYXRpb24NCktpdGEgbGloYXQgdGVybGViaWggZGFodWx1IGJlYmVyYXBhIGJhcmlzIHBlcnRhbWEgZGFuIHRlcmFraGlyIGRhcmkgZGF0YS4NCmBgYHtyfQ0KbWFsbF9jdXN0b21lcnMgJT4lIGhlYWQoKQ0KbWFsbF9jdXN0b21lcnMgJT4lIHRhaWwoKQ0KYGBgDQoNCkFwYWthaCBhZGEgbmlsYWkgeWFuZyBrb3NvbmcgKG1pc3NpbmcgdmFsdWUpPw0KYGBge3J9DQpzdW0oaXMubmEobWFsbF9jdXN0b21lcnMpKQ0KYGBgDQpUZXJueWF0YSBkYXJpIGRhdGEgdGVyc2VidXQgc2VtdWFueWEgdGVyaXNpIGRhbiB0aWRhayBhZGEgbWlzc2luZyB2YWx1ZS4NCg0KYGBge3J9DQptYWxsX2N1c3RvbWVycyAlPiUgc3VtbWFyeSgpDQpgYGANCg0KYGBge3J9DQptYWxsX2N1c3RvbWVycyAlPiUgDQogIGNvdW50KEdlbmRlcikgJT4lIA0KICBtdXRhdGUocGVyY2VudGFnZSA9IG4vc3VtKG4pKSAlPiUgDQogIGdncGxvdChhZXMoeCA9IEdlbmRlciwgeSA9IHBlcmNlbnRhZ2UpKSArDQogIGdlb21fYmFyKHN0YXQgPSAiaWRlbnRpdHkiLCBjb2xvciA9ICJ3aGl0ZSIsIGZpbGwgPSAic2t5Ymx1ZSIpICsNCiAgZ2VvbV90ZXh0KGFlcyhsYWJlbCA9IG4pLCB2anVzdCA9IC0wLjI1KSArDQogIHNjYWxlX3lfY29udGludW91cyhsYWJlbHMgPSBwZXJjZW50LCBicmVha3MgPSBzZXEoMCwgMC42MCwgYnkgPSAwLjIpLCBsaW1pdHMgPSBjKDAsIDAuNjApKSArDQogIGxhYnModGl0bGUgPSAiUHJvcG9ydGlvbiBvZiBHZW5kZXIiLA0KICAgICAgIHkgPSAiJSBvZiBDdXN0b21lcnMiLA0KICAgICAgIHggPSAiR2VuZGVyIikNCmBgYA0KQ3VzdG9tZXIgeWFuZyBkYXRhbmcgdW50dWsgYmVyYmVsYW5qYSBsZWJpaCBiYW55YWsgcGVyZW1wdWFuLCBtZXNraXB1biB0aWRhayB0ZXJsYWx1IGphdWggYmVkYSBqaWthIGRpbGloYXQgZGFyaSBwZXJzZW50YXNlbnlhLg0KDQpgYGB7cn0NCm1hbGxfY3VzdG9tZXJzICU+JSANCiAgZ2dwbG90KGFlcyh4ID0gQWdlLCB5ID0gLi5kZW5zaXR5Li4pKSArDQogIGdlb21faGlzdG9ncmFtKGJpbndpZHRoID0gMSwgY29sb3IgPSAic2t5Ymx1ZSIsIGZpbGwgPSAibGlnaHRibHVlIikgKw0KICBzY2FsZV94X2NvbnRpbnVvdXMoYnJlYWtzID0gc2VxKDE1LCA3NSwgYnkgPSA1KSkgKw0KICBzY2FsZV95X2NvbnRpbnVvdXMobGFiZWxzID0gcGVyY2VudCwgYnJlYWtzID0gc2VxKDAsIDAuMTAsIGJ5ID0gMC4wMjUpLCBsaW1pdHMgPSBjKDAsIDAuMTApKSArDQogIGxhYnModGl0bGUgPSAiRGlzdHJpYnV0aW9uIG9mIEFnZSIsDQogICAgICAgeSA9ICIlIG9mIEN1c3RvbWVycyIsDQogICAgICAgeCA9ICJBZ2UiKQ0KYGBgDQoNCkRpbGloYXQgYmVyZGFzYXJrYW4gdXNpYW55YSwgc2ViYWdpYW4gYmVzYXIgYmVydXNpYSBkaSBiYXdhaCA0MCB0YWh1bi4gQ3VzdG9tZXIgZGVuZ2FuIHVzaWEgMzMgdGFodW4geWFuZyBwYWxpbmcgYmFueWFrIGJlcmJlbGFuamEuDQoNCmBgYHtyfQ0KbWFsbF9jdXN0b21lcnMgJT4lIA0KICBnZ3Bsb3QoYWVzKHggPSBHZW5kZXIsIHkgPSBBZ2UpKSArDQogIGdlb21fYm94cGxvdChjb2xvciA9ICJza3libHVlIiwgZmlsbCA9ICJsaWdodGJsdWUiKSArDQogIHNjYWxlX3lfY29udGludW91cyhicmVha3MgPSBzZXEoMTUsIDc1LCBieSA9IDUpKSArDQogIGNvb3JkX2ZsaXAoKSArDQogIGxhYnModGl0bGUgPSAiU3VtbWFyeSBvZiBBZ2UgdnMgR2VuZGVyIikgKw0KICB0aGVtZShsaW5lID0gZWxlbWVudF9ibGFuaygpKQ0KYGBgDQoNCkppa2EgZGlwaXNhaGthbiBiZXJkYXNhcmthbiBqZW5pcyBrZWxhbWlubnlhLCBjdXN0b21lciBwZXJlbXB1YW4gc2ViYWdpYW4gYmVzYXIgbGViaWggbXVkYSwgeWFpdHUgMzUgdGFodW4sIGRpYmFuZGluZ2thbiBkZW5nYSBjdXN0b21lciBsYWtpLWxha2kgeWFuZyBzZWJhZ2lhbiBiZXNhciBiZXJ1c2lhIHNla2l0YXIgMzcgdGFodW4uIEN1c3RvbWVyIGxha2ktbGFraSBqdWdhIG1lbWlsaWtpIHJlbnRhbmcgdXNpYSB5YW5nIGxlYmloIGJlc2FyIGRpYmFuZGluZ2thbiBjdXN0b21lciBwZXJlbXB1YW4uDQoNCmBgYHtyfQ0KbWFsbF9jdXN0b21lcnMgJT4lIA0KICBnZ3Bsb3QoYWVzKHggPSBgQW5udWFsIEluY29tZSAoayQpYCwgeSA9IC4uZGVuc2l0eS4uKSkgKw0KICBnZW9tX2hpc3RvZ3JhbShiaW53aWR0aCA9IDUsIGNvbG9yID0gInNreWJsdWUiLCBmaWxsID0gImxpZ2h0Ymx1ZSIpICsNCiAgc2NhbGVfeF9jb250aW51b3VzKGJyZWFrcyA9IHNlcSgxMCwgMTc1LCBieSA9IDEwKSkgKw0KICBzY2FsZV95X2NvbnRpbnVvdXMobGFiZWxzID0gcGVyY2VudCwgbGltaXRzID0gYygwLCAwLjAyKSkgKw0KICBsYWJzKHRpdGxlID0gIkRpc3RyaWJ1dGlvbiBvZiBBbm51YWwgSW5jb21lIiwNCiAgICAgICB5ID0gIiUgb2YgQ3VzdG9tZXJzIiwNCiAgICAgICB4ID0gIkFubnVhbCBJbmNvbWUgKGskKSIpDQpgYGANCg0KQ3VzdG9tZXIgeWFuZyBiZXJiZWxhbmphIHNlYmFnaWFuIGJlc2FyIG1lbXB1bnlhaSBwZW5naGFzaWxhbiBhbnRhcmEgXCQ0MGsgZGFuIFwkODVrIHBlciB0YWh1bi4gQ3VzdG9tZXIgeWFuZyBwYWxpbmcgYmFueWFrIGJlcmJlbGFuamEgYWRhbGFoIHlhbmcgcGVuZ2hhc2lsYW5ueWEgc2ViZXNhciBcJDYwayBwZXIgdGFodW4uDQoNCmBgYHtyfQ0KbWFsbF9jdXN0b21lcnMgJT4lIA0KICBncm91cF9ieShHZW5kZXIpICU+JSANCiAgc3VtbWFyaXNlKGBBdmcgQW5udWFsIEluY29tZSAoayQpYCA9IG1lYW4oYEFubnVhbCBJbmNvbWUgKGskKWApKQ0KDQptYWxsX2N1c3RvbWVycyAlPiUgDQogIGdncGxvdChhZXMoeCA9IEdlbmRlciwgeSA9IGBBbm51YWwgSW5jb21lIChrJClgKSkgKw0KICBnZW9tX2JveHBsb3QoY29sb3IgPSAic2t5Ymx1ZSIsIGZpbGwgPSAibGlnaHRibHVlIiwgb3V0bGllci5jb2xvciA9ICJyZWQiKSArDQogIHNjYWxlX3lfY29udGludW91cyhicmVha3MgPSBzZXEoMTAsIDE3NSwgYnkgPSAxMCkpICsNCiAgY29vcmRfZmxpcCgpICsNCiAgbGFicyh0aXRsZSA9ICJTdW1tYXJ5IG9mIEFubnVhbCBJbmNvbWUgdnMgR2VuZGVyIiwNCiAgICAgICB5ID0gIkFubnVhbCBJbmNvbWUgKGskKSIpICsNCiAgdGhlbWUobGluZSA9IGVsZW1lbnRfYmxhbmsoKSkNCmBgYA0KDQpDdXN0b21lciBsYWtpLWxha2kgeWFuZyBiZXJiZWxhbmphIHJhdGEtcmF0YSBtZW1wdW55YWkgcGVuZ2hhc2lsYW4gcGVyIHRhaHVuIGxlYmloIGJlc2FyIGRpYmFuZGluZ2thbiBkZW5nYW4gY3VzdG9tZXIgcGVyZW1wdWFuLiBOYW11biBqaWthIGRpbGloYXQgZGFyaSBncmFmaWsgYm94cGxvdCBkaSBhdGFzLCB0ZXJsaWhhdCBhZGEgY3VzdG9tZXIgbGFraS1sYWtpIHlhbmcgcGVuZ2hhc2lsYW5ueWEgamF1aCBkaSBhdGFzIGN1c3RvbWVyIHlhbmcgbGFpbi4gRGF0YSBzZXBlcnRpIGluaSB5YW5nIGRpc2VidXQgc2ViYWdhaSBwZW5jaWxhbiBhdGF1IG91dGxpZXIuDQoNCmBgYHtyfQ0KbWFsbF9jdXN0b21lcnMgJT4lIA0KICBmaWx0ZXIoYEFubnVhbCBJbmNvbWUgKGskKWAgPCAxMzApICU+JSANCiAgZ3JvdXBfYnkoR2VuZGVyKSAlPiUgDQogIHN1bW1hcmlzZShgQXZnIEFubnVhbCBJbmNvbWUgKGskKWAgPSBtZWFuKGBBbm51YWwgSW5jb21lIChrJClgKSkNCmBgYA0KDQpTZXRlbGFoIHBlbmNpbGFuIHRlcnNlYnV0IGRpa2VsdWFya2FuLCB0ZXJsaWhhdCBiYWh3YSByYXRhLXJhdGEgcGVuZ2hhc2lsYW4gcGVyIHRhaHVuIGFudGFyYSBjdXN0b21lciBsYWtpLWxha2kgZGFuIHBlcmVtcHVhbiB0aWRhayBqYXVoIGJlcmJlZGEuDQoNCmBgYHtyfQ0KbWFsbF9jdXN0b21lcnMgJT4lIA0KICBnZ3Bsb3QoYWVzKHggPSBgU3BlbmRpbmcgU2NvcmUgKDEtMTAwKWAsIHkgPSAuLmNvdW50Li4vc3VtKC4uY291bnQuLikpKSArDQogIGdlb21faGlzdG9ncmFtKGJpbndpZHRoID0gNSwgY29sb3IgPSAic2t5Ymx1ZSIsIGZpbGwgPSAibGlnaHRibHVlIikgKw0KICBzY2FsZV94X2NvbnRpbnVvdXMoYnJlYWtzID0gc2VxKDAsIDEwMCwgYnkgPSAxMCkpICsNCiAgc2NhbGVfeV9jb250aW51b3VzKGxhYmVscyA9IHBlcmNlbnQsIGxpbWl0cyA9IGMoMCwgMC4xNSkpICsNCiAgbGFicyh0aXRsZSA9ICJEaXN0cmlidXRpb24gb2YgU3BlbmRpbmcgU2NvcmUiLA0KICAgICAgIHkgPSAiJSBvZiBDdXN0b21lcnMiLA0KICAgICAgIHggPSAiU3BlbmRpbmcgU2NvcmUiKQ0KYGBgDQoNCmBgYHtyfQ0KbWFsbF9jdXN0b21lcnMgJT4lIA0KICBnZ3Bsb3QoYWVzKHggPSBHZW5kZXIsIHkgPSBgU3BlbmRpbmcgU2NvcmUgKDEtMTAwKWApKSArDQogIGdlb21fYm94cGxvdChjb2xvciA9ICJza3libHVlIiwgZmlsbCA9ICJsaWdodGJsdWUiLCBvdXRsaWVyLmNvbG9yID0gInJlZCIpICsNCiAgc2NhbGVfeV9jb250aW51b3VzKGJyZWFrcyA9IHNlcSgwLCAxMDAsIGJ5ID0gMTApKSArDQogIGNvb3JkX2ZsaXAoKSArDQogIGxhYnModGl0bGUgPSAiU3VtbWFyeSBvZiBTcGVuZGluZyBTY29yZSB2cyBHZW5kZXIiLA0KICAgICAgIHkgPSAiU3BlbmRpbmcgU2NvcmUiKSArDQogIHRoZW1lKGxpbmUgPSBlbGVtZW50X2JsYW5rKCkpDQpgYGANCg0KIyBDbHVzdGVyaW5nDQoNCmBgYHtyfQ0KbWFsbF9jdXN0b21lcnMgJT4lIGhlYWQoKQ0KYGBgDQpQYWRhIGRhdGEgeWFuZyBkaWd1bmFrYW4sIHRlcmRhcGF0IHBldWJhaCBgQ3VzdG9tZXJJRGAgeWFuZyBtZXJ1cGFrYW4gSUQgYmFnaSBtYXNpbmctbWFzaW5nIGN1c3RvbWVyIGRhbiBgR2VuZGVyYCB5YW5nIGJlcnVwIG5pbGFpIGthdGVnb3Jpay4gS2FyZW5hIGFsZ29yaXRtYSB5YW5nIGFrYW4gZGlndW5ha2FuIHVudHVrIG1lbGFrdWthbiBzZWdtZW50YXNpIGluaSBhZGFsYWggay1tZWFucywgbWFrYSBkdWEgcGV1YmFoIHRlcnNlYnV0IHRpZGFrIGFrYW4gZGlndW5ha2FuLg0KDQpgYGB7cn0NCmFidCA8LSBtYWxsX2N1c3RvbWVycyAlPiUgDQogIHNlbGVjdCgtQ3VzdG9tZXJJRCwgLUdlbmRlcikgJT4lIA0KICByZW5hbWUoSW5jb21lID0gYEFubnVhbCBJbmNvbWUgKGskKWAsIFNwZW5kaW5nID0gYFNwZW5kaW5nIFNjb3JlICgxLTEwMClgKQ0KYGBgDQoNCiMjIENvcnJlbGF0aW9uIENoZWNrDQoNCktpdGEgbGloYXQgdGVybGViaWggZGFodWx1IGtvcmVsYXNpIGFudGFyIHBldWJhaCB5YW5nIGFrYW4gZGlndW5ha2FuIHVudHVrIHNlZ21lbnRhc2kuDQpgYGB7cn0NCmxpYnJhcnkoY29ycnBsb3QpDQpjb3JybWF0cml4IDwtIGNvcihhYnQpDQpjb3JycGxvdChjb3JybWF0cml4LCBtZXRob2QgPSAibnVtYmVyIikNCmNvcnJ0ZXN0IDwtIGNvci5tdGVzdChjb3JybWF0cml4KSRwICU+JSBhcy5kYXRhLmZyYW1lKHJvdy5uYW1lcyA9IG5hbWVzKGFidCkpDQpuYW1lcyhjb3JydGVzdCkgPC0gbmFtZXMoYWJ0KQ0KY29ycnRlc3QNCmBgYA0KDQpCZXJkYXNhcmthbiBwbG90IGtvcmVsYXNpIGRpIGF0YXMsIHRpZGFrIGFkYSBwZXViYWggeWFuZyBzYWxpbmcgYmVya29yZWxhc2kgY3VrdXAgdGluZ2dpIGRhbiBkYXJpIGhhc2lsIHVqaW55YSBwdW4gdGlkYWsgYWRhIG5pbGFpIHAtdmFsdWUgeWFuZyBrdXJhbmcgZGFyaSAwLjA1ICh0YXJmIG55YXRhIDUlKS4gSmlrYSBhZGEgcGV1YmFoIHlhbmcgc2FsaW5nIGJlcmtvcmVsYXNpIHNpZ25pZmlrYW4sIG1ha2Egc2ViYWlrbnlhIGRpbGFrdWthbiByZWR1a3NpIGRpbWVuc2kgdGVybGViaWggZGFodWx1LCBtaXNhbG55YSBkZW5nYW4gbWV0b2RlIFByaW5jaXBhbCBDb21wb25lbnQgQW5hbHlzaXMgKFBDQSkuDQoNCiMjIFN0YW5kYXJkaXNhc2kNCmBgYHtyfQ0KYWJ0ICU+JSBoZWFkKCkNCmBgYA0KDQpgYGB7cn0NCmFidCA8LSBhYnQgJT4lIHNjYWxlKCkNCmBgYA0KDQojIyBOdW1iZXIgb2YgQ2x1c3Rlcg0KYGBge3J9DQpzZXQuc2VlZCgyMDE5KQ0KayA8LSAxNQ0Kd3NzIDwtIGxhcHBseSgyOmssIGZ1bmN0aW9uKHgpa21lYW5zKHggPSBhYnQsIGNlbnRlcnMgPSB4LCBpdGVyLm1heCA9IDEwMDAsIG5zdGFydCA9IDI1KSR0b3Qud2l0aGluc3MpDQp3c3MgPC0gdW5saXN0KHdzcykNCnFwbG90KHggPSAyOmssIHkgPSB3c3MsIGdlb20gPSAibGluZSIpICsNCiAgZ2VvbV9wb2ludCgpICsNCiAgc2NhbGVfeF9jb250aW51b3VzKGJyZWFrcyA9IDI6aykgKw0KICBsYWJzKHRpdGxlID0gIk9wdGltdW0gTnVtYmVyIG9mIENsdXN0ZXIgLSBFbGJvdyBNZXRob2QiLA0KICAgICAgIHggPSAiTnVtYmVyIG9mIENsdXN0ZXIiKQ0KYGBgDQoNCkRlbmdhbiBtZW5nZ3VuYWthbiBtZXRvZGUgRWxib3csIG1hc2loIGFnYWsgbWVtYmluZ3VuZ2thbiBhbnRhcmEgayA9IDQgYXRhdSBrPSA1IGF0YXUgYmFoa2FuIGsgPSA2LiBLaXRhIGd1bmFrYW4gYmFudHVhbiBsYWluIHVudHVrIG1lbXBlcm11ZGFoIGtpdGEgZGFsYW0gbWVtYmFuZGluZ2thbiBoYXNpbCB0ZXJzZWJ1dC4NCmBgYHtyfQ0KbGlicmFyeShncmlkRXh0cmEpDQpsaWJyYXJ5KGZhY3RvZXh0cmEpDQpzZXQuc2VlZCgyMDE5KQ0KcDEgPC0gZnZpel9uYmNsdXN0KHggPSBhYnQsIEZVTmNsdXN0ZXIgPSBrbWVhbnMsIG1ldGhvZCA9ICJ3c3MiLCBrLm1heCA9IDE1KQ0KcDIgPC0gZnZpel9uYmNsdXN0KHggPSBhYnQsIEZVTmNsdXN0ZXIgPSBrbWVhbnMsIG1ldGhvZCA9ICJzaWxob3VldHRlIiwgay5tYXggPSAxNSkNCg0KZ3JpZC5hcnJhbmdlKHAxLCBwMikNCmBgYA0KQmVyZGFzYXJrYW4gbmlsYWkgc2lsaG91ZXR0ZSBraXRhIGRhcGF0a2FuIGsgPSA1Lg0KDQpgYGB7cn0NCnNldC5zZWVkKDIwMTkpDQprY2wgPC0ga21lYW5zKHggPSBhYnQsIGNlbnRlcnMgPSA1LCBpdGVyLm1heCA9IDEwMDAsIG5zdGFydCA9IDI1KQ0KYGBgDQoNCiMjIENsdXN0ZXIgVmlzdWFsaXphdGlvbg0KYGBge3J9DQpmdml6X2NsdXN0ZXIob2JqZWN0ID0ga2NsLCBkYXRhID0gYWJ0LCBnZ3RoZW1lID0gdGhlbWVfbWluaW1hbCgpLCBzaGFwZSA9IDE5LCBzaG93LmNsdXN0LmNlbnQgPSBUUlVFLCBnZW9tID0gInBvaW50IikNCmBgYA0KDQoNCiMjIFByb2ZpbGluZw0KYGBge3J9DQptYWxsX2N1c3RvbWVycyA8LSBtYWxsX2N1c3RvbWVycyAlPiUgDQogIG11dGF0ZShDbHVzdGVyID0ga2NsJGNsdXN0ZXIpDQoNCm1hbGxfY3VzdG9tZXJzICU+JSANCiAgY291bnQoQ2x1c3RlciwgR2VuZGVyKSAlPiUgDQogIGdyb3VwX2J5KENsdXN0ZXIpICU+JSANCiAgbXV0YXRlKHBjdCA9IG4vc3VtKG4pKSAlPiUgDQogIGdncGxvdChhZXMoeCA9IENsdXN0ZXIsIHkgPSBwY3QsIGZpbGwgPSBHZW5kZXIpKSArDQogIGdlb21fYmFyKHN0YXQgPSAiaWRlbnRpdHkiKSArDQogIHNjYWxlX3lfY29udGludW91cyhsYWJlbHMgPSBwZXJjZW50KSArDQogIGxhYnModGl0bGUgPSAiUHJvcG9ydGlvbiBvZiBHZW5kZXIgYnkgQ2x1c3RlciIsDQogICAgICAgeSA9ICIlIG9mIEdlbmRlciBpbiBDbHVzdGVyIikNCmBgYA0KDQpgYGB7cn0NCmNsdXN0ZXIgPC0gbWFsbF9jdXN0b21lcnMgJT4lIA0KICBncm91cF9ieShDbHVzdGVyKSAlPiUgDQogIHN1bW1hcmlzZShNZW1iZXIgPSBuKCksDQogICAgICAgICAgICBBZ2UgPSBtZWFuKEFnZSksDQogICAgICAgICAgICBJbmNvbWUgPSBtZWFuKGBBbm51YWwgSW5jb21lIChrJClgKSwNCiAgICAgICAgICAgIFNwZW5kaW5nID0gbWVhbihgU3BlbmRpbmcgU2NvcmUgKDEtMTAwKWApKSAgJT4lIA0KICBtdXRhdGUobGFiZWxzID0gY2FzZV93aGVuKENsdXN0ZXIgPT0gMSB+ICJZb3VuZyAmIFNwZW5kZXIiLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgIENsdXN0ZXIgPT0gMiB+ICJUdWEgJiBNYXBhbiIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgQ2x1c3RlciA9PSAzIH4gIlR1YSAmIE1lbmFidW5nIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBDbHVzdGVyID09IDQgfiAiTWlkZGxlICYgTWVuYWJ1bmciLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgIENsdXN0ZXIgPT0gNSB+ICJNaWRkbGUgJiBIZW1hdCIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgVFJVRSB+ICJVbmNhdGVnb3JpemVkIikpDQpjbHVzdGVyDQpgYGANCg0KYGBge3J9DQpjbHVzdGVyICU+JSANCiAgbXV0YXRlKHBjdCA9IE1lbWJlci9zdW0oTWVtYmVyKSkgJT4lIA0KICBnZ3Bsb3QoYWVzKHggPSBsYWJlbHMsIHkgPSBwY3QpKSArDQogIGdlb21fYmFyKHN0YXQgPSAiaWRlbnRpdHkiLCBjb2xvciA9ICJza3libHVlIiwgZmlsbCA9ICJsaWdodGJsdWUiKSArDQogIGdlb21fdGV4dChhZXMobGFiZWwgPSBNZW1iZXIpLCB2anVzdCA9IC0wLjI1KSArDQogIHNjYWxlX3lfY29udGludW91cyhsYWJlbHMgPSBwZXJjZW50LCBsaW1pdHMgPSBjKDAsIDAuNCkpICsNCiAgbGFicyh0aXRsZSA9ICJTaXplIG9mIENsdXN0ZXIiLA0KICAgICAgIHggPSAiQ2x1c3RlciIsDQogICAgICAgeSA9ICIlIG9mIEN1c3RvbWVycyIpDQpgYGANCg0KYGBge3J9DQoNCmBgYA0KDQo=