Compiled extensions: performance-critical inner loops
DecisionTreeClassifier and its siblings in sklearn/tree/_classes.py are thin Python orchestrators: they validate input and pick a Splitter, but the actual recursive tree growth and per-sample prediction run in sklearn/tree/_tree.pyx, a Cython file compiled by meson into its own optimized C extension. Because that hot loop is compiled, not interpreted, editing it doesn't take effect like editing a .py file — the extension must be rebuilt (meson-python does this automatically on import under an editable install).
Growing a tree without Python recursion
sklearn/tree/_tree.pyx · lines 126–261Building starts by pushing the root node's sample range (the whole training set) onto an explicit stack, all inside a with nogil: block so the entire build runs without touching the Python interpreter.
Each iteration pops one pending node off the stack, restores its (start, end) sample range and parent-impurity bookkeeping, then asks the Splitter to reset its internal state to that range.
Cheap stopping checks (max depth, min samples, min weight, near-zero impurity) decide whether this node must be a leaf before any expensive split search is attempted.
If the node isn't already forced to be a leaf, the Splitter searches for the best split; the outcome can still turn it into a leaf, e.g. when the improvement falls below min_impurity_decrease.
The decided node (leaf or split) is written into the Tree's preallocated C arrays via _add_node; running out of capacity surfaces as INTPTR_MAX and aborts the build (a MemoryError is raised once outside the nogil block).
with nogil: # push root node onto stack builder_stack.push({ "start": 0, "end": n_node_samples, "depth": 0, "parent": _TREE_UNDEFINED, "is_left": 0, "impurity": INFINITY, "n_constant_features": 0, "lower_bound": -INFINITY, "upper_bound": INFINITY, }) while not builder_stack.empty(): stack_record = builder_stack.top() builder_stack.pop() start = stack_record.start end = stack_record.end depth = stack_record.depth parent = stack_record.parent is_left = stack_record.is_left parent_record.impurity = stack_record.impurity parent_record.n_constant_features = stack_record.n_constant_features parent_record.lower_bound = stack_record.lower_bound parent_record.upper_bound = stack_record.upper_bound n_node_samples = end - start splitter.node_reset(start, end, &weighted_n_node_samples) is_leaf = (depth >= max_depth or n_node_samples < min_samples_split or n_node_samples < 2 * min_samples_leaf or weighted_n_node_samples < 2 * min_weight_leaf) if first: parent_record.impurity = splitter.node_impurity() first = 0 # impurity == 0 with tolerance due to rounding errors is_leaf = is_leaf or parent_record.impurity <= EPSILON if not is_leaf: splitter.node_split( &parent_record, &split, ) # If EPSILON=0 in the below comparison, float precision # issues stop splitting, producing trees that are # dissimilar to v0.18 is_leaf = (is_leaf or split.pos >= end or (split.improvement + EPSILON < min_impurity_decrease)) node_id = tree._add_node(parent, is_left, is_leaf, split.feature, split.threshold, parent_record.impurity, n_node_samples, weighted_n_node_samples, split.missing_go_to_left) if node_id == INTPTR_MAX: rc = -1 break
cdef const float32_t[:, :] X_ndarray = X cdef intp_t n_samples = X.shape[0] cdef float32_t X_i_node_feature # Initialize output cdef intp_t[:] out = np.zeros(n_samples, dtype=np.intp) # Initialize auxiliary data-structure cdef Node* node = NULL cdef intp_t i = 0 with nogil: for i in range(n_samples): node = self.nodes # While node not a leaf while node.left_child != _TREE_LEAF: X_i_node_feature = X_ndarray[i, node.feature] # ... and node.right_child != _TREE_LEAF: if isnan(X_i_node_feature): if node.missing_go_to_left: node = &self.nodes[node.left_child] else: node = &self.nodes[node.right_child] elif X_i_node_feature <= node.threshold: node = &self.nodes[node.left_child] else: node = &self.nodes[node.right_child] out[i] = <intp_t>(node - self.nodes) # node offset return np.asarray(out)
sklearn/tree/_tree.pyx · lines 954–996threshold and feature until a leaf (left_child == _TREE_LEAF) is reached. This is the payoff of compiling the core — an O(depth) traversal per row that never re-enters the Python interpreter.Check your understanding
Answered in place — nothing is graded, everything is explained. 0 / 3 passed
How does DepthFirstTreeBuilder grow the tree without Python-level recursion?
Why does the tree-building and prediction code run inside `with nogil:` blocks?
You edit a splitting rule inside sklearn/tree/_tree.pyx. What has to happen before your change actually affects DecisionTreeClassifier.fit()?