Skip to main content

semaclust

CI PyPI version Python versions License: MIT

semaclust (semantic + clustering) is a small Python library for clustering similar strings using sentence embeddings and agglomerative clustering. It is useful for deduplicating free-text fields, normalizing user-entered values, and collapsing spelling or formatting variants into a canonical form.

Documentation: https://cobanov.github.io/semaclust/

Installation

pip install semaclust
# or
uv add semaclust

Install from source:

pip install git+https://github.com/cobanov/semaclust.git

Quickstart

from semaclust import TextClusterer

texts = [
    "New York", "NYC", "new york city",
    "Los Angeles", "LA",
    "San Francisco", "San Fran", "SF",
]

clusterer = TextClusterer(distance_threshold=1.0)
clusterer.fit(texts)

print(clusterer.n_clusters_)
# 3

print(clusterer.clusters_)
# {0: ['New York', 'NYC', 'new york city'],
#  1: ['Los Angeles', 'LA'],
#  2: ['San Francisco', 'San Fran', 'SF']}

print(clusterer.transform())
# ['NYC', 'NYC', 'NYC', 'LA', 'LA', 'SF', 'SF', 'SF']

fit_transform is the one-call shortcut:

TextClusterer(distance_threshold=1.0).fit_transform(texts)

API at a glance

Method Returns Purpose
fit(texts) self Cluster and store fitted attributes
fit_predict(texts) ndarray[int] Cluster labels per input text
fit_transform(texts) list[str] Each text replaced with its representative
transform(texts=None) list[str] Replace texts seen at fit time
get_replacement_map() dict[str, str] Mapping from original to representative
result_ ClusterResult Frozen dataclass with labels, clusters, reps

Fitted attributes (sklearn convention): labels_, clusters_, representatives_, n_clusters_, texts_.

Choosing a model and threshold

The default encoder is all-MiniLM-L6-v2 (22M params, 384-dim, ~80 MB). It's fast, small, and a sensible drop-in, but it isn't the strongest option for every task. The table below summarizes 9 encoders on three workloads of escalating difficulty: short text with abbreviations (cities), medium text with synonyms (job titles), and long sentences with topical themes (customer feedback). See benchmarks.md for the full methodology, raw thresholds, and per-test breakdowns.

ARI (Adjusted Rand Index) of 1.0 means an exact match with the ground-truth clustering. Bold = perfect.

Model Params Dim cities job titles feedback Good threshold
all-MiniLM-L6-v2 (default) 23M 384 1.000 0.672 1.000 1.0 (cities) / 1.3 (feedback)
all-MiniLM-L12-v2 33M 384 1.000 0.512 1.000 1.20
all-mpnet-base-v2 109M 768 0.619 0.512 1.000 -
BAAI/bge-small-en-v1.5 33M 384 1.000 0.672 1.000 0.95 - 1.00
BAAI/bge-m3 568M 1024 1.000 0.520 1.000 -
nomic-ai/nomic-embed-text-v1.5 * 137M 768 1.000 0.520 1.000 0.70 - 0.85
nomic-ai/nomic-embed-text-v2-moe * 475M 768 1.000 0.672 1.000 1.15 - 1.35
mixedbread-ai/mxbai-embed-large-v1 335M 1024 1.000 0.815 1.000 0.95 - 1.05
Qwen/Qwen3-Embedding-0.6B 596M 1024 0.529 0.672 1.000 -

* Requires a clustering: prefix on each input. The bench applies this automatically; if you swap the model in directly, wrap it in a custom encoder that prepends the prefix.

