├── .gitignore ├── .travis.yml ├── README.md ├── dbscan.go └── dbscan_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - tip 4 | sudo: false 5 | notifications: 6 | email: false -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-dbscan 2 | [![Build Status](https://travis-ci.org/sohlich/go-dbscan.svg?branch=master)](https://travis-ci.org/sohlich/go-dbscan) 3 | Implementation of DBSCAN clustering algorithm in Go lang. 4 | This library should be usable for various structures. 5 | -------------------------------------------------------------------------------- /dbscan.go: -------------------------------------------------------------------------------- 1 | package dbscan 2 | 3 | const ( 4 | NOISE = false 5 | CLUSTERED = true 6 | ) 7 | 8 | type Clusterable interface { 9 | Distance(c interface{}) float64 10 | GetID() string 11 | } 12 | 13 | type Cluster []Clusterable 14 | 15 | func Clusterize(objects []Clusterable, minPts int, eps float64) []Cluster { 16 | clusters := make([]Cluster, 0) 17 | visited := map[string]bool{} 18 | for _, point := range objects { 19 | neighbours := findNeighbours(point, objects, eps) 20 | if len(neighbours)+1 >= minPts { 21 | visited[point.GetID()] = CLUSTERED 22 | cluster := make(Cluster, 1) 23 | cluster[0] = point 24 | cluster = expandCluster(cluster, neighbours, visited, minPts, eps) 25 | 26 | if len(cluster) >= minPts { 27 | clusters = append(clusters, cluster) 28 | } 29 | } else { 30 | visited[point.GetID()] = NOISE 31 | } 32 | } 33 | return clusters 34 | } 35 | 36 | //Finds the neighbours from given array 37 | //depends on Eps variable, which determines 38 | //the distance limit from the point 39 | func findNeighbours(point Clusterable, points []Clusterable, eps float64) []Clusterable { 40 | neighbours := make([]Clusterable, 0) 41 | for _, potNeigb := range points { 42 | if point.GetID() != potNeigb.GetID() && potNeigb.Distance(point) <= eps { 43 | neighbours = append(neighbours, potNeigb) 44 | } 45 | } 46 | return neighbours 47 | } 48 | 49 | //Try to expand existing clutser 50 | func expandCluster(cluster Cluster, neighbours []Clusterable, visited map[string]bool, minPts int, eps float64) Cluster { 51 | seed := make([]Clusterable, len(neighbours)) 52 | copy(seed, neighbours) 53 | for _, point := range seed { 54 | pointState, isVisited := visited[point.GetID()] 55 | if !isVisited { 56 | currentNeighbours := findNeighbours(point, seed, eps) 57 | if len(currentNeighbours)+1 >= minPts { 58 | visited[point.GetID()] = CLUSTERED 59 | cluster = merge(cluster, currentNeighbours) 60 | } 61 | } 62 | 63 | if isVisited && pointState == NOISE { 64 | visited[point.GetID()] = CLUSTERED 65 | cluster = append(cluster, point) 66 | } 67 | } 68 | 69 | return cluster 70 | } 71 | 72 | func merge(one []Clusterable, two []Clusterable) []Clusterable { 73 | mergeMap := make(map[string]Clusterable) 74 | putAll(mergeMap, one) 75 | putAll(mergeMap, two) 76 | merged := make([]Clusterable, 0) 77 | for _, val := range mergeMap { 78 | merged = append(merged, val) 79 | } 80 | 81 | return merged 82 | } 83 | 84 | //Function to add all values from list to map 85 | //map keys is then the unique collecton from list 86 | func putAll(m map[string]Clusterable, list []Clusterable) { 87 | for _, val := range list { 88 | m[val.GetID()] = val 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /dbscan_test.go: -------------------------------------------------------------------------------- 1 | package dbscan 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "math" 7 | "testing" 8 | ) 9 | 10 | type SimpleClusterable struct { 11 | position float64 12 | } 13 | 14 | func (s SimpleClusterable) Distance(c interface{}) float64 { 15 | distance := math.Abs(c.(SimpleClusterable).position - s.position) 16 | return distance 17 | } 18 | 19 | func (s SimpleClusterable) GetID() string { 20 | return fmt.Sprint(s.position) 21 | } 22 | 23 | func TestPutAll(t *testing.T) { 24 | testMap := make(map[string]Clusterable) 25 | clusterList := []Clusterable{ 26 | SimpleClusterable{10}, 27 | SimpleClusterable{12}, 28 | } 29 | putAll(testMap, clusterList) 30 | mapSize := len(testMap) 31 | if mapSize != 2 { 32 | t.Errorf("Map does not contain expected size 2 but was %d", mapSize) 33 | } 34 | } 35 | 36 | //Test find neighbour function 37 | func TestFindNeighbours(t *testing.T) { 38 | log.Println("Executing TestFindNeighbours") 39 | clusterList := []Clusterable{ 40 | SimpleClusterable{0}, 41 | SimpleClusterable{1}, 42 | SimpleClusterable{-1}, 43 | SimpleClusterable{1.5}, 44 | SimpleClusterable{-0.5}, 45 | } 46 | 47 | eps := 1.0 48 | neighbours := findNeighbours(clusterList[0], clusterList, eps) 49 | 50 | assertEquals(t, 3, len(neighbours)) 51 | } 52 | 53 | func TestMerge(t *testing.T) { 54 | log.Println("Executing TestMerge") 55 | expected := 6 56 | one := []Clusterable{ 57 | SimpleClusterable{0}, 58 | SimpleClusterable{1}, 59 | SimpleClusterable{2.1}, 60 | SimpleClusterable{2.2}, 61 | SimpleClusterable{2.3}, 62 | } 63 | 64 | two := []Clusterable{ 65 | one[0], 66 | one[1], 67 | SimpleClusterable{2.4}, 68 | } 69 | 70 | output := merge(one, two) 71 | assertEquals(t, expected, len(output)) 72 | } 73 | 74 | func TestExpandCluster(t *testing.T) { 75 | log.Println("Executing TestExpandCluster") 76 | expected := 4 77 | clusterList := []Clusterable{ 78 | SimpleClusterable{0}, 79 | SimpleClusterable{1}, 80 | SimpleClusterable{2}, 81 | SimpleClusterable{2.1}, 82 | SimpleClusterable{5}, 83 | } 84 | 85 | eps := 1.0 86 | minPts := 3 87 | visitMap := make(map[string]bool) 88 | cluster := make(Cluster, 0) 89 | cluster = expandCluster(cluster, clusterList, visitMap, minPts, eps) 90 | assertEquals(t, expected, len(cluster)) 91 | } 92 | 93 | func TestClusterize(t *testing.T) { 94 | log.Println("Executing TestClusterize") 95 | clusterList := []Clusterable{ 96 | SimpleClusterable{1}, 97 | SimpleClusterable{0.5}, 98 | SimpleClusterable{0}, 99 | SimpleClusterable{5}, 100 | SimpleClusterable{4.5}, 101 | SimpleClusterable{4}, 102 | } 103 | eps := 1.0 104 | minPts := 2 105 | clusters := Clusterize(clusterList, minPts, eps) 106 | assertEquals(t, 2, len(clusters)) 107 | if 2 == len(clusters) { 108 | assertEquals(t, 3, len(clusters[0])) 109 | assertEquals(t, 3, len(clusters[1])) 110 | } 111 | } 112 | 113 | func TestClusterizeNoData(t *testing.T) { 114 | log.Println("Executing TestClusterizeNoData") 115 | clusterList := []Clusterable{} 116 | eps := 1.0 117 | minPts := 3 118 | clusters := Clusterize(clusterList, minPts, eps) 119 | assertEquals(t, 0, len(clusters)) 120 | } 121 | 122 | //Assert function. If the expected value not equals result, function 123 | //returns error. 124 | func assertEquals(t *testing.T, expected, result int) { 125 | if expected != result { 126 | t.Errorf("Expected %d but got %d", expected, result) 127 | } 128 | } 129 | --------------------------------------------------------------------------------