Introduction
Density-Based Spatial Clustering of Applications with Noise (or DBSCAN) is an algorithm used in cluster analysis which is described in Wikipedia.
The basic idea of cluster analysis is to partition a set of points into clusters which have some relationship to each other. In the case of DBSCAN the user chooses the minimum number of points required to form a cluster and the maximum distance between points in each cluster. Each point is then considered in turn, along with its neighbours, and allocated to a cluster.
I was recently asked if I could implement this algorithm in C# as there appears to be no other implementation which is freely available.
The source code is listed below in case it is of interest to anyone else involved in this specialized field.
Source Code
using System;
using System.Collections.Generic;
using System.Linq;
class Point
{
public const int NOISE = -1;
public const int UNCLASSIFIED = 0;
public int X, Y, ClusterId;
public Point(int x, int y)
{
this.X = x;
this.Y = y;
this.ClusterId = UNCLASSIFIED;
}
public override string ToString()
{
return String.Format("({0}, {1})", X, Y);
}
public static int DistanceSquared(Point p1, Point p2)
{
int diffX = p2.X - p1.X;
int diffY = p2.Y - p1.Y;
return diffX * diffX + diffY * diffY;
}
}
static class DBSCAN
{
static void Main()
{
List<Point> points = new List<Point>();
// sample data
points.Add(new Point(0, 100));
points.Add(new Point(0, 200));
// ... (add more points)
double eps = 100.0;
int minPts = 3;
List<List<Point>> clusters = GetClusters(points, eps, minPts);
Console.Clear();
// Print points to console
Console.WriteLine("The {0} points are:\n", points.Count);
foreach (Point p in points)
Console.Write(" {0} ", p);
Console.WriteLine();
// Print clusters to console
int total = 0;
for (int i = 0; i < clusters.Count; i++)
{
int count = clusters[i].Count;
total += count;
string plural = (count != 1) ? "s" : "";
Console.WriteLine("\nCluster {0} consists of the following {1} point{2}:\n", i + 1, count, plural);
foreach (Point p in clusters[i])
Console.Write(" {0} ", p);
Console.WriteLine();
}
// Print any points which are NOISE
total = points.Count - total;
if (total > 0)
{
string plural = (total != 1) ? "s" : "";
string verb = (total != 1) ? "are" : "is";
Console.WriteLine("\nThe following {0} point{1} {2} NOISE:\n", total, plural, verb);
foreach (Point p in points)
{
if (p.ClusterId == Point.NOISE)
Console.Write(" {0} ", p);
}
Console.WriteLine();
}
else
{
Console.WriteLine("\nNo points are NOISE");
}
Console.ReadKey();
}
static List<List<Point>> GetClusters(List<Point> points, double eps, int minPts)
{
if (points == null)
return null;
List<List<Point>> clusters = new List<List<Point>>();
eps *= eps; // square eps
int clusterId = 1;
for (int i = 0; i < points.Count; i++)
{
Point p = points[i];
if (p.ClusterId == Point.UNCLASSIFIED)
{
if (ExpandCluster(points, p, clusterId, eps, minPts))
clusterId++;
}
}
// sort out points into their clusters, if any
int maxClusterId = points.OrderByDescending(p => p.ClusterId).First().ClusterId;
if (maxClusterId < 1)
return clusters; // no clusters, so list is empty
for (int i = 0; i < maxClusterId; i++)
clusters.Add(new List<Point>());
foreach (Point p in points)
{
if (p.ClusterId > 0)
clusters[p.ClusterId - 1].Add(p);
}
return clusters;
}
static List<Point> GetRegion(List<Point> points, Point p, double eps)
{
List<Point> region = new List<Point>();
for (int i = 0; i < points.Count; i++)
{
int distSquared = Point.DistanceSquared(p, points[i]);
if (distSquared <= eps)
region.Add(points[i]);
}
return region;
}
static bool ExpandCluster(List<Point> points, Point p, int clusterId, double eps, int minPts)
{
List<Point> seeds = GetRegion(points, p, eps);
if (seeds.Count < minPts) // no core point
{
p.ClusterId = Point.NOISE;
return false;
}
else // all points in seeds are density reachable from point 'p'
{
foreach (Point seed in seeds)
seed.ClusterId = clusterId;
seeds.Remove(p);
while (seeds.Count > 0)
{
Point currentP = seeds[0];
List<Point> result = GetRegion(points, currentP, eps);
if (result.Count >= minPts)
{
foreach (Point resultP in result)
{
if (resultP.ClusterId == Point.UNCLASSIFIED || resultP.ClusterId == Point.NOISE)
{
if (resultP.ClusterId == Point.UNCLASSIFIED)
seeds.Add(resultP);
resultP.ClusterId = clusterId;
}
}
}
seeds.Remove(currentP);
}
return true;
}
}
}
Output


