Mean Shift Clusterer
Procedure: mean shift
Mean shift finds clusters by repeatedly shifting each point towards the mean of its neighbours within a bandwidth, until
points converge onto the same mode; the number of clusters is discovered from the data rather than fixed in advance by a
Clusters/Components argument.
Package installed beyond the shared set: meanShiftR.
library(meanShiftR)
set.seed(1)
n <- 90
x <- c(rnorm(n / 3, 10, 2), rnorm(n / 3, 40, 2), rnorm(n / 3, 70, 2))
y <- c(rnorm(n / 3, 10, 2), rnorm(n / 3, 45, 2), rnorm(n / 3, 15, 2))
merchants <- as.matrix(scale(data.frame(x, y)))
model <- meanShift(merchants, bandwidth = c(0.7, 0.7))
print(table(model$assignment))
print(model$value[!duplicated(model$assignment), ])
Console output:
1 2 3
30 30 30
[,1] [,2]
[1,] -1.207934290 -0.8594380
[2,] 0.008710314 1.3635883
[3,] 1.199488686 -0.5065909
Tool equivalent:
Jube.Service.Repository.AnalyticsTrainingRun.AdvancedAnalyticsMeanShiftClustererTrainingService / tool
SubmitMeanShiftClustererTraining — Kind = Clusterer, recall class MeanShiftClustererRecall. Bandwidth
matches meanShift’s bandwidth; Kernel (MeanShiftKernel, default Uniform) picks the kernel shape used to weight
neighbours, and MaxNeighbours caps the neighbour search, an efficiency control the plain
meanShiftR call above has no equivalent of. Note this DTO still carries a Clusters-shaped absence correctly — there
is no Clusters/Components field at all, consistent with mean shift discovering the cluster count rather than being
told it.
Code audit
SubmitTrainingAsync
(Jube.Service/Repository/AnalyticsTrainingRun/AdvancedAnalyticsMeanShiftClustererTrainingService.cs)
queues the run; AdvancedAnalyticsTrainingWorker constructs a MeanShiftClustererTrainingExecutor
(Jube.Engine/AdvancedAnalytics/Training/AdvancedAnalyticsTrainingWorker.cs:605-606) and calls its
ExecuteAsync (line 615), which runs MeanShiftClustererTrainer.TrainWithSplit/Train
(Jube.Engine/AdvancedAnalytics/Clustering/MeanShiftClustererTrainer.cs:139-147). That trainer instantiates
Accord.MachineLearning.MeanShift (Accord.MachineLearning/Clustering/MeanShift/MeanShift.cs) with a kernel built by
its own KernelOf switch (MeanShiftClustererTrainer.cs:280-289) and calls Learn.
Kernel: a genuine choice of flat/uniform, Gaussian or Epanechnikov — default is flat
MeanShiftClustererOptions.Kernel defaults to MeanShiftKernel.Uniform
(Jube.Engine/AdvancedAnalytics/Clustering/MeanShiftClustererOptions.cs:19-30), and KernelOf
(MeanShiftClustererTrainer.cs:280-289) maps the three enum values to real, distinct Accord kernel classes:
UniformKernel (flat/box), GaussianKernel(dimension), and EpanechnikovKernel(dimension). This is not cosmetic —
MeanShift.Learn (Accord.MachineLearning/Clustering/MeanShift/MeanShift.cs:365-467) branches on the kernel type at
lines 394-403 (if (kernel is UniformKernel) func = uniform; else func = general;) and the two paths compute genuinely
different neighbour weightings:
uniform(lines 590-619) is an unweighted arithmetic mean of every neighbour inside the search radius — every neighbour counts equally regardless of distance, the textbook flat/box kernel.general(lines 554-588) computes a real kernel-weighted mean: for each neighbour it derives a weightg = -kernel.Derivative(u / h²) * weight(uthe squared distance,hthe bandwidth) and accumulatesg * point[i]before dividing by the summed weights — the standard Comaniciu & Meer (2002) mean-shift update rule, evaluated through each kernel’s own derivative function. SelectingGaussianorEpanechnikovgenuinely changes which points influence a mode and by how much; it is not a label with no runtime effect.
Bandwidth: a fixed hyperparameter, no data-driven estimator
MeanShiftClustererOptions.Bandwidth is a plain user-supplied double with default 2.0
(MeanShiftClustererOptions.cs:28), validated only for being finite and positive (line 48-49). Nothing in
MeanShiftClustererTrainer.cs or in Accord.MachineLearning.MeanShift derives a bandwidth from the data — there is no
Silverman’s rule, no Scott’s rule, no plug-in or cross-validated bandwidth selection anywhere in this path. The caller
must choose Bandwidth themselves (in standardised z-score units when Standardise
is true, the default), and a mode-count sanity check is the only feedback the product gives after the fact:
MeanShiftWarningCodes.SingleMode fires if the fit collapses to one mode (MeanShiftClustererTrainer.cs:165-168,
“consider a smaller bandwidth”), and ManyModes fires if the mode count reaches at least half the training row count
for 10+ rows (lines 169-173, “consider a larger bandwidth”) — both are after-the-fact diagnostics, not a selection
mechanism.
How a mode search actually runs and how modes are deduplicated
MeanShift.Learn (MeanShift.cs:365-467) seeds one search point per training row (createSeeds, called with bin size
2 * Bandwidth, lines 385) and, for each seed, move
(MeanShift.cs:469-531) repeatedly: finds neighbours within Bandwidth * 3 via a KD-tree, capped at
Maximum neighbours (Jube’s MaxNeighbours, default 0 meaning unlimited —
MeanShiftClustererOptions.cs:32, MeanShift.cs:154-156 documents 0 as “unlimited”); computes the (possibly
kernel-weighted) mean of those neighbours; moves the current point to that mean; and stops either when it comes within
Bandwidth of an already-found mode (lines 506-520, at which point it is snapped onto that mode, deduplicating it) or
when the shift vector’s Euclidean norm falls below Tolerance * Bandwidth
(line 524, citing Comaniciu 2002 p.606 directly in the comment), bounded by MaxIterations (default 100,
MeanShiftClustererOptions.cs:34, checked at MeanShift.cs:483). Because nearby-mode snapping happens during the
search itself, the final mode count is bandwidth-dependent and discovered, not fixed — consistent with the doc prose
above.
