Releases: ilgrad/betula-cluster
Releases · ilgrad/betula-cluster
Release list
betula-cluster v0.1.5
Added
method="leiden"/method="leiden-cpm"— graph clustering / community detection over the
microcluster affinity graph via the full Leiden algorithm (Traag, Waltman & van Eck 2019):
local moving → refinement (sub-communities grown from singletons along edges, so each is
connected by construction — Leiden's guarantee over Louvain) → aggregation seeded from the
pre-refinement partition. It discovers the community count — nok(like the density head).
Aresolution(γ) knob trades community count against size; the modularity objective
("leiden", γ = 1 default) has a resolution limit, the CPM objective ("leiden-cpm") is
resolution-limit-free (γ on a smaller, density scale). Pure Rust — no eigensolver, NumPy-only.
Best for community/blob structure at a moderatethreshold; usemethod="spectral"for elongated
manifolds. The self-tuning k-NN affinity graph is shared between the spectral and Leiden heads.betula_cluster.consensus(X, n_clusters, n_runs=…)— clustersXunder several random
insertion-order permutations and votes, turning the CF-tree's insertion-order sensitivity
(Known Limitation #1) into a measurable quantity: a consensus labelling plus a per-point
stability score in[0, 1](ConsensusResult.confidence— low on unstable boundaries, high
where every order agrees). NumPy-only; for the partitional heads at a fixedn_clusters. The
independent runs parallelize across threads withn_jobs(the Rust core releases the GIL).method="spectral"— spectral clustering over the CF-tree leaf microclusters for non-convex /
manifold clusters (moons, rings, spirals) that the centroid heads cannot separate. Self-tuning
symmetric k-NN affinity (Zelnik-Manor & Perona local scaling), the normalized Laplacian embedding
(Ng-Jordan-Weiss) via the in-house Jacobi eigensolver — no LAPACK/ARPACK, the crate stays
NumPy-only — with a k-means landmark reduction above 256 microclusters so theO(m³)solve stays
bounded. Dense input only; pair it with a smallthreshold(many leaves) so the microclusters
resolve the manifold. No built-in cluster-count selection:n_clusters=0defaults to 2.threshold="auto"for theBetulaestimator — removes the one hyperparameter users most often
have to guess. A bounded-subsample pilot fits athreshold=0tree at the samemax_leavesand
reads the threshold it converges to, warm-starting the full fit near-converged instead of growing
it from zero (fewer rebuild passes, lower peak leaf count on largen). Cached across refits /
streaming batches; below the pilot cap it is a no-op (growing from zero is already cheap), and it
is dense-only (raises on sparse input).
Changed
- Benchmarks now cover every head (spectral, Leiden added to
bench/comprehensive.py) and the
compression heads run atmax_leaves = 4000: betula-kmeans is at exact parity with scikit-learn
(blobs 0.861 = 0.861) and Ward beats raw Ward while running the fullN. Docs / README / docs site
surface the spectral, Leiden and consensus additions; test counts reconciled (190 Python, 158
Rust). The docs site now renders the CHANGELOG and redeploys on every published release.
betula-cluster v0.1.4
Added
MapperGraph.persistence_diagram/MapperGraph.persistence(filtration=…)— 0-D persistent homology
of the Mapper nerve by single-linkage union-find (elder rule,O(E log E), pure Rust). Two
filtrations:"overlap"(the1 − edge_overlapBhattacharyya gap — a finite bar's death is the depth
of a bottleneck, ranking the booleanbridges) and"lens"(the lens sublevel diagram). Essential
connected-component classes carryinfdeath.- Greedy weighted k-means++ init (scikit-learn's default): lower-inertia, lower-variance seeds at
~ln k× the negligible init cost over the leaves. objective="dbcv"fortune— Density-Based Clustering Validation (Moulavi et al. 2014, in
[-1, 1]). Unlike the convex Calinski-Harabasz / Davies-Bouldin metrics (which penalise correct
non-convex partitions), DBCV validates variable-density / non-convex clusters, so it is the right
selection metric for the HDBSCAN-CF and DbStream density heads. NumPy-only, computed over a
subsample.
Changed
fit_predict_sparse/ the_coreCSR entry points now capn_features(MAX_SPARSE_FEATURES) and
validate CSR arrays through the pure-Rustsparse::validate_csr, closing an unbounded-allocation DoS
where a hostile caller could force an ~8 EB allocation with a single-nonzero row.- Docs reconciled to the current suite: 172-case Python suite, 147 Rust tests (143 unit + 4
integration under default features; thepython/persistence/clisurfaces add more, 155 total).
Tests
- Mutation-testing infrastructure (
cargo-mutantsscoped to the CF math core,mutmutfor the Python
wrapper, a weekly non-blocking workflow) plus a CSR-fuzzing proptest and the two coverage gaps it
surfaced (the CF-tree absorption boundary, exact tune-metric values).
betula-cluster v0.1.3
Added
betula_cluster.tune— memory-aware hyperparameter search over the CF knobs, scored by an internal
metric (Calinski-Harabasz / Davies-Bouldin) or ARI, with a multi-objective quality / memory /
speed Pareto mode. NumPy-only by default; an optional Optuna backend (TPE / NSGA-II) via
pip install 'betula-cluster[tune]'.- Property-based tests (
proptest, dev-only) for the CF-tree invariants: the clustering feature is a
commutative monoid (mergeis associative/commutative and equals a sequential build), folding a
tree's leaf features reconstructs the whole-dataset feature, the full-covariance upper-triangular
index is a bijection (incl.dim ≥ 4), and the Frequent-Directions sketch is lossless on low-rank
data and never overshoots the exact scatter. - Sparse-text benchmark (20 newsgroups, TF-IDF): the
O(nnz)fit_predict_sparsepath and the
standard reduce-then-cluster pipeline (TruncatedSVD / NMF → k-means) vs scikit-learn, written up
honestly inbench/RESULTS.md(raw high-dTF-IDF concentrates for every fast clusterer; on NMF
topics betula matches sklearn). MapperGraph.edge_overlap— a Bhattacharyya coefficient in(0, 1]per Mapper edge, from the pooled
diagonal-Gaussian summaries of the two nodes' member microclusters. Surfaced onto_networkx()edges
asoverlap=…, so a bridge between well-separated regions reads as a lower-weight edge than one
inside a dense blob.- Documentation site (MkDocs Material +
mkdocstringsAPI autodoc, MathJax-rendered math) built from
docs/, with a GitHub Pages deploy workflow;pip install 'betula-cluster[docs]'for the toolchain.
Changed
- Coverage floor (
cargo llvm-cov, ≥95 % lines) now also measures thepersistenceandclifeature
sets, not just the default core. - Declared
rust-version = "1.82"(MSRV) and lowered the real floor to it — the streaming heads had an
implicit 1.87 dependency (u64::is_multiple_of), now rewritten. AddedDocumentation/Changelog
project URLs. - Docs reconciled to the current suite: 167-case Python suite, 141 Rust tests (137 unit + 4
integration), and five end-to-end use cases (README, DESIGN.md). - Repository hardening:
macOS/WindowsCI test legs, an sdist install smoke test, a nightly
cargo auditcron, Dependabot, andSECURITY.md/CONTRIBUTING.md/ issue templates.
betula-cluster 0.1.2
Maintenance & docs release — no API changes; the engine and benchmarks are unchanged from 0.1.1.
Install / upgrade: pip install -U betula-cluster
Added
betula_cluster.__version__, resolved from the installed package metadata.
Changed
- README repositioned around the compress-then-cluster story: the test/coverage story is surfaced at the top, a "When to use it" section added, and capabilities split into stable core vs experimental. The density head is labelled HDBSCAN-CF consistently in prose (the
method="hdbscan"API string is unchanged).
Fixed
- Stale docs corrected: the Python suite is 153 cases (was 123);
betula-indexreferences now point to lexindex (the renamed indexing companion).
betula-cluster 0.1.1 — first public release
Fast, memory-bounded, numerically stable BETULA CF-tree clustering with a from-scratch Rust core and a scikit-learn API. No LAPACK, no SciPy at runtime.
Install: pip install betula-cluster
Highlights
- ⚡ 15–40× faster than scikit-learn at N = 1,000,000 — k-means labels 1M points in 0.20 s (vs KMeans 3.3 s, Birch 8.0 s, GaussianMixture 5.5 s).
- 🪶 Bounded memory — streaming 10M points peaks at ~57 MB, flat in N (clusters data larger than RAM); an in-core KMeans needs ~5 GB.
- 🎯 Quality at parity — k-means / GMM / Ward within ≈0.01 ARI of scikit-learn; full-covariance GMM recovers anisotropic clusters; HDBSCAN-on-CF nails non-convex shapes (ARI 1.00).
- 🔢 High-dimensional
normalize=True— MNIST-784 ARI 0.04 → 0.44, beating scikit-learn (0.32). - 🧱 Numerically stable —
(n, μ, S)Welford/Chan updates, PSD by construction; no catastrophic cancellation far from the origin.
What's inside
- Heads: weighted k-means (Hamerly), GMM (diagonal & full), exact Ward-HAC, HDBSCAN-on-CF, Mapper topology; automatic
k(BIC / dendrogram cut). - Streaming:
partial_fitat bounded memory,DenStream/DbStream, mergeable KLL / DDSketch quantiles. - Data: dense f32/f64,
scipy.sparse(O(nnz)), mixed numeric+categorical (k-prototypes). - Beyond labels:
predict_proba, coresets, outliers, near-duplicate pairs, representatives, drift snapshots, COP-KMeans constraints, robust (Huber) insertion. - Engineering: scikit-learn API (Pipeline / clone / GridSearchCV), typed abi3 wheel, save/load + pickle, dependency-free CLI, reusable Rust crate.
Quality bar
153-case Python suite at 100% wrapper coverage + 129 Rust tests; clippy -D warnings + fmt clean across all feature sets; Python 3.11–3.14 (single abi3 wheel).
Wheels
abi3 (CPython 3.11+): manylinux x86_64 + aarch64, macOS x86_64 + arm64, Windows x64, + sdist.
📊 Benchmarks · 📖 Docs · 📓 Examples · 📝 Changelog