hcr is a pip-installable Python package for hierarchical KMeans clustering with post-hoc refinement.
Bring your own embeddings. hcr operates on any L2-normalised float32 numpy array — no text loading, no model dependencies baked in.
pip install hcrFor UMAP visualisations:
pip install "hcr[visualization]"import numpy as np
from hcr import HierarchicalClusteringRefinement
# Your embeddings: shape (N, D), float32, L2-normalised rows.
# Produce them however you like (sentence-transformers, OpenAI, E5, etc.)
X = np.load("embeddings.npy").astype(np.float32)
# k is auto-estimated from the data when not specified
hcr = HierarchicalClusteringRefinement(seed=42)
result = hcr.fit(X)
print(result.labels) # np.ndarray, shape (N,) — cluster id per row, -1 = outlier
print(result.resolved_k) # {"first_level_k": 14, "second_level_k": 14, "leaf_k": 14, "auto_estimated": True}
print(result.summary) # dict: counts of merges, removals, reassignments
print(result.tree) # pd.DataFrame: the hierarchical tree structure# All three levels specified — used as-is
hcr = HierarchicalClusteringRefinement(first_level_k=20, second_level_k=15, leaf_k=10, seed=42)
# Only first two — leaf_k inherits from second_level_k
hcr = HierarchicalClusteringRefinement(first_level_k=20, second_level_k=15, seed=42)
# Only first — second_level_k and leaf_k both inherit from first_level_k
hcr = HierarchicalClusteringRefinement(first_level_k=20, seed=42)texts = ["some sentence", "another sentence", ...] # len(texts) == N
result = hcr.fit(X, texts=texts)
print(result.report) # pd.DataFrame: per-cluster stats + top TF-IDF terms + examplesAll three k parameters (first_level_k, second_level_k, leaf_k) default to None. The values used are resolved inside fit() according to this priority:
| Provided | first_level_k | second_level_k | leaf_k |
|---|---|---|---|
| All three | as given | as given | as given |
| first + second | as given | as given | = second_level_k |
| first only | as given | = first_level_k | = first_level_k |
| none | estimated | estimated | estimated |
When none are given, estimate_k_from_similarity_distribution(X) is called. It samples random pairs from X, fits a Gaussian (μ, σ) to the cosine similarity distribution, and returns:
k = clamp(int(1 / (1 - μ + σ)), min=2, max=50)
A high mean similarity (dense, similar embeddings) produces a larger k; a wider spread (σ) inflates k further. The resolved values are always stored on the result for inspection:
print(result.resolved_k)
# {"first_level_k": 14, "second_level_k": 14, "leaf_k": 14, "auto_estimated": True}Control sampling cost with k_estimation_sample_pairs (default 50_000).
| Field | Type | Description |
|---|---|---|
labels |
np.ndarray (N,) |
Refined cluster label per row; -1 = outlier |
raw_labels |
np.ndarray (N,) |
Pre-refinement leaf labels |
resolved_k |
dict |
k values used + "auto_estimated" bool |
summary |
dict |
Counts of removes, merges, reassignments, outliers |
report |
pd.DataFrame | None |
Per-cluster stats + TF-IDF terms; None if no texts given |
tree |
pd.DataFrame |
Full hierarchical tree (one row per node) |
assignments |
pd.DataFrame |
Row-level path/depth/node from the clustering step |
hcr --embeddings embeddings.npy \
--data texts.csv --text-col text \
--first-level-k 20 --second-level-k 20 \
--output-dir runs_hOmit all k flags to use auto-estimation:
hcr --embeddings embeddings.npy --output-dir runs_hKey flags:
| Flag | Default | Description |
|---|---|---|
--first-level-k |
auto | Clusters at depth 1 |
--second-level-k |
auto | Clusters at depth 2 |
--leaf-k |
auto | k for adaptive leaf splits |
--k-estimation-sample-pairs |
50000 | Pairs sampled for auto k estimation |
--sim-threshold |
0.80 | Cosine similarity target for leaf quality |
--refine-merge-threshold |
0.90 | Similarity floor to attempt cluster merging |
--seed |
42 | Global random seed |
Run hcr --help for all options.
| Parameter | Default | Description |
|---|---|---|
first_level_k |
None (auto) |
Clusters at depth 1 |
second_level_k |
None (auto) |
Clusters at depth 2 |
leaf_k |
None (auto) |
k for adaptive splits at leaf nodes |
k_estimation_sample_pairs |
50_000 |
Random pairs sampled for k auto-estimation |
sim_threshold |
0.80 |
Cosine similarity target for leaf quality |
alpha |
0.6 |
Centring strength at each recursive level |
refine_remove_abs_threshold |
0.45 |
Hard similarity floor for member removal |
refine_merge_threshold |
0.90 |
Minimum rep-similarity to attempt merging |
refine_reassign_threshold |
0.60 |
Similarity needed to reassign a removed item |
seed |
42 |
Global random seed (used for k estimation too) |
hcr/
├── __init__.py HierarchicalClusteringRefinement, ClusteringResult
├── clustering.py hierarchical_kmeans, estimate_k_from_similarity_distribution, …
├── refinement.py remove_bad_members_B_rule, greedy_iterative_merge, …
├── reporting.py cluster_report, build_leaf_row_table, …
├── sampling.py kcenter_greedy_indices
├── preprocessing.py center_then_l2, l2_normalize_rows, mask_numbers, …
├── visualization.py reduce_and_plot, plot_all_node_tsnes, …
└── cli.py Config, parse_args, main
tests/
└── test_smoke.py pytest smoke tests (no external data, no torch)
Hierarchical Clustering Refinement (HCR) is a post-clustering framework designed to improve the quality of hierarchical clustering results in large-scale intent discovery, semantic grouping, and embedding-based clustering tasks.
Traditional clustering methods such as K-Means or Hierarchical K-Means often suffer from structural limitations when applied to high-dimensional embedding spaces:
- forced assignment of samples to incorrect clusters
- cluster fragmentation caused by recursive splits
- semantic overlap between clusters
- noisy samples degrading cluster representations
HCR introduces a refinement stage applied after hierarchical clustering to repair these issues and produce more coherent clusters.
When clustering large embedding datasets (e.g., customer messages, search queries, support tickets), hierarchical K-Means provides scalability and interpretability, but it often produces:
- over-segmented clusters
- semantically duplicated clusters
- clusters contaminated with outliers
- samples assigned to the wrong branch during early splits
HCR addresses these problems by introducing a structured cluster repair stage that operates on the final leaf clusters.
Hierarchical Clustering Refinement (HCR) is designed as a post-clustering refinement framework that operates on the output of hierarchical clustering algorithms such as Hierarchical K-Means.
The overall workflow consists of two main phases:
-
Hierarchical Clustering
The dataset is first clustered using a hierarchical strategy (e.g., recursive K-Means). This stage produces the initial set of leaf clusters. -
Cluster Refinement (HCR)
HCR is then applied to the resulting clusters to repair common clustering issues such as outliers, fragmented clusters, and incorrect assignments.
The full processing pipeline can be summarized as follows:
flowchart TD
A[Embedding Generation] --> B[Hierarchical K-Means Clustering]
B --> C[Leaf Clusters]
C --> D[HCR Refinement Stage]
D --> D1[Outlier Removal]
D1 --> D2[Greedy Cluster Merge]
D2 --> D3[Sample Reassignment]
D3 --> E[Improved Clusters]
The refinement process consists of three core steps.
Cluster members that are significantly dissimilar from their cluster representation are removed.
Instead of using a fixed threshold alone, HCR uses a hybrid rule combining absolute and relative thresholds:
remove if similarity < max(
absolute_threshold,
cluster_p10 - margin
)
Where:
- cluster_p10 is the 10th percentile similarity within the cluster
- margin is a tolerance parameter
This allows clusters with different densities to be treated adaptively.
Clusters that are extremely similar are iteratively merged using a greedy similarity-based merge strategy.
The algorithm repeatedly:
- computes similarity between cluster representatives
- finds the most similar cluster pair
- merges them if similarity exceeds a threshold
- recomputes the merged cluster representation
This process continues until no cluster pair exceeds the merge threshold.
This approach prevents chain merges commonly observed in graph-based merging strategies.
Samples removed during the outlier stage are evaluated again.
For each removed sample:
assign to cluster if max_similarity ≥ reassign_threshold
otherwise mark as outlier
This allows incorrectly assigned samples to recover and join the correct cluster.
Cluster representatives are computed using the mean embedding of cluster members, followed by normalization.
cluster_rep = normalize(mean(cluster_member_embeddings))
Using the actual cluster mean instead of K-Means centroids ensures that the representative vector accurately reflects the final cluster structure.
- adaptive k selection — auto-estimated from the similarity distribution when not specified
- hierarchical clustering compatible
- scalable to large embedding datasets
- adaptive outlier detection (hybrid absolute + relative threshold)
- greedy cluster consolidation with internal quality guards
- semantic reassignment of removed samples
- embedding-model agnostic
HCR is particularly useful for:
- intent discovery
- customer support message clustering
- search query grouping
- topic discovery in large text datasets
- semantic dataset exploration
- Generate sentence embeddings
- Run hierarchical K-Means clustering
- Extract final leaf clusters
- Apply HCR refinement
- Analyze improved cluster structure
See the LICENSE file for full license text.