This analysis groups survey respondents according to their response patterns. The goal is to identify differences that could help a business understand its customers and develop more relevant marketing strategies.
The code uses an uploaded course CSV when available. Otherwise, it tries the dataset linked in the instructor’s tutorial.
possible_files <- c(
"customer_segmentation.csv",
"customer_segmentation (1).csv",
"customer_segmetation.csv"
)
available_files <- possible_files[file.exists(possible_files)]
data_source <- if (length(available_files) > 0) {
available_files[1]
} else {
paste0(
"https://raw.githubusercontent.com/",
"utjimmyx/regression/master/customer_segmentation.csv"
)
}
survey <- tryCatch(
read.csv(data_source, check.names = FALSE),
error = function(e) {
stop(
paste(
"The survey CSV could not be opened.",
"Upload the course file to this Posit Cloud project,",
"name it customer_segmentation.csv, and click Knit again."
),
call. = FALSE
)
}
)
names(survey) <- trimws(names(survey))
# The instructor's version uses Product_needed for this item.
if ("Product_needed" %in% names(survey) &&
!("All_Products" %in% names(survey))) {
names(survey)[names(survey) == "Product_needed"] <- "All_Products"
}
if (tolower(names(survey)[1]) != "id") {
stop("The first CSV column must contain respondent IDs.")
}
names(survey)[1] <- "ID"
if (nrow(survey) < 3 || anyDuplicated(survey$ID) ||
anyNA(survey$ID)) {
stop("The survey must have at least three respondents with unique IDs.")
}
items <- survey[, -1, drop = FALSE]
if (!all(vapply(items, is.numeric, logical(1)))) {
stop("All survey columns after ID must contain numeric response codes.")
}
if (anyNA(items) || !all(is.finite(as.matrix(items)))) {
stop("The survey contains missing or non-finite values. Check the CSV.")
}
if (any(vapply(items, sd, numeric(1)) == 0)) {
stop("A survey column has no variation and cannot be standardized.")
}
required_items <- c("CS_helpful", "Recommend", "Come_again", "Pick_up")
if (!all(required_items %in% names(items))) {
stop("Use the customer segmentation survey supplied for this lab.")
}
cat("Respondents:", nrow(survey), "\n")
## Respondents: 22
cat("Columns:", ncol(survey), "\n")
## Columns: 15
head(survey)
## ID CS_helpful Recommend Come_again All_Products Profesionalism Limitation Online_grocery delivery Pick_up
## 1 1 2 2 2 2 2 2 2 3 4
## 2 2 1 2 1 1 1 1 2 3 3
## 3 3 2 1 1 1 1 2 3 3 2
## 4 4 3 3 2 4 1 2 3 3 2
## 5 5 2 1 3 5 2 1 2 3 1
## 6 6 1 1 3 2 1 1 1 2 1
## Find_items other_shops Gender Age Education
## 1 1 2 1 2 2
## 2 1 2 1 2 2
## 3 1 3 1 2 2
## 4 2 2 1 3 5
## 5 2 3 2 4 2
## 6 1 4 1 2 5
The ID column identifies each person and is excluded from clustering. Standardization puts the coded survey variables on a comparable scale.
scaled_items <- scale(items)
This reproduces the tutorial’s two-stage distance calculation: it first measures distances between respondents, then compares their distance profiles. That second calculation is also present in the reference RPubs example. It differs from clustering the original respondent distances directly.
The exercise treats all non-ID codes as numeric. For a practical marketing study, nominal variables such as gender would need appropriate categorical treatment rather than assuming meaningful numeric distances.
respondent_distances <- stats::dist(scaled_items)
profile_distances <- stats::dist(
as.matrix(respondent_distances)
)
cluster_tree <- stats::hclust(
profile_distances,
method = "complete"
)
plot(
cluster_tree,
labels = survey$ID,
main = "Customer Segmentation Dendrogram",
xlab = "Respondent ID",
sub = "",
ylab = "Height"
)
rect.hclust(cluster_tree, k = 3, border = c(2, 3, 4))
Three groups are used to reproduce the lab example. This choice is not a statistical demonstration that three is the optimal number of segments.
membership <- cutree(cluster_tree, k = 3)
table(membership)
## membership
## 1 2 3
## 17 2 3
cluster_sizes <- data.frame(
Cluster = 1:3,
Respondents = as.integer(table(factor(membership, levels = 1:3)))
)
cluster_sizes$Percent <- round(
100 * cluster_sizes$Respondents / nrow(survey),
1
)
knitr::kable(cluster_sizes)
| Cluster | Respondents | Percent |
|---|---|---|
| 1 | 17 | 77.3 |
| 2 | 2 | 9.1 |
| 3 | 3 | 13.6 |
for (g in 1:3) {
cat("\nCluster", g, "respondent IDs:\n")
print(survey$ID[membership == g])
}
##
## Cluster 1 respondent IDs:
## [1] 1 2 3 6 7 8 9 10 11 12 13 14 15 16 17 18 21
##
## Cluster 2 respondent IDs:
## [1] 4 22
##
## Cluster 3 respondent IDs:
## [1] 5 19 20
These first two tables reproduce the lab’s comparison. An average or median ID is only an arithmetic result and has no marketing interpretation.
median_with_id <- aggregate(
survey, by = list(Cluster = membership), FUN = median
)
mean_with_id <- aggregate(
survey, by = list(Cluster = membership), FUN = mean
)
knitr::kable(median_with_id, digits = 2, caption = "Medians including ID")
| Cluster | ID | CS_helpful | Recommend | Come_again | All_Products | Profesionalism | Limitation | Online_grocery | delivery | Pick_up | Find_items | other_shops | Gender | Age | Education |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | 11 | 1 | 1.0 | 1.0 | 2 | 1.0 | 1 | 2 | 2 | 3.0 | 1 | 2.0 | 1 | 2.0 | 2 |
| 2 | 13 | 3 | 2.5 | 1.5 | 3 | 1.5 | 2 | 3 | 3 | 2.5 | 2 | 1.5 | 1 | 2.5 | 5 |
| 3 | 19 | 2 | 1.0 | 3.0 | 3 | 2.0 | 1 | 2 | 3 | 1.0 | 2 | 3.0 | 2 | 2.0 | 2 |
knitr::kable(mean_with_id, digits = 2, caption = "Means including ID")
| Cluster | ID | CS_helpful | Recommend | Come_again | All_Products | Profesionalism | Limitation | Online_grocery | delivery | Pick_up | Find_items | other_shops | Gender | Age | Education |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | 10.76 | 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 | 13.00 | 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 | 14.67 | 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(
items, by = list(Cluster = membership), FUN = median
)
cluster_means <- aggregate(
items, by = list(Cluster = membership), FUN = mean
)
knitr::kable(cluster_medians, digits = 2, caption = "Medians without ID")
| 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 |
knitr::kable(cluster_means, digits = 2, caption = "Means without ID")
| 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 |
respondent_clusters <- data.frame(
ID = survey$ID,
Cluster = membership
)
write.csv(
respondent_clusters,
"clusterID.csv",
row.names = FALSE
)
write.csv(
cluster_means,
"cluster_means.csv",
row.names = FALSE
)
Both CSV files are saved in the project folder and can be opened in Excel.
There are 22 respondents in this dataset. Cluster 1 contains 17 people (77.3%), Cluster 2 contains 2 people (9.1%), and Cluster 3 contains 3 people (13.6%).
Cluster 1 is the largest group in this sample. The smaller groups should be interpreted carefully because each person’s response has a large influence on the results. The sample proportions do not establish the proportions in the whole market.
A cluster label does not explain what makes its members similar. Comparing their responses helps identify which survey items separate the groups. For example, service and return-intention responses can suggest topics for follow-up research. The ID column should be excluded from these comparisons. Category codes also need their labels before they can be interpreted.
For individual ordered survey items, I would start with the median and response frequencies because the difference between adjacent response categories may not be equal. I would also examine the means as a descriptive comparison if treating the scale as equally spaced is reasonable. Means show smaller numerical differences, but they can change noticeably in tiny clusters. A mean of a nominal code, such as gender, is not a meaningful customer characteristic; category counts and percentages are better.
Useful summaries include the number of people in each group, its percentage of the sample, response distributions, and medians. Means and standard deviations can supplement these when the measurement scale supports them. Demographic categories should be described using counts and percentages.
| Cluster | CS_helpful | Recommend | Come_again | Pick_up |
|---|---|---|---|---|
| 1 | 1.29 | 1.12 | 1.24 | 2.71 |
| 2 | 3.00 | 2.50 | 1.50 | 2.50 |
| 3 | 2.33 | 1.67 | 2.67 | 1.00 |
Cluster 1 has the lowest average customer-service code, while Cluster 2 has the highest. The highest average return-intention code is in Cluster 3. These are differences in response codes; the survey’s answer labels are needed to decide which responses are favorable.
If lower codes represent more positive experiences, the group with lower service and recommendation codes could be considered for a small referral campaign. A group with less favorable service responses would be a priority for follow-up questions and service improvements. If the coding runs in the opposite direction, the interpretation would reverse. I would test these ideas with more customers before committing to a larger campaign.
K-means requires a chosen number of groups and repeatedly updates group centers to reduce within-group variation. Random starting centers can lead to different solutions, so multiple starts are useful. Hierarchical clustering instead builds a nested tree by combining groups, and the tree can then be cut into different numbers of clusters.
I prefer hierarchical clustering for this small classroom dataset because the dendrogram makes the grouping process easier to inspect. For a much larger numeric dataset, K-means can be more practical. Neither method automatically proves that the resulting groups are useful customer segments.
In the visible Indeed search results for “cluster analysis,” I identified six distinct analytics-related job titles:
This counts six examples reviewed, not all vacancies or all distinct titles on Indeed. Search results change over time. Apple’s listing specifically mentions segmentation and cluster analysis, which connects directly to this assignment.
Search reviewed November 7, 2026: Indeed search results.