0Pricing
Machine Learning Academy · Lesson

Building a Tree: Splits, Nodes, and Leaves

Learners will trace how a decision tree recursively partitions data at each node, from root to leaf, and make predictions by following branches.

Building a Tree: Splits, Nodes, and Leaves is a free Machine Learning Academy lesson on CoddyKit — lesson 1 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Machine Learning Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

What Is a Decision Tree?

A decision tree is a flowchart-like structure where each internal node asks a yes/no question about one feature, each branch represents an answer, and each leaf node contains a prediction. To classify a new sample, you start at the root, follow branches according to feature values, and arrive at a leaf whose label is the prediction. Decision trees are interpretable by design — you can trace exactly why any prediction was made by reading the sequence of questions answered, which makes them popular in regulated industries like finance and healthcare.

# Conceptual tree for predicting loan default:
#
# Is income > 50000?
# |--- Yes: Is credit_score > 700?
# |         |--- Yes: APPROVE (leaf)
# |         |--- No:  Is debt_ratio < 0.4?
# |                   |--- Yes: APPROVE (leaf)
# |                   |--- No:  REJECT (leaf)
# |--- No: REJECT (leaf)

print('Decision tree makes predictions by asking questions')
print('Each path from root to leaf = one decision rule')

Nodes, Branches, and Leaves

A decision tree has three types of components: root node (the first question asked — the most informative split of the entire dataset), internal nodes (intermediate questions that further partition subsets of the data), and leaf nodes (terminal nodes where predictions are stored). Each internal node splits data into two or more subsets based on a feature threshold. The depth of a tree is the length of the longest path from root to any leaf. Deeper trees can represent more complex patterns but are more prone to overfitting.

# Tree anatomy example
print('Root node: first split on most informative feature')
print('Internal nodes: further splits on subsets')
print('Leaf nodes: final predictions')
print()
print('Depth=1 tree (stump): one question, two leaves')
print('Depth=2 tree: up to three questions, four leaves')
print('Depth=d tree: up to 2^d leaves')
print()
print('More depth = more flexible but higher overfitting risk')

Recursive Partitioning: How the Algorithm Splits

Decision tree construction is a greedy, recursive algorithm. At each node, it evaluates every possible split on every feature and threshold, picks the split that best separates the classes (measured by Gini impurity or information gain), applies that split, and then recursively repeats the process on each resulting subset. This continues until a stopping criterion is met: maximum depth reached, minimum samples per node, or no more useful splits exist. Greedy means the locally best split is chosen at each step, without backtracking — this can miss globally optimal splits but makes the algorithm tractable.

# Pseudocode for recursive tree building
def build_tree(X, y, depth=0, max_depth=3):
    # Stopping conditions
    if len(set(y)) == 1:      # All same class
        return {'leaf': True, 'prediction': y[0]}
    if depth >= max_depth:    # Max depth reached
        from collections import Counter
        return {'leaf': True, 'prediction': Counter(y).most_common(1)[0][0]}
    
    # Find best split
    best_feature, best_threshold = find_best_split(X, y)
    
    # Partition data
    left_mask  = X[:, best_feature] <= best_threshold
    right_mask = ~left_mask
    
    return {
        'leaf': False,
        'feature': best_feature,
        'threshold': best_threshold,
        'left':  build_tree(X[left_mask],  y[left_mask],  depth+1, max_depth),
        'right': build_tree(X[right_mask], y[right_mask], depth+1, max_depth)
    }

Axis-Aligned Splits: Thresholds on Single Features

Decision trees in scikit-learn always use axis-aligned (orthogonal) splits: each question asks if one feature is above or below a threshold (e.g., age <= 35?). This creates rectangular decision regions in 2D feature space. While this approach is simple and interpretable, it cannot efficiently represent diagonal decision boundaries — for example, separating two classes along a 45-degree line requires many splits. Tree ensembles (Random Forests) overcome this by combining many trees, each with different axis-aligned splits that together approximate any boundary shape.

import numpy as np

# Simulate finding a split on one feature
feature_values = np.array([10, 20, 30, 40, 50])
labels = np.array([0, 0, 0, 1, 1])

