fast-hdbscan 0.1.0__tar.gz

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.
@@ -0,0 +1,24 @@
1
+ BSD 2-Clause License
2
+
3
+ Copyright (c) 2023, Tutte Institute for Mathematics and Computing
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
19
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
21
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
22
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
23
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,121 @@
1
+ Metadata-Version: 1.2
2
+ Name: fast_hdbscan
3
+ Version: 0.1.0
4
+ Summary: A fast multicore version of hdbscan for low dimensional euclidean spaces
5
+ Home-page: https://github.com/TutteInstitute/fast_hdbscan
6
+ Author: Leland McInnes
7
+ Author-email: leland.mcinnes@gmail.com
8
+ Maintainer: Leland McInnes
9
+ Maintainer-email: leland.mcinnes@gmail.com
10
+ License: BSD 2-Clause License
11
+ Description: .. -*- mode: rst -*-
12
+
13
+ .. image:: doc/hdbscan_logo.png
14
+ :width: 600
15
+ :alt: HDBSCAN logo
16
+ :align: center
17
+
18
+ ======================
19
+ Fast Multicore HDBSCAN
20
+ ======================
21
+
22
+ Ahe ``fast_hdbscan`` library provides a simple implementation of the HDBSCAN clustering algorithm designed specifically
23
+ for high performance on multicore machine with low dimensional data. The algorithm runs in parallel and can make
24
+ effective use of as many cores as you wish to throw at a problem. It is thus ideal for large SMP systems, and even
25
+ modern multicore laptops.
26
+
27
+ This library provides a
28
+ re-implementation of a subset of the HDBSCAN algorithm that is compatible with the
29
+ `hdbscan <https://github.com/scikit-learn-contrib/hdbscan>`_ library for data that is Euclidean and
30
+ low dimensional. The primary advantages of this library over the standard ``hdbscan`` library are:
31
+
32
+
33
+ * this library can easily use all available cores to speed up computation;
34
+ * this library has much faster implementations of tree condensing and cluster extraction;
35
+ * this library is much simpler and more approachable for extending or using components from;
36
+ * this library is built on numba and has less issues with binaries and compilation.
37
+
38
+ This library does not support all the features and input formats available in the hdbscan library.
39
+
40
+ -----------
41
+ Basic Usage
42
+ -----------
43
+
44
+ The ``fast_hdbscan`` library follows the ``hdbscan`` library in using the sklearn API. You can use the ``fast_hdbscan``
45
+ class ``HDBSCAN`` exactly as you wuld that of the ``hdbscan`` library with the caveat that ``fast_hdbscan`` only
46
+ supports a subset of the parameters and options of ``hdbscan``. Nonetheless, if you have low-dimensional
47
+ Euclidean data (e.g. the output of UMAP), you can use this library as a straightforward drop in replacement for
48
+ ``hdbscan``:
49
+
50
+ .. code:: python
51
+
52
+ import fast_hdbscan
53
+ from sklearn.datasets import make_blobs
54
+
55
+ data, _ = make_blobs(1000)
56
+
57
+ clusterer = fast_hdbscan.HDBSCAN(min_cluster_size=10)
58
+ cluster_labels = clusterer.fit_predict(data)
59
+
60
+ ------------
61
+ Installation
62
+ ------------
63
+ fast_hdbscan requires:
64
+
65
+ * numba
66
+ * numpy
67
+ * scikit-learn
68
+
69
+ fast_hdbscan can be installed via pip:
70
+
71
+ .. code:: bash
72
+
73
+ pip install fast-hdbscan
74
+
75
+ To manually install this package:
76
+
77
+ .. code:: bash
78
+
79
+ wget https://github.com/TutteInstitute/fast_hdbscan/archive/main.zip
80
+ unzip main.zip
81
+ rm main.zip
82
+ cd fast_hdbscan-main
83
+ python setup.py install
84
+
85
+ ----------
86
+ References
87
+ ----------
88
+
89
+ The algorithm used here is an adaptation of the algorithms described in the papers:
90
+
91
+ McInnes L, Healy J. *Accelerated Hierarchical Density Based Clustering*
92
+ In: 2017 IEEE International Conference on Data Mining Workshops (ICDMW), IEEE, pp 33-42.
93
+ 2017 `[pdf] <http://ieeexplore.ieee.org/stamp/stamp.jsp?tp=&arnumber=8215642>`_
94
+
95
+ R. Campello, D. Moulavi, and J. Sander, *Density-Based Clustering Based on
96
+ Hierarchical Density Estimates*
97
+ In: Advances in Knowledge Discovery and Data Mining, Springer, pp 160-172.
98
+ 2013
99
+
100
+ -------
101
+ License
102
+ -------
103
+
104
+ fast_hdbscan is BSD (2-clause) licensed. See the LICENSE file for details.
105
+
106
+ ------------
107
+ Contributing
108
+ ------------
109
+
110
+ Contributions are more than welcome! If you have ideas for features of projects please get in touch. Everything from
111
+ code to notebooks to examples and documentation are all *equally valuable* so please don't feel you can't contribute.
112
+ To contribute please `fork the project <https://github.com/TutteInstitute/fast_hdbscan/issues#fork-destination-box>`_ make your
113
+ changes and submit a pull request. We will do our best to work through any issues with you and get your code merged in.
114
+ Keywords: cluster,clustering,hdbscan,dbscan
115
+ Platform: UNKNOWN
116
+ Classifier: License :: OSI Approved :: BSD License
117
+ Classifier: Programming Language :: Python :: 3.9
118
+ Classifier: Programming Language :: Python :: 3.10
119
+ Classifier: Development Status :: 4 - Beta
120
+ Classifier: Operating System :: OS Independent
121
+ Requires-Python: >=3.9
@@ -0,0 +1,103 @@
1
+ .. -*- mode: rst -*-
2
+
3
+ .. image:: doc/hdbscan_logo.png
4
+ :width: 600
5
+ :alt: HDBSCAN logo
6
+ :align: center
7
+
8
+ ======================
9
+ Fast Multicore HDBSCAN
10
+ ======================
11
+
12
+ Ahe ``fast_hdbscan`` library provides a simple implementation of the HDBSCAN clustering algorithm designed specifically
13
+ for high performance on multicore machine with low dimensional data. The algorithm runs in parallel and can make
14
+ effective use of as many cores as you wish to throw at a problem. It is thus ideal for large SMP systems, and even
15
+ modern multicore laptops.
16
+
17
+ This library provides a
18
+ re-implementation of a subset of the HDBSCAN algorithm that is compatible with the
19
+ `hdbscan <https://github.com/scikit-learn-contrib/hdbscan>`_ library for data that is Euclidean and
20
+ low dimensional. The primary advantages of this library over the standard ``hdbscan`` library are:
21
+
22
+
23
+ * this library can easily use all available cores to speed up computation;
24
+ * this library has much faster implementations of tree condensing and cluster extraction;
25
+ * this library is much simpler and more approachable for extending or using components from;
26
+ * this library is built on numba and has less issues with binaries and compilation.
27
+
28
+ This library does not support all the features and input formats available in the hdbscan library.
29
+
30
+ -----------
31
+ Basic Usage
32
+ -----------
33
+
34
+ The ``fast_hdbscan`` library follows the ``hdbscan`` library in using the sklearn API. You can use the ``fast_hdbscan``
35
+ class ``HDBSCAN`` exactly as you wuld that of the ``hdbscan`` library with the caveat that ``fast_hdbscan`` only
36
+ supports a subset of the parameters and options of ``hdbscan``. Nonetheless, if you have low-dimensional
37
+ Euclidean data (e.g. the output of UMAP), you can use this library as a straightforward drop in replacement for
38
+ ``hdbscan``:
39
+
40
+ .. code:: python
41
+
42
+ import fast_hdbscan
43
+ from sklearn.datasets import make_blobs
44
+
45
+ data, _ = make_blobs(1000)
46
+
47
+ clusterer = fast_hdbscan.HDBSCAN(min_cluster_size=10)
48
+ cluster_labels = clusterer.fit_predict(data)
49
+
50
+ ------------
51
+ Installation
52
+ ------------
53
+ fast_hdbscan requires:
54
+
55
+ * numba
56
+ * numpy
57
+ * scikit-learn
58
+
59
+ fast_hdbscan can be installed via pip:
60
+
61
+ .. code:: bash
62
+
63
+ pip install fast-hdbscan
64
+
65
+ To manually install this package:
66
+
67
+ .. code:: bash
68
+
69
+ wget https://github.com/TutteInstitute/fast_hdbscan/archive/main.zip
70
+ unzip main.zip
71
+ rm main.zip
72
+ cd fast_hdbscan-main
73
+ python setup.py install
74
+
75
+ ----------
76
+ References
77
+ ----------
78
+
79
+ The algorithm used here is an adaptation of the algorithms described in the papers:
80
+
81
+ McInnes L, Healy J. *Accelerated Hierarchical Density Based Clustering*
82
+ In: 2017 IEEE International Conference on Data Mining Workshops (ICDMW), IEEE, pp 33-42.
83
+ 2017 `[pdf] <http://ieeexplore.ieee.org/stamp/stamp.jsp?tp=&arnumber=8215642>`_
84
+
85
+ R. Campello, D. Moulavi, and J. Sander, *Density-Based Clustering Based on
86
+ Hierarchical Density Estimates*
87
+ In: Advances in Knowledge Discovery and Data Mining, Springer, pp 160-172.
88
+ 2013
89
+
90
+ -------
91
+ License
92
+ -------
93
+
94
+ fast_hdbscan is BSD (2-clause) licensed. See the LICENSE file for details.
95
+
96
+ ------------
97
+ Contributing
98
+ ------------
99
+
100
+ Contributions are more than welcome! If you have ideas for features of projects please get in touch. Everything from
101
+ code to notebooks to examples and documentation are all *equally valuable* so please don't feel you can't contribute.
102
+ To contribute please `fork the project <https://github.com/TutteInstitute/fast_hdbscan/issues#fork-destination-box>`_ make your
103
+ changes and submit a pull request. We will do our best to work through any issues with you and get your code merged in.
@@ -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"]
@@ -0,0 +1,277 @@
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()
8
+ def merge_components(disjoint_set, candidate_neighbors, candidate_neighbor_distances, point_components):
9
+ component_edges = {0: (0, np.int32(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 = 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] = (i, candidate_neighbors[i], candidate_neighbor_distances[i])
17
+ else:
18
+ component_edges[from_component] = (i, 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
+ def parallel_boruvka(tree, min_samples=10):
251
+ components_disjoint_set = ds_rank_create(tree.data.shape[0])
252
+ point_components = np.arange(tree.data.shape[0])
253
+ node_components = np.full(tree.node_data.shape[0], -1)
254
+ n_components = point_components.shape[0]
255
+
256
+ if min_samples > 1:
257
+ distances, neighbors = parallel_tree_query(tree, tree.data, k=min_samples + 1, output_rdist=True)
258
+ core_distances = distances.T[-1]
259
+ edges = initialize_boruvka_from_knn(neighbors, distances, core_distances, components_disjoint_set)
260
+ update_component_vectors(tree, components_disjoint_set, node_components, point_components)
261
+ else:
262
+ core_distances = np.zeros(tree.data.shape[0], dtype=np.float32)
263
+ distances, neighbors = parallel_tree_query(tree, tree.data, k=2)
264
+ edges = initialize_boruvka_from_knn(neighbors, distances, core_distances, components_disjoint_set)
265
+ update_component_vectors(tree, components_disjoint_set, node_components, point_components)
266
+
267
+ while n_components > 1:
268
+ candidate_distances, candidate_indices = boruvka_tree_query(tree, node_components, point_components,
269
+ core_distances)
270
+ new_edges = merge_components(components_disjoint_set, candidate_indices, candidate_distances, point_components)
271
+ update_component_vectors(tree, components_disjoint_set, node_components, point_components)
272
+
273
+ edges = np.vstack((edges, new_edges))
274
+ n_components = np.unique(point_components).shape[0]
275
+
276
+ edges[:, 2] = np.sqrt(edges.T[2])
277
+ return edges