Mathematical foundation & improvements¶
Every formula below is verified symbolically (Maxima) and/or numerically (mpmath/Julia ground
truth) — see DESIGN.md and research/.
Numerically stable cluster features (n, μ, S)¶
Classic BIRCH stores \((N,\ LS = \sum x,\ SS = \sum x^2)\) and recovers variance as \(SS/N - (LS/N)^2\) — a
difference of two large, nearly-equal numbers. On real data with an offset (timestamps, money,
geo-coordinates, un-centered embeddings) this catastrophically cancels: in f64 the variance
collapses to noise — and can go negative — around coordinate magnitude 1e7, silently corrupting
every downstream radius, covariance, and label.
betula-cluster stores \((n, \mu, S)\) — weight, mean, and the sum of squared deviations \(S = \sum w\,(x - \mu)(x - \mu)^\top\). \(S\) is a sum of non-negative terms, so the variance \(S/n \ge 0\) and the covariance is positive-semidefinite by construction — there is nothing to cancel. The updates are algebraically exact (not approximations):
(full covariance: \(\delta \odot \delta \to \delta\delta^\top\), \(\Delta \odot \Delta \to \Delta\Delta^\top\)). Tested bit-stable at offset 1e7–1e8 against an
mpmath reference, where the classic \((N, LS, SS)\) form loses all significant digits.
GMM E-step: expected-log (variant C), not the paper's convolution¶
Running EM on the leaf CFs, each leaf is a mini-Gaussian \(\mathcal{N}(\mu_i, \Sigma_i)\). The textbook / paper move
treats it by convolution — \(\mathcal{N}(\mu_i \mid \mu_k, \Sigma_k + \Sigma_i)\) — which inflates each component by the
leaf's own spread and washes out components on a coarse CF-tree. betula-cluster instead uses the
expected log-likelihood responsibility, which is measured to beat the convolution on ARI and is
the correct expected log-likelihood under the leaf model; against the simpler plug-in that drops the
trace it is a wash outside coarse, overlapping summaries (research/RESULTS-estep.md, three seeds):
The M-step folds the within-leaf scatter back in, so the fitted components stay exact:
with NIW/MAP regularization \(\Sigma_k = (\Psi + \dots)/(\nu + N_k + d + 1)\) so a 1-point leaf never yields a singular covariance.
High-dimensional floor. With few effective leaves per component, \(\Sigma_k\) can still go
near-singular along a low-variance direction, which makes the \(-\tfrac12\mathrm{tr}(\Sigma_k^{-1}\Sigma_i)\)
correction over-confident and starves the component — its responsibility collapses to zero and the
recovered count drops below k. A per-dimension floor on each component's covariance diagonal at
\(10^{-3}\,(\Sigma_\text{global})_{dd}\) — relative to the global per-dimension variance (not the mean
scale, which between-cluster separation inflates), with off-diagonals / orientation left untouched —
keeps every \(\Sigma_k\) well-conditioned. On 64-dimensional digits it holds all 10 components (an
unfloored fit starves one to 9) and the floored full-covariance head reaches ARI 0.575 against
scikit-learn's GaussianMixture at 0.463 (median of seeds 0/1/2), while low-dimensional and
rotated-anisotropic fits are unchanged. The unfloored figure of 0.39 that this pair was first measured
against dates from a 0.2.0 build and has not been re-taken; the floored-vs-scikit-learn gap has. The
diagonal GMM floors its per-dimension variance the same way (\(10^{-3}\,(\Sigma_\text{global})_{dd}\)).
Toeplitz / AR covariance for stationary signals¶
For an ordered wide-sense-stationary signal the covariance is Toeplitz, \(\Sigma_{ts} = c(|t-s|)\), so
it is fixed by an autocovariance sequence rather than a dense \(d\times d\) matrix. method="gmm-toeplitz"
models each component covariance as an AR(w) process. The pooled biased autocovariance
(from the leaf mean deviations, with the within-leaf variance folded into the zero lag) is mapped by the
Levinson-Durbin recursion, kept at every intermediate order \(m = 0..w\) (predictor \(\phi_m\), error
variance \(v_m\)). The precision is the exact Gohberg-Semencul form \(\Gamma = (1/\sigma^2)(BB^\top - ZZ^\top)\)
(\(B\) lower-triangular Toeplitz from the AR coefficients; \(Z\) the corner correction), evaluated by the
prediction-error decomposition
\(\log p(\delta) = -\tfrac12\sum_{t=0}^{d-1}\bigl[\ln(2\pi v_{m_t}) + (\delta_t - \phi_{m_t}\!\cdot\delta_{t-1:t-m_t})^2/v_{m_t}\bigr]\),
\(m_t = \min(t, w)\) — so the first \(w\) boundary positions are modelled exactly (the \(-ZZ^\top\) term)
rather than dropped by a conditional likelihood (measured \(+0.04\) ARI at short windows). It is
positive-definite by construction — the reflection-coefficient clamp \(|k_m| \le 0.999\) (\(\Rightarrow v_m > 0\))
is the GS box constraint — with \(O(w)\) parameters and \(O(d\,w)\) cost per leaf. The order \(w\) is chosen
per component by BIC, and the mean is a single stationary scalar (one parameter,
not \(d\)). This is well-posed at \(N_k \ll d\), where full covariance is singular and a diagonal model is
blind to the neighbour correlation. Ordered coordinates only — a permutation destroys the structure.
(Gohberg-Semencul Toeplitz-precision estimation, arXiv:2311.14995; see
docs/adr/001-gmm-toeplitz.md.)
General (non-AR) Toeplitz covariance (method="gmm-toeplitz-full"). AR(w) has a banded precision,
so it cannot represent an autocovariance whose support exceeds order \(w\) (e.g. a single echo at lag
\(K > w\)). The general head drops the banded assumption: it forms the dense Toeplitz covariance
\(\Sigma_{ts} = r^{\mathrm b}_c(|t-s|)\) directly from the biased (\(\div d\)) autocovariance \(r^{\mathrm b}_c\).
The biased sequence is periodogram-consistent, so \(\Sigma\) is positive-semidefinite by construction
(each leaf contributes the autocorrelation of its zero-padded deviation, a nonnegative spectrum; a
nonnegative-weighted sum stays PSD), and the \(\div d\) bias shrinks high-lag terms — a free regularization
exactly where \(N_k \ll d\). A ridge (the within-leaf variance plus \(10^{-6}\,r^{\mathrm b}_c(0)\)) makes it
strictly PD; a Cholesky factor gives the exact multivariate-Gaussian log-density
\(-\tfrac12(d\ln 2\pi + \ln|\Sigma| + \delta^\top\Sigma^{-1}\delta)\). Cost is \(O(d^2)\) parameters and \(O(d^3)\)
per component (vs AR's \(O(d\,w)\)), so it is the opt-in rung for signals a low-order AR cannot fit: on a
long-lag-echo mixture (\(K\in\{16,28,40\}>w_{\max}=10\)) it reaches ARI \(0.73\!\to\!1.00\) as the window grows
where the AR head sits at chance, and it matches the AR head on AR-generated signals
(bench/toeplitz_ar_mixture.py).
A third route, method="gmm-toeplitz-gs", estimates the general Toeplitz precision by the paper's
Gohberg-Semencul MLE: a full-order (≤ 16) Yule-Walker (Levinson) fit refined by coordinate ascent of
the exact log-likelihood over the reflection coefficients \(k_m\), positive-definite by \(|k_m| < 1\) and
deterministic. The reflection-coefficient parameterization makes the constraint free — the step-up
recursion maps any \(|k| < 1\) to a stable predictor with \(v_m > 0\) — so the MLE refines the moment
estimator toward the likelihood optimum without leaving the PD cone. Its \(O(m \cdot d \cdot p)\) E-step is
cheaper than the dense \(O(d^3)\) covariance route at large \(d\); it captures structure up to its order cap
(mid-lag echoes the banded head misses), while the covariance route covers arbitrarily long lags.
CF-weighted NMF for nonnegative data¶
For nonnegative features (TF-IDF, bag-of-words, event counts, spectrogram magnitudes, histograms) a
nonnegative low-rank factorization \(X \approx W H\), \(W, H \ge 0\), is often the natural representation — its
parts \(H\) are interpretable and additive. Factorizing the raw \(N \times d\) matrix is \(O(N d r)\) per
iteration and holds an \(N \times r\) code matrix, defeating BETULA's compression. projection="weighted-nmf"
factorizes the \(M \ll N\) leaf centroids instead. Assigning every point in leaf \(C_j\) the same code
\(z_j\) (the hard-leaf approximation every Phase-3 head already makes), König-Huygens gives
so minimizing the full-data objective is equivalent — up to that constant — to the weighted centroid problem \(\min_{Z,H \ge 0} \sum_j n_j \lVert \mu_j - z_j H \rVert^2 = \lVert \tilde X - \tilde Z H \rVert_F^2\) with \(\tilde X_j = \sqrt{n_j}\,\mu_j\), \(\tilde Z_j = \sqrt{n_j}\,z_j\). The factorization runs over the microclusters (\(O(M d r)\) per sweep, memory-bounded) and any head then clusters the nonnegative codes \(z_j\). The solver is a dependency-free weighted HALS (coordinate descent, reusing the Gram / cross-product matrices \(HH^\top\), \(\tilde X H^\top\), \(W^\top W\), \(W^\top \tilde X\) across sweeps); because \(M\) is small the matrices are tiny, so no BLAS is needed — the compression, not a fast NMF kernel, is the speed-up. Nonnegative input only; signed data is rejected rather than shifted (a shift would destroy angles). For signed embeddings use the directional heads or reduce with PCA / TruncatedSVD first.
For count data (word counts, event tallies) the Frobenius objective assumes Gaussian noise, which is
mis-specified — counts are Poisson. projection="weighted-nmf-kl" minimizes the generalized-KL
(I-divergence) \(\sum_{ij} [X_{ij}\ln(X_{ij}/(WH)_{ij}) - X_{ij} + (WH)_{ij}]\) by Lee-Seung multiplicative
updates over the raw centroids, with the shared components \(H\) weighted by leaf mass \(n_j\) (the per-row
\(W\) update is weight-invariant, each row minimized independently) — the Poisson maximum-likelihood fit.
The gain is rate-dependent: largest at sparse counts (measured up to +0.5 ARI over Frobenius on a
Poisson-count mixture at mean rate \(< 0.5\)), narrowing to a few points as the mean count grows past \(\sim1.5\)
and the central-limit theorem pulls Poisson toward Gaussian.
Initialization and the scale gauge. Both solvers start from NNDSVDar (Boutsidis & Gallopoulos, 2008): a rank-\(r\) truncated SVD — obtained from a randomized range finder (Halko-Martinsson-Tropp: Gaussian sketch, two power iterations, then a small eigendecomposition of \(BB^\top\), so no LAPACK is needed) — whose \(k\)-th singular triplet yields a nonnegative pair by keeping whichever of its positive / negative parts carries more energy. The resulting zeros must be filled: zero is a fixed point of both HALS and the multiplicative updates, so an entry left at zero could never recover.
Two details of that fill decide whether the factorization is usable at all, because zero is absorbing — a component driven to zero on one sweep is gone permanently:
- Rank-deficient triplets. The right vector is recovered as \(v = B^\top u / \sigma\), so once \(\sigma\) falls to the noise floor the division amplifies round-off into a vector of arbitrary magnitude. Such triplets are cut at the LAPACK numerical-rank threshold \(\sigma \le \sigma_{\max}\,\max(M,d)\,\varepsilon\) and reported as exact zeros, to be seeded by the fill instead.
- Fill scale. A filled component is a rank-1 block of constant magnitude \(f^2\) per entry, and \(r\) of
them add up against data entries of size \(\operatorname{mean}(X)\). The plain
avariant's \(f = \operatorname{mean}(X)\) therefore swamps the data as \(r\) grows — measured on a rank-12 matrix at \(r = 32\): initial relative residual 13.5, and the first sweep annihilated 28 of 32 components. Thearvariant's \(f = \operatorname{mean}(X)\cdot U(0,1)/100\) keeps the fill a perturbation, and the randomness breaks the degeneracy a constant fill would create (identical, linearly dependent columns that no coordinate descent can separate). Measured effect: 0 components dead, converged residual 270× lower, and ondigitsat \(r = 24\) the reconstruction error drops 0.33 → 0.20 — matchingscikit-learn's own NMF to three decimals at every rank tested (\(r = 10, 16, 24, 32\)).
Both objectives are invariant to \((WD, D^{-1}H)\) for any positive diagonal \(D\) — the optimizer therefore leaves an arbitrary per-component scale (measured spreads of \(70\times\) between components on a converged fit). That gauge freedom is harmless for the reconstruction but not for us: \(W\) leaves the factorizer as a Euclidean feature vector for the Phase-3 head, where a per-component scale is a per-dimension weight, so the head silently clusters along whichever component drew the largest number. The returned factorization is therefore canonical — \(\lVert H_k \rVert_2 = 1\) with the scale absorbed into \(W\), and components ordered by descending energy. Measured on a 4-topic nonnegative mixture over 8 seeds, at \(N = 8\,000 / 40\,000 / 160\,000\): median ARI 0.81 / 0.99 / 0.97 → 1.00 and seed spread ±0.37 → ±0.00. The gain is determinism, not accuracy in the mean. Convergence is tested on the size of the update — the total coordinate movement of a sweep, against the first sweep's — not on the size of the objective. A relative test on the residual never fires here: HALS converges sublinearly, so it keeps buying more than \(\texttt{tol}\) of relative improvement for hundreds of sweeps and the iteration budget ends up the only brake. The movement is scale-free, does converge, and falls out of the sweep at no extra cost.
Directional clustering: spherical k-means & von Mises–Fisher¶
On L2-normalized data every point lies on the unit sphere S^{d-1}, where cosine — not Euclidean —
similarity is the meaningful geometry (CLIP / face / sentence / speaker embeddings). Two heads cluster
by direction: spherical k-means (hard) and a mixture of von Mises–Fisher distributions (soft).
Exact merge on the sphere. A leaf summarizing points {xₚ} on the sphere is reduced to its
weighted mean μ_i = (Σ xₚ)/n_i, whose length ‖μ_i‖ = R̄_i ∈ [0, 1] is the leaf's mean resultant
length — a direct measure of within-leaf angular concentration. The cluster resultant is
R_c = Σ_{i∈c} n_i μ_i = Σ_{p∈c} xₚ, additive across leaves and independent of how points were
grouped: the BETULA exact-merge property carries through unchanged. The MLE mean direction is
μ̂_c = R_c / ‖R_c‖ and R̄_c = ‖R_c‖ / N_c. Keeping μ_i un-normalized is essential —
re-normalizing each leaf to a unit direction discards R̄_i, makes the compression look artificially
concentrated, over-estimates κ, and fragments the mixture.
Concentration. The vMF concentration uses the Banerjee et al. (2005) closed form
κ̂ ≈ R̄(d − R̄²)/(1 − R̄²), which avoids inverting the Bessel ratio. The normalizer
C_d(κ) = κ^{d/2−1} / ((2π)^{d/2} I_{d/2−1}(κ)) still needs log I_ν(κ), and no Bessel library is
pulled in for it (the crate stays NumPy-only) — two evaluators are split at κ = 10⁴:
- below, the all-positive power series in log-space — pull out
(κ/2)^ν, accumulate the term ratio(κ/2)² / (m(ν+m))with an online log-sum-exp. Nothing overflows, but the peak term sits atm ≈ κ/2, so the cost isO(κ)and the loop's own stop truncates before the peak aboveκ ≈ 4·10⁵. Largeκis not a stability problem for this series; it is a cost problem that turns into a correctness problem. - above, DLMF 10.41.3, the uniform asymptotic expansion for large order, in
O(1):I_ν(νz) ~ e^{νη}/(√(2πν)·(1+z²)^{1/4}) · Σₖ Uₖ(p)/ν^kwithη = √(1+z²) + ln(z/(1+√(1+z²)))andp = 1/√(1+z²). Three terms (U₀..U₂) are exactly what f64 needs fromκ = 10⁴up: measured against 50-digit arithmetic overν ∈ [1, 2047], the f64 result lands within 0.8 ulp, while stopping atU₁costs some 300 ulps and a fourth term changes nothing.
The expansion is written in z = κ/ν and divides by ν = d/2 − 1, so it exists only for d ≥ 4.
Below that the series is the sole evaluator, and the concentration cap is what keeps it in range:
10⁶ for d ≥ 4, 10⁴ for d ≤ 3. The cap is a limit of the normalizer, not of the model — a
cluster tighter than it is already effectively a point. Extending d ≤ 3 would need a separate
small-order expansion and its own validation.
The EM E-step is the exact expected log-likelihood of a leaf's points under component c,
n_i·[ln π_c + log C_d(κ_c)] + κ_c · μ_c · R_i with R_i = n_i μ_i the raw resultant, so a
spread-out leaf contributes proportionally weaker evidence — the directional analogue of the
full-covariance GMM's within-leaf −½ tr(Σ_c⁻¹ Σ_i) correction. predict_proba returns this true
posterior; n_clusters=0 selects the component count by BIC.
Labelling a raw point¶
The CF-tree is a summary, not the model. A head fits its parameters to the leaves, and the label of a new point follows from that head's own objective — not from where the tree happens to route it:
| head | rule |
|---|---|
kmeans, spherical-kmeans |
\(\arg\min_c \lVert x - c \rVert^2\) over the cluster centres (spherical compares unit-normalized centres, where the Euclidean argmin and the cosine argmax agree) |
gmm, gmm-full, vmf, gmm-toeplitz{,-full,-gs} |
\(\arg\max_c\ \ln \pi_c + \ln p(x \mid \theta_c)\) |
ward, spectral, leiden, hdbscan, scale-space |
nearest leaf entry, then that entry's label |
The third row is not a fallback but the only defined answer: those clusters need not be convex, so any centre or density rule would impose a partition the head exists to avoid. The first two rows were computed that way until they were fixed, and the tree descent is greedy — an approximate nearest-microcluster search that disagreed with the model on 3–28% of points.
The mixture densities are the plain component log-densities: the E-step's within-leaf correction
\(-\tfrac{1}{2}\operatorname{tr}(\Sigma_c^{-1}\Sigma_i)\) exists because a leaf has scatter, and a single
observation has none. Each head keeps the exact numbers its own EM converged to — the floored diagonal
variances, the ridge-regularized Cholesky, the AR predictor bank or Toeplitz factor — so the point rule
cannot drift from the fit. predict_proba normalizes the same scores, which makes
predict_proba(X).argmax(1) equal to predict(X) by construction. A component that ends up claiming
no leaf is silenced, so predict can only name a label the fitted partition actually uses.
Where the diagonal head is weak. Being the model's own rule is not the same as being the most
accurate one, and the gap shows up wherever the model is a poor fit. method="gmm" treats every
dimension as independent, so scoring a raw point sums d separate penalties and a per-dimension
modelling error accumulates across all of them; the leaf-level score damps this through the
\(-\tfrac{1}{2}\operatorname{tr}(\Sigma_c^{-1}\Sigma_i)\) term, which a single observation does not have.
On raw image pixels — strongly correlated, and exactly what a diagonal covariance cannot represent —
that costs accuracy: MNIST-20k ARI 0.340 by leaf descent against 0.185 by posterior, where
TF-IDF text gains (20-newsgroups gmm 0.027 → 0.054) and so does digits (0.489 → 0.507). The
fitted covariance is what costs it: on the same fit a nearest-centre rule, which ignores the covariance
entirely, scores 0.378 — above both. The loss is specific to a fine leaf budget, not to the head:
at max_leaves=300 the posterior wins for gmm (0.206 → 0.239) and for gmm-full
(0.207 → 0.212) alike. For raw images prefer kmeans or a projection.
Geometry-aware graph (GeoBETULA) and scale-space modes¶
Two heads exploit the geometry within each microcluster, on the M ≪ N leaves.
GeoBETULA (method="leiden"). The self-tuning k-NN affinity graph normally uses the centroid
distance ‖μ_i − μ_j‖². Two optional terms make it geometry-aware: a log-Euclidean covariance
term β·‖logΣ_i − logΣ_j‖²_F (the SPD-manifold metric, covariance_weight) so neighbours agree in
shape, and a Grassmann term γ·d²_Gr(U_i, U_j) with d²_Gr = r − ‖U_iᵀ U_j‖²_F — the
projection distance between the two rank-r principal subspaces U_i (top-r eigenvectors of Σ_i),
tangent_weight — so neighbours agree in manifold orientation. This separates crossing or adjacent
manifolds that share a centroid neighbourhood but differ in local tangent. Both reuse the in-house
Jacobi eigensolver; both default to 0 (plain centroid affinity).
Scale-space modes (method="scale-space"). Treat the leaves as a weighted sample and take the
modes of the KDE ρ_h(x) = Σ_j n_j exp(−‖x−μ_j‖²/2h²) (found by mean-shift) as clusters. Increasing
the bandwidth h merges modes — a one-parameter Morse filtration. Rather than fix h (or k), the
head sweeps h log-spaced and reports the labelling at the most persistent mode count: the widest
plateau of the "number of modes vs log h" curve, with the trivial fully-merged tail winning only when
no multi-mode structure is at least as persistent. At each scale, raw mean-shift modes separated by
only a shallow density valley (ρ along the connecting segment stays ≥ VALLEY_RATIO = 0.8 of the
lower peak) are merged by prominence — this collapses the spurious sub-peaks a single cluster produces
at fine bandwidths, cleaning the curve so the persistent plateau is unambiguous (robust from 2 to ~8+
clusters). This is parameter-free and non-convex-aware.
Other verified improvements¶
- Distances
D0–D4are the BIRCH measures re-derived on \((n, \mu, S)\) (Maxima-verified equivalent, computed stably). Variance-increase / Ward is \(D_4 = \tfrac{n_A n_B}{n_{AB}}\,\|\Delta\mu\|^2\) — the \(S\) terms cancel by König–Huygens, so it is an exact centroid measure (no Lance-Williams approximation). - k-means on CFs minimizes the true point objective, not the leaf-centroid proxy: \(\text{SSE} = \sum_i \bigl[S_i + n_i\|\mu_i - c\|^2\bigr]\) folds each leaf's own scatter \(S_i\) back in, so compressing to a CF-tree first does not change what is being optimized.
- Full covariance uses a matrix Welford (PSD) with on-demand Cholesky for
logdet/mahalanobis; the packed upper-triangular index is the tested \((j-1)j/2\) form — a reference implementation shipped a \((j-1)\cdot\mathrm{dim}/2\) variant that silently corrupts \(\mathrm{dim} \ge 4\). - \(\chi^2\) absorption gate (
absorb="chi2"): a mass-invariant Mahalanobis-\(\chi^2\) threshold with a Normal-Inverse-Gamma prior \(\text{var}_\text{eff} = (S + \kappa s_0)/(n + \kappa)\), finite at \(n = 1\). Fixes the size-imbalance failure where a 12-point vs a \(10^4\)-point cluster decide differently (sklearn #22854). - Frequent-Directions sketch (high \(d\)): the full-cov GMM consumes it in low-rank form — \(\mathrm{tr}(\Sigma_k^{-1}\Sigma_i) = \sum_r \|L_k^{-1} f_r\|^2\) — so it never materializes a \(d \times d\) matrix per leaf and keeps \(O(\ell d)\) memory through clustering. Identical math to the dense path.
- Rebuild merges the \(k\) closest within-leaf sibling pairs, where \(k\) is what the leaf budget asks for, and raises the threshold to the widest gap it took (monotone, \(O(M \cdot \text{capacity})\) scan, no global all-pairs). Two consequences. In place: merging two entries inside one leaf node leaves every node CF exactly equal to the merge of its subtree, so no ancestor is touched; the reinsertion that follows merges nothing and only re-routes, which is the one thing compaction cannot do (shrink a mixed leaf, yes; split it, no). Cliff-safe: in high dimension distances concentrate, so the leaf count is near-discontinuous in the threshold (measured on 3000-d TF-IDF: 7755 leaves at \(\tau = 1.0\), 12 at \(\tau = 1.3\)) and any threshold-first policy either fails to reduce or collapses the tree; choosing \(k\) instead caps a rebuild at one merge per entry.
- Mass-balanced budget (
balance = b, optional): the absorption gate above is purely geometric, so nothing bounds how much mass one leaf accumulates. Withbalanceset, a leaf entry is a candidate for absorption or for a compaction merge only while its weight stays under
\(w_{\max} = \max\bigl(b \cdot W / M,\ 2\bigr), \qquad W = \text{total mass seen so far},\ M = \texttt{max\_leaves}\)
so \(W/M\) is the perfectly balanced ideal and \(b\) the slack allowed above it. Reading \(W\) from the
root CF makes the cap self-tuning on a stream — it tightens as data arrives — and the floor of 2
keeps it from forbidding the merge of two singletons during warm-up. The cap is enforced at both
sites because either one alone undoes the other: absorption refuses a full entry and starts a new
one, and the rebuild skips a pair that would overflow. It is soft and \(M\) is hard: a rebuild
that cannot reach its target under the cap merges over it rather than leave the tree over budget.
- Robust insertion (huber_k = k, optional): before a point \(x\) is folded into its target
microcluster \((n, \mu, S)\), each coordinate is winsorized to the cluster's own scale,
\(\tilde{x}_j = \mathrm{clip}\bigl(x_j,\ \mu_j - k\sigma_j,\ \mu_j + k\sigma_j\bigr), \quad \sigma_j = \sqrt{S_j / n}\)
and the stable Welford update runs on \(\tilde{x}\) instead of \(x\). A coordinate with \(\sigma_j = 0\) (no scale yet) passes through unchanged, and the clip is skipped until the target holds ≥ 5 points (so the scale estimate is trustworthy). This bounds any single point's pull on the centroid to \(O(k\sigma/n)\) — outliers can no longer drag a centroid or inflate a radius — while leaving the CF a valid \((n, \mu, S)\) triple, so every downstream head is unchanged. The clipped value flows identically into the leaf entry and its ancestors, preserving the "each node = merge of its subtree" invariant.
Relation to BIRCH and BETULA¶
This library is a from-scratch Rust implementation of the BETULA cluster feature — the numerically stable \((n, \mu, S)\) summary introduced by Lang & Schubert to replace classic BIRCH's cancellation-prone \((N, LS, SS)\):
- BETULA: Numerically Stable CF-Trees for BIRCH Clustering — Andreas Lang & Erich Schubert, SISAP 2020 (arXiv:2006.12881 · Springer); extended journal version, Information Systems 2022 (ScienceDirect).
- BIRCH — Zhang, Ramakrishnan & Livny, SIGMOD 1996.
Reference implementations: ELKI (Java), and betulars (source) — a Rust+PyO3 package by paper co-author Andreas Lang. betulars is a faithful, highly optimised Phase-1 CF-tree builder: it builds the tree and exposes leaf cluster statistics, but (as of v0.1.0) produces no cluster labels and no global clustering — k-means / GMM / hierarchical are listed as planned. If you need just the canonical BETULA CF-tree primitive, fast, use betulars.
betula-cluster is a different thing: an end-to-end clustering library. It re-derives the same stable CF from scratch and then adds everything betulars leaves to the user:
- the full Phase-2 pipeline — k-means / GMM / full-covariance GMM / Ward / HDBSCAN with
automatic
k— producing per-point labels andpredict, behind a real scikit-learn API (the de-facto Python BIRCH,sklearn.cluster.Birch, is classic BIRCH: the unstable CF); f32trees, streamingpartial_fit, a mass-invariant χ² absorption gate, a Frequent-Directions sketch for highd,normalize=Truefor embeddings, an inspection API (outliers / near-duplicates / representatives / geometry), and serde persistence;- AVX2/FMA distance kernels with a scalar fallback, and rayon-parallel build + labeling.
The concrete, reproducible quality/speed/memory comparison is against the labeled scikit-learn
clusterers practitioners actually reach for: at matching ARI, betula labels 1 M points 30×
faster than sklearn.cluster.Birch (8.01 s → 0.26 s) and 9× faster than KMeans, while
streaming memory stays flat at ~60 MB; see bench/RESULTS.md and the
method-comparison notebook. (betulars produces no labels, so
it is not in that comparison; on the raw Phase-1 build the two are at parity — betula-cluster builds
an identical tree at every N and, with matched target-cpu=native flags, matches betulars'
wall-clock to within ~2 %.)