Hackathon Project
Submitted By : Ajesh
Introduction-
Customer Segementation:
The process of division of customer base into several groups of individuals that share a similarity in different ways that are relevant to marketing and
for this we are using k-means clustering technique.
Clustering:
The process of dividing the entire data into groups known as clusters based on the patterns in the data.
K-means clustering:
A simple unsupervised learning algorithm that is used to solve clustering problems. It follows a simple procedure of classifying a given data set into a number of clusters, defined by the letter “k,” which is fixed beforehand. The clusters are then positioned as points and all observations or data points are associated with the nearest cluster, computed, adjusted and then the process starts over using the new adjustments until a desired result is reached.
#loading data
customer_data=data.frame(mallcustomer)
#top 6 data
head(customer_data)
The above shown is the top 6 observation of the data.
#showing the structure of the data
str(customer_data)
'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 ...
The above shown is the structure of the data showing the datatype of the variables.
#summary of the data
summary(customer_data)
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
The above shown is the discriptive analysis of the customer.
#summary of Age variable
summary(customer_data$Age)
Min. 1st Qu. Median Mean 3rd Qu. Max.
18.00 28.75 36.00 38.85 49.00 70.00
#standard deviation of age variable
print(paste("standard deviation",sd(customer_data$Age)))
[1] "standard deviation 13.9690073315589"
The above information is showing the discriptive analysis Age variable and its standard deviation.
#summary of Annual income
summary(customer_data$Annual.Income..k..)
Min. 1st Qu. Median Mean 3rd Qu. Max.
15.00 41.50 61.50 60.56 78.00 137.00
#standard deviation of Annual income
print(paste("standard deviation",sd(customer_data$Annual.Income..k..)))
[1] "standard deviation 26.2647211652712"
The above information is showing the discriptive analysis Annual income variable and its standard deviation.
#summary of Spending score
summary(customer_data$Spending.Score..1.100.)
Min. 1st Qu. Median Mean 3rd Qu. Max.
1.00 34.75 50.00 50.20 73.00 99.00
#standard deviation of Spending score
print(paste("standard deviation",sd(customer_data$Spending.Score..1.100.)))
[1] "standard deviation 25.8235216683702"
The above information is showing the discriptive analysis Age variable and its standard deviation.
Visualisation of Data
library(ggplot2)
ggplot(customer_data,aes(Gender,fill=Gender))+geom_bar()+scale_fill_viridis_d()+theme_light()+ggtitle("Barplot for Gender ")

From the above plot ,We understand that number of females are more than males but to know the proportion of females and males in the data we can do visualisation using pie chart.
library(plotrix)
per=round(t/sum(t)*100)
lbs=paste(c("Female","Male")," ",per,"%",sep=" ")
pie3D(t,labels=lbs,
main="Pie Chart Depicting Ratio of Female and Male")

From the above graph, we conclude that the proportion of females is 56%, whereas the proportion of male in the customer dataset is 44%.
Now to plot the frequency of the age of the customers,We will do histogram plot.
#barplot for Age
ggplot(customer_data,aes(Age))+geom_bar(fill="green",col="purple",alpha=0.5)+theme_dark()+ggtitle("Age Distribution ")

The above graph shows the distribution of Age variable throughout the data.
It is very clear from the above graph that people of Age between 30 to 32 are more in count in the data.
#Discriptive boxplot for Age and annual income
ggplot(customer_data,aes(Age,Annual.Income..k..,group=Gender,fill=Gender))+geom_boxplot()+ggtitle("Discriptive boxplot for Age and annual income")

From the above plot: We conclude that the customer of Age between 25 and 42 are more in count the minimum age of customers is 18, whereas, the maximum age is 70.
Now doing visualization to get insights the annual income.
hist(customer_data$Annual.Income..k..,
col="#660033",
main="Histogram for Annual Income",
xlab="Annual Income Class",
ylab="Frequency",
labels=TRUE)

From the above data,we conclude that most of the customer’s annual income are under 70 to 80k.
Now the density plot of customer’s annual income data.
ggplot(customer_data,aes(Annual.Income..k..,fill=Gender))+geom_density(alpha=0.5,col="lightblue")+scale_fill_manual(values = c("red","blue"))+theme_minimal()+ggtitle("Density plot for Annual income")

The above density plot explains the distribution annual income for female and male separately and it is clear that annual income is irrespective of the Gender.
Now to make the understanding about spending score graphically ,We will do some visualisation.
boxplot(customer_data$Spending.Score..1.100.,
horizontal=TRUE,
col="#990000",
main="BoxPlot for Descriptive Analysis of Spending Score")

NA
NA
The above boxplot we can conclude that maximum and minimum spending scores are 1 and 99 respectively,and average spending score is near 50 (50.20 exact from the summary ).
Barplot for Spending score
hist(customer_data$Spending.Score..1.100.,
main="HistoGram for Spending Score",
xlab="Spending Score Class",
ylab="Frequency",
col="#6600cc",
labels=TRUE)

