sdbscan
Version:
DBSCAN implementation (density based spatial clustering) for javascript. Works in node and browser
150 lines (128 loc) • 3.33 kB
JavaScript
const
Distance = require("./distance.js"),
eudist = Distance.eudist;
/*
DBSCAN(D, epsilon, min_points):
C = 0
for each unvisited point P in dataset
mark P as visited
sphere_points = regionQuery(P, epsilon)
if sizeof(sphere_points) < min_points
ignore P
else
C = next cluster
expandCluster(P, sphere_points, C, epsilon, min_points)
expandCluster(P, sphere_points, C, epsilon, min_points):
add P to cluster C
for each point P’ in sphere_points
if P’ is not visited
mark P’ as visited
sphere_points’ = regionQuery(P’, epsilon)
if sizeof(sphere_points’) >= min_points
sphere_points = sphere_points joined with sphere_points’
if P’ is not yet member of any cluster
add P’ to cluster C
regionQuery(P, epsilon):
return all points within the n-dimensional sphere centered at P with radius epsilon (including P)
*/
class Point {
constructor(v,idx) {
this.v = v;
this.idx = idx || 0;
this.k = 0;
this.visited = false;
}
}
class DBScan {
constructor(data,eps,min) {
this._multi = data[0].length>0;
this._data = this.initData(data);
this._eps = eps;
this._min = min;
}
initData(data) {
let ret = [], len = data.length;
let multi = this._multi;
for(let i=0;i<len;i++) {
ret.push(new Point(multi? data[i] : [data[i]], i));
}
return ret;
}
regionQuery(p) {
let eps = this._eps, data = this._data,
ret = [], len = data.length;
for(let i=0;i<len;i++) {
let np = data[i];
if(np!=p && np.visited) continue;
if(eudist(np.v,p.v,true) <= eps)
ret.push(np);
}
return ret;
}
expandCluster(p, region, k) {
let eps = this._eps, data = this._data, min = this._min;
// Add p to cluster k
p.k = k.id;
k.data.push(p.v);
// region.length is dynamic becouse items added
// from newRegion to region
for(let j=0;j<region.length;j++) {
let np = region[j];
if(!np.visited) {
np.visited = true;
let
newRegion = this.regionQuery(np),
rlen = newRegion.length;
if(rlen >= min) {
for(let i=0;i<rlen;i++)
region.push(newRegion[i]);
}
if(!np.k) {
np.k = k.id;
k.data.push(np.v);
}
}
}
}
dbscan() {
let data = this._data, min = this._min,
len = data.length,
kid = 0,
ks = [], // Clusters
noise = [], // Noise
k = null; // Current cluster
for(let j=0;j<len;j++) {
let p = data[j];
if(!p.visited) {
// Mark as visited
p.visited = true;
// Get the reachable region for this point
let region = this.regionQuery(p);
// Too small region
if(region.length<min) {
noise.push(p);
}
else {
k = {id:kid++, data:[]};
ks.push(k);
this.expandCluster(p, region, k);
}
}
}
// Restore unidimiensional data that was transformed to
// multidimensional for the algoryth purposes
if(!this._multi) {
ks.forEach(k=>{
k.data = k.data.map(v=>v[0]);
});
noise.forEach(p=>p.v=p.v[0]);
}
return {
noise : noise.map(p=>p.v),
clusters : ks
}
}
}
module.exports = function(data,eps,min) {
return (new DBScan(data,eps,min)).dbscan();
}