Skip to content

Instantly share code, notes, and snippets.

@cmkilger
Created October 18, 2017 23:01
Show Gist options
  • Save cmkilger/d2cac33df9ae8203d8f1ec005b8fece7 to your computer and use it in GitHub Desktop.
Save cmkilger/d2cac33df9ae8203d8f1ec005b8fece7 to your computer and use it in GitHub Desktop.
OPTICS algorithm in Swift
import Foundation
public protocol Distanceable {
associatedtype Distance: Comparable
func distance(_ other: Self) -> Distance
}
public class Clusterer {
private class Point<ValueType: Distanceable> {
let value: ValueType
var isProcessed = false
var coreDistance: ValueType.Distance?
var reachabilityDistance: ValueType.Distance?
init(_ value: ValueType) {
self.value = value
}
func distance(_ point: Point<ValueType>) -> ValueType.Distance {
return value.distance(point.value)
}
func setCoreDistance(neighbors: [Point], maxDistance: ValueType.Distance, minPoints: Int) {
guard minPoints > 0 && minPoints < neighbors.count else { return }
coreDistance = neighbors.map({ $0.distance(self) }).sorted()[minPoints-1]
}
func neighbors(points: [Point], maxDistance: ValueType.Distance) -> [Point] {
return points.filter { $0.distance(self) <= maxDistance }
}
}
public static func cluster<ValueType: Distanceable>(values: [ValueType], maxDistance: ValueType.Distance, minPoints: Int) -> (clusters: [[ValueType]], noise: [ValueType]) {
let points = values.map { Point($0) }
var orderedFile = [Point<ValueType>]()
for point in points {
if !point.isProcessed {
expandClusterOrder(points: points, point: point, maxDistance: maxDistance, minPoints: minPoints, orderedFile: &orderedFile)
}
}
return extractClusters(clusterOrderedPoints: orderedFile, maxDistance: maxDistance, minPoints: minPoints)
}
private static func expandClusterOrder<ValueType>(points: [Point<ValueType>], point: Point<ValueType>, maxDistance: ValueType.Distance, minPoints: Int, orderedFile: inout [Point<ValueType>]) {
var neighbors = point.neighbors(points: points, maxDistance: maxDistance)
point.isProcessed = true
point.reachabilityDistance = nil
point.setCoreDistance(neighbors: neighbors, maxDistance: maxDistance, minPoints: minPoints)
orderedFile.append(point)
var seeds = [Point<ValueType>]()
if point.coreDistance != nil {
update(neighbors: neighbors, currentPoint: point, seeds: &seeds)
while !seeds.isEmpty {
let currentObject = seeds.removeFirst()
neighbors = currentObject.neighbors(points: points, maxDistance: maxDistance)
currentObject.isProcessed = true
currentObject.setCoreDistance(neighbors: neighbors, maxDistance: maxDistance, minPoints: minPoints)
orderedFile.append(currentObject)
if currentObject.coreDistance != nil {
update(neighbors: neighbors, currentPoint: currentObject, seeds: &seeds)
}
}
}
}
private static func update<ValueType>(neighbors: [Point<ValueType>], currentPoint: Point<ValueType>, seeds: inout [Point<ValueType>]) {
let coreDistance = currentPoint.coreDistance!
for point in neighbors {
if !point.isProcessed {
let rechabilityDistance = max(coreDistance, currentPoint.distance(point))
if point.reachabilityDistance == nil {
point.reachabilityDistance = rechabilityDistance
seeds.append(point)
} else if rechabilityDistance < point.reachabilityDistance! {
point.reachabilityDistance = rechabilityDistance
}
}
}
seeds.sort {$0.reachabilityDistance! < $1.reachabilityDistance! }
}
private static func extractClusters<ValueType>(clusterOrderedPoints: [Point<ValueType>], maxDistance: ValueType.Distance, minPoints: Int) -> (clusters: [[ValueType]], noise: [ValueType]) {
var noise = [ValueType]()
var clusters = [[ValueType]]()
for point in clusterOrderedPoints {
if let reachabilityDistance = point.reachabilityDistance, reachabilityDistance <= maxDistance {
clusters[clusters.count-1].append(point.value)
} else if let coreDistance = point.coreDistance, coreDistance <= maxDistance {
clusters.append([point.value])
} else {
noise.append(point.value)
}
}
return (clusters, noise)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment