Purpose

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.

Import the survey data

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

Standardize the responses

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)

Create the dendrogram

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))

Form three clusters

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

Identify the respondents in each cluster

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

Compare cluster summaries

Summaries including ID

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")
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")
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

Summaries excluding ID

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")
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")
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

Export the results

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.

Discussion answers

1. Cluster sizes

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.

2. Why compare means and medians?

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.

3. Mean or median?

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.

4. Customer profiles and possible targeting

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.

Selected mean response codes by cluster
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.

5. Hierarchical clustering compared with K-means

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.

6. Careers connected with cluster analysis

In the visible Indeed search results for “cluster analysis,” I identified six distinct analytics-related job titles:

  1. Market Research & Analysis Manager, iPhone - Apple
  2. Research Scientist, Demography and Survey Sciences - Meta
  3. Analytic Scientist II - FICO
  4. Associate / Associate Consultant - Analytics - Blue Matter
  5. Operations Research Analyst - Saitech Inc
  6. Research Data Manager - California Department of Finance

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.

Sources