20 struct ClusterNodeStats {
21 double densityIndicator;
23 double nearestDistance;
32 std::unordered_map<size_t, bool> visitedNodes;
33 std::unordered_map<size_t, ClusterNodeStats> clusterNodeStats;
34 mutable std::unordered_map<size_t, double> distanceCache;
39 double attributeValue;
41 bool operator==(
const ClusterNode & other)
const {
42 return this->
id == other.id;
46 struct ClusterNodeHash {
47 size_t operator()(
const ClusterNode & node)
const {
52 auto ClusterNodeOrdering()
const noexcept {
53 return [
this](
const ClusterNode & lhs,
const ClusterNode & rhs) {
54 const auto &lhsStats = clusterNodeStats.at(lhs.id);
55 const auto &rhsStats = clusterNodeStats.at(rhs.id);
58 return lhsStats.densityIndicator > rhsStats.densityIndicator;
60 return lhsStats.meanDistance < rhsStats.meanDistance;
64 auto ClusterNodeOrderingReverse()
const noexcept {
65 return [ordering = ClusterNodeOrdering()](
const ClusterNode & lhs,
const ClusterNode & rhs) {
66 return not ordering(rhs, lhs);
70 double spatial_distance(
const ClusterNode & lhs,
const ClusterNode & rhs)
const noexcept {
72 if (distanceCache.contains(hash)) {
73 return distanceCache.at(hash);
75 double dist = distanceFunction(lhs.node, rhs.node);
76 distanceCache[hash] = dist;
80 bool inline spatially_directly_reachable(
const ClusterNode & p,
const ClusterNode & q)
const noexcept {
81 return spatial_distance(p, q) <= eps;
84 double inline attribute_distance(
const ClusterNode & p,
const ClusterNode & q)
const noexcept {
85 return std::fabs(p.attributeValue - q.attributeValue);
89 double avg_clu =
fishnet::math::avg(temporalCluster, &ClusterNode::attributeValue).value_or_throw();
90 return std::fabs(q.attributeValue - avg_clu) <= T1;
95 if constexpr (std::remove_cvref_t<
decltype(graph)>::edge_type::isDirected()) {
104 auto clusterGraph = getClusterGraphType(graph);
105 size_t nodeIndex = 0;
106 std::unordered_map<T, ClusterNode> nodeMap;
107 for(
const auto & node : graph.getNodes()){
108 nodeMap.try_emplace(node, ClusterNode{
111 .attributeValue = attributeExtractor(node)
114 clusterGraph.addNodes(std::views::values(nodeMap));
115 for(
const auto & node: graph.getNodes()){
116 for(
const auto & nbr: graph.getNeighbours(node)){
117 clusterGraph.addEdge(nodeMap.at(node), nodeMap.at(nbr));
125 for(
const ClusterNode & clusterNode : graph.getNodes()){
126 visitedNodes[clusterNode.id] =
false;
127 auto neighbors = graph.getNeighbours(clusterNode);
130 clusterNodeStats[clusterNode.id] = ClusterNodeStats{
131 .densityIndicator = 0.0,
133 .nearestDistance = 0.0
137 double N_SDR =
static_cast<double>(std::ranges::count_if(neighbors, [&](
const auto & nbr) {
138 return spatially_directly_reachable(clusterNode, nbr);
140 const ClusterNode & nearestNeighbour = *std::ranges::min_element(neighbors, [&](
const auto & lhs,
const auto & rhs) {
141 return spatial_distance(clusterNode, lhs) < spatial_distance(clusterNode, rhs);
143 double nearestDistance = attribute_distance(clusterNode, nearestNeighbour);
145 return attribute_distance(clusterNode, nbr);
147 double densityIndicator = N_SDR + N_SDR/N_size;
148 clusterNodeStats[clusterNode.id] = ClusterNodeStats{
149 .densityIndicator = densityIndicator,
150 .meanDistance = meanDistance,
151 .nearestDistance = nearestDistance
154 double avgAttributeDiff =
fishnet::math::mean(std::views::values(clusterNodeStats),&ClusterNodeStats::nearestDistance).value_or_throw(
"Illegal state, graph has at least one node, so attributeDiffs should not be empty");
155 double stdAttributeDiff =
fishnet::math::std(std::views::values(clusterNodeStats), &ClusterNodeStats::nearestDistance).value_or_throw(
"Illegal state, graph has at least one node, so attributeDiffs should not be empty");
156 const double T1 =
fishnet::math::mean(std::views::values(clusterNodeStats) | std::views::filter([avgAttributeDiff, stdAttributeDiff](
const ClusterNodeStats & data) {
157 return avgAttributeDiff - 3 * stdAttributeDiff <= data.nearestDistance && data.nearestDistance <= avgAttributeDiff + 3 * stdAttributeDiff;
158 }), &ClusterNodeStats::nearestDistance).value_or_throw(
"Illegal state, there should be at least one non-outlier attribute difference, so the filtered range should not be empty");
162 bool is_expanding_core(
const ClusterNode & node,
fishnet::graph::Graph auto const & graph)
const noexcept {
163 bool visited = this->visited(node);
164 size_t counter= std::ranges::count_if(graph.getNeighbours(node), [
this,&node](
const auto & nbr) {
165 return spatially_directly_reachable(node, nbr);
167 return !visited && counter > 0;
170 bool visited(
const ClusterNode & node)
const {
171 return visitedNodes.at(node.id);
174 void markVisited(
const ClusterNode & node) {
175 visitedNodes[node.id] =
true;
179 std::vector<ClusterNode> cluster;
180 cluster.push_back(core);
181 std::priority_queue<ClusterNode,std::vector<ClusterNode>,
decltype(ClusterNodeOrderingReverse())> expandingCores(ClusterNodeOrderingReverse());
182 expandingCores.push(core);
183 while (!expandingCores.empty()) {
184 const ClusterNode current = expandingCores.top();
185 expandingCores.pop();
186 for (
const ClusterNode & nbr : graph.getNeighbours(current)) {
187 if (!visited(nbr) && spatially_directly_reachable(current,nbr) && reachable(nbr, cluster)) {
188 if (is_expanding_core(nbr, graph)){
189 expandingCores.push(nbr);
192 cluster.push_back(nbr);
201 : eps(eps), beta(beta), minPts(minPts), distanceFunction(
std::forward<
fishnet::util::BiFunction_t<T,T,double>>(distanceFunction)), attributeExtractor(
std::forward<
fishnet::util::UnaryFunction_t<T, double>>(attributeExtractor)) {}
204 :eps(eps), beta(beta), minPts(minPts), distanceFunction(
std::forward<
fishnet::util::BiFunction_t<T,T,double>>(distanceFunction)), attributeExtractor([](){
return 0.0;}) {}
211 static_assert(std::derived_from<
typename std::decay_t<
decltype(input_graph)>::node_type, T>,
"Graph node type must be compatible with DBSC point type");
212 auto graph = getClusterGraph(input_graph);
213 auto initalEdges = graph.getEdges();
214 auto edgeDistanceMapper = [
this](
const auto & edge) {
return spatial_distance(edge.getFrom(), edge.getTo());};
215 const double global_mean =
fishnet::math::mean(initalEdges, edgeDistanceMapper).value_or(0.0);
216 const double global_variation =
fishnet::math::std(initalEdges, edgeDistanceMapper, global_mean).value_or(0.0);
217 auto local_mean = [&](
const ClusterNode & node) ->
double{
219 return spatial_distance(node, nbr);
222 auto local_variation = [&](
const ClusterNode & node) ->
double{
224 return spatial_distance(node, nbr);
229 std::vector<std::pair<ClusterNode, ClusterNode>> edgesToRemove;
230 for(
const auto & node : graph.getNodes()){
231 double global_distance_constraint = global_mean + (global_mean/local_mean(node)) * global_variation;
232 for(
const auto & nbr : graph.getNeighbours(node)){
233 if(spatial_distance(node, nbr) > global_distance_constraint){
234 edgesToRemove.emplace_back(node, nbr);
238 std::ranges::for_each(edgesToRemove, [&graph](
const auto & edge){
239 graph.removeEdge(edge.first, edge.second);
242 edgesToRemove.clear();
244 for (
const auto & node: graph.getNodes()){
246 auto order2_mean =
fishnet::math::mean(order2_graph.getEdges(), edgeDistanceMapper).value_or(0.0);
247 auto mean_local_variation =
fishnet::math::mean(order2_graph.getNodes(), local_variation).value_or(0.0);
248 auto local_distance_constraint = order2_mean +
static_cast<double>(beta) * mean_local_variation;
249 for(
const auto & edge: order2_graph.getEdges()){
250 if(spatial_distance(edge.getFrom(), edge.getTo()) > local_distance_constraint){
251 edgesToRemove.emplace_back(edge.getFrom(), edge.getTo());
255 std::ranges::for_each(edgesToRemove, [&graph](
const auto & edge){
256 graph.removeEdge(edge.first, edge.second);
259 double calculatedT1 =prepareClusteringData(graph);
260 this->T1 = std::isnan(this->T1) ? calculatedT1 : this->T1;
261 std::vector<ClusterNode> clusteringData(graph.getNodes().begin(), graph.getNodes().end());
262 std::ranges::sort(clusteringData, ClusterNodeOrdering());
263 assert(!std::isnan(this->T1) &&
"T1 should have been set either by user or calculated from the data, but it is still NaN, indicating an illegal state");
266 for(
const ClusterNode & core : clusteringData){
270 auto result = expand(core, graph);
274 std::ranges::for_each(result, [&clusterResult](
auto && clusterNode){
275 clusterResult.
noise.push_back(std::move(clusterNode.node));
279 return clusterResult;