Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Working with Unlabeled Data — Clustering Analysis

Open in Colab Open in Kaggle

Working with Unlabeled Data — Clustering Analysis


Prework

Import essentials

Check package version (optional)

Add folder to path in order to load from the check_packages.py script.

Check recommended package versions.

[OK] numpy 2.0.2
[OK] scipy 1.16.3
[OK] matplotlib 3.10.0
[OK] sklearn 1.6.1
[OK] pandas 2.2.2

Watermark (optional)

Optional watermark extension is a small IPython notebook plugin that I developed to make the code reproducible.

You can install watermark Jupyter extension via

conda install watermark -c conda-forge  

or

pip install watermark   

For more information, please see: https://github.com/rasbt/watermark.

   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 4.9/4.9 MB 23.8 MB/s eta 0:00:00
Author: Sebastian Raschka, Johnny Yu

Last updated: 2026-08-13

Python implementation: CPython
Python version       : 3.12.13
IPython version      : 7.34.0

numpy     : 2.0.2
pandas    : 2.2.2
matplotlib: 3.10.0
scipy     : 1.16.3
sklearn   : 1.6.1


Table of Contents


10.1.Grouping objects by similarity using k-means

10.1.1.K-means clustering using scikit-learn

The 150 randomly generated points that are roughly grouped into three regions and two dimensions with higher density.

Visualize the dataset in a scatter plot.

<Figure size 640x480 with 1 Axes>

Apply k-means algorithm to our example dataset using the KMeans class from scikit-learn’s cluster module.

Problem: large max_iter

It is possible that k-means does not reach convergence for a particular run, which can be problematic (computationally expensive) if we choose relatively large values for max_iter.

Solution: large tol

One way to deal with convergence problems is to choose larger values for tol, which is a parameter that controls the tolerance with regard to the changes in the within-cluster SSE to declare convergence. In the preceding code, we chose a tolerance of 1e-04 (=0.0001).

Visualize the clusters that k-means identified in the dataset together with the cluster centroids. These are stored under the cluster_centers_ attribute of the fitted KMeans object.

<Figure size 640x480 with 1 Axes>

10.1.2.A smarter way of placing the initial cluster centroids using k-means++

To use k-means++ with scikit-learn’s KMeans object, we just need to set the init parameter to 'k-means++', which is strongly recommended in practice.

10.1.3.Hard versus soft clustering

Unfortunately, the FCM algorithm is not implemented in scikit-learn currently, but interested readers can try out the FCM implementation from the scikit-fuzzy package, which is available at https://github.com/scikit-fuzzy/scikit-fuzzy.

10.1.4.Using the elbow method to find the optimal number of clusters

Conveniently, we don’t need to compute the within-cluster SSE explicitly when we are using scikit-learn, as it is already accessible via the inertia_attribute after fitting a KMeans model.

From previous k-means result, we have the following distortion:

Distortion: 72.48

Based on the within-cluster SSE, we can use elbow method to estimate the optimal number of clusters k for a given task. And recall that the lower value of inertia (= lesser distortion) indicates better clustering.

In the following plot, the elbow is located at k = 3, so this is supporting evidence that k = 3 is a good choice for this dataset.

<Figure size 640x480 with 1 Axes>

10.1.5.Quantifying the quality of clustering via silhouette plots

The silhouette coefficient is available as silhouette_samples from scikit-learn’s metric module, and optionally, the silhouette_scores function can be imported for convenience. The silhouette_scores function calculates the average silhouette coefficient across all examples, which is equivalent to numpy.mean(silhouette_samples(...)).


There are two examples in the following sections: good and bad clustering.

1) Ex: good clustering (three centroids)

  • Silhouette coefficients are not close to 0

  • Silhouette coefficients are approximately equally far away from the average silhouette coefficients (dotted line in the following figure)

<Figure size 640x480 with 1 Axes>

2.1) Ex: bad clustering (two centroids) scatter plot

  • One of the centroids falls between two of the three spherical groupings of the input data.

<Figure size 640x480 with 1 Axes>

2.2) Ex: bad clustering (two centroids) silhouette plot

Typically we don’t have the luxury of visualizing datasets in two-dimensional scatterplots in real-world problems (data in higher dimensions). Thus, it’s better for to use silhouette plot for evaluating the clustering results.

  • Silhouette coefficient of cluster 2 is closer to 0

  • Silhouette coefficients are not equally far away from the average silhouette coefficients (dotted line in the following figure)

<Figure size 640x480 with 1 Axes>



10.2.Organizing clusters as a hierarchical tree

