Hello! Welcome to the next module in our course on building supervised learning models from scratch.
In the last few modules, we've focused on models like SVMs that find a geometric boundary (a line or a curve) to separate data. Today, we're pivoting to a completely different and highly intuitive family of models: Decision Trees. Instead of finding one complex boundary, decision trees learn a series of simple, hierarchical if/else questions to arrive at a decision.
The core challenge in building a decision tree is to figure out which questions to ask and in what order. Today's lesson tackles the very first step: how to quantitatively measure if a question is "good" or not. Our goal is to implement the two most common metrics for this task: Gini impurity and information gain. These functions will become the engine of our tree-building algorithm.
The Core Idea: Purity and Information
A decision tree works by recursively splitting a dataset into smaller and smaller subsets. The ideal split is one that separates the data into "pure" nodes, where each node contains samples of only one class.
To get a clear picture of this process, let's watch a short conceptual overview.
Let’s Write a Decision Tree Classifier from Scratch - Machine Learning Recipes #8
The video 'Let’s Write a Decision Tree Classifier from Scratch' by Google for Developers provides an excellent introduction to the core concepts.
Please watch the segment from 01:45 to 07:05. Pay close attention to these key ideas: The goal is to 'unmix' the labels at each split. Gini Impurity is introduced as a metric to quantify the uncertainty (or 'mixed-up-ness') at a single node. Information Gain is the metric that measures how much a specific question reduces that uncertainty.
As the video explained, we need two things: a way to measure impurity in a node and a way to measure the improvement a split provides. Let's implement both from scratch.
1. Measuring Impurity: The Gini Index
The Gini index (or Gini impurity) is a measure of how often a randomly chosen element from a set would be incorrectly labeled if it was randomly labeled according to the distribution of labels in the set.
Its formula is:
where is the number of classes and is the probability of class in the node.
- A Gini score of 0 is a perfect score. It means the node is pure (all samples belong to one class).
- For a binary classification problem, the worst-case Gini score is 0.5, which means the node is perfectly mixed (50% class A, 50% class B).
Let's look at how to translate this into a clean Python function.
How to code decision tree in Python from scratch
The article 'How to code decision tree in Python from scratch' by Ander Fernandez provides a very clear implementation of Gini impurity using Pandas and NumPy, which aligns well with your background.
Please read the section 'Calculate impurity using the Gini index'. Focus on the formula and the provided gini_impurity Python function. We'll adapt it slightly to work directly with NumPy arrays.
Now, let's write our own NumPy-based version. This function will be a core building block.
import numpy as np
from collections import Counter
def gini_impurity(y):
"""
Calculates the Gini impurity for a vector of labels.
Args:
y: A NumPy array of labels.
Returns:
The Gini impurity score (float).
"""
# Count the occurrences of each class
counts = Counter(y)
# If the node is empty or pure, impurity is 0
if len(counts) <= 1:
return 0
impurity = 1.0
num_samples = len(y)
for label in counts:
prob_of_label = counts[label] / num_samples
impurity -= prob_of_label**2
return impurity
# --- Let's test it ---
# A pure node (all class 0)
pure_node = np.array([0, 0, 0, 0, 0])
print(f"Impurity of a pure node: {gini_impurity(pure_node):.4f}")
# A perfectly mixed node (50/50)
mixed_node = np.array([0, 1, 0, 1, 0, 1])
print(f"Impurity of a mixed node: {gini_impurity(mixed_node):.4f}")
# An imbalanced node
imbalanced_node = np.array([0, 0, 0, 0, 1])
print(f"Impurity of an imbalanced node: {gini_impurity(imbalanced_node):.4f}")
You should see an output of 0.0, 0.5, and 0.32 respectively, which confirms our understanding.
2. Measuring the Split's Quality: Information Gain
Now that we can measure how "bad" a node is, we can measure how much "better" a split makes things. This is Information Gain.
It's calculated as the impurity of the parent node minus the weighted average of the impurities of the child nodes.
Where is the number of samples in a node. A higher information gain means a better split.

