AgglomerativeClustering
sklearn
tree traversal
data science
machine learning

How to traverse a tree from sklearn AgglomerativeClustering?

ML System Design practice on Codemia

Design recommenders, ranking systems and training pipelines the way ML interviews actually ask for them, with worked solutions.

Practice ML system design

Introduction

AgglomerativeClustering builds a hierarchical merge tree, but scikit-learn does not hand you a ready-made recursive node structure. Instead, the fitted model exposes arrays such as children_ and optionally distances_, and you reconstruct the tree logic from those arrays.

Understand What children_ Contains

After fitting, children_ stores one row per merge. Each row contains the two cluster indices that were merged at that step.

python
1import numpy as np
2from sklearn.cluster import AgglomerativeClustering
3
4X = np.array([
5    [0.0, 0.0],
6    [0.1, 0.0],
7    [1.0, 1.0],
8    [1.1, 1.0],
9])
10
11model = AgglomerativeClustering(
12    n_clusters=None,
13    distance_threshold=0,
14    compute_distances=True,
15)
16model.fit(X)
17
18print(model.children_)
19print(model.distances_)

For n_samples original points, leaf nodes are indexed from 0 to n_samples - 1. Internal merged nodes start at n_samples and continue upward in merge order.

Reconstruct a Tree Recursively

A common way to traverse the hierarchy is to write a recursive function that turns a node index into either a leaf or an internal node.

python
1from dataclasses import dataclass
2
3@dataclass
4class Node:
5    index: int
6    left: "Node | None" = None
7    right: "Node | None" = None
8    distance: float | None = None
9    size: int = 1
10
11
12def build_tree(model, node_id, n_samples):
13    if node_id < n_samples:
14        return Node(index=node_id, size=1)
15
16    child_row = model.children_[node_id - n_samples]
17    left = build_tree(model, child_row[0], n_samples)
18    right = build_tree(model, child_row[1], n_samples)
19    distance = None
20    if hasattr(model, "distances_"):
21        distance = float(model.distances_[node_id - n_samples])
22
23    return Node(
24        index=node_id,
25        left=left,
26        right=right,
27        distance=distance,
28        size=left.size + right.size,
29    )
30
31
32n_samples = X.shape[0]
33root_id = n_samples + model.children_.shape[0] - 1
34root = build_tree(model, root_id, n_samples)
35print(root)

The key offset is node_id - n_samples. That converts an internal node index into the correct row of children_.

Traverse the Tree for Leaves or Merge Order

Once you have a recursive structure, ordinary tree traversal patterns work naturally.

python
1def collect_leaves(node):
2    if node.left is None and node.right is None:
3        return [node.index]
4    return collect_leaves(node.left) + collect_leaves(node.right)
5
6
7print(collect_leaves(root))

This is useful when you want the original sample indices contained in a cluster or subtree.

You can also walk the internal nodes to inspect merge distances and cluster sizes.

python
1def print_merges(node):
2    if node.left is None and node.right is None:
3        return
4    print(f"node={node.index} distance={node.distance} size={node.size}")
5    print_merges(node.left)
6    print_merges(node.right)
7
8
9print_merges(root)

Build a Linkage Matrix for Dendrogram Tools

If the goal is plotting with SciPy's dendrogram utilities, many users build a linkage matrix from children_, distances_, and cluster sizes.

python
1def count_leaves(model, n_samples):
2    counts = np.zeros(model.children_.shape[0], dtype=int)
3    for i, (left, right) in enumerate(model.children_):
4        total = 0
5        for child in (left, right):
6            if child < n_samples:
7                total += 1
8            else:
9                total += counts[child - n_samples]
10        counts[i] = total
11    return counts
12
13
14counts = count_leaves(model, n_samples)
15linkage_matrix = np.column_stack([model.children_, model.distances_, counts]).astype(float)
16print(linkage_matrix)

That format is handy when you want standard dendrogram plotting rather than custom recursive traversal logic.

Common Pitfalls

  • Forgetting that internal node indices start at n_samples, not at zero.
  • Indexing children_ directly with a node id instead of subtracting n_samples for internal nodes.
  • Expecting distances_ to exist without fitting in a way that computes distances.
  • Confusing original sample indices with internal cluster-node indices.
  • Reconstructing the tree manually without also tracking subtree sizes when dendrogram-style output is needed.

Summary

  • 'AgglomerativeClustering exposes the merge tree through arrays such as children_.'
  • Leaves are original sample indices, and internal nodes begin at n_samples.
  • Reconstruct the hierarchy by recursively resolving internal node indices back into rows of children_.
  • Use recursive traversal to inspect leaves, merge order, distances, or subtree sizes.
  • Build a linkage matrix if your real goal is dendrogram tooling rather than a custom tree object.

Related reading
Course
Intermediate
27 lessons
15 hours
DSA Fundamentals

Master algorithmic patterns and data structures through hands-on LeetCode-style problems - from arrays and hashing to dynamic programming and advanced graphs.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

ML System Design practice on Codemia

Design recommenders, ranking systems and training pipelines the way ML interviews actually ask for them, with worked solutions.

Practice ML system design

All Rights Reserved.