R ZPosted Apr 30, 2020, 10:42 AM
Distance squared should be the sqrt of the sum.
Patel KamleshbhaiPosted Jan 10, 2017, 4:01 AM
I only need first and last element of cluster then what should i do?
Harshitha SolaiPosted Mar 27, 2016, 1:42 AM
Please ccan i get the source code for p-dbscan (photo ) clustering ... based on density threshold and adaptive density methods ?
Ken HPosted May 14, 2015, 11:34 PM
Good article.
Amal SalahPosted Apr 21, 2015, 12:10 PM
i just wanna say thanks that is really great
VulpesPosted Jun 3, 2014, 6:12 PM
I don't think my version is much different from yours but those points are very close together and for an epsilon of 1.0, there isn't in fact any noise. However, if you change epsilon to 0.05, one point will be noise.
VulpesPosted Jun 3, 2014, 6:09 PM
using System;using System.Collections.Generic; using System.Linq; class Point { public const int NOISE = -1; public const int UNCLASSIFIED = 0; public double X, Y; public int ClusterId; public Point(double x, double y) { this.X = x; this.Y = y; } public override string ToString() { return String.Format("({0}, {1})", X, Y); } public static double DistanceSquared(Point p1, Point p2) { double diffX = p2.X - p1.X; double diffY = p2.Y - p1.Y; return diffX * diffX diffY * diffY; } } static class DBSCAN { static void Main() { List<Point> points = new List<Point>(); // sample data points.Add(new Point(-70.603252, 41.437540)); points.Add(new Point(-70.529638, 41.393986)); points.Add(new Point(-70.617956, 41.456391)); points.Add(new Point(-70.617956, 41.456391)); points.Add(new Point(-70.604605, 41.458231)); points.Add(new Point(-70.604605, 41.458231)); points.Add(new Point(-70.509126, 41.392920)); points.Add(new Point(-70.541246, 41.377458)); points.Add(new Point(-70.662396, 41.421751)); points.Add(new Point(-70.510349, 41.392725)); points.Add(new Point(-70.510349, 41.392725)); points.Add(new Point(-70.528652, 41.360732)); points.Add(new Point(-70.527980, 41.376286)); points.Add(new Point(-70.549138, 41.386424)); double eps = 1.0; int minPts = 3; List<List<Point>> clusters = GetClusters(points, eps, minPts); Console.Clear(); // print points to console Console.WriteLine("The {0} points are :\n", points.Count); foreach (Point p in points) Console.Write(" {0} ", p); Console.WriteLine(); // print clusters to console int total = 0; for (int i = 0; i < clusters.Count; i ) { int count = clusters[i].Count; total = count; string plural = (count != 1) ? "s" : ""; Console.WriteLine("\nCluster {0} consists of the following {1} point{2} :\n", i 1, count, plural); foreach (Point p in clusters[i]) Console.Write(" {0} ", p); Console.WriteLine(); } // print any points which are NOISE total = points.Count - total; if (total > 0) { string plural = (total != 1) ? "s" : ""; string verb = (total != 1) ? "are" : "is"; Console.WriteLine("\nThe following {0} point{1} {2} NOISE :\n", total, plural, verb); foreach (Point p in points) { if (p.ClusterId == Point.NOISE) Console.Write(" {0} ", p); } Console.WriteLine(); } else { Console.WriteLine("\nNo points are NOISE"); } Console.ReadKey(); } static List<List<Point>> GetClusters(List<Point> points, double eps, int minPts) { if (points == null) return null; List<List<Point>> clusters = new List<List<Point>>(); eps *= eps; // square eps int clusterId = 1; for (int i = 0; i < points.Count; i ) { Point p = points[i]; if (p.ClusterId == Point.UNCLASSIFIED) { if (ExpandCluster(points, p, clusterId, eps, minPts)) clusterId ; } } // sort out points into their clusters, if any int maxClusterId = points.OrderBy(p => p.ClusterId).Last().ClusterId; if (maxClusterId < 1) return clusters; // no clusters, so list is empty for (int i = 0; i < maxClusterId; i ) clusters.Add(new List<Point>()); foreach (Point p in points) { if (p.ClusterId > 0) clusters[p.ClusterId - 1].Add(p); } return clusters; } static List<Point> GetRegion(List<Point> points, Point p, double eps) { List<Point> region = new List<Point>(); for (int i = 0; i < points.Count; i ) { double distSquared = Point.DistanceSquared(p, points[i]); if (distSquared <= eps) region.Add(points[i]); } return region; } static bool ExpandCluster(List<Point> points, Point p, int clusterId, double eps, int minPts) { List<Point> seeds = GetRegion(points, p, eps); if (seeds.Count < minPts) // no core point { p.ClusterId = Point.NOISE; return false; } else // all points in seeds are density reachable from point 'p' { for (int i = 0; i < seeds.Count; i ) seeds[i].ClusterId = clusterId; seeds.Remove(p); while (seeds.Count > 0) { Point currentP = seeds[0]; List<Point> result = GetRegion(points, currentP, eps); if (result.Count >= minPts) { for (int i = 0; i < result.Count; i ) { Point resultP = result[i]; if (resultP.ClusterId == Point.UNCLASSIFIED || resultP.ClusterId == Point.NOISE) { if (resultP.ClusterId == Point.UNCLASSIFIED) seeds.Add(resultP); resultP.ClusterId = clusterId; } } } seeds.Remove(currentP); } return true; } } }
VulpesPosted Jun 2, 2014, 7:08 PM
Although I did write this on the assumption that the points would be integers, when I changed X and Y in the Point class to double and the various distance calculations (including the DistanceSquared method) to double as well, the results (with integral points) were exactly the same as before with 3 points being noise. So I think you must have made an error somewhere when converting it.
GrzegorzPosted Apr 2, 2014, 7:17 AM
I want to use this implementation with UCI dataset => http://archive.ics.uci.edu/ml/datasets.html. What value of epsilon and MinPts I must set ? Because every time when I used I got only 2 groups created for result - i.e for IRIS datset, I can't created more then 2 groups. I think that this is problem with wrong values of this 2 params.
Roatin MartheditedPosted Apr 3, 2013, 12:17 PMEdited Apr 3, 2013, 12:48 PM
.
VladPosted Feb 3, 2013, 11:31 AM
anyone verified the implementation?
waleed makaremeditedPosted Jul 8, 2011, 4:22 AMEdited Jul 9, 2011, 6:55 AM
Thanks so much for your genius code . I was searching for it for the last 2 weeks . Thanks,but are there update to include rough dbscan . I used it with large dataset of 10000 point , and it tooks 25 seconds. Are there a faster updated. Thanks, [email protected]