fast-hdbscan 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fast_hdbscan/__init__.py +10 -0
- fast_hdbscan/boruvka.py +299 -0
- fast_hdbscan/cluster_trees.py +680 -0
- fast_hdbscan/disjoint_set.py +56 -0
- fast_hdbscan/hdbscan.py +408 -0
- fast_hdbscan/numba_kdtree.py +220 -0
- fast_hdbscan-0.2.0.dist-info/LICENSE +24 -0
- fast_hdbscan-0.2.0.dist-info/METADATA +125 -0
- fast_hdbscan-0.2.0.dist-info/RECORD +11 -0
- fast_hdbscan-0.2.0.dist-info/WHEEL +5 -0
- fast_hdbscan-0.2.0.dist-info/top_level.txt +1 -0
fast_hdbscan/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from .hdbscan import HDBSCAN, fast_hdbscan
|
|
2
|
+
|
|
3
|
+
# Force JIT compilation on import
|
|
4
|
+
import numpy as np
|
|
5
|
+
random_state = np.random.RandomState(42)
|
|
6
|
+
random_data = random_state.random(size=(100, 3))
|
|
7
|
+
HDBSCAN(allow_single_cluster=True).fit(random_data)
|
|
8
|
+
HDBSCAN(cluster_selection_method="leaf").fit(random_data)
|
|
9
|
+
|
|
10
|
+
__all__ = ["HDBSCAN", "fast_hdbscan"]
|
fast_hdbscan/boruvka.py
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import numba
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
from .disjoint_set import ds_rank_create, ds_find, ds_union_by_rank
|
|
5
|
+
from .numba_kdtree import parallel_tree_query, rdist, point_to_node_lower_bound_rdist
|
|
6
|
+
|
|
7
|
+
@numba.njit(locals={"i": numba.types.int64})
|
|
8
|
+
def merge_components(disjoint_set, candidate_neighbors, candidate_neighbor_distances, point_components):
|
|
9
|
+
component_edges = {np.int64(0): (np.int64(0), np.int64(1), np.float32(0.0)) for i in range(0)}
|
|
10
|
+
|
|
11
|
+
# Find the best edges from each component
|
|
12
|
+
for i in range(candidate_neighbors.shape[0]):
|
|
13
|
+
from_component = np.int64(point_components[i])
|
|
14
|
+
if from_component in component_edges:
|
|
15
|
+
if candidate_neighbor_distances[i] < component_edges[from_component][2]:
|
|
16
|
+
component_edges[from_component] = (np.int64(i), np.int64(candidate_neighbors[i]), candidate_neighbor_distances[i])
|
|
17
|
+
else:
|
|
18
|
+
component_edges[from_component] = (np.int64(i), np.int64(candidate_neighbors[i]), candidate_neighbor_distances[i])
|
|
19
|
+
|
|
20
|
+
result = np.empty((len(component_edges), 3), dtype=np.float64)
|
|
21
|
+
result_idx = 0
|
|
22
|
+
|
|
23
|
+
# Add the best edges to the edge set and merge the relevant components
|
|
24
|
+
for edge in component_edges.values():
|
|
25
|
+
from_component = ds_find(disjoint_set, np.int32(edge[0]))
|
|
26
|
+
to_component = ds_find(disjoint_set, np.int32(edge[1]))
|
|
27
|
+
if from_component != to_component:
|
|
28
|
+
result[result_idx] = (np.float64(edge[0]), np.float64(edge[1]), np.float64(edge[2]))
|
|
29
|
+
result_idx += 1
|
|
30
|
+
|
|
31
|
+
ds_union_by_rank(disjoint_set, from_component, to_component)
|
|
32
|
+
|
|
33
|
+
return result[:result_idx]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@numba.njit(parallel=True)
|
|
37
|
+
def update_component_vectors(tree, disjoint_set, node_components, point_components):
|
|
38
|
+
for i in numba.prange(point_components.shape[0]):
|
|
39
|
+
point_components[i] = ds_find(disjoint_set, np.int32(i))
|
|
40
|
+
|
|
41
|
+
for i in range(tree.node_data.shape[0] - 1, -1, -1):
|
|
42
|
+
node_info = tree.node_data[i]
|
|
43
|
+
|
|
44
|
+
# Case 1:
|
|
45
|
+
# If the node is a leaf we need to check that every point
|
|
46
|
+
# in the node is of the same component
|
|
47
|
+
if node_info.is_leaf:
|
|
48
|
+
candidate_component = point_components[tree.idx_array[node_info.idx_start]]
|
|
49
|
+
for j in range(node_info.idx_start + 1, node_info.idx_end):
|
|
50
|
+
idx = tree.idx_array[j]
|
|
51
|
+
if point_components[idx] != candidate_component:
|
|
52
|
+
break
|
|
53
|
+
else:
|
|
54
|
+
node_components[i] = candidate_component
|
|
55
|
+
|
|
56
|
+
# Case 2:
|
|
57
|
+
# If the node is not a leaf we only need to check
|
|
58
|
+
# that both child nodes are in the same component
|
|
59
|
+
else:
|
|
60
|
+
left = 2 * i + 1
|
|
61
|
+
right = left + 1
|
|
62
|
+
|
|
63
|
+
if node_components[left] == node_components[right]:
|
|
64
|
+
node_components[i] = node_components[left]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@numba.njit()
|
|
68
|
+
def component_aware_query_recursion(
|
|
69
|
+
tree,
|
|
70
|
+
node,
|
|
71
|
+
point,
|
|
72
|
+
heap_p,
|
|
73
|
+
heap_i,
|
|
74
|
+
current_core_distance,
|
|
75
|
+
core_distances,
|
|
76
|
+
current_component,
|
|
77
|
+
node_components,
|
|
78
|
+
point_components,
|
|
79
|
+
dist_lower_bound,
|
|
80
|
+
component_nearest_neighbor_dist,
|
|
81
|
+
):
|
|
82
|
+
node_info = tree.node_data[node]
|
|
83
|
+
|
|
84
|
+
# ------------------------------------------------------------
|
|
85
|
+
# Case 1a: query point is outside node radius:
|
|
86
|
+
# trim it from the query
|
|
87
|
+
if dist_lower_bound > heap_p[0]:
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
# ------------------------------------------------------------
|
|
91
|
+
# Case 1b: we can't improve on the best distance for this component
|
|
92
|
+
# trim it from the query
|
|
93
|
+
elif dist_lower_bound > component_nearest_neighbor_dist[0] or current_core_distance > \
|
|
94
|
+
component_nearest_neighbor_dist[0]:
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
# ------------------------------------------------------------
|
|
98
|
+
# Case 1c: node contains only points in same component as query
|
|
99
|
+
# trim it from the query
|
|
100
|
+
elif node_components[node] == current_component:
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
# ------------------------------------------------------------
|
|
104
|
+
# Case 2: this is a leaf node. Update set of nearby points
|
|
105
|
+
elif node_info.is_leaf:
|
|
106
|
+
for i in range(node_info.idx_start, node_info.idx_end):
|
|
107
|
+
idx = tree.idx_array[i]
|
|
108
|
+
if point_components[idx] != current_component and core_distances[idx] < component_nearest_neighbor_dist[0]:
|
|
109
|
+
d = max(rdist(point, tree.data[idx]), current_core_distance, core_distances[idx])
|
|
110
|
+
if d < heap_p[0]:
|
|
111
|
+
heap_p[0] = d
|
|
112
|
+
heap_i[0] = idx
|
|
113
|
+
if d < component_nearest_neighbor_dist[0]:
|
|
114
|
+
component_nearest_neighbor_dist[0] = d
|
|
115
|
+
|
|
116
|
+
# ------------------------------------------------------------
|
|
117
|
+
# Case 3: Node is not a leaf. Recursively query subnodes
|
|
118
|
+
# starting with the closest
|
|
119
|
+
else:
|
|
120
|
+
left = 2 * node + 1
|
|
121
|
+
right = left + 1
|
|
122
|
+
dist_lower_bound_left = point_to_node_lower_bound_rdist(tree.node_bounds[0, left], tree.node_bounds[1, left],
|
|
123
|
+
point)
|
|
124
|
+
dist_lower_bound_right = point_to_node_lower_bound_rdist(tree.node_bounds[0, right], tree.node_bounds[1, right],
|
|
125
|
+
point)
|
|
126
|
+
|
|
127
|
+
# recursively query subnodes
|
|
128
|
+
if dist_lower_bound_left <= dist_lower_bound_right:
|
|
129
|
+
component_aware_query_recursion(
|
|
130
|
+
tree,
|
|
131
|
+
left,
|
|
132
|
+
point,
|
|
133
|
+
heap_p,
|
|
134
|
+
heap_i,
|
|
135
|
+
current_core_distance,
|
|
136
|
+
core_distances,
|
|
137
|
+
current_component,
|
|
138
|
+
node_components,
|
|
139
|
+
point_components,
|
|
140
|
+
dist_lower_bound_left,
|
|
141
|
+
component_nearest_neighbor_dist
|
|
142
|
+
)
|
|
143
|
+
component_aware_query_recursion(
|
|
144
|
+
tree,
|
|
145
|
+
right,
|
|
146
|
+
point,
|
|
147
|
+
heap_p,
|
|
148
|
+
heap_i,
|
|
149
|
+
current_core_distance,
|
|
150
|
+
core_distances,
|
|
151
|
+
current_component,
|
|
152
|
+
node_components,
|
|
153
|
+
point_components,
|
|
154
|
+
dist_lower_bound_right,
|
|
155
|
+
component_nearest_neighbor_dist
|
|
156
|
+
)
|
|
157
|
+
else:
|
|
158
|
+
component_aware_query_recursion(
|
|
159
|
+
tree,
|
|
160
|
+
right,
|
|
161
|
+
point,
|
|
162
|
+
heap_p,
|
|
163
|
+
heap_i,
|
|
164
|
+
current_core_distance,
|
|
165
|
+
core_distances,
|
|
166
|
+
current_component,
|
|
167
|
+
node_components,
|
|
168
|
+
point_components,
|
|
169
|
+
dist_lower_bound_right,
|
|
170
|
+
component_nearest_neighbor_dist
|
|
171
|
+
)
|
|
172
|
+
component_aware_query_recursion(
|
|
173
|
+
tree,
|
|
174
|
+
left,
|
|
175
|
+
point,
|
|
176
|
+
heap_p,
|
|
177
|
+
heap_i,
|
|
178
|
+
current_core_distance,
|
|
179
|
+
core_distances,
|
|
180
|
+
current_component,
|
|
181
|
+
node_components,
|
|
182
|
+
point_components,
|
|
183
|
+
dist_lower_bound_left,
|
|
184
|
+
component_nearest_neighbor_dist
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
return
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@numba.njit(parallel=True)
|
|
191
|
+
def boruvka_tree_query(tree, node_components, point_components, core_distances):
|
|
192
|
+
candidate_distances = np.full(tree.data.shape[0], np.inf, dtype=np.float32)
|
|
193
|
+
candidate_indices = np.full(tree.data.shape[0], -1, dtype=np.int32)
|
|
194
|
+
component_nearest_neighbor_dist = np.full(tree.data.shape[0], np.inf, dtype=np.float32)
|
|
195
|
+
|
|
196
|
+
data = np.asarray(tree.data.astype(np.float32))
|
|
197
|
+
|
|
198
|
+
for i in numba.prange(tree.data.shape[0]):
|
|
199
|
+
distance_lower_bound = point_to_node_lower_bound_rdist(tree.node_bounds[0, 0], tree.node_bounds[1, 0],
|
|
200
|
+
tree.data[i])
|
|
201
|
+
heap_p, heap_i = candidate_distances[i:i + 1], candidate_indices[i:i + 1]
|
|
202
|
+
component_aware_query_recursion(
|
|
203
|
+
tree,
|
|
204
|
+
0,
|
|
205
|
+
data[i],
|
|
206
|
+
heap_p,
|
|
207
|
+
heap_i,
|
|
208
|
+
core_distances[i],
|
|
209
|
+
core_distances,
|
|
210
|
+
point_components[i],
|
|
211
|
+
node_components,
|
|
212
|
+
point_components,
|
|
213
|
+
distance_lower_bound,
|
|
214
|
+
component_nearest_neighbor_dist[point_components[i]:point_components[i] + 1]
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
return candidate_distances, candidate_indices
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
@numba.njit(parallel=True)
|
|
221
|
+
def initialize_boruvka_from_knn(knn_indices, knn_distances, core_distances, disjoint_set):
|
|
222
|
+
# component_edges = {0:(np.int32(0), np.int32(1), np.float32(0.0)) for i in range(0)}
|
|
223
|
+
component_edges = np.full((knn_indices.shape[0], 3), -1, dtype=np.float64)
|
|
224
|
+
|
|
225
|
+
for i in numba.prange(knn_indices.shape[0]):
|
|
226
|
+
for j in range(1, knn_indices.shape[1]):
|
|
227
|
+
k = np.int32(knn_indices[i, j])
|
|
228
|
+
if core_distances[i] >= core_distances[k]:
|
|
229
|
+
component_edges[i] = (np.float64(i), np.float64(k), np.float64(core_distances[i]))
|
|
230
|
+
break
|
|
231
|
+
|
|
232
|
+
result = np.empty((len(component_edges), 3), dtype=np.float64)
|
|
233
|
+
result_idx = 0
|
|
234
|
+
|
|
235
|
+
# Add the best edges to the edge set and merge the relevant components
|
|
236
|
+
for edge in component_edges:
|
|
237
|
+
if edge[0] < 0:
|
|
238
|
+
continue
|
|
239
|
+
from_component = ds_find(disjoint_set, np.int32(edge[0]))
|
|
240
|
+
to_component = ds_find(disjoint_set, np.int32(edge[1]))
|
|
241
|
+
if from_component != to_component:
|
|
242
|
+
result[result_idx] = (np.float64(edge[0]), np.float64(edge[1]), np.float64(edge[2]))
|
|
243
|
+
result_idx += 1
|
|
244
|
+
|
|
245
|
+
ds_union_by_rank(disjoint_set, from_component, to_component)
|
|
246
|
+
|
|
247
|
+
return result[:result_idx]
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
@numba.njit(parallel=True)
|
|
251
|
+
def sample_weight_core_distance(distances, neighbors, sample_weights, min_samples):
|
|
252
|
+
core_distances = np.zeros(distances.shape[0], dtype=np.float32)
|
|
253
|
+
for i in numba.prange(distances.shape[0]):
|
|
254
|
+
total_weight = 0.0
|
|
255
|
+
j = 0
|
|
256
|
+
while total_weight < min_samples and j < neighbors.shape[1]:
|
|
257
|
+
total_weight += sample_weights[neighbors[i, j]]
|
|
258
|
+
j += 1
|
|
259
|
+
|
|
260
|
+
core_distances[i] = distances[i, j - 1]
|
|
261
|
+
|
|
262
|
+
return core_distances
|
|
263
|
+
|
|
264
|
+
def parallel_boruvka(tree, min_samples=10, sample_weights=None):
|
|
265
|
+
components_disjoint_set = ds_rank_create(tree.data.shape[0])
|
|
266
|
+
point_components = np.arange(tree.data.shape[0])
|
|
267
|
+
node_components = np.full(tree.node_data.shape[0], -1)
|
|
268
|
+
n_components = point_components.shape[0]
|
|
269
|
+
|
|
270
|
+
if sample_weights is not None:
|
|
271
|
+
mean_sample_weight = np.mean(sample_weights)
|
|
272
|
+
expected_neighbors = min_samples / mean_sample_weight
|
|
273
|
+
distances, neighbors = parallel_tree_query(tree, tree.data, k=int(2 * expected_neighbors))
|
|
274
|
+
core_distances = sample_weight_core_distance(distances, neighbors, sample_weights, min_samples)
|
|
275
|
+
edges = initialize_boruvka_from_knn(neighbors, distances, core_distances, components_disjoint_set)
|
|
276
|
+
update_component_vectors(tree, components_disjoint_set, node_components, point_components)
|
|
277
|
+
else:
|
|
278
|
+
if min_samples > 1:
|
|
279
|
+
distances, neighbors = parallel_tree_query(tree, tree.data, k=min_samples + 1, output_rdist=True)
|
|
280
|
+
core_distances = distances.T[-1]
|
|
281
|
+
edges = initialize_boruvka_from_knn(neighbors, distances, core_distances, components_disjoint_set)
|
|
282
|
+
update_component_vectors(tree, components_disjoint_set, node_components, point_components)
|
|
283
|
+
else:
|
|
284
|
+
core_distances = np.zeros(tree.data.shape[0], dtype=np.float32)
|
|
285
|
+
distances, neighbors = parallel_tree_query(tree, tree.data, k=2)
|
|
286
|
+
edges = initialize_boruvka_from_knn(neighbors, distances, core_distances, components_disjoint_set)
|
|
287
|
+
update_component_vectors(tree, components_disjoint_set, node_components, point_components)
|
|
288
|
+
|
|
289
|
+
while n_components > 1:
|
|
290
|
+
candidate_distances, candidate_indices = boruvka_tree_query(tree, node_components, point_components,
|
|
291
|
+
core_distances)
|
|
292
|
+
new_edges = merge_components(components_disjoint_set, candidate_indices, candidate_distances, point_components)
|
|
293
|
+
update_component_vectors(tree, components_disjoint_set, node_components, point_components)
|
|
294
|
+
|
|
295
|
+
edges = np.vstack((edges, new_edges))
|
|
296
|
+
n_components = np.unique(point_components).shape[0]
|
|
297
|
+
|
|
298
|
+
edges[:, 2] = np.sqrt(edges.T[2])
|
|
299
|
+
return edges
|