Practical takeaways:

  • Bigger isn't better. all-mpnet-base-v2 and Qwen3-Embedding-0.6B both fail the simplest test (cities) - they merge Los Angeles with San Francisco before merging SF with San Francisco.
  • Best small drop-in: BAAI/bge-small-en-v1.5 matches the default's size class and has a single threshold window (0.95-1.00) that works for both cities and feedback.
  • Best overall: mixedbread-ai/mxbai-embed-large-v1 - the only model to exceed 0.8 ARI on job titles, with a 0.10-wide cities+feedback overlap at 0.95-1.05. Costs 15x more parameters than the default.
  • No model solves the job-titles case. Synonymy across roles (SWE ~ Programmer ~ Software Engineer, Product Manager ~ Product Owner) defeats every encoder we tested.

To swap the encoder, pass a string or a custom Encoder:

from semaclust import TextClusterer

clusterer = TextClusterer(
    encoder="BAAI/bge-small-en-v1.5",
    distance_threshold=1.0,
)

Hardware acceleration

device="auto" is the default. It picks CUDA if a GPU is visible, MPS on Apple Silicon, and otherwise lets sentence-transformers fall back to CPU. Mac users get the GPU for free; no code change needed.

TextClusterer(device="auto")   # default; cuda > mps > cpu
TextClusterer(device="mps")    # force MPS
TextClusterer(device="cuda")   # force CUDA
TextClusterer(device="cpu")    # force CPU
TextClusterer(device=None)     # delegate to sentence-transformers (no MPS auto-pick)

Plugging in your own encoder

Any object implementing encode(texts: list[str]) -> np.ndarray satisfies the Encoder protocol:

from semaclust import TextClusterer, Encoder
import numpy as np

class MyEncoder:
    def encode(self, texts: list[str]) -> np.ndarray:
        return np.random.rand(len(texts), 384).astype(np.float32)

clusterer = TextClusterer(encoder=MyEncoder())

CLI

semaclust ships a small typer-based CLI:

# Cluster lines from a file, write JSON
semaclust cluster items.txt --threshold 1.0 --output clusters.json

# Replace each line with its cluster representative
cat items.txt | semaclust replace --threshold 1.0

Run semaclust --help for the full reference.

Migration from 0.1.x

The 0.3 release is a breaking change. The single cluster(texts) entry point is gone; the new API mirrors scikit-learn:

0.1.x 0.3.x
clusterer.cluster(texts) clusterer.fit(texts).clusters_
clusterer.get_replacement_map(texts) clusterer.fit(texts).get_replacement_map()
clusterer.replace_values(texts) clusterer.fit_transform(texts)

The representative_selector argument moved from per-call to the constructor.

Development

git clone https://github.com/cobanov/semaclust.git
cd semaclust
uv sync --extra dev
uv run pytest
uv run ruff check src tests
uv run mypy src/semaclust

Install the pre-commit hooks once:

uv run pre-commit install

License

MIT, see LICENSE.

Release files for semaclust 0.4.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for semaclust 0.4.2
File Size Uploaded
semaclust-0.4.2.tar.gz 17.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for semaclust 0.4.2
File Interpreter ABI Platform
semaclust-0.4.2-py3-none-any.whl Python 3 none any Details

Total release size: 30.0 kB

Release files / semaclust-0.4.2.tar.gz

Download URL semaclust-0.4.2.tar.gz
Size 17.1 kB
Tags Source
SHA-256 checksum
How to use checksums
f9e5b732bce38267cb1dc6b9cebf116cc2a3e57f653060047592a782f2874f38
BLAKE2b-256 checksum
How to use checksums
0e6120cfbc5d218e31f033599b0b9bdd12df8fb40b649f0459e20df84ce50caa
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on May 15, 2026.

Transparency log

Release files / semaclust-0.4.2-py3-none-any.whl

Download URL semaclust-0.4.2-py3-none-any.whl
Size 12.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
62d99d92ec4971d7191c7be972786339a5758b383adc1974a771f55a7a8790f2
BLAKE2b-256 checksum
How to use checksums
183a9b99d4157c02dd1721bf5e12819cd77dece01f7d717e2635c4d24c76b6a4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on May 15, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.4.2 This release

2 release files

0.4.1

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page