# For each possible threshold between consecutive values:
for threshold in [15, 25, 35, 45]:
    left_labels  = labels[feature_values <= threshold]
    right_labels = labels[feature_values > threshold]
    print(f'Threshold {threshold}: left={list(left_labels)}, right={list(right_labels)}')
# Threshold 35 gives perfect separation [0,0,0] vs [1,1]

Training a Decision Tree with scikit-learn

Scikit-learn's DecisionTreeClassifier trains in a single call to fit(). Key parameters include max_depth (maximum tree depth — critical for controlling overfitting), criterion (split quality measure: 'gini' or 'entropy'), and min_samples_split (minimum samples to split a node — prevents splitting tiny groups). The tree is ready to predict immediately after fitting. Unlike KNN, prediction is O(log N) — just follow the learned branches — making decision trees fast at inference time.

from sklearn.tree import DecisionTreeClassifier
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split

X, y = load_iris(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

tree = DecisionTreeClassifier(
    max_depth=3,
    criterion='gini',
    random_state=42
)
tree.fit(X_train, y_train)

print('Train accuracy:', tree.score(X_train, y_train).round(3))
print('Test  accuracy:', tree.score(X_test, y_test).round(3))
print('Tree depth:', tree.get_depth())
print('Number of leaves:', tree.get_n_leaves())

Following a Prediction Path

The power of decision trees is that you can trace every prediction step by step. The decision_path() method returns a sparse matrix indicating which nodes each sample passed through. The apply() method returns the leaf node index for each sample. These tools let you explain to a user exactly which questions were asked and what answers led to the prediction — essential for compliance, debugging, and building trust with non-technical stakeholders.

from sklearn.tree import DecisionTreeClassifier, export_text
from sklearn.datasets import load_iris

X, y = load_iris(return_X_y=True)
tree = DecisionTreeClassifier(max_depth=3, random_state=42)
tree.fit(X, y)

# Print human-readable decision rules
rules = export_text(tree, feature_names=load_iris().feature_names)
print(rules[:500])  # First 500 chars of the rule printout

# Which leaf does sample 0 land in?
leaf = tree.apply(X[[0]])
print('Sample 0 lands in leaf node:', leaf)

Tree Predictions at Leaf Nodes

Each leaf node stores a class distribution from the training samples that reached it. For classification, the predicted class is the majority class in the leaf. For probability estimation, predict_proba() returns the fraction of each class in the leaf. A leaf containing 10 samples: 9 class A, 1 class B predicts class A with probability 0.9. Trees with fewer samples per leaf produce less reliable probability estimates, which is why regularisation parameters like min_samples_leaf matter for calibrated probability outputs.

from sklearn.tree import DecisionTreeClassifier
from sklearn.datasets import load_iris
import numpy as np

X, y = load_iris(return_X_y=True)
tree = DecisionTreeClassifier(max_depth=3, random_state=42)
tree.fit(X, y)

# Predicted class and probabilities for first three samples
preds = tree.predict(X[:3])
probas = tree.predict_proba(X[:3])

for i in range(3):
    print(f'Sample {i}: class={preds[i]}, probabilities={probas[i].round(3)}')

Decision Tree for Regression

DecisionTreeRegressor works the same way as the classifier but predicts the mean target value of training samples in each leaf. The split criterion changes: instead of Gini impurity, it minimises mean squared error (or mean absolute error) within each resulting child node. Regression trees produce step-function predictions — constant values within rectangular regions. With enough depth, they can fit any training data exactly, but this leads to severe overfitting. Control depth and min_samples_leaf to regularise the regression tree.

from sklearn.tree import DecisionTreeRegressor
import numpy as np
import matplotlib.pyplot as plt

np.random.seed(0)
X = np.sort(5 * np.random.rand(80, 1), axis=0)
y = np.sin(X).ravel() + np.random.randn(80) * 0.3

for depth in [1, 3, 10]:
    reg = DecisionTreeRegressor(max_depth=depth)
    reg.fit(X, y)
    mse = np.mean((reg.predict(X) - y)**2)
    print(f'max_depth={depth}: train MSE={mse:.4f}')
# depth=10 nearly zero MSE (memorised training data)

Feature Importance from Decision Trees

After fitting, tree.feature_importances_ provides a measure of how much each feature contributed to the splits. Feature importance is calculated as the total reduction in impurity (Gini or entropy) attributed to each feature, weighted by the fraction of samples reaching each split. Values sum to 1.0. The most important feature gets the highest score. This provides a quick, interpretable way to identify which inputs drive predictions most — useful for feature selection, business insight, and detecting potential data issues.

from sklearn.tree import DecisionTreeClassifier
from sklearn.datasets import load_iris
import pandas as pd

X, y = load_iris(return_X_y=True)
feature_names = load_iris().feature_names

tree = DecisionTreeClassifier(max_depth=3, random_state=42)
tree.fit(X, y)

importances = pd.Series(tree.feature_importances_, index=feature_names)
print('Feature Importances:')
print(importances.sort_values(ascending=False))

Scale Invariance: Trees Do Not Need Scaling

A key practical advantage of decision trees is that they are completely scale-invariant. A split on income <= 50000 and a split on income_thousands <= 50 produce identical tree structures. Adding 100 to all values in a feature, or multiplying by 1000, does not change which splits are chosen. You never need to apply StandardScaler or MinMaxScaler before a decision tree. This also means decision trees handle features on wildly different scales without any preprocessing, making pipelines simpler.

from sklearn.tree import DecisionTreeClassifier
from sklearn.datasets import load_iris
from sklearn.preprocessing import StandardScaler
import numpy as np

X, y = load_iris(return_X_y=True)

# Without scaling
tree1 = DecisionTreeClassifier(random_state=42)
tree1.fit(X, y)

# With scaling (same result expected)
X_scaled = StandardScaler().fit_transform(X)
tree2 = DecisionTreeClassifier(random_state=42)
tree2.fit(X_scaled, y)

print('Without scaling accuracy:', tree1.score(X, y).round(3))
print('With scaling accuracy:   ', tree2.score(X_scaled, y).round(3))
# Identical -- scaling has no effect on tree splits

Handling Missing Values in Trees

Decision trees handle missing values more gracefully than many algorithms. Scikit-learn's DecisionTreeClassifier supports missing values natively when splitter='best' — samples with missing values in the split feature are sent to the child that minimises impurity on the non-missing data. Alternatively, you can use surrogate splits: when the primary split feature is missing for a sample, a correlated feature is used instead. This robustness to missing data is one practical advantage of tree-based models over distance-based methods like KNN that require complete feature vectors.

from sklearn.tree import DecisionTreeClassifier
import numpy as np

# Tree can handle NaN values with missing_values support
# In scikit-learn >= 1.0, DecisionTreeClassifier accepts NaN
X = np.array([
    [1, 2], [np.nan, 3], [3, np.nan], [4, 5]
])
y = np.array([0, 1, 0, 1])

tree = DecisionTreeClassifier(random_state=42)
tree.fit(X, y)
preds = tree.predict(X)
print('Predictions with NaN features:', preds)
# Tree routes NaN samples gracefully

Quick Check

Test your understanding of Machine Learning with Python concepts from this lesson.

Lesson Recap

In this lesson you learned: how decision trees recursively partition data using axis-aligned splits, that each leaf stores the class distribution for majority-vote prediction, and that decision trees are scale-invariant and require no feature scaling. Next up we explore Gini impurity and information gain — the criteria that determine which split to choose at each node.

Frequently asked questions

Is the “Building a Tree: Splits, Nodes, and Leaves” lesson free?

Yes — the full text of “Building a Tree: Splits, Nodes, and Leaves” is free to read here on the web, and the Machine Learning Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Machine Learning Academy course, upgrade to CoddyKit PRO.

What will I learn in “Building a Tree: Splits, Nodes, and Leaves”?

Learners will trace how a decision tree recursively partitions data at each node, from root to leaf, and make predictions by following branches. You practise Machine Learning Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start Machine Learning Academy?

No prior experience is required. Machine Learning Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 1 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Building a Tree: Splits, Nodes, and Leaves” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this Machine Learning Academy lesson?

Yes. Every Machine Learning Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. Building a Tree: Splits, Nodes, and Leaves
  2. Gini Impurity and Information Gain
  3. Controlling Tree Depth to Prevent Overfitting
  4. Visualising and Interpreting Decision Trees
← Back to Machine Learning Academy