fast-hdbscan 0.1.3__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,122 @@
1
+ Metadata-Version: 2.1
2
+ Name: fast_hdbscan
3
+ Version: 0.1.3
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
+ Keywords: cluster,clustering,hdbscan,dbscan
12
+ Classifier: License :: OSI Approved :: BSD License
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Development Status :: 4 - Beta
16
+ Classifier: Operating System :: OS Independent
17
+ Requires-Python: >=3.9
18
+ License-File: LICENSE
19
+
20
+ .. -*- mode: rst -*-
21
+
22
+ .. image:: doc/hdbscan_logo.png
23
+ :width: 600
24
+ :alt: HDBSCAN logo
25
+ :align: center
26
+
27
+ ======================
28
+ Fast Multicore HDBSCAN
29
+ ======================
30
+
31
+ Ahe ``fast_hdbscan`` library provides a simple implementation of the HDBSCAN clustering algorithm designed specifically
32
+ for high performance on multicore machine with low dimensional data (2D to about 20D). The algorithm runs in parallel and can make
33
+ effective use of as many cores as you wish to throw at a problem. It is thus ideal for large SMP systems, and even
34
+ modern multicore laptops.
35
+
36
+ This library provides a
37
+ re-implementation of a subset of the HDBSCAN algorithm that is compatible with the
38
+ `hdbscan <https://github.com/scikit-learn-contrib/hdbscan>`_ library for data that is Euclidean and
39
+ low dimensional. The primary advantages of this library over the standard ``hdbscan`` library are:
40
+
41
+
42
+ * this library can easily use all available cores to speed up computation;
43
+ * this library has much faster implementations of tree condensing and cluster extraction;
44
+ * this library is much simpler and more approachable for extending or using components from;
45
+ * this library is built on numba and has less issues with binaries and compilation.
46
+
47
+ This library does not support all the features and input formats available in the hdbscan library.
48
+
49
+ -----------
50
+ Basic Usage
51
+ -----------
52
+
53
+ The ``fast_hdbscan`` library follows the ``hdbscan`` library in using the sklearn API. You can use the ``fast_hdbscan``
54
+ class ``HDBSCAN`` exactly as you would that of the ``hdbscan`` library with the caveat that ``fast_hdbscan`` only
55
+ supports a subset of the parameters and options of ``hdbscan``. Nonetheless, if you have low-dimensional
56
+ Euclidean data (e.g. the output of UMAP), you can use this library as a straightforward drop in replacement for
57
+ ``hdbscan``:
58
+
59
+ .. code:: python
60
+
61
+ import fast_hdbscan
62
+ from sklearn.datasets import make_blobs
63
+
64
+ data, _ = make_blobs(1000)
65
+
66
+ clusterer = fast_hdbscan.HDBSCAN(min_cluster_size=10)
67
+ cluster_labels = clusterer.fit_predict(data)
68
+
69
+ ------------
70
+ Installation
71
+ ------------
72
+ fast_hdbscan requires:
73
+
74
+ * numba
75
+ * numpy
76
+ * scikit-learn
77
+
78
+ fast_hdbscan can be installed via pip:
79
+
80
+ .. code:: bash
81
+
82
+ pip install fast_hdbscan
83
+
84
+ To manually install this package:
85
+
86
+ .. code:: bash
87
+
88
+ wget https://github.com/TutteInstitute/fast_hdbscan/archive/main.zip
89
+ unzip main.zip
90
+ rm main.zip
91
+ cd fast_hdbscan-main
92
+ python setup.py install
93
+
94
+ ----------
95
+ References
96
+ ----------
97
+
98
+ The algorithm used here is an adaptation of the algorithms described in the papers:
99
+
100
+ McInnes L, Healy J. *Accelerated Hierarchical Density Based Clustering*
101
+ In: 2017 IEEE International Conference on Data Mining Workshops (ICDMW), IEEE, pp 33-42.
102
+ 2017 `[pdf] <http://ieeexplore.ieee.org/stamp/stamp.jsp?tp=&arnumber=8215642>`_
103
+
104
+ R. Campello, D. Moulavi, and J. Sander, *Density-Based Clustering Based on
105
+ Hierarchical Density Estimates*
106
+ In: Advances in Knowledge Discovery and Data Mining, Springer, pp 160-172.
107
+ 2013
108
+
109
+ -------
110
+ License
111
+ -------
112
+
113
+ fast_hdbscan is BSD (2-clause) licensed. See the LICENSE file for details.
114
+
115
+ ------------
116
+ Contributing
117
+ ------------
118
+
119
+ Contributions are more than welcome! If you have ideas for features of projects please get in touch. Everything from
120
+ code to notebooks to examples and documentation are all *equally valuable* so please don't feel you can't contribute.
121
+ To contribute please `fork the project <https://github.com/TutteInstitute/fast_hdbscan/issues#fork-destination-box>`_ make your
122
+ 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,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 (2D to about 20D). 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 would 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(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
+ 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