Purpose

This analysis groups survey respondents into three segments based on similarities in their responses, following the supplied classroom tutorial.

Import and review the data

mydata <- read.csv("customer_segmentation.csv",
                   check.names = FALSE)

names(mydata) <- trimws(names(mydata))

cat("Number of respondents:", nrow(mydata), "\n")
## Number of respondents: 22
cat("Number of columns:", ncol(mydata), "\n")
## Number of columns: 15
knitr::kable(head(mydata))
ID CS_helpful Recommend Come_again All_Products Profesionalism Limitation Online_grocery delivery Pick_up Find_items other_shops Gender Age Education
1 2 2 2 2 2 2 2 3 4 1 2 1 2 2
2 1 2 1 1 1 1 2 3 3 1 2 1 2 2
3 2 1 1 1 1 2 3 3 2 1 3 1 2 2
4 3 3 2 4 1 2 3 3 2 2 2 1 3 5
5 2 1 3 5 2 1 2 3 1 2 3 2 4 2
6 1 1 3 2 1 1 1 2 1 1 4 1 2 5
summary(mydata)
##        ID          CS_helpful      Recommend       Come_again   
##  Min.   : 1.00   Min.   :1.000   Min.   :1.000   Min.   :1.000  
##  1st Qu.: 6.25   1st Qu.:1.000   1st Qu.:1.000   1st Qu.:1.000  
##  Median :11.50   Median :1.000   Median :1.000   Median :1.000  
##  Mean   :11.50   Mean   :1.591   Mean   :1.318   Mean   :1.455  
##  3rd Qu.:16.75   3rd Qu.:2.000   3rd Qu.:1.000   3rd Qu.:2.000  
##  Max.   :22.00   Max.   :3.000   Max.   :3.000   Max.   :3.000  
##   All_Products   Profesionalism    Limitation  Online_grocery     delivery    
##  Min.   :1.000   Min.   :1.000   Min.   :1.0   Min.   :1.000   Min.   :1.000  
##  1st Qu.:1.250   1st Qu.:1.000   1st Qu.:1.0   1st Qu.:2.000   1st Qu.:2.000  
##  Median :2.000   Median :1.000   Median :1.0   Median :2.000   Median :3.000  
##  Mean   :2.091   Mean   :1.409   Mean   :1.5   Mean   :2.273   Mean   :2.409  
##  3rd Qu.:2.000   3rd Qu.:2.000   3rd Qu.:2.0   3rd Qu.:3.000   3rd Qu.:3.000  
##  Max.   :5.000   Max.   :3.000   Max.   :4.0   Max.   :3.000   Max.   :3.000  
##     Pick_up        Find_items     other_shops        Gender     
##  Min.   :1.000   Min.   :1.000   Min.   :1.000   Min.   :1.000  
##  1st Qu.:2.000   1st Qu.:1.000   1st Qu.:1.250   1st Qu.:1.000  
##  Median :2.000   Median :1.000   Median :2.000   Median :1.000  
##  Mean   :2.455   Mean   :1.455   Mean   :2.591   Mean   :1.273  
##  3rd Qu.:3.000   3rd Qu.:2.000   3rd Qu.:3.750   3rd Qu.:1.750  
##  Max.   :5.000   Max.   :3.000   Max.   :5.000   Max.   :2.000  
##       Age          Education    
##  Min.   :2.000   Min.   :1.000  
##  1st Qu.:2.000   1st Qu.:2.000  
##  Median :2.000   Median :2.500  
##  Mean   :2.455   Mean   :3.182  
##  3rd Qu.:3.000   3rd Qu.:5.000  
##  Max.   :4.000   Max.   :5.000

Prepare the data

The ID column identifies respondents and is excluded from clustering. Following the tutorial, all remaining numeric variables are standardized to a mean of zero and a standard deviation of one.

survey <- mydata[, names(mydata) != "ID", drop = FALSE]

stopifnot(
  all(vapply(survey, is.numeric, logical(1))),
  !anyNA(survey),
  all(vapply(survey, sd, numeric(1)) > 0)
)

scaled_data <- scale(survey)

Hierarchical cluster analysis

The supplied tutorial calculates Euclidean distances between standardized responses and then calculates distances between the resulting distance profiles. This report follows that procedure using complete linkage and divides the tree into three clusters.

response_distances <- dist(scaled_data)
profile_distances <- dist(as.matrix(response_distances))

seg.hclust <- hclust(profile_distances, method = "complete")

plot(seg.hclust,
     labels = mydata$ID,
     main = "Customer Segmentation Dendrogram",
     xlab = "Respondent ID",
     sub = "",
     ylab = "Cluster merge height")

rect.hclust(seg.hclust, k = 3, border = 2:4)

groups.3 <- cutree(seg.hclust, k = 3)

Number of respondents in each cluster

cluster_sizes <- as.data.frame(table(Cluster = groups.3))
names(cluster_sizes)[2] <- "Respondents"

knitr::kable(cluster_sizes)
Cluster Respondents
1 17
2 2
3 3

Cluster membership

membership <- data.frame(
  ID = mydata$ID,
  Cluster = groups.3
)

knitr::kable(membership)
ID Cluster
1 1
2 1
3 1
4 2
5 3
6 1
7 1
8 1
9 1
10 1
11 1
12 1
13 1
14 1
15 1
16 1
17 1
18 1
19 3
20 3
21 1
22 2

Compare cluster means

cluster_means <- aggregate(
  survey,
  by = list(Cluster = groups.3),
  FUN = mean
)

knitr::kable(cluster_means, digits = 2)
Cluster CS_helpful Recommend Come_again All_Products Profesionalism Limitation Online_grocery delivery Pick_up Find_items other_shops Gender Age Education
1 1.29 1.12 1.24 1.82 1.24 1.35 2.24 2.24 2.71 1.29 2.65 1.18 2.41 3.12
2 3.00 2.50 1.50 3.00 1.50 2.00 3.00 3.00 2.50 2.00 1.50 1.00 2.50 5.00
3 2.33 1.67 2.67 3.00 2.33 2.00 2.00 3.00 1.00 2.00 3.00 2.00 2.67 2.33

Compare cluster medians

cluster_medians <- aggregate(
  survey,
  by = list(Cluster = groups.3),
  FUN = median
)

knitr::kable(cluster_medians, digits = 2)
Cluster CS_helpful Recommend Come_again All_Products Profesionalism Limitation Online_grocery delivery Pick_up Find_items other_shops Gender Age Education
1 1 1.0 1.0 2 1.0 1 2 2 3.0 1 2.0 1 2.0 2
2 3 2.5 1.5 3 1.5 2 3 3 2.5 2 1.5 1 2.5 5
3 2 1.0 3.0 3 2.0 1 2 3 1.0 2 3.0 2 2.0 2

Interpretation considerations

Cluster means and medians help identify differences in survey response patterns. Medians can be useful for ordered survey responses because they are less affected by extreme values.

The survey coding guide is needed before interpreting higher scores as greater satisfaction or assigning meanings to demographic codes. Means of demographic codes are not actual ages, education levels, or meaningful averages for nominal categories.

This is a small classroom sample. The three-cluster solution is specified by the tutorial and has not been established as the optimal number of customer segments.

Export results