Hierarchical clustering preserves the order and distance of every merge instead of returning only a final partition. SciPy stores that nested structure in a linkage matrix, while the boundary that turns it into flat clusters remains a separate modeling decision.
The sample uses nine labeled observations with two already standardized numeric features. Ward linkage is stated explicitly with Euclidean distance because its variance-minimization calculation is defined for Euclidean geometry.
The ordered merge distances expose a wide gap between the last within-group merge and the first between-group merge. A distance cut of 1.0 falls inside that gap, and label sets verify the resulting memberships without depending on SciPy's arbitrary numeric cluster IDs.
Steps to build and cut a SciPy linkage hierarchy:
Prepare labeled observations
- Prepare hierarchical_clustering.py with SciPy imports, labels, and the standardized observation matrix.
- hierarchical_clustering.py
import numpy as np from scipy.cluster.hierarchy import fcluster, linkage labels = np.array( [ "web-1", "web-2", "web-3", "cache-1", "cache-2", "cache-3", "db-1", "db-2", "db-3", ] ) observations = np.array( [ [0.0, 0.1], [0.2, -0.1], [-0.2, 0.0], [0.1, 4.8], [-0.1, 5.1], [0.3, 5.0], [5.1, 4.9], [4.8, 5.2], [5.0, 5.1], ] )
Each row is one observation and each column is one standardized numeric feature. Scale real columns before Ward clustering when their units or ranges differ.
Define the linkage boundary
- Declare Ward linkage with Euclidean distance for the standardized features.
linkage_method = "ward" distance_metric = "euclidean"
Ward, centroid, and median linkage are defined only for Euclidean pairwise distances.
- Construct the linkage matrix from the observation rows.
linkage_matrix = linkage( observations, method=linkage_method, metric=distance_metric, )
- Extract the hierarchy's ordered merge distances for cut inspection.
merge_distances = linkage_matrix[:, 2]
Nine observations produce eight linkage rows. The third column records the distance at each successive merge.
Select and apply the flat-cluster cut
- Select a distance cut of 1.0 between the within-group and between-group merges.
cut_distance = 1.0
- Assign a flat cluster ID to each observation at the selected distance cut.
cluster_ids = fcluster( linkage_matrix, t=cut_distance, criterion="distance", )
The distance criterion keeps observations together only while their cophenetic distance stays at or below the selected cut.
Report memberships independently of cluster numbers
- Group the observation labels by their assigned flat cluster ID.
memberships = {} for label, cluster_id in zip(labels, cluster_ids): memberships.setdefault(int(cluster_id), []).append(label) membership_rows = sorted( sorted(members) for members in memberships.values() )
- Print the linkage choice, merge distances, cut, and sorted membership rows.
print(f"linkage: {linkage_method} ({distance_metric})") print("merge_distances:", np.round(merge_distances, 3).tolist()) print(f"cut_distance: {cut_distance:.1f}") print("memberships:") for members in membership_rows: print(f" {', '.join(members)}")
Verify the expected grouping
- Define the expected memberships without using SciPy's numeric cluster IDs.
expected_groups = { frozenset(["web-1", "web-2", "web-3"]), frozenset(["cache-1", "cache-2", "cache-3"]), frozenset(["db-1", "db-2", "db-3"]), }
- Reject any membership set that differs from the expected service groups.
observed_groups = { frozenset(members) for members in membership_rows } if observed_groups != expected_groups: raise RuntimeError("Flat clusters did not match the expected groups") print("membership_check: passed")
Execute the completed hierarchy
- Execute the completed clustering script with Python 3.
$ python3 hierarchical_clustering.py linkage: ward (euclidean) merge_distances: [0.224, 0.224, 0.283, 0.37, 0.387, 0.416, 8.431, 11.24] cut_distance: 1.0 memberships: cache-1, cache-2, cache-3 db-1, db-2, db-3 web-1, web-2, web-3 membership_check: passed
The script exits with an error instead of printing membership_check: passed when any observed label set differs from the expected groups.
Mohd Shakir Zakaria is a cloud architect with deep roots in software development and open-source advocacy. Certified in AWS, Red Hat, VMware, ITIL, and Linux, he specializes in designing and managing robust cloud and on-premises infrastructures.