Written and maintained by CASRAI Editorial Board
Last updated
Cluster analysis in Python means grouping observations so that members of a group are more similar to each other than to other groups, using scikit-learn for K-means, agglomerative (hierarchical) clustering and DBSCAN, and SciPy for classic linkage matrices and dendrograms. The decision that matters most is not the code but the method: the three algorithms differ in what you must specify up front, whether they can label new data, and how they treat outliers. This guide gives a method table taken from the scikit-learn documentation, a worked example you can run and reproduce, and the defaults that trip people up (SciPy and scikit-learn do not share a default linkage).
What cluster analysis is used for in research
Researchers use cluster analysis to find structure in data without a pre-defined outcome label: grouping survey respondents by response pattern, grouping samples by measured profile, or grouping cells or documents by similarity. Because no outcome variable guides it, clustering is an exploratory, unsupervised method (see supervised vs unsupervised learning), and the groups it returns are a description of the data, not a confirmed classification. The same workflow in a menu-driven package is covered in cluster analysis in SPSS; a single-method primer is at what is K-means clustering.
Which method: K-means, hierarchical or DBSCAN?
Choose by what you can specify and what you need back. The scikit-learn clustering overview describes K-means as general-purpose with even cluster size and flat geometry, suited to a not-too-large number of clusters; agglomerative clustering takes either a number of clusters or a distance threshold; DBSCAN suits non-flat geometry and uneven cluster sizes and removes outliers.
| Method | Python class | You must specify | Can label new data? | Outliers |
|---|---|---|---|---|
| K-means | sklearn.cluster.KMeans |
n_clusters (default 8) |
Yes (inductive): predict |
Every point is assigned to a cluster |
| Agglomerative (hierarchical) | sklearn.cluster.AgglomerativeClustering; scipy.cluster.hierarchy.linkage |
Number of clusters or a distance threshold; linkage type; distance | No (transductive): labels only for the data you fitted | Every point is assigned to a cluster |
| DBSCAN | sklearn.cluster.DBSCAN |
eps and min_samples |
No (transductive) | Noise points are labelled -1 |
The documentation states that transductive methods are not designed to be applied to new, unseen data, so a fitted AgglomerativeClustering or DBSCAN object has no predict method for new rows. K-means minimises inertia, the within-cluster sum of squares, and the documentation cautions that inertia assumes clusters are convex and isotropic, which is not always the case.
Install or access
Install into a virtual environment (the scikit-learn installation page calls this optional but strongly recommended). The commands below are for macOS (on Linux the installation page uses python3 and pip3); on Windows the activation line is sklearn-env\Scripts\activate.
python -m venv sklearn-env
source sklearn-env/bin/activate
pip install -U scikit-learn scipy
Check what you installed with python -m pip show scikit-learn or python -c "import sklearn; sklearn.show_versions()". Record the versions: they belong in a methods section. The output in this guide was produced with scikit-learn 1.9.0 and SciPy 1.18.0. For pipelines, cross-validation and the rest of the library, see the scikit-learn tutorial.
Cluster analysis step by step
Step 1. Prepare and scale the data
Remove or handle missing values first (data cleaning steps), then put the features on a comparable scale. The StandardScaler documentation explains why: if a feature has a variance orders of magnitude larger than the others, it might dominate the objective function. StandardScaler computes z = (x – u) / s per feature, and the documentation notes it is sensitive to outliers. Distance-based clustering uses the same kind of distances, so unscaled features can dominate the grouping.
import numpy as np
from sklearn.preprocessing import StandardScaler
X = np.array([[1, 2], [1, 4], [1, 0], [10, 2], [10, 4], [10, 0]])
X_scaled = StandardScaler().fit_transform(X) # use X_scaled for real, mixed-unit data
The toy matrix X is the official KMeans documentation example, so you can check your output against the docs. In the steps below it is fitted unscaled because both columns are already on comparable scales.
Step 2. Fit K-means
from sklearn.cluster import KMeans
km = KMeans(n_clusters=2, random_state=0, n_init="auto").fit(X)
print(km.labels_) # [1 1 1 0 0 0]
print(km.cluster_centers_) # [[10. 2.] [ 1. 2.]]
print(km.inertia_) # 16.0
print(km.predict([[0, 0], [12, 3]])) # [1 0]
The signature is KMeans(n_clusters=8, *, init='k-means++', n_init='auto', max_iter=300, tol=0.0001, ...). With n_init='auto' the algorithm runs once when init='k-means++' and ten times when init='random', keeping the run with the lowest inertia. Set random_state to an integer so the result is reproducible. Cluster numbers are arbitrary: the docs example returns the left-hand group as cluster 1.
Step 3. Choose the number of clusters (cluster analysis step by step: how many?)
Inertia is not a normalised metric: lower is better and zero is optimal, so inertia always falls as k grows and cannot choose k alone. Pair it with the silhouette score. silhouette_score(X, labels) is defined only when 2 <= number of labels <= n_samples – 1, runs from -1 to 1, and higher means better-defined clusters; values near 0 indicate overlapping clusters and negative values generally indicate samples assigned to the wrong cluster.
from sklearn.metrics import silhouette_score
for k in (2, 3, 4):
m = KMeans(n_clusters=k, random_state=0, n_init="auto").fit(X)
print(k, round(m.inertia_, 2), round(silhouette_score(X, m.labels_), 3))
| k | Inertia | Silhouette |
|---|---|---|
| 2 | 16.00 | 0.713 |
| 3 | 10.00 | 0.436 |
| 4 | 4.00 | 0.167 |
Inertia keeps falling (16, 10, 4) while the silhouette score peaks at k = 2, which matches the two visible groups. These values were computed by running the code above on the toy matrix. On real data, report both and the reasoning, not only the k you kept.
Step 4. Hierarchical (agglomerative) clustering
from sklearn.cluster import AgglomerativeClustering
ag = AgglomerativeClustering(n_clusters=2, linkage="ward").fit(X)
print(ag.labels_) # [1 1 1 0 0 0]
The linkage criterion decides which clusters merge. In the scikit-learn documentation, Ward minimises the sum of squared differences within all clusters, complete linkage minimises the maximum distance between observations of pairs of clusters, average linkage uses the average of the distances, and single linkage uses the distance between the closest observations. Ward gives the most regular cluster sizes, and for non-Euclidean metrics average linkage is described as a good alternative. Ward accepts only the Euclidean metric.
To cut the tree by distance instead of by count, set n_clusters=None and give a distance_threshold (the two are mutually exclusive: n_clusters must be None when distance_threshold is set). Pass compute_distances=True to get distances_ for a dendrogram, at the cost of extra computation and memory.
ag2 = AgglomerativeClustering(n_clusters=None, distance_threshold=5, compute_distances=True).fit(X)
print(ag2.n_clusters_) # 2
print(ag2.distances_) # merge distances, last merge is about 15.59
Step 5. Hierarchical clustering with SciPy and a dendrogram
from scipy.cluster.hierarchy import linkage, fcluster
Z = linkage(X, method="ward")
print(fcluster(Z, t=2, criterion="maxclust")) # [1 1 1 2 2 2]
The signature is linkage(y, method='single', metric='euclidean', optimal_ordering=False). Note that SciPy’s default method is 'single', while scikit-learn’s AgglomerativeClustering default is 'ward', so the same data can give different trees if you rely on defaults in each library. The SciPy documentation also says centroid, median and ward are only correctly defined if a Euclidean pairwise metric is used. Always pass method= explicitly and report it. SciPy numbers cluster labels from 1, scikit-learn from 0.
Step 6. DBSCAN for noise and irregular shapes
from sklearn.cluster import DBSCAN
Xo = np.vstack([X, [[50, 50]]]) # add one far-away point
db = DBSCAN(eps=3, min_samples=2).fit(Xo)
print(db.labels_) # [ 0 0 0 1 1 1 -1]
DBSCAN found the two groups without being told how many and labelled the far-away point -1, noise. The documentation says eps is crucial to choose appropriately for the data set and distance function and usually cannot be left at the default value, and that min_samples primarily controls how tolerant the algorithm is towards noise. The value eps=3 here suits this toy matrix only.
Output and how to read it
- Labels.
labels_is one integer per row. Numbers carry no order or meaning. DBSCAN’s-1means noise, not a cluster. - Centres. K-means
cluster_centers_gives each cluster’s mean position; interpret them in the original units, so inverse-transform if you scaled the data. - Inertia.
inertia_is the sum of squared distances of samples to their closest cluster centre. Compare it only across runs on the same data. - Silhouette. For each sample, s = (b – a) / max(a, b), where a is the mean intra-cluster distance and b the mean distance to the nearest other cluster; the function returns the mean over samples.
- Merge distances. In hierarchical output, a large jump in merge distance (here 3.46 to 15.59) marks where merging starts to join dissimilar groups; cutting before the jump gives the two-cluster solution.
Plot results with the approach in scatter plot in Python, colouring points by label. For clustering used on single-cell data, see Leiden clustering for single-cell data.
Reporting for reproducibility
State the algorithm and library versions, the scaling applied, n_clusters or eps/min_samples or the distance threshold, the linkage and metric, random_state and n_init, and the criteria used to choose the solution (for example inertia and silhouette). Treat the clusters as exploratory and avoid presenting them as confirmed groups.
Troubleshooting
- The silhouette score cannot be computed. It is defined only for 2 to n_samples – 1 labels. A DBSCAN run that finds one cluster, or labels everything noise, falls outside that range; adjust
epsandmin_samples. Because noise is labelled-1, decide deliberately whether to score with or without the noise points. - Different results on each run. K-means starts from a random initialisation. Set
random_stateand keepn_initexplicit. - An error when passing a non-Euclidean metric with Ward. Ward accepts only
euclidean; use average linkage for other metrics. - An error about
n_clustersanddistance_threshold. Set exactly one;n_clustersmust beNonewhen a threshold is given. - No
predicton AgglomerativeClustering or DBSCAN. These are transductive. To label new rows, use K-means, or fit on all rows together. - One variable dominates the groups. Scale features (Step 1) before fitting.
- Clusters look wrong for ring or elongated shapes. K-means assumes convex, isotropic clusters; try DBSCAN or agglomerative clustering.
Alternatives: scikit-learn and SciPy routes compared
| Task | scikit-learn | SciPy |
|---|---|---|
| Hierarchical clustering | AgglomerativeClustering, default linkage 'ward' |
scipy.cluster.hierarchy.linkage, default method 'single' |
| Metric with Ward | Only euclidean |
Ward is only correctly defined with a Euclidean metric |
| Dendrogram data | compute_distances=True, then distances_; extra computation and memory |
The linkage matrix is the tree |
| Cut the tree | n_clusters or distance_threshold (one of the two) |
fcluster |
FAQ
How do I do cluster analysis in Python?
Scale your features, fit a method from sklearn.cluster (KMeans, AgglomerativeClustering or DBSCAN), read labels_, and judge the solution with inertia, the silhouette score and your subject knowledge. The steps above give a runnable example for each.
Can I do cluster analysis in Excel?
This guide covers Python, where the methods are documented and reproducible with a fixed seed and recorded versions. For a menu-driven alternative with a documented workflow, see cluster analysis in SPSS.
What is the difference between K-means and hierarchical clustering?
K-means needs the number of clusters in advance, can label new data with predict and minimises inertia. Hierarchical clustering builds a tree by merging clusters according to a linkage rule and can be cut by count or by distance, but it cannot label new data.
How do I choose the number of clusters?
Compare inertia and silhouette across a range of k with a fixed seed, look for a clear peak in the silhouette score, and check that the clusters make substantive sense. Inertia alone always improves as k grows.
Why do SciPy and scikit-learn give different dendrograms?
Their default linkage methods differ: 'single' in scipy.cluster.hierarchy.linkage and 'ward' in AgglomerativeClustering. Set the method explicitly in both.
Sources
Official documentation, checked 2026-10-29.








