Nearest-Neighbor Clustering with jpclust and sNNclust

knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>",
  fig.width = 6,
  fig.height = 4
)
library(dbscan)

The dbscan package implements two clustering algorithms based on shared nearest neighbors:

Both algorithms replace the original distances between observations with a local similarity: two observations are more similar when their lists of nearest neighbors have a large overlap. This can be useful when Euclidean distance alone does not describe local cluster structure well, especially in data with irregular shapes or differing local densities.

Shared nearest neighbors

For each observation, first find its k nearest neighbors. The shared-neighbor similarity between two observations is the number of neighbors appearing in both lists. The package treats each observation as part of its own neighborhood when counting shared neighbors, and the resulting similarity is between 0 and k.

The algorithms use this information differently:

| Function | Link or neighborhood rule | Cluster formation | Noise model | |:--|:--|:--|:--| | jpclust() | Mutual nearest neighbors sharing at least kt neighbors | Connected components | No; isolated observations form small clusters | | sNNclust() | Mutual nearest neighbors sharing at least eps neighbors | DBSCAN-style core, border, and density-connected points | Yes; label 0 denotes noise |

The thresholds kt and eps are counts of shared neighbors. In particular, eps in sNNclust() is not a distance radius and has a different meaning from eps in dbscan().

Example data

We use the DS3 data supplied with the package. It contains six irregularly-shaped groups together with noise and a sinusoidal structure that intersects the groups.

data("DS3")
x <- as.matrix(DS3)

plot(x, pch = 19, cex = 0.25, asp = 1, main = "DS3 data")

For numeric matrices and data frames, both functions use Euclidean distance and fast kd-tree nearest-neighbor search. Variable scales therefore matter. Standardize variables when their units are not comparable and scaling is appropriate for the application.

Jarvis--Patrick clustering

Jarvis--Patrick clustering links two observations when both of the following conditions hold:

  1. Each observation is in the other's k-nearest-neighbor list.
  2. Their neighborhoods share at least kt neighbors.

Connected observations form clusters. Here we use neighborhoods of 20 points and require 12 shared neighbors.

jp <- jpclust(x, k = 20, kt = 12)
c(clusters = ncluster(jp), noise = nnoise(jp))

Cluster assignments are stored in jp$cluster in the same order as the rows of x. The result also records the algorithm name, distance metric, and parameters.

names(jp)
jp$param
head(jp$cluster)

The convenience function clplot() marks observations by cluster. It uses the first two columns when the data have more than two dimensions.

clplot(x, jp, cex = 0.25, main = "Jarvis-Patrick clustering")

Jarvis--Patrick clustering has no explicit concept of noise. Observations not connected to a larger group become singleton or other small clusters rather than receiving label 0. Conversely, a chain of qualifying links can join larger structures. In this example, the sinusoidal points can connect groups that otherwise appear separate.

Increasing kt makes the link rule stricter. This removes edges from the shared-neighbor graph and can split clusters or create more small components. Decreasing kt adds edges and can merge clusters through chaining.

jp_settings <- c(10, 12, 14)
jp_fits <- lapply(
  jp_settings,
  function(threshold) jpclust(x, k = 20, kt = threshold)
)

data.frame(
  kt = jp_settings,
  clusters = vapply(jp_fits, ncluster, integer(1)),
  singleton_clusters = vapply(
    jp_fits,
    function(fit) sum(table(fit$cluster) == 1L),
    integer(1)
  )
)

The required range is 1 <= kt <= k. A useful setting should preserve stable, meaningful components without fragmenting the data into many tiny clusters.

Shared nearest neighbor clustering

sNNclust() adds a density model to the shared-neighbor graph:

  1. Build k-nearest-neighbor lists and their shared-neighbor similarities.
  2. Retain mutual-neighbor relationships with similarity at least eps.
  3. Treat an observation as a core point if its SNN neighborhood contains at least minPts points.
  4. Join density-connected core points and optionally assign border points.

