---
title: "Nearest-Neighbor Clustering with jpclust and sNNclust"
author: "Michael Hahsler"
output:
  rmarkdown::html_vignette:
    toc: true
vignette: >
  %\VignetteIndexEntry{Nearest-Neighbor Clustering with jpclust and sNNclust}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r setup, include=FALSE}
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:

* `jpclust()` implements Jarvis--Patrick clustering; and
* `sNNclust()` implements shared nearest neighbor (SNN) clustering with a
  DBSCAN-style density step.

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.

```{r data}
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.

```{r jp-fit}
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.

```{r jp-result}
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.

```{r jp-plot}
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.

```{r jp-sensitivity}
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.

```{r snn-fit}
snn <- sNNclust(x, k = 20, eps = 7, minPts = 16)
snn
```

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

```{r snn-plot}
clplot(x, snn, cex = 0.25, main = "Shared nearest neighbor clustering")
```

The parameters control different parts of the algorithm:

* `k` determines the scale at which local neighborhoods are compared. Small
  values emphasize very local structure; large values smooth the similarities
  over a wider neighborhood.
* `eps` is the minimum number of shared neighbors needed for an SNN
  relationship. Increasing it makes similarity more selective.
* `minPts` is the density requirement in the thresholded SNN graph. Increasing
  it makes core-point status more difficult to attain and generally produces
  more noise.
* `borderPoints = TRUE`, the default, assigns non-core observations adjacent
  to a core cluster. Set it to `FALSE` for a core-points-only result analogous
  to DBSCAN*.

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

```{r snn-sensitivity}
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.

```{r reuse-knn}
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.

```{r inspect-snn}
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.

```{r other-distance, eval=FALSE}
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](https://doi.org/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](https://doi.org/10.1137/1.9781611972733.5).