Let's implement this. Our function will take the parent labels and the labels of the two children produced by a split.
def information_gain(parent_y, left_y, right_y, impurity_func=gini_impurity):
"""
Calculates the information gain of a split.
Args:
parent_y: Labels of the data in the parent node.
left_y: Labels of the data in the left child node.
right_y: Labels of the data in the right child node.
impurity_func: The function to use for calculating impurity (e.g., gini_impurity).
Returns:
The information gain score (float).
"""
# If a child node is empty, the split is not useful
if len(left_y) == 0 or len(right_y) == 0:
return 0
# Calculate impurity of the parent
parent_impurity = impurity_func(parent_y)
# Calculate weighted average impurity of children
n_parent = len(parent_y)
n_left, n_right = len(left_y), len(right_y)
weight_left = n_left / n_parent
weight_right = n_right / n_parent
weighted_child_impurity = (weight_left * impurity_func(left_y) +
weight_right * impurity_func(right_y))
# Calculate information gain
ig = parent_impurity - weighted_child_impurity
return ig
# --- Let's test it with a simple split ---
parent_node = np.array([0, 0, 1, 1, 0, 1]) # Gini = 0.5
# A "perfect" split that separates the classes
perfect_split_left = np.array([0, 0, 0])
perfect_split_right = np.array([1, 1, 1])
ig_perfect = information_gain(parent_node, perfect_split_left, perfect_split_right)
print(f"Information gain of a perfect split: {ig_perfect:.4f}")
# A "useless" split that doesn't improve purity
useless_split_left = np.array([0, 1, 0])
useless_split_right = np.array([1, 0, 1])
ig_useless = information_gain(parent_node, useless_split_left, useless_split_right)
print(f"Information gain of a useless split: {ig_useless:.4f}")
The perfect split gives an information gain of 0.5, completely removing all impurity. The useless split results in an information gain of around 0.056, showing it barely helped.
Test your understanding!
Imagine you calculate the information gain for two potential splits:
- Splitting on
feature_A < 10gives an information gain of0.25. - Splitting on
feature_B == 'red'gives an information gain of0.35.
Which split should the decision tree algorithm choose, and why?
Show answer
The algorithm should choose to split on feature_B == 'red'. This is because it has a higher information gain (0.35 > 0.25), meaning this split does a better job of reducing the overall impurity and creating purer child nodes.
3. An Alternative: Entropy
Gini impurity is not the only metric. Another popular choice is Entropy, which comes from information theory. It also measures uncertainty or randomness.
Its formula is:
- Entropy is 0 for a pure node.
- For a binary problem, it's 1 for a perfectly mixed node.
The concept of Information Gain remains exactly the same; you just swap out the Gini function for the Entropy function.
How to code decision tree in Python from scratch
Let's briefly look at the Entropy implementation from the same article.
Read the section 'Calculate impurity with entropy'. Note the formula and the entropy function. The addition of a small number (1e-9) inside the log is a common numerical stability trick to prevent log(0), which is undefined.
Here is the implementation for entropy:
def entropy(y):
"""
Calculates the Entropy for a vector of labels.
"""
counts = Counter(y)
if len(counts) <= 1:
return 0
entropy_val = 0.0
num_samples = len(y)
for label in counts:
prob_of_label = counts[label] / num_samples
entropy_val -= prob_of_label * np.log2(prob_of_label)
return entropy_val
# --- Test it on the same nodes ---
print(f"\nEntropy of a pure node: {entropy(pure_node):.4f}")
print(f"Entropy of a mixed node: {entropy(mixed_node):.4f}")
print(f"Entropy of an imbalanced node: {entropy(imbalanced_node):.4f}")
You'll see scores of 0.0, 1.0, and 0.7219. While the scales are different, they rank the nodes' purity in the same order as Gini. In practice, Gini is often the default because it's slightly faster to compute (no logarithm), but both are excellent choices.
Conclusion
Great work today! You have implemented the fundamental metrics that drive the construction of a decision tree. These functions allow the algorithm to make a quantitative, data-driven choice at every step of the tree-building process.
Key Takeaways:
- Impurity measures the "mixed-up-ness" of labels in a node. We implemented two common metrics: Gini Impurity and Entropy.
- Information Gain measures the reduction in impurity achieved by a split. It is the core criterion for choosing the best feature and value to split on.
- The goal of the algorithm is to always choose the split that maximizes information gain.
Preview of the Next Lesson:
We now have the tools to evaluate a split, but we still need to find the best one. In the next lesson, we will implement a function to find the best possible split for a dataset. This will involve iterating through every feature and every possible split point, calculating the information gain for each, and keeping track of the one that performs best. This is the heart of the decision tree learning algorithm.