From the above plot we can clearly see that most of the customers are having spending scores in between 40-50.
K-means modeling:
For customer segementation we are using k-means algorithm and deviding the customer data into 2 groups.
k2<-kmeans(customer_data[,3:5],2,iter.max=100,nstart=50,algorithm="Lloyd")
k2
K-means clustering with 2 clusters of sizes 115, 85
Cluster means:
Age Annual.Income..k.. Spending.Score..1.100.
1 46.16522 59.36522 32.88696
2 28.95294 62.17647 73.62353
Clustering vector:
[1] 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1 2
[35] 1 2 1 2 1 2 1 2 1 2 1 2 1 1 1 1 1 2 2 1 1 1 1 1 2 1 1 2 1 1 1 2 1 1
[69] 2 1 1 1 1 1 1 2 1 1 2 1 1 2 1 1 2 1 1 2 2 1 1 1 1 1 1 2 1 2 1 2 1 1
[103] 1 2 1 1 1 1 1 1 1 2 1 2 2 2 1 2 1 1 2 1 2 2 1 2 1 2 1 2 1 2 1 2 1 2
[137] 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1 2
[171] 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1 2
Within cluster sum of squares by cluster:
[1] 124540.05 88300.12
(between_SS / total_SS = 31.1 %)
Available components:
[1] "cluster" "centers" "totss" "withinss"
[5] "tot.withinss" "betweenss" "size" "iter"
[9] "ifault"
The above information shows the grouping of the customer_data and the mean values set for the clusters, but the ratio of between_ss and total_ss(ss=sum of squares) is very less ,so we need to improve the quality of k-means model
For the better quality k-means model we need to find the optimum value for k(i.e number of clusters).
Average Silhouette Method:
Using this method we can determine the optimum value of k.
In silhouette method,The average sil_width is the measure of quality of cluster.The higher the value of average sil_width , the better the quality within the intra-clusters.
sil(2)
sil_width
0.2931661
The above is the mean value of sil_width for k=2.
So now in order to determine the optimum value of k for clustering we have to check the average sil_width for different values of k then select the max value of average sil_width and the k corresponds to that will be the optimum value.
So we will make a function which will iterate the average sil_width for different value of k and returns the value of k with max sil_width.
op_sil_k=function(data){
s3=map_dbl(2:10,sil)
s5=as.matrix(s3)
for(index in 1:ncol(s5)){
s6=which(s5==max(s5))
s8=s6+1
}
print(paste("optimum value of k = ",s8))
}
op_sil_k()
[1] "optimum value of k = 6"
Here we got 6 as the optimum value for k.
Now we can also confirm the optimum value of k by visualisation.

Hence from the above plot we can see the highest value of average sil_width corresponds to k=6.
Now we can make a model using k=6 for better quality.
k6<-kmeans(customer_data[,3:5],6,iter.max=100,nstart=50,algorithm="Lloyd")
k6
K-means clustering with 6 clusters of sizes 45, 22, 21, 38, 35, 39
Cluster means:
Age Annual.Income..k.. Spending.Score..1.100.
1 56.15556 53.37778 49.08889
2 25.27273 25.72727 79.36364
3 44.14286 25.14286 19.52381
4 27.00000 56.65789 49.13158
5 41.68571 88.22857 17.28571
6 32.69231 86.53846 82.12821
Clustering vector:
[1] 3 2 3 2 3 2 3 2 3 2 3 2 3 2 3 2 3 2 3 2 3 2 3 2 3 2 3 2 3 2 3 2 3 2
[35] 3 2 3 2 3 2 1 2 1 4 3 2 1 4 4 4 1 4 4 1 1 1 1 1 4 1 1 4 1 1 1 4 1 1
[69] 4 4 1 1 1 1 1 4 1 4 4 1 1 4 1 1 4 1 1 4 4 1 1 4 1 4 4 4 1 4 1 4 4 1
[103] 1 4 1 4 1 1 1 1 1 4 4 4 4 4 1 1 1 1 4 4 4 6 4 6 5 6 5 6 5 6 4 6 5 6
[137] 5 6 5 6 5 6 4 6 5 6 5 6 5 6 5 6 5 6 5 6 5 6 5 6 5 6 5 6 5 6 5 6 5 6
[171] 5 6 5 6 5 6 5 6 5 6 5 6 5 6 5 6 5 6 5 6 5 6 5 6 5 6 5 6 5 6
Within cluster sum of squares by cluster:
[1] 8062.133 4099.818 7732.381 7742.895 16690.857 13972.359
(between_SS / total_SS = 81.1 %)
Available components:
[1] "cluster" "centers" "totss" "withinss"
[5] "tot.withinss" "betweenss" "size" "iter"
[9] "ifault"
We have grouped the data into 6 clusters with better quality as we got the ratio of between ss and total ss(ss=sum odf squares) increase to 81%.
Visualising the cluster result using ggplot2
library(ggplot2)
set.seed(1)
ggplot(customer_data, aes(x =Annual.Income..k.., y = Spending.Score..1.100.)) +
geom_point(stat = "identity", aes(color = as.factor(k6$cluster)),size=4,alpha=0.6) +
scale_color_discrete(name=" ",
breaks=c("1", "2", "3", "4", "5","6"),
labels=c("Cluster 1", "Cluster 2", "Cluster 3", "Cluster 4", "Cluster 5","Cluster 6")) +
ggtitle("Segments of Mall Customers", subtitle = "Using K-means Clustering")+theme(
panel.background = element_rect(fill = "lightblue", colour = "black",
size = 2, linetype = "solid"),plot.background = element_rect(fill = "#6D9EC1"))

Hence from the above graph we came to know that:
Cluster6 - having high annual income and high spending score.
Cluster5 - having high annual income but low spending score.
Cluster4 & Cluster1- having medium annual income and medium spending score.
Cluster3 - having low annual income and low spending score.
Visualisation for Age and Annual income.
ggplot(customer_data, aes(x =Annual.Income..k.., y =Age)) +
geom_point(stat = "identity", aes(color = as.factor(k6$cluster)),size=4,alpha=0.6) +
scale_color_discrete(name=" ",
breaks=c("1", "2", "3", "4", "5","6"),
labels=c("Cluster 1", "Cluster 2", "Cluster 3", "Cluster 4", "Cluster 5","Cluster 6")) +
ggtitle("Segments of Mall Customers", subtitle = "Using K-means Clustering")

