Decision Tree Classifier with Bagging - Supervised Learning
Implement Decision Tree Classifier with Bagging in Python with NumPy. hard exercise with examples, hints, and instant feedback. No setup or signup required.
Decision Tree Classifier with Bagging
Grow a full classification tree with recursive Gini splits (build_tree, predict_tree), then combine trees trained on bootstrap samples by majority vote (bagging_predict).
Background
A single deep tree has low bias but high variance: small changes in the data change the tree. Bagging (bootstrap aggregating) averages many such trees to cut the variance; random forests add per-split feature subsampling on top. This problem uses the impurity from Gini Impurity for Decision-Tree Splits.
Gini impurity
gini(S) = 1 - sum_c p_c^2 # p_c = fraction of S with class c
split impurity = (n_L * gini(L) + n_R * gini(R)) / n
Growing the tree (build_tree)
At a node holding samples S at depth depth (the root has depth 0):
- The node is a leaf if
max_depthis not None anddepth >= max_depth, orlen(S) < min_samples_split, orSis pure. - Otherwise search every feature
f(in increasing order) and every thresholdt(in increasing order), where the thresholds are the midpoints between consecutive distinct sorted values of featurefwithinS. A sample goes left ifx[f] <= t, right otherwise. - Start with
best = gini(S). A candidate replaces the current best only if its split impurity is lower by more than 1e-12. So ties keep the earlier candidate (smaller feature index, then smaller threshold), and a split is made only if it strictly reduces impurity. - If no candidate qualified, the node is a leaf; otherwise recurse on both sides with
depth + 1.
A leaf predicts the majority class of its samples; ties go to the smallest class label.
Tree format
- Leaf:
{'leaf': True, 'value': c}withca Python int - Internal node:
{'leaf': False, 'feature': f, 'threshold': t, 'left': subtree, 'right': subtree}withfan int andta float
Bagging (bagging_predict)
- Create
rng = np.random.default_rng(seed)once. - For each of the
n_estimatorstrees, in order: drawidx = rng.integers(0, n, size=n)(sampling with replacement), fitbuild_tree(X_train[idx], y_train[idx], max_depth, min_samples_split), and predictX_test. - Each test sample gets the class with the most votes (hard voting); ties go to the smallest label.
Inputs
X,X_train,X_test: float arrays(n_samples, n_features)y,y_train: non-negative integer labels(n_samples,)max_depth: int or None;min_samples_split: int (default 2);n_estimators,seed: ints
Expected Return Format
build_tree(X, y, max_depth=None, min_samples_split=2): the root node dict (format above)predict_tree(tree, X): int array(n_samples,)bagging_predict(X_train, y_train, X_test, n_estimators=10, max_depth=None, min_samples_split=2, seed=0): int array(n_test,)
Conventions vs scikit-learn
- Same as
DecisionTreeClassifier(criterion='gini'): midpoint thresholds,<=goes left,max_depthandmin_samples_splitmean the same. sklearn shuffles the features at each node, so it breaks exact ties at random (fixed byrandom_state); here ties are deterministic. BaggingClassifieraverages the trees' predicted probabilities (soft voting) when it can; here you use hard majority voting.
Constraints
- NumPy only; no sklearn.
Follow-up
- What does a random forest change (
max_features), and why does decorrelating the trees help more than adding trees? - About 63.2% of the distinct samples land in each bootstrap sample. How can the remaining "out-of-bag" samples estimate test error for free?
Examples
Input: build_tree(np.array([[2.0, 3.0], [1.0, 1.0], [3.0, 1.0], [6.0, 5.0], [7.0, 7.0], [8.0, 6.0], [4.0, 4.0], [5.0, 2.0]]), np.array([0, 0, 0, 1, 1, 1, 0, 1]), max_depth=1)
Output: {'leaf': False, 'feature': 0, 'threshold': 4.5, 'left': {'leaf': True, 'value': 0}, 'right': {'leaf': True, 'value': 1}}x0 <= 4.5 separates the classes perfectly, so its split impurity is 0.
Input: predict_tree(tree, np.array([[0.0, 0.0], [4.5, 1.0], [4.5, 6.0], [9.0, 9.0]])) # tree: same data, no max_depth Output: array([0, 0, 0, 1])
(4.5, 6) has x0 equal to the threshold 4.5, so it goes left (x[f] <= t).
Practice in Python
import numpy as np
def build_tree(X, y, max_depth=None, min_samples_split=2):
"""
Grow a classification tree with Gini splits.
Args:
X: Features, array (n_samples, n_features)
y: Non-negative integer labels, array (n_samples,)
max_depth: Maximum depth (root = depth 0), or None for no limit
min_samples_split: Nodes with fewer samples become leaves
Returns:
Root node: {'leaf': True, 'value': c} or
{'leaf': False, 'feature': f, 'threshold': t, 'left': ..., 'right': ...}
(c and f Python ints, t a Python float)
"""
# Your code here
pass
def predict_tree(tree, X):
"""Return the predicted class for each row of X, as an int array (n_samples,)."""
# Your code here
pass
def bagging_predict(X_train, y_train, X_test, n_estimators=10, max_depth=None,
min_samples_split=2, seed=0):
"""
Bagging: trees on bootstrap samples drawn with rng = np.random.default_rng(seed)
(idx = rng.integers(0, n, size=n) per tree), combined by majority vote.
Returns:
Predicted classes for X_test, int array (n_test,) (vote ties -> smallest label)
"""
# Your code here
pass
The interactive editor loads with Python and NumPy. Run your implementation against 10 tests, or use the progressive hints.