This analysis groups survey respondents into three segments based on similarities in their responses, following the supplied classroom tutorial.
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
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)
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)
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 |
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 |
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 |
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 |
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.