Hence from the above graph we came to know that:
Cluster6 - having customer of age group 30-40 and high annual .
Cluster5 - having customer of age group 40-60 but high annual income.
Cluster4 - having customer of age group 20-40 and medium annual income.
Cluster3 - having customer of age group 40-60 and low annual income.
Cluster2 - having customer of age group 20-40 but low annual income.
Cluster1 - having customer of age group 40-60 and medium annual income.
Visualizing cluster using fviz function.
fviz_cluster(k6, data = customer_data[,3:5],geom = "points")

Hence with the help of the clustering method we can disect/segementise the customers for better approach.
LS0tDQp0aXRsZTogIkN1c3RvbWVyIFNlZ2VtZW50YXRpb24gdXNpbmcgTWFjaGluZSBMZWFybmluZyINCm91dHB1dDogaHRtbF9ub3RlYm9vaw0KLS0tDQoNCg0KDQoNCg0KDQoNCg0KDQoNCjxoMT5IYWNrYXRob24gUHJvamVjdDxoMT4NCjxoMz5TdWJtaXR0ZWQgQnkgOiBBamVzaDxoMz4NCg0KDQoNCg0KDQoNCg0KDQoNCg0KDQoNCg0KDQoNCg0KPGgxPkludHJvZHVjdGlvbi08L2gxPg0KDQoNCg0KPGgyPkN1c3RvbWVyIFNlZ2VtZW50YXRpb246PGgyPg0KPGg1PlRoZSBwcm9jZXNzIG9mIGRpdmlzaW9uIG9mIGN1c3RvbWVyIGJhc2UgaW50byBzZXZlcmFsIGdyb3VwcyBvZiBpbmRpdmlkdWFscyB0aGF0IHNoYXJlIGEgc2ltaWxhcml0eSBpbiBkaWZmZXJlbnQgd2F5cyB0aGF0IGFyZSByZWxldmFudCB0byBtYXJrZXRpbmcgYW5kIDxoNT5mb3IgdGhpcyB3ZSBhcmUgdXNpbmcgay1tZWFucyBjbHVzdGVyaW5nIHRlY2huaXF1ZS48aDU+DQoNCjxoMj5DbHVzdGVyaW5nOjxoMj4NCjxoNT5UaGUgcHJvY2VzcyBvZiBkaXZpZGluZyB0aGUgZW50aXJlIGRhdGEgaW50byBncm91cHMgIGtub3duIGFzIGNsdXN0ZXJzIGJhc2VkIG9uIHRoZSBwYXR0ZXJucyBpbiB0aGUgZGF0YS48aDU+DQoNCjxoMj5LLW1lYW5zIGNsdXN0ZXJpbmc6PGgyPiAgDQo8aDU+QSBzaW1wbGUgdW5zdXBlcnZpc2VkIGxlYXJuaW5nIGFsZ29yaXRobSB0aGF0IGlzIHVzZWQgdG8gc29sdmUgY2x1c3RlcmluZyBwcm9ibGVtcy4gSXQgZm9sbG93cyBhIHNpbXBsZSBwcm9jZWR1cmUgb2YgY2xhc3NpZnlpbmcgYSBnaXZlbiBkYXRhIHNldCBpbnRvIGEgbnVtYmVyIG9mIGNsdXN0ZXJzLCBkZWZpbmVkIGJ5IHRoZSBsZXR0ZXIgImssIiB3aGljaCBpcyBmaXhlZCBiZWZvcmVoYW5kLiBUaGUgY2x1c3RlcnMgYXJlIHRoZW4gcG9zaXRpb25lZCBhcyBwb2ludHMgYW5kIGFsbCBvYnNlcnZhdGlvbnMgb3IgZGF0YSBwb2ludHMgYXJlIGFzc29jaWF0ZWQgd2l0aCB0aGUgbmVhcmVzdCBjbHVzdGVyLCBjb21wdXRlZCwgYWRqdXN0ZWQgYW5kIHRoZW4gdGhlIHByb2Nlc3Mgc3RhcnRzIG92ZXIgdXNpbmcgdGhlIG5ldyBhZGp1c3RtZW50cyB1bnRpbCBhIGRlc2lyZWQgcmVzdWx0IGlzIHJlYWNoZWQuPGg1Pg0KDQpgYGB7cn0NCiNsb2FkaW5nIGRhdGENCmN1c3RvbWVyX2RhdGE9ZGF0YS5mcmFtZShtYWxsY3VzdG9tZXIpDQojdG9wIDYgZGF0YQ0KaGVhZChjdXN0b21lcl9kYXRhKQ0KYGBgDQo8aDU+VGhlIGFib3ZlIHNob3duIGlzIHRoZSB0b3AgNiBvYnNlcnZhdGlvbiBvZiB0aGUgZGF0YS48aDU+DQpgYGB7cn0NCiNzaG93aW5nIHRoZSBzdHJ1Y3R1cmUgb2YgdGhlIGRhdGENCnN0cihjdXN0b21lcl9kYXRhKQ0KYGBgDQo8aDU+VGhlIGFib3ZlIHNob3duIGlzIHRoZSBzdHJ1Y3R1cmUgb2YgdGhlIGRhdGEgc2hvd2luZyB0aGUgZGF0YXR5cGUgb2YgdGhlIHZhcmlhYmxlcy48aDU+DQpgYGB7cn0NCiNzdW1tYXJ5IG9mIHRoZSBkYXRhDQpzdW1tYXJ5KGN1c3RvbWVyX2RhdGEpDQpgYGANCjxoNT5UaGUgYWJvdmUgc2hvd24gaXMgdGhlIGRpc2NyaXB0aXZlIGFuYWx5c2lzIG9mIHRoZSBjdXN0b21lci48aDU+DQpgYGB7cn0NCiNzdW1tYXJ5IG9mIEFnZSB2YXJpYWJsZQ0Kc3VtbWFyeShjdXN0b21lcl9kYXRhJEFnZSkNCiNzdGFuZGFyZCBkZXZpYXRpb24gb2YgYWdlIHZhcmlhYmxlDQpwcmludChwYXN0ZSgic3RhbmRhcmQgZGV2aWF0aW9uIixzZChjdXN0b21lcl9kYXRhJEFnZSkpKQ0KDQpgYGANCjxoNT5UaGUgYWJvdmUgaW5mb3JtYXRpb24gaXMgc2hvd2luZyB0aGUgZGlzY3JpcHRpdmUgYW5hbHlzaXMgQWdlIHZhcmlhYmxlIGFuZCBpdHMgc3RhbmRhcmQgZGV2aWF0aW9uLjxoNT4NCmBgYHtyfQ0KI3N1bW1hcnkgb2YgQW5udWFsIGluY29tZQ0Kc3VtbWFyeShjdXN0b21lcl9kYXRhJEFubnVhbC5JbmNvbWUuLmsuLikNCiNzdGFuZGFyZCBkZXZpYXRpb24gb2YgQW5udWFsIGluY29tZQ0KcHJpbnQocGFzdGUoInN0YW5kYXJkIGRldmlhdGlvbiIsc2QoY3VzdG9tZXJfZGF0YSRBbm51YWwuSW5jb21lLi5rLi4pKSkNCmBgYA0KPGg1PlRoZSBhYm92ZSBpbmZvcm1hdGlvbiBpcyBzaG93aW5nIHRoZSBkaXNjcmlwdGl2ZSBhbmFseXNpcyBBbm51YWwgaW5jb21lIHZhcmlhYmxlIGFuZCBpdHMgc3RhbmRhcmQgZGV2aWF0aW9uLjxoNT4NCmBgYHtyfQ0KI3N1bW1hcnkgb2YgU3BlbmRpbmcgc2NvcmUNCnN1bW1hcnkoY3VzdG9tZXJfZGF0YSRTcGVuZGluZy5TY29yZS4uMS4xMDAuKQ0KI3N0YW5kYXJkIGRldmlhdGlvbiBvZiBTcGVuZGluZyBzY29yZQ0KcHJpbnQocGFzdGUoInN0YW5kYXJkIGRldmlhdGlvbiIsc2QoY3VzdG9tZXJfZGF0YSRTcGVuZGluZy5TY29yZS4uMS4xMDAuKSkpDQpgYGANCjxoNT5UaGUgYWJvdmUgaW5mb3JtYXRpb24gaXMgc2hvd2luZyB0aGUgZGlzY3JpcHRpdmUgYW5hbHlzaXMgQWdlIHZhcmlhYmxlIGFuZCBpdHMgc3RhbmRhcmQgZGV2aWF0aW9uLjxoNT4NCjxoMz5WaXN1YWxpc2F0aW9uIG9mIERhdGE8aDM+DQpgYGB7cn0NCmxpYnJhcnkoZ2dwbG90MikNCmdncGxvdChjdXN0b21lcl9kYXRhLGFlcyhHZW5kZXIsZmlsbD1HZW5kZXIpKStnZW9tX2JhcigpK3NjYWxlX2ZpbGxfdmlyaWRpc19kKCkrdGhlbWVfbGlnaHQoKStnZ3RpdGxlKCJCYXJwbG90IGZvciBHZW5kZXIgIikNCmBgYA0KPGg1PkZyb20gdGhlIGFib3ZlIHBsb3QgLFdlIHVuZGVyc3RhbmQgdGhhdCBudW1iZXIgb2YgZmVtYWxlcyBhcmUgbW9yZSB0aGFuIG1hbGVzIGJ1dCB0byBrbm93IHRoZSBwcm9wb3J0aW9uIG9mIGZlbWFsZXMgYW5kIG1hbGVzIGluIHRoZSBkYXRhIHdlIGNhbiBkbyB2aXN1YWxpc2F0aW9uIHVzaW5nIHBpZSBjaGFydC48aDU+DQoNCmBgYHtyfQ0KbGlicmFyeShwbG90cml4KQ0KcGVyPXJvdW5kKHQvc3VtKHQpKjEwMCkNCmxicz1wYXN0ZShjKCJGZW1hbGUiLCJNYWxlIiksIiAiLHBlciwiJSIsc2VwPSIgIikNCnBpZTNEKHQsbGFiZWxzPWxicywNCiAgIG1haW49IlBpZSBDaGFydCBEZXBpY3RpbmcgUmF0aW8gb2YgRmVtYWxlIGFuZCBNYWxlIikNCmBgYA0KPGg1PkZyb20gdGhlIGFib3ZlIGdyYXBoLCB3ZSBjb25jbHVkZSB0aGF0IHRoZSBwcm9wb3J0aW9uIG9mIGZlbWFsZXMgaXMgNTYlLCB3aGVyZWFzIHRoZSBwcm9wb3J0aW9uIG9mIG1hbGUgaW4gdGhlIGN1c3RvbWVyIGRhdGFzZXQgaXMgNDQlLjxoNT4NCg0KPGg1Pk5vdyB0byBwbG90IHRoZSBmcmVxdWVuY3kgb2YgdGhlIGFnZSBvZiB0aGUgY3VzdG9tZXJzLFdlIHdpbGwgZG8gaGlzdG9ncmFtIHBsb3QuPGg1Pg0KYGBge3J9DQojYmFycGxvdCBmb3IgQWdlDQpnZ3Bsb3QoY3VzdG9tZXJfZGF0YSxhZXMoQWdlKSkrZ2VvbV9iYXIoZmlsbD0iZ3JlZW4iLGNvbD0icHVycGxlIixhbHBoYT0wLjUpK3RoZW1lX2RhcmsoKStnZ3RpdGxlKCJBZ2UgRGlzdHJpYnV0aW9uICIpDQpgYGANCjxoNT5UaGUgYWJvdmUgZ3JhcGggc2hvd3MgdGhlIGRpc3RyaWJ1dGlvbiBvZiBBZ2UgdmFyaWFibGUgdGhyb3VnaG91dCB0aGUgZGF0YS48aDU+DQoNCjxoNT5JdCBpcyB2ZXJ5IGNsZWFyIGZyb20gdGhlIGFib3ZlIGdyYXBoIHRoYXQgcGVvcGxlIG9mIEFnZSBiZXR3ZWVuIDMwIHRvIDMyIGFyZSBtb3JlIGluIGNvdW50IGluIHRoZSBkYXRhLjxoNT4NCmBgYHtyfQ0KI0Rpc2NyaXB0aXZlIGJveHBsb3QgZm9yIEFnZSBhbmQgYW5udWFsIGluY29tZQ0KZ2dwbG90KGN1c3RvbWVyX2RhdGEsYWVzKEFnZSxBbm51YWwuSW5jb21lLi5rLi4sZ3JvdXA9R2VuZGVyLGZpbGw9R2VuZGVyKSkrZ2VvbV9ib3hwbG90KCkrZ2d0aXRsZSgiRGlzY3JpcHRpdmUgYm94cGxvdCBmb3IgQWdlIGFuZCBhbm51YWwgaW5jb21lIikNCmBgYA0KPGg1PkZyb20gdGhlIGFib3ZlIHBsb3Q6DQpXZSBjb25jbHVkZSB0aGF0IHRoZSAgY3VzdG9tZXIgIG9mIEFnZSAgYmV0d2VlbiAyNSBhbmQgNDIgYXJlIG1vcmUgaW4gY291bnQgIHRoZSBtaW5pbXVtIGFnZSBvZiBjdXN0b21lcnMgaXMgMTgsIHdoZXJlYXMsIHRoZSBtYXhpbXVtIGFnZSBpcyA3MC48aDU+DQoNCg0KPGg1Pk5vdyBkb2luZyB2aXN1YWxpemF0aW9uIHRvIGdldCBpbnNpZ2h0cyB0aGUgYW5udWFsIGluY29tZS48aDU+DQpgYGB7cixmaWcuaGVpZ2h0PTcsZmlnLndpZHRoPTh9DQpoaXN0KGN1c3RvbWVyX2RhdGEkQW5udWFsLkluY29tZS4uay4uLA0KICBjb2w9IiM2NjAwMzMiLA0KICBtYWluPSJIaXN0b2dyYW0gZm9yIEFubnVhbCBJbmNvbWUiLA0KICB4bGFiPSJBbm51YWwgSW5jb21lIENsYXNzIiwNCiAgeWxhYj0iRnJlcXVlbmN5IiwNCiAgbGFiZWxzPVRSVUUpDQoNCmBgYA0KPGg1PkZyb20gdGhlIGFib3ZlIGRhdGEsd2UgY29uY2x1ZGUgdGhhdCBtb3N0IG9mIHRoZSBjdXN0b21lcidzIGFubnVhbCBpbmNvbWUgYXJlIHVuZGVyIDcwIHRvIDgway4NCg0KTm93IHRoZSBkZW5zaXR5IHBsb3Qgb2YgY3VzdG9tZXIncyBhbm51YWwgaW5jb21lIGRhdGEuPGg1Pg0KYGBge3J9DQpnZ3Bsb3QoY3VzdG9tZXJfZGF0YSxhZXMoQW5udWFsLkluY29tZS4uay4uLGZpbGw9R2VuZGVyKSkrZ2VvbV9kZW5zaXR5KGFscGhhPTAuNSxjb2w9ImxpZ2h0Ymx1ZSIpK3NjYWxlX2ZpbGxfbWFudWFsKHZhbHVlcyA9IGMoInJlZCIsImJsdWUiKSkrdGhlbWVfbWluaW1hbCgpK2dndGl0bGUoIkRlbnNpdHkgcGxvdCBmb3IgQW5udWFsIGluY29tZSIpDQpgYGANCjxoNT5UaGUgYWJvdmUgZGVuc2l0eSBwbG90IGV4cGxhaW5zIHRoZSBkaXN0cmlidXRpb24gYW5udWFsIGluY29tZSBmb3IgZmVtYWxlIGFuZCBtYWxlIHNlcGFyYXRlbHkgYW5kIGl0IGlzIGNsZWFyIHRoYXQgYW5udWFsIGluY29tZSBpcyBpcnJlc3BlY3RpdmUgb2YgdGhlIEdlbmRlci48aDU+DQoNCjxoNT5Ob3cgdG8gbWFrZSB0aGUgdW5kZXJzdGFuZGluZyBhYm91dCBzcGVuZGluZyBzY29yZSBncmFwaGljYWxseSAsV2Ugd2lsbCBkbyBzb21lIHZpc3VhbGlzYXRpb24uPGg1Pg0KDQpgYGB7cn0NCmJveHBsb3QoY3VzdG9tZXJfZGF0YSRTcGVuZGluZy5TY29yZS4uMS4xMDAuLA0KICAgaG9yaXpvbnRhbD1UUlVFLA0KICAgY29sPSIjOTkwMDAwIiwNCiAgIG1haW49IkJveFBsb3QgZm9yIERlc2NyaXB0aXZlIEFuYWx5c2lzIG9mIFNwZW5kaW5nIFNjb3JlIikNCg0KDQpgYGANCjxoNT5UaGUgYWJvdmUgYm94cGxvdCB3ZSBjYW4gY29uY2x1ZGUgIHRoYXQgbWF4aW11bSBhbmQgbWluaW11bSBzcGVuZGluZyBzY29yZXMgYXJlIDEgYW5kIDk5IHJlc3BlY3RpdmVseSxhbmQgYXZlcmFnZSBzcGVuZGluZyBzY29yZSBpcyBuZWFyIDUwICg1MC4yMCBleGFjdCBmcm9tIHRoZSBzdW1tYXJ5ICkuDQoNCkJhcnBsb3QgZm9yIFNwZW5kaW5nIHNjb3JlPGg1Pg0KYGBge3IsZmlnLmhlaWdodD04LGZpZy53aWR0aD04fQ0KaGlzdChjdXN0b21lcl9kYXRhJFNwZW5kaW5nLlNjb3JlLi4xLjEwMC4sDQogICAgbWFpbj0iSGlzdG9HcmFtIGZvciBTcGVuZGluZyBTY29yZSIsDQogICAgeGxhYj0iU3BlbmRpbmcgU2NvcmUgQ2xhc3MiLA0KICAgIHlsYWI9IkZyZXF1ZW5jeSIsDQogICAgY29sPSIjNjYwMGNjIiwNCiAgICBsYWJlbHM9VFJVRSkNCmBgYA0KPGg1PkZyb20gdGhlIGFib3ZlIHBsb3Qgd2UgY2FuIGNsZWFybHkgc2VlIHRoYXQgbW9zdCBvZiB0aGUgY3VzdG9tZXJzIGFyZSBoYXZpbmcgc3BlbmRpbmcgc2NvcmVzIGluIGJldHdlZW4gNDAtNTAuPGg1Pg0KDQo8aDM+Sy1tZWFucyBtb2RlbGluZzo8aDM+DQo8aDU+Rm9yIGN1c3RvbWVyIHNlZ2VtZW50YXRpb24gd2UgYXJlIHVzaW5nIGstbWVhbnMgYWxnb3JpdGhtIGFuZCBkZXZpZGluZyB0aGUgY3VzdG9tZXIgZGF0YSBpbnRvIDIgZ3JvdXBzLjxoNT4NCmBgYHtyfQ0KazI8LWttZWFucyhjdXN0b21lcl9kYXRhWywzOjVdLDIsaXRlci5tYXg9MTAwLG5zdGFydD01MCxhbGdvcml0aG09Ikxsb3lkIikNCmsyDQpgYGANCjxoNT5UaGUgYWJvdmUgaW5mb3JtYXRpb24gc2hvd3MgdGhlIGdyb3VwaW5nIG9mIHRoZSBjdXN0b21lcl9kYXRhIGFuZCB0aGUgbWVhbiB2YWx1ZXMgc2V0IGZvciB0aGUgY2x1c3RlcnMsIGJ1dCB0aGUgcmF0aW8gb2YgYmV0d2Vlbl9zcyBhbmQgdG90YWxfc3Moc3M9c3VtIG9mIHNxdWFyZXMpIGlzIHZlcnkgbGVzcyAsc28gd2UgbmVlZCB0byBpbXByb3ZlIHRoZSBxdWFsaXR5IG9mIGstbWVhbnMgbW9kZWwNCg0KRm9yIHRoZSBiZXR0ZXIgcXVhbGl0eSBrLW1lYW5zIG1vZGVsIHdlIG5lZWQgdG8gZmluZCB0aGUgb3B0aW11bSB2YWx1ZSBmb3IgayhpLmUgbnVtYmVyIG9mIGNsdXN0ZXJzKS48aDU+DQoNCjxoMz5BdmVyYWdlIFNpbGhvdWV0dGUgTWV0aG9kOjxoMz4NCg0KPGg1PlVzaW5nIHRoaXMgbWV0aG9kIHdlIGNhbiBkZXRlcm1pbmUgdGhlIG9wdGltdW0gdmFsdWUgb2Ygay4NCg0KSW4gc2lsaG91ZXR0ZSBtZXRob2QsVGhlIGF2ZXJhZ2Ugc2lsX3dpZHRoIGlzIHRoZSBtZWFzdXJlIG9mIHF1YWxpdHkgb2YgY2x1c3Rlci5UaGUgaGlnaGVyIHRoZSB2YWx1ZSBvZiBhdmVyYWdlIHNpbF93aWR0aCAsIHRoZSBiZXR0ZXIgdGhlIHF1YWxpdHkgIHdpdGhpbiB0aGUgaW50cmEtY2x1c3RlcnMuPGg1Pg0KDQpgYGB7cn0NCmxpYnJhcnkoY2x1c3RlcikNCmxpYnJhcnkoZ3JpZEV4dHJhKQ0KbGlicmFyeShncmlkKQ0KbGlicmFyeShkcGx5cikNCmxpYnJhcnkodGlkeXZlcnNlKQ0KbGlicmFyeShwdXJycikNCnNpbD1mdW5jdGlvbihrKXsNCiAgazI8LWttZWFucyhjdXN0b21lcl9kYXRhWywzOjVdLGssaXRlci5tYXg9MTAwLG5zdGFydD01MCxhbGdvcml0aG09Ikxsb3lkIikNCiBzMjwtc2lsaG91ZXR0ZShrMiRjbHVzdGVyLGRpc3QoY3VzdG9tZXJfZGF0YVssMzo1XSwiZXVjbGlkZWFuIikpDQogY29sTWVhbnMoczIpWzNdDQp9DQpzaWwoMikNCg0KDQpgYGANCjxoNT5UaGUgYWJvdmUgaXMgdGhlIG1lYW4gdmFsdWUgb2Ygc2lsX3dpZHRoIGZvciBrPTIuDQoNClNvIG5vdyBpbiBvcmRlciB0byBkZXRlcm1pbmUgdGhlIG9wdGltdW0gdmFsdWUgb2YgayBmb3IgY2x1c3RlcmluZyB3ZSBoYXZlIHRvIGNoZWNrIHRoZSBhdmVyYWdlIHNpbF93aWR0aCBmb3IgZGlmZmVyZW50IHZhbHVlcyBvZiBrIHRoZW4gc2VsZWN0IHRoZSBtYXggdmFsdWUgb2YgYXZlcmFnZSBzaWxfd2lkdGggYW5kIHRoZSBrIGNvcnJlc3BvbmRzIHRvIHRoYXQgd2lsbCBiZSB0aGUgb3B0aW11bSB2YWx1ZS4NCg0KU28gd2Ugd2lsbCBtYWtlIGEgZnVuY3Rpb24gd2hpY2ggd2lsbCBpdGVyYXRlIHRoZSBhdmVyYWdlIHNpbF93aWR0aCBmb3IgZGlmZmVyZW50IHZhbHVlICBvZiBrIGFuZCByZXR1cm5zIHRoZSB2YWx1ZSBvZiBrIHdpdGggbWF4IHNpbF93aWR0aC48aDU+DQpgYGB7cn0NCm9wX3NpbF9rPWZ1bmN0aW9uKGRhdGEpew0KICANCiAgczM9bWFwX2RibCgyOjEwLHNpbCkNCiAgczU9YXMubWF0cml4KHMzKQ0KICBmb3IoaW5kZXggaW4gMTpuY29sKHM1KSl7DQogIHM2PXdoaWNoKHM1PT1tYXgoczUpKQ0KICBzOD1zNisxDQogIH0NCiAgcHJpbnQocGFzdGUoIm9wdGltdW0gdmFsdWUgb2YgayA9ICIsczgpKQ0KfQ0Kb3Bfc2lsX2soKQ0KYGBgDQo8aDU+SGVyZSB3ZSBnb3QgNiBhcyB0aGUgb3B0aW11bSB2YWx1ZSBmb3Igay4NCg0KTm93IHdlIGNhbiBhbHNvIGNvbmZpcm0gdGhlIG9wdGltdW0gdmFsdWUgb2YgayBieSB2aXN1YWxpc2F0aW9uLjxoNT4NCmBgYHtyfQ0KaW5zdGFsbC5wYWNrYWdlcygiTmJDbHVzdCIpDQpsaWJyYXJ5KE5iQ2x1c3QpDQpsaWJyYXJ5KGZhY3RvZXh0cmEpDQpmdml6X25iY2x1c3QoY3VzdG9tZXJfZGF0YVssMzo1XSwga21lYW5zLCBtZXRob2QgPSAic2lsaG91ZXR0ZSIpDQoNCmBgYA0KPGg1PkhlbmNlIGZyb20gdGhlIGFib3ZlIHBsb3Qgd2UgY2FuIHNlZSB0aGUgaGlnaGVzdCB2YWx1ZSBvZiBhdmVyYWdlIHNpbF93aWR0aCBjb3JyZXNwb25kcyB0byBrPTYuDQoNCk5vdyB3ZSBjYW4gbWFrZSBhIG1vZGVsIHVzaW5nIGs9NiBmb3IgYmV0dGVyIHF1YWxpdHkuPGg1Pg0KYGBge3J9DQprNjwta21lYW5zKGN1c3RvbWVyX2RhdGFbLDM6NV0sNixpdGVyLm1heD0xMDAsbnN0YXJ0PTUwLGFsZ29yaXRobT0iTGxveWQiKQ0KazYNCmBgYA0KPGg1PldlIGhhdmUgZ3JvdXBlZCB0aGUgZGF0YSBpbnRvIDYgY2x1c3RlcnMgd2l0aCBiZXR0ZXIgcXVhbGl0eSBhcyB3ZSBnb3QgIHRoZSByYXRpbyBvZiBiZXR3ZWVuIHNzIGFuZCB0b3RhbCBzcyhzcz1zdW0gb2RmIHNxdWFyZXMpIGluY3JlYXNlIHRvIDgxJS4NCg0KVmlzdWFsaXNpbmcgdGhlIGNsdXN0ZXIgcmVzdWx0IHVzaW5nIGdncGxvdDI8aDU+DQpgYGB7cn0NCmxpYnJhcnkoZ2dwbG90MikNCnNldC5zZWVkKDEpDQpnZ3Bsb3QoY3VzdG9tZXJfZGF0YSwgYWVzKHggPUFubnVhbC5JbmNvbWUuLmsuLiwgeSA9IFNwZW5kaW5nLlNjb3JlLi4xLjEwMC4pKSArIA0KICBnZW9tX3BvaW50KHN0YXQgPSAiaWRlbnRpdHkiLCBhZXMoY29sb3IgPSBhcy5mYWN0b3IoazYkY2x1c3RlcikpLHNpemU9NCxhbHBoYT0wLjYpICsNCiAgc2NhbGVfY29sb3JfZGlzY3JldGUobmFtZT0iICIsDQogICAgICAgICAgICAgIGJyZWFrcz1jKCIxIiwgIjIiLCAiMyIsICI0IiwgIjUiLCI2IiksDQogICAgICAgICAgICAgIGxhYmVscz1jKCJDbHVzdGVyIDEiLCAiQ2x1c3RlciAyIiwgIkNsdXN0ZXIgMyIsICJDbHVzdGVyIDQiLCAiQ2x1c3RlciA1IiwiQ2x1c3RlciA2IikpICsNCiAgZ2d0aXRsZSgiU2VnbWVudHMgb2YgTWFsbCBDdXN0b21lcnMiLCBzdWJ0aXRsZSA9ICJVc2luZyBLLW1lYW5zIENsdXN0ZXJpbmciKSt0aGVtZSgNCiAgcGFuZWwuYmFja2dyb3VuZCA9IGVsZW1lbnRfcmVjdChmaWxsID0gImxpZ2h0Ymx1ZSIsIGNvbG91ciA9ICJibGFjayIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHNpemUgPSAyLCBsaW5ldHlwZSA9ICJzb2xpZCIpLHBsb3QuYmFja2dyb3VuZCA9IGVsZW1lbnRfcmVjdChmaWxsID0gIiM2RDlFQzEiKSkNCmBgYA0KPGg1PkhlbmNlIGZyb20gdGhlIGFib3ZlIGdyYXBoIHdlIGNhbWUgdG8ga25vdyB0aGF0Og0KDQpDbHVzdGVyNiAtIGhhdmluZyBoaWdoIGFubnVhbCBpbmNvbWUgYW5kIGhpZ2ggc3BlbmRpbmcgc2NvcmUuDQoNCkNsdXN0ZXI1IC0gaGF2aW5nIGhpZ2ggYW5udWFsIGluY29tZSBidXQgbG93IHNwZW5kaW5nIHNjb3JlLg0KDQpDbHVzdGVyNCAmIENsdXN0ZXIxLSBoYXZpbmcgbWVkaXVtIGFubnVhbCBpbmNvbWUgYW5kIG1lZGl1bSBzcGVuZGluZyBzY29yZS4NCg0KQ2x1c3RlcjMgLSBoYXZpbmcgbG93IGFubnVhbCBpbmNvbWUgYW5kIGxvdyBzcGVuZGluZyBzY29yZS48aDU+DQoNCg0KDQo8aDQ+VmlzdWFsaXNhdGlvbiBmb3IgQWdlIGFuZCBBbm51YWwgaW5jb21lLjxoNT4NCmBgYHtyfQ0KZ2dwbG90KGN1c3RvbWVyX2RhdGEsIGFlcyh4ID1Bbm51YWwuSW5jb21lLi5rLi4sIHkgPUFnZSkpICsgDQogIGdlb21fcG9pbnQoc3RhdCA9ICJpZGVudGl0eSIsIGFlcyhjb2xvciA9IGFzLmZhY3RvcihrNiRjbHVzdGVyKSksc2l6ZT00LGFscGhhPTAuNikgKw0KICBzY2FsZV9jb2xvcl9kaXNjcmV0ZShuYW1lPSIgIiwNCiAgICAgICAgICAgICAgICAgICAgICBicmVha3M9YygiMSIsICIyIiwgIjMiLCAiNCIsICI1IiwiNiIpLA0KICAgICAgICAgICAgICAgICAgICAgIGxhYmVscz1jKCJDbHVzdGVyIDEiLCAiQ2x1c3RlciAyIiwgIkNsdXN0ZXIgMyIsICJDbHVzdGVyIDQiLCAiQ2x1c3RlciA1IiwiQ2x1c3RlciA2IikpICsNCiAgZ2d0aXRsZSgiU2VnbWVudHMgb2YgTWFsbCBDdXN0b21lcnMiLCBzdWJ0aXRsZSA9ICJVc2luZyBLLW1lYW5zIENsdXN0ZXJpbmciKQ0KYGBgDQo8aDU+SGVuY2UgZnJvbSB0aGUgYWJvdmUgZ3JhcGggd2UgY2FtZSB0byBrbm93IHRoYXQ6DQoNCkNsdXN0ZXI2IC0gaGF2aW5nIGN1c3RvbWVyIG9mIGFnZSBncm91cCAzMC00MCBhbmQgaGlnaCBhbm51YWwgLg0KDQpDbHVzdGVyNSAtIGhhdmluZyBjdXN0b21lciBvZiBhZ2UgZ3JvdXAgNDAtNjAgYnV0IGhpZ2ggYW5udWFsIGluY29tZS4NCg0KQ2x1c3RlcjQgLSBoYXZpbmcgY3VzdG9tZXIgb2YgYWdlIGdyb3VwIDIwLTQwICBhbmQgbWVkaXVtIGFubnVhbCBpbmNvbWUuDQoNCkNsdXN0ZXIzIC0gaGF2aW5nIGN1c3RvbWVyIG9mIGFnZSBncm91cCA0MC02MCBhbmQgbG93IGFubnVhbCBpbmNvbWUuDQoNCkNsdXN0ZXIyIC0gaGF2aW5nIGN1c3RvbWVyIG9mIGFnZSBncm91cCAyMC00MCBidXQgbG93IGFubnVhbCBpbmNvbWUuDQoNCkNsdXN0ZXIxIC0gaGF2aW5nIGN1c3RvbWVyIG9mIGFnZSBncm91cCA0MC02MCBhbmQgbWVkaXVtIGFubnVhbCBpbmNvbWUuDQoNCjxoND5WaXN1YWxpemluZyBjbHVzdGVyIHVzaW5nIGZ2aXogZnVuY3Rpb24uPGg1Pg0KYGBge3J9DQpmdml6X2NsdXN0ZXIoazYsIGRhdGEgPSBjdXN0b21lcl9kYXRhWywzOjVdLGdlb20gPSAicG9pbnRzIikNCmBgYA0KPGg1PkhlbmNlIHdpdGggdGhlIGhlbHAgb2YgdGhlIGNsdXN0ZXJpbmcgbWV0aG9kIHdlIGNhbiBkaXNlY3Qvc2VnZW1lbnRpc2UgdGhlIGN1c3RvbWVycyBmb3IgYmV0dGVyIGFwcHJvYWNoLjxoNT4NCg0K