/
githubmirror
/
scikit-learn
Обзор
Документация
Войти
/
githubmirror
/
scikit-learn
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
sklearn/tree/_utils.pyx
243 строки
9 KB
Itamar Turner-Trauring
MNT Ensure inlining of Cython utility functions (#34559)
24 июл 2026, 16:27
Не верифицирован
24 июл 2026, 16:27
e03ae10
Код
Авторство
О чём код?
# Authors: The scikit-learn developers # SPDX-License-Identifier: BSD-3-Clause from libc.stdlib cimport free from libc.string cimport memset cimport numpy as cnp cnp.import_array() # ============================================================================= # Helper functions # ============================================================================= cdef int safe_realloc(realloc_ptr* p, size_t nelems) except -1 nogil: # sizeof(realloc_ptr[0]) would be more like idiomatic C, but causes Cython # 0.20.1 to crash. cdef size_t nbytes = nelems * sizeof(p[0][0]) if nbytes / sizeof(p[0][0]) != nelems: # Overflow in the multiplication raise MemoryError(f"could not allocate ({nelems} * {sizeof(p[0][0])}) bytes") cdef realloc_ptr tmp = <realloc_ptr>realloc(p[0], nbytes) if tmp == NULL: raise MemoryError(f"could not allocate {nbytes} bytes") p[0] = tmp return 0 def _realloc_test(): # Helper for tests. Tries to allocate <size_t>(-1) / 2 * sizeof(size_t) # bytes, which will always overflow. cdef intp_t* p = NULL safe_realloc(&p, <size_t>(-1) / 2) if p != NULL: free(p) assert False cdef class WeightedFenwickTree: """ Fenwick tree (Binary Indexed Tree) specialized for maintaining: - prefix sums of weights - prefix sums of weight * target (y) Notes: - Implementation uses 1-based indexing internally for the Fenwick tree arrays, hence the +1 sized buffers. 1-based indexing is customary for this data structure and makes the some index handling slightly more efficient and natural. - Memory ownership: this class allocates and frees the underlying C buffers. - Typical operations: add(rank, y, w) -> O(log n) search(t) -> O(log n), finds the smallest rank with cumulative weight > t (see search for details). """ def __cinit__(self, intp_t capacity): self.tree_w = NULL self.tree_wy = NULL # Allocate arrays of length (capacity + 1) because indices are 1-based. safe_realloc(&self.tree_w, capacity + 1) safe_realloc(&self.tree_wy, capacity + 1) cdef void reset(self, intp_t size) noexcept nogil: """ Reset the tree to hold 'size' elements and clear all aggregates. """ cdef intp_t p cdef intp_t n_bytes = (size + 1) * sizeof(float64_t) # +1 for 1-based storage # Public size and zeroed aggregates. self.size = size memset(self.tree_w, 0, n_bytes) memset(self.tree_wy, 0, n_bytes) self.total_w = 0.0 self.total_wy = 0.0 # highest power of two <= size p = 1 while p <= size: p <<= 1 self.max_pow2 = p >> 1 def __dealloc__(self): if self.tree_w != NULL: free(self.tree_w) if self.tree_wy != NULL: free(self.tree_wy) cdef void add(self, intp_t idx, float64_t y_value, float64_t weight) noexcept nogil: """ Add a weighted observation to the Fenwick tree. Parameters ---------- idx : intp_t The 0-based index where to add the observation y_value : float64_t The target value (y) of the observation weight : float64_t The sample weight Notes ----- Updates both weight sums and weighted target sums in O(log n) time. """ cdef float64_t weighted_y = weight * y_value cdef intp_t fenwick_idx = idx + 1 # Convert to 1-based indexing # Update Fenwick tree nodes by traversing up the tree while fenwick_idx <= self.size: self.tree_w[fenwick_idx] += weight self.tree_wy[fenwick_idx] += weighted_y # Move to next node using bit manipulation: add lowest set bit fenwick_idx += fenwick_idx & -fenwick_idx # Update global totals self.total_w += weight self.total_wy += weighted_y cdef intp_t search( self, float64_t target_weight, float64_t* cumul_weight_out, float64_t* cumul_weighted_y_out, intp_t* prev_idx_out, ) noexcept nogil: """ Binary search to find the position where cumulative weight reaches target. This method performs a binary search on the Fenwick tree to find indices such that the cumulative weight at 'prev_idx' is < target_weight and the cumulative weight at the returned index is >= target_weight. Parameters ---------- target_weight : float64_t The target cumulative weight to search for cumul_weight_out : float64_t* Output pointer for cumulative weight up to returned index (exclusive) cumul_weighted_y_out : float64_t* Output pointer for cumulative weighted y-sum up to returned index (exclusive) prev_idx_out : intp_t* Output pointer for the previous index (largest index with cumul_weight < target) Returns ------- intp_t The index where cumulative weight first reaches or exceeds target_weight Notes ----- - O(log n) complexity - Ignores nodes with zero weights (corresponding to uninserted y-values) - Assumes at least one active (positive-weight) item exists - Assumes 0 <= target_weight <= total_weight """ cdef: intp_t current_idx = 0 intp_t next_idx, prev_idx, equal_bit float64_t cumul_weight = 0.0 float64_t cumul_weighted_y = 0.0 intp_t search_bit = self.max_pow2 # Start from highest power of 2 float64_t node_weight, equal_target # Phase 1: Standard Fenwick binary search with prefix accumulation # Traverse down the tree, moving right when we can consume more weight while search_bit != 0: next_idx = current_idx + search_bit if next_idx <= self.size: node_weight = self.tree_w[next_idx] if target_weight == node_weight: # Exact match found - store state for later processing equal_target = target_weight equal_bit = search_bit break elif target_weight > node_weight: # We can consume this node's weight - move right and accumulate target_weight -= node_weight current_idx = next_idx cumul_weight += node_weight cumul_weighted_y += self.tree_wy[next_idx] search_bit >>= 1 # If no exact match, we're done with standard search if search_bit == 0: cumul_weight_out[0] = cumul_weight cumul_weighted_y_out[0] = cumul_weighted_y prev_idx_out[0] = current_idx return current_idx # Phase 2: Handle exact match case - find prev_idx # Search for the largest index with cumulative weight < original target prev_idx = current_idx while search_bit != 0: next_idx = prev_idx + search_bit if next_idx <= self.size: node_weight = self.tree_w[next_idx] if target_weight > node_weight: target_weight -= node_weight prev_idx = next_idx search_bit >>= 1 # Phase 3: Complete the exact match search # Restore state and search for the largest index with # cumulative weight <= original target (and this is case, we know we have ==) search_bit = equal_bit target_weight = equal_target while search_bit != 0: next_idx = current_idx + search_bit if next_idx <= self.size: node_weight = self.tree_w[next_idx] if target_weight >= node_weight: target_weight -= node_weight current_idx = next_idx cumul_weight += node_weight cumul_weighted_y += self.tree_wy[next_idx] search_bit >>= 1 # Output results cumul_weight_out[0] = cumul_weight cumul_weighted_y_out[0] = cumul_weighted_y prev_idx_out[0] = prev_idx return current_idx cdef class PytestWeightedFenwickTree(WeightedFenwickTree): """Used for testing only""" def py_reset(self, intp_t n): self.reset(n) def py_add(self, intp_t idx, float64_t y, float64_t w): self.add(idx, y, w) def py_search(self, float64_t t): cdef float64_t w, wy cdef intp_t prev_idx idx = self.search(t, &w, &wy, &prev_idx) return prev_idx, idx, w, wy