10.2.1.Grouping clusters in bottom-up fashion

<IPython.core.display.Image object>

Generate a random data sample.

  • rows: different observations (IDs 0-4)

  • columns: different features (X, Y, Z) of those examples

Loading...

10.2.2.Performing hierarchical clustering on a distance matrix

Create a condensed distance matrix as input for the hierarchical clustering algorithm by pdist function from SciPy’s spatial.distance submodule. And use squareform function to create a symmetrical pairwise distance matrix, shown in the following result.

Loading...

We can either pass a condensed distance matrix (upper triangular) from the pdist function, or we can pass the “original” data array and define the metric='euclidean' argument in linkage function in the following code. However, we should not pass the squareform distance matrix, which would yield different distance values although the overall clustering could be the same.

To sum it up, the three possible scenarios are listed here:

  1. incorrect approach: Squareform distance matrix

  2. correct approach: Condensed distance matrix

  3. correct approach: Input matrix

/tmp/ipykernel_964/1052818803.py:6: ClusterWarning: The symmetric non-negative hollow observation matrix looks suspiciously like an uncondensed distance matrix
  row_clusters = linkage(row_dist, method='complete', metric='euclidean')
Loading...
Loading...
Loading...

To take a closer look at the clustering results, we can turn those results into a pandas DataFrame as the following code.

As shown in the following result, the linkage matrix consists of several rows where each row represents one merge.

  • The first and second columns denote the most dissimilar members in each cluster

  • The third column reports the distance between those members

  • The last column returns the count of the members in each cluster

Loading...

Now that we have computed the linkage matrix, we can visualize the results in the form of a dendrogram.

Such a dendrogram summarizes the different clusters that were formed during the agglomerative hierarchical clustering; for example, you can see that the examples ID_0 and ID_4, followed by ID_1 and ID_2, are the most similar ones based on the Euclidean distance metric.

<Figure size 640x480 with 1 Axes>

10.2.3.Attaching dendrograms to a heat map

Attach a dendrogram to a heat map with the following steps:

  1. Create a new figure object and define the x-axis position, y-axis position, width, and height of the dendrogram via the add_axes attribute. Furthermore, we rotate the dendrogram 90 degrees counterclockwise.

  2. Reorder the data in our initial DataFrame according to the clustering labels that can be accessed from the dendrogram object, which is essentially a Python dictionary, via the leaves key.

  3. Modify the aesthetics of the dendrogram by removing the axis ticks and hiding the axis spines.

  4. Construct the heat map from the reordered DataFrame and position it next to the dendrogram.

  5. Add a color bar and assign the feature and data record names to the x and y axis tick labels.

<Figure size 800x800 with 3 Axes>

10.2.4.Applying agglomerative clustering via scikit-learn

This AgglomerativeClustering function in scikit-learn allows us to choose the number of clusters that we want to return, which is useful if we want to prune the hierarchical cluster tree.

In this case, we set number of clusters as n_clusters=3

From the result Cluster labels: [1 0 0 2 1], first entity in the vector represents ID_0 belongs to label 1, vice versa. With such, we can read the result Cluster labels: [1 0 0 2 1] as follows:

  • ID_0 and ID_4 belongs to label 1

  • ID_1 and ID_2 belongs to label 0

  • ID_3 belongs to label 2

Cluster labels: [1 0 0 2 1]

Now, change number of clusters to n_clusters=2, we can see that

  • ID_0, ID_3 and ID_4 belongs to label 0

  • ID_1 and ID_2 belongs to label 1

Cluster labels: [0 1 1 0 0]



10.3.Locating regions of high density via DBSCAN

<IPython.core.display.Image object>

In this section, we will compare three different clustering approaches: k-means, ACL, DBSCAN, with a dataset of half-moon-shaped structures. (recall that ACL = Agglomerative Complete Linkage)

First, let’s create the dataset with the build-in dataset in scikit-learn.

<Figure size 640x480 with 1 Axes>

1) K-means and ACL algorithm

Based on the visualized clustering results, we can see that the k-means algorithm was unable to separate the two clusters, and also, the hierarchical clustering algorithm (ACL) was challenged by those complex shapes.

<Figure size 800x300 with 2 Axes>

2) DBSCAN

The DBSCAN algorithm can successfully detect the half-moon shapes, which highlights one of the strengths of DBSCAN—clustering data of arbitrary shapes.

<Figure size 640x480 with 1 Axes>



Readers may ignore the next cell.

Skipping: convert_notebook_to_script.py not present in this environment.