For the example, two neighbors must share at least 7 of their 20 neighbors, and an observation needs a sufficiently dense SNN neighborhood of at least 16 points to become a core point.

snn <- sNNclust(x, k = 20, eps = 7, minPts = 16)
snn

As with DBSCAN, positive integers are arbitrary cluster identifiers and 0 denotes noise.

clplot(x, snn, cex = 0.25, main = "Shared nearest neighbor clustering")

The parameters control different parts of the algorithm:

Parameter effects interact, so inspect several nearby settings rather than selecting each value independently.

snn_settings <- data.frame(
  eps = c(5, 7, 9),
  minPts = c(16, 16, 16)
)

snn_fits <- lapply(
  seq_len(nrow(snn_settings)),
  function(i) sNNclust(
    x,
    k = 20,
    eps = snn_settings$eps[i],
    minPts = snn_settings$minPts[i]
  )
)

transform(
  snn_settings,
  clusters = vapply(snn_fits, ncluster, integer(1)),
  noise = vapply(snn_fits, nnoise, integer(1))
)

Cluster counts alone do not identify the best solution. Examine whether the important groups persist, whether points labeled as noise are plausible, and whether conclusions are stable under small parameter changes.

Reuse a nearest-neighbor search

Nearest-neighbor search can be computed once and reused. This is especially helpful when comparing algorithms or parameter settings on a large data set. Compute at least the largest k that any later fit will need.

nn <- kNN(x, k = 30)

jp_from_nn <- jpclust(nn, k = 20, kt = 12)
snn_from_nn <- sNNclust(nn, k = 20, eps = 7, minPts = 16)

c(
  jp_same = identical(jp$cluster, jp_from_nn$cluster),
  snn_same = identical(snn$cluster, snn_from_nn$cluster)
)

When the full stored neighborhood should be used, k may be omitted from jpclust() because it can be recovered from the kNN object. Keep k explicit when using only the first part of a larger precomputed neighborhood.

Inspect the shared-neighbor graph

The lower-level sNN() function exposes the shared-neighbor calculation. Its shared matrix gives the similarity between each observation and each of its k nearest neighbors.

shared_nn <- sNN(nn, k = 20, jp = TRUE, sort = FALSE)
shared_nn
table(shared_nn$shared)

Using jp = TRUE reproduces the mutual-neighbor similarity used internally by sNNclust(). Use this distribution as a diagnostic when selecting a shared-neighbor threshold. sNN() can also apply a threshold directly and its graph can be inspected with adjacencylist() or plotted for small data sets; see ?sNN.

Use a precomputed distance matrix

Both clustering functions also accept a dist object. This permits other distance measures, at the cost of storing all pairwise distances and giving up the fast kd-tree search.

d <- dist(my_data, method = "manhattan")

jp_manhattan <- jpclust(d, k = 20, kt = 12)
snn_manhattan <- sNNclust(d, k = 20, eps = 7, minPts = 16)

The distance matrix must be finite. Choose a distance measure, transformations, and scaling based on the meaning of the variables rather than solely on the resulting number of clusters.

Which method should you use?

Use jpclust() when a simple connected-components interpretation of mutual shared-neighbor links is appropriate and small components are meaningful. Use sNNclust() when the analysis needs an explicit density requirement, noise labels, or control over core and border points. For either method, k sets the neighborhood scale and should reflect the smallest local structure that needs to be retained.

References

Jarvis, R. A. and Patrick, E. A. (1973). Clustering Using a Similarity Measure Based on Shared Near Neighbors. IEEE Transactions on Computers, 22(11), 1025--1034. doi:10.1109/T-C.1973.223640.

Ertoz, L., Steinbach, M., and Kumar, V. (2003). Finding Clusters of Different Sizes, Shapes, and Densities in Noisy, High Dimensional Data. Proceedings of the 2003 SIAM International Conference on Data Mining, 47--58. doi:10.1137/1.9781611972733.5.



Try the dbscan package in your browser

Any scripts or data that you put into this service are public.

dbscan documentation built on Oct. 5, 2026, 9:07 a.m.