Why tags are not ordinary feature vectors
A tag is a short label (“beach”, “family beach trip”), not a row of measurements. Three properties change how clustering behaves. Sparsity. Each tag string populates a tiny number of features. If I represent the vocabulary as one-hot or bag-of-tokens over 50,000 distinct words, a tag like “spa retreat” lights up 2 of 50,000 dimensions. Euclidean distance in that space is dominated by which words happen to be absent, and two tags about the same thing with different words land far apart. This is why I almost always embed first and cluster on the embedding. High cardinality. There are vastly more distinct tags than there are meaningful groups. A travel platform might carry hundreds of thousands of distinct tags collapsing into a few hundred real concepts. Clustering has to tolerate a very long tail of one-offs without inventing a cluster per typo. Co-occurrence structure. The signal in tags is not the spelling of a single tag, it is which tags appear together on the same item or user. “beach” and “sunbathing” cluster because the same listings carry both, even though they share zero characters or tokens. This is the property that graph and embedding methods exploit and that pure string distance misses completely. To cluster, I need vectors. The cheap option is feature hashing: token (or character n-gram) → bucket in a fixed-width vector, then cosine similarity. The better option in production is a learned embedding — a co-occurrence matrix (PMI-weighted) or a tag2vec / sentence-transformer embedding — so that “wine tasting” and “winery tour” sit near each other even with no shared token. My toy example below uses hashed token vectors so it runs anywhere; the clustering code is identical if you swap in real embeddings.How each algorithm behaves on tag data
Hierarchical / agglomerative clustering
This merges tags bottom-up into a tree (dendrogram): every tag starts as its own cluster, and at each step the two closest clusters merge under a linkage rule — single, complete, average, or Ward. It is ideal when the total number of clusters is unknown, because I do not pick K: I cut the dendrogram at a distance threshold or at a desired granularity, and read off the groups. On tags this is my default, because I can eyeball the merge order and stop when it starts gluing unrelated concepts. Average-linkage is the sane middle ground; single-linkage suffers the chain effect where “food tour” links “wine tasting” through an overlapping bridge tag, and complete-linkage fragments legitimate clusters. Complexity is roughlyO(n^2) in tags and O(n^3) naive, so it is a fit for the distinct-tag set (tens of thousands), not for raw occurrences (millions).
K-Means and the extended vector space model
K-Means partitions tags into K predefined groups by minimizing the distance between points and their cluster centers, iterating assign-then-recompute-centroid. The “extended vector space model” framing is the same idea viewed from information retrieval: represent each tag as a numeric vector and cluster in that space. It is fast (O(n · K · iters)), scales to more tags than agglomerative, and is what I reach for when I already know how many buckets I want (say, force the tail into ~200 canonical topics). Two catches on tag data: the mean of sparse binary/hash vectors is a fuzzy prototype that can be less interpretable than a real tag, and K-Means assumes convex, roughly equal-sized blobs — but tag frequencies are power-law, so a couple of huge hubs distort the centroids and orphan the tail. For sparse text vectors I prefer spherical K-Means (normalize, cosine distance, renormalize centroids).
Graph-based and neighborhood methods
These connect related tags by local density rather than global centroids.- KNN directed graph: build edges from each tag to its k nearest neighbors by cosine similarity; connected components or community detection (label propagation, Louvain) become the clusters. This is exactly the right shape for co-occurrence: nodes are tags, edges are “appears together,” and clusters fall out of graph structure.
- Affinity Propagation: messages passed between candidates let the data elect its own exemplars, so it chooses the number of clusters automatically and returns a real representative tag per cluster — nice for tag normalization. It is
O(n^2)memory and finicky about its damping/preference knobs, so I avoid it past a few tens of thousands of tags. - Local Information Passing Clustering (LIPC): a neighborhood-density approach suited to long-tailed, unevenly distributed labels like tags; it propagates local similarity to decide merges, which handles the sparse high-cardinality case better than centroid methods.
When the cluster count is unknown
This is the decision that picks the algorithm for me:
If someone asks me to “just run K-Means” without knowing K, I push back: on tag data K is rarely known in advance, and the elbow/silhouette search over K is more expensive and less stable than a single agglomerative pass with a threshold I can reason about.
A runnable example (pure standard library)
My machine had neither NumPy nor scikit-learn available, so I wrote this with only the Python standard library (zlib for a deterministic feature hash, math for normalization). It embeds tags by hashing their word tokens into a fixed-width unit vector, then runs average-linkage agglomerative clustering with a cosine-distance threshold. zlib.crc32 is deterministic across runs, unlike the built-in hash(), which is salted per process — that matters if you want reproducible output. Swap the embed function for a real tag2vec model and the rest is unchanged.
python3:
What this leaves out for production
- Scale. The loop is
O(n^3)-ish and the vectors are dense lists. For 100K+ distinct tags I move to a sparse CSR matrix and scikit-learn’sAgglomerativeClusteringwith a precomputed cosine distance matrix, or I go graph-based (build a KNN index over embeddings, run Louvain), which is the path I describe in Scalable vector database. - Representation before clustering. Most of the quality comes from the embedding, not the algorithm. Before tuning linkage I check whether a co-occurrence/PMI embedding or a transformer tag encoder pulls semantically-equal, lexically-different tags together.
- Evaluating without ground truth. I use silhouette score on the cosine distances, or better, a small hand-labeled set of “should be same / should be different” tag pairs, and report precision on the merges.
- Downstream use. Folded tag clusters become canonical features for Customer segmentation — a user who uses five different “beach” tags collapses to one interest signal, which is the whole reason I normalize tags before profiling.