Code
library(tidyverse)
library(palmerpenguins)
data(penguins)
# For demo only, take a sample of 50 penguins
set.seed(253)
penguins <- sample_n(penguins, 50) %>%
select(bill_length_mm, bill_depth_mm)Notes and in-class exercises
You can download the .qmd file for this activity here and open in R-studio. The rendered version is posted in the course website (Activities tab). I often experiment with the class activities (and see it in live!) and make updates, but I always post the final version before class starts. To be sure you have the most up-to-date copy, please download it once you’ve settled in before class begins.
GOALS
Suppose we have a set of feature variables \((x_1,x_2,...,x_p)\) but NO outcome variable \(y\).
Thus instead of our goal being to predict/classify/explain y, we might simply want to…
Techniques: hierarchical clustering & K-means clustering
K-means is the clustering technique behind Cartograph.info
can be computationally expensive: we have to calculate the distance between each pair of objects in our sample to start the algorithm
greedy algorithm: This algorithm makes the best local decision of which clusters to fuse at each step in the dendrogram. Once clusters are fused they remain fused throughout the dendrogram. However, meaningful clusters may not always be hierarchical or nested.
Split cases into \(K\) non-overlapping, homogeneous clusters or groups of cases noted as \(C_1, C_2,..., C_K\) which minimize the total within cluster variance: \[\sum_{k=1}^K W(C_k)\]
where each \(W(\cdot)\) measures the within-cluster variance of each cluster, \(C_k\):
\[W(C_k)= \frac{1}{ |C_k|} \sum_{\mathbf{x_i},\mathbf{x_j}\in C_k} ||\mathbf{x_i}-\mathbf{x_j}||^2\]
\[= \frac{1}{\text{no. of cases in }C_k} (\text{total Euclidean distance}^2 \text{ btwn all pairs in $C_k$})\]
Specifically, \(W(C_k)\) is the average squared distance between all pairs of cases in cluster \(C_k\).
Pick K
Appropriate values of K are context dependent and can be deduced from prior knowledge, data, or the results of hierarchical clustering. Try multiple!
Initialization
Randomly select the location of \(K\) centroids. Assign each case to the nearest centroid. This random composition defines the first set of clusters \(C_1\) through \(C_K\).
Centroid calculation
Calculate the centroid, ie. average location of the cases \(x\) in each \(C_i\).
Cluster assignment
Re-assign each case to the \(C_i\) with the nearest centroid.
Repeat!
Iterate between steps 2 and 3 until the clusters stabilize, ie. the cluster centroids do not change much from iteration to iteration.
Recall the hierarchical algorithm.
Revisit screenshots from the shiny app to explore the results of this algorithm. Why can the “greediness” of this algorithm sometimes produce strange results?
Clusters are hierarchical / nested. Once we combine two clusters, we can’t separate them.
Here’s the idea and more details are above:
Initialization
Centroid calculation
Calculate the centroid of each cluster.
Cluster assignment
Re-assign each data point to the cluster with the nearest centroid.
Repeat! Iterate!
Iterate between steps 2 and 3 until the clusters stabilize.
Naftali Harris made a really nice interactive app for playing around with this algorithm.
You’ll notice 3 natural clusters.
To understand how and if K-means clustering with K = 3 might detect these clusters, play around:
Challenge:
Let’s do K-means clustering in R using our penguin data:
We’ll cluster these penguins based on their bill lengths and depths:
# Run the K-means algorithm
set.seed(253)
kmeans_3_round_1 <- kmeans(scale(penguins), centers = 3)
# Plot the cluster assignments
penguins %>%
mutate(kmeans_cluster = as.factor(kmeans_3_round_1$cluster)) %>%
ggplot(aes(x = bill_length_mm, y = bill_depth_mm, color = kmeans_cluster)) +
geom_point(size = 3) +
theme(legend.position = "none") +
labs(title = "K-means with K = 3 (round 1)") +
theme_minimal()The initial centroids are randomly selected. K-means algorithm assigns data point to clusters using distances (we don’t want these assignments to be skewed by the scales of features x).
# Run the K-means algorithm using a seed of 8
set.seed(8)
kmeans_3_round_2 <- kmeans(scale(penguins), centers = 3)
# Plot the cluster assignments
penguins %>%
mutate(kmeans_cluster = as.factor(kmeans_3_round_2$cluster)) %>%
ggplot(aes(x = bill_length_mm, y = bill_depth_mm, color = kmeans_cluster)) +
geom_point(size = 3) +
theme(legend.position = "none") +
labs(title = "K-means with K = 3 (round 2)") +
theme_minimal()The 2 runs of the K-means started out with different initial centroids and this was enough to produce different results!
Make sure our results aren’t overly sensitive to / skewed by the random centroids we happened to start with.
To implement K-means clustering we must choose an appropriate K! Use the following example to explore the goldilocks challenge of picking K.
penguins_sub <- penguins %>%
select(bill_length_mm, bill_depth_mm) %>%
na.omit()
# Run K-means
set.seed(253)
k_2 <- kmeans(scale(penguins_sub), centers = 2)
k_20 <- kmeans(scale(penguins_sub), centers = 20)
# plot cluster assignments (K = 2)
penguins_sub %>%
mutate(cluster_2 = as.factor(k_2$cluster)) %>%
ggplot(aes(x = bill_length_mm, y = bill_depth_mm, color = cluster_2)) +
geom_point(size = 3) +
labs(title = "K = 2")When K is too small, we can end up with big and overly general clusters. When K is too big, we can end up with small clusters that are too local / lose the general patterns.
Calculate the total within-cluster sum of squares (SS) for each of the choices of \(K\) we explored in the previous question.
Then, repeat this calculation for all choices of \(K\) between 1 and 20. Comment on what patterns you notice here, and how these results complement your discussion of the goldilocks challenge of picking \(K\) from the previous question.
# calculate total within-cluster SS for K from 1 to 20
tibble(K = 1:20) %>%
mutate(SS = map(K, ~ kmeans(scale(penguins_sub), centers = .x)$tot.withinss)) %>%
unnest(cols = c(SS)) %>%
ggplot(aes(x = K, y = SS)) +
geom_point()As K gets bigger, the total within-cluster SS decreases.
Although smaller within-cluster SS is good in some ways, we probably don’t want to choose K based solely on this metric. Choosing the value of K with the smallest total SS would usually just yield the largest K we tried. But, we saw above that if K is too big we can end up with small clusters that are too local / lose the general patterns.
Sometimes, a strategy that people use is to choose a value of K where the change in total SS starts to level off: in this case, around K = 3 to 5.
Let’s compare and contrast the results of the hierarchical and K-means algorithms. To this end, let’s use both to identify 2 penguin clusters:
# Run hierarchical algorithm
hier_alg <- hclust(dist(scale(penguins)))
# Run K-means
set.seed(253)
kmeans_alg <- kmeans(scale(penguins), centers = 2)
# Include the clustering assignments
cluster_data <- penguins %>%
mutate(
hier_cluster = as.factor(cutree(hier_alg, 2)),
kmeans_cluster = as.factor(kmeans_alg$cluster))no
K-means is better. The hierarchical clusters are odd – it fused two penguins early on, and could not unfuse them later.
Name 1 pro and 2 drawbacks of the K-means algorithm.
Pro: not greedy
Cons: We have to pre-specify the number of clusters we’re interested in. We can get different results based on where we put the first centroids.
Choosing \(K\) - Average Silhouette
Goal: Choose \(K\) that maximizes the distance between clusters and minimizes distance within clusters
\[a(i) = \frac{1}{|C_I|} \sum_{j\in C_I, i\not=j} ||\mathbf{x}_i - \mathbf{x_j}||^2\] \[ = \text{ avg. within cluster variance or distance from point i}\]
\[b(i) = \min_{J\not=I}\frac{1}{|C_J|} \sum_{j\in C_J} ||\mathbf{x}_i - \mathbf{x_j}||^2\] \[=\text{ min avg. variance / distance from point i to points in another cluster}\]
Silhouette for point \(i\) (\(-1\leq s(i) \leq 1\)):
\[s(i) = \frac{b(i) - a(i)}{\max\{a(i),b(i)\}}\] \[ = \text{relative difference in within and between distance}\] \[ = \text{measure of how tightly clustered a group of points is relative to other groups }\]
Properties of Silhouette
If \(b(i) >> a(i)\) (distance to other clusters is far relative to distance within cluster), \(s(i) = 1\). This means it is an appropriate cluster.
If \(b(i) << a(i)\), point \(i\) is more similar to the neighboring cluster than its own (not great), \(s(i) = -1\).
If \(b(i) = a(i)\), then \(s(i) = 0\). It is more of a flip of a coin of which cluster point i should be in (on the border between 2).
We plot the average silhouette, \(\frac{1}{n}\sum_{i=1}^n s(i)\), and choose \(K\) that maximizes the average silhouette.
The tidymodels package is built for models of some outcome variable y. We can’t use it for clustering.
Instead, we’ll use a variety of new packages that use specialized, but short, syntax.
Suppose we have a set of sample_data with multiple feature columns x, and (possibly) a column named id which labels each data point.
PROCESS THE DATA
If there’s a column that’s an identifying variable or label, not a feature of the data points, convert it to a row name.
K-means can’t handle NA values! There are a couple options.
IF you have at least 1 categorical / factor feature, you’ll need to pre-process the data even further. You should NOT do this if you have quantitative and/or logical features.
RUN THE K-MEANS ALGORITHM FOR SPECIFIC K
TUNING K-MEANS: TRY A BUNCH OF K
Calculate the total within-cluster sum of squares (SS) for each possible number of clusters K from 1 to n.
DEFINING CLUSTER ASSIGNMENTS
For the rest of the class, work together on Ex 4–6 on HW6.
EXAMPLE 2: Interact with the K-Means algorithm
example of a good K-means outcome
example of a bad K-means outcome