Skip to main content
v2026.11,858 entries · CC-BY 4.0

Cluster Analysis in Python: K-Means, Hierarchical, DBSCAN

Cluster analysis in Python with scikit-learn and SciPy: a method table, runnable K-means, hierarchical and DBSCAN steps, and how to read the output.

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 -1 means 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 eps and min_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_state and keep n_init explicit.
  • An error when passing a non-Euclidean metric with Ward. Ward accepts only euclidean; use average linkage for other metrics.
  • An error about n_clusters and distance_threshold. Set exactly one; n_clusters must be None when a threshold is given.
  • No predict on 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.

Follow CASRAI

Research-administration guidance, standards updates and independent tool reviews.

Ask CASRAI · free to try

Ask about Cluster Analysis in Python: K-Means, Hierarchical, DBSCAN

Ask your first 2 questions free below. Subscribers get 150 a day for $29 a month.

An AI assistant specialized in research administration. It cites the sources behind every answer, labels web answers and says when it can't answer.

Answers draw on CASRAI's guides and dictionary plus the federal and funder documents we index: Federal Register, Grants.gov, Regulations.gov and UKRI.

Works on this site and inside Claude, Cursor and the AI tools you already use.

Everything CASRAI publishes — this page, the dictionary, the guides and the news — stays free to read, with no account and no card.

Referenced across the research world

University of Cambridge logoColumbia University logoCrossref logoUniversity of Edinburgh logoHarvard University logoUniversity of Oxford logoPrinceton University logoStanford School of Medicine logoUniversity College London logoORCID logoUniversity of Cambridge logoColumbia University logoCrossref logoUniversity of Edinburgh logoHarvard University logoUniversity of Oxford logoPrinceton University logoStanford School of Medicine logoUniversity College London logoORCID logo
  • University of Cambridge logo
  • Columbia University logo
  • Crossref logo
  • University of Edinburgh logo
  • Harvard University logo
  • University of Oxford logo
  • Princeton University logo
  • Stanford School of Medicine logo
  • University College London logo
  • ORCID logo

View CASRAI adoption →

Ask CASRAI · Regulatory Radar

Research-admin question? Get an answer that links its sources.

An AI assistant specialized in research administration. Every answer links its sources to check before you act. 2 questions free, no account. $29/month after.

  • Answers draw on CASRAI's guides and dictionary plus the federal and funder documents we index: Federal Register, Grants.gov, Regulations.gov and UKRI.
  • Every answer numbers its sources and links each one, so you can check the source yourself.