Module · Decision Trees
Gini impurity — how mixed is this pile?
Lesson 2 of 8 · 12 min
Twenty questions chose questions by how evenly they split the field. That rule works until a leaf holds a mixture of answers.
Then you need to know how mixed. That is a number, and this lesson builds it.
The number is called Gini impurity. Every later lesson in this path uses it.
Ten deals: 6 won and 4 lost
| Outcome | Deals | Share of the pile |
|---|---|---|
| won | 6 | 6 / 10 = 0.6 |
| lost | 4 | 4 / 10 = 0.4 |
won
- Deals
- 6
- Share of the pile
- 6 / 10 = 0.6
lost
- Deals
- 4
- Share of the pile
- 4 / 10 = 0.4
It is ten deals so that you can check every number on this page on paper. The job is to put one number on that pile. An all-won pile should score lowest. A half-and-half pile should score highest.
Gini impurity is how often two picks from a pile disagree
Reach into the pile and take a deal. Put it back. Take another. Did the two agree?
| The pile | How often two picks disagree |
|---|---|
| all won | never |
| half won, half lost | about half the time |
| 6 won, 4 lost | somewhere in between |
all won
- How often two picks disagree
- never
half won, half lost
- How often two picks disagree
- about half the time
6 won, 4 lost
- How often two picks disagree
- somewhere in between
That is Gini impurity. It behaves the way a tree needs, and it was defined without a formula.
So play it before deriving anything. The notebook takes two deals at random, a hundred thousand times, and counts the disagreements. That run gives 0.483.
They agree 0.52 of the time, so they disagree 0.48
Now the arithmetic. Two picks agree if both are won, or if both are lost.
- both won: 0.6 × 0.6 = 0.36
- both lost: 0.4 × 0.4 = 0.16
- they agree: 0.36 + 0.16 = 0.52
- so they disagree: 1 − 0.52 = 0.48
Square each share, add them up, subtract from one. That is all of it.
The arithmetic gives 0.48. Playing the game gives 0.483.
The formula and the game give the same answer. The squares are there for a reason. A share squared is the chance of drawing that outcome twice in a row.
A pile that agrees scores 0. A half-and-half pile scores 0.5.
The curve is zero at both ends and highest in the middle. That is the behaviour a tree needs from this number.
| The pile | Gini impurity |
|---|---|
| 5 won, 5 lost | 0.5 |
| 6 won, 4 lost | 0.48 |
| 9 won, 1 lost | 0.18 |
| 10 won, 0 lost | 0 |
5 won, 5 lost
- Gini impurity
- 0.5
6 won, 4 lost
- Gini impurity
- 0.48
9 won, 1 lost
- Gini impurity
- 0.18
10 won, 0 lost
- Gini impurity
- 0
Notice how flat the top is. Moving one deal from a 5-and-5 pile changes the score by only 0.02. A tree can therefore make a split that looks small and find a much better one below it.
With three outcomes the worst score is 0.667, not 0.5
Nothing above assumed there were two outcomes. Add a third, for deals that broke even, and the same arithmetic works. Five won, three lost and two even scores 0.62.
| Outcomes, evenly spread | Worst possible impurity |
|---|---|
| 2 | 0.5 |
| 3 | 0.667 |
| 4 | 0.75 |
| 10 | 0.9 |
2
- Worst possible impurity
- 0.5
3
- Worst possible impurity
- 0.667
4
- Worst possible impurity
- 0.75
10
- Worst possible impurity
- 0.9
So 0.6 is a bad score for a yes-or-no question and a good one for a question with five answers. Compare gini scores inside one problem. Never compare them across two.
Weight the two sides of a split by how many deals are in them
A question does not score itself. It takes one pile and makes two. What you compare against is the impurity of the two piles you are left with.
One side comes out clean and the other stays messy. The question now is how to combine those two numbers into one.
| Way of combining the two sides | Result |
|---|---|
| weighted by how many deals are on each side | 0.267 |
| plain average of the two sides | 0.222 |
weighted by how many deals are on each side
- Result
- 0.267
plain average of the two sides
- Result
- 0.222
Always weight by size. A plain average lets four clean deals count as much as six messy ones. It reports a better number for a worse split.
What you now have
One number for how mixed a pile is. It is zero when everything agrees. It peaks in the middle. Its ceiling depends on the number of outcomes. After a split, weight it by pile size.
There is a second measure that does the same job. It answers a question this path has already asked once: how many yes-or-no questions is the answer worth? That is entropy, and it is the next lesson.
content/notebooks/decision-trees/how-mixed-is-this-pile.ipynb
Change the pile and predict the score before running it. Worth trying: replace “lost” with three different loss reasons. The pile has not changed and the impurity has.
Show the code6 cells
DEALS = ["won"] * 6 + ["lost"] * 4
n = record("n_deals", len(DEALS))
won = record("n_won", DEALS.count("won"))
lost = record("n_lost", DEALS.count("lost"))
print(f"{won} won, {lost} lost, out of {n}")
print(f"share won = {won}/{n} = {won / n}")
print(f"share lost = {lost}/{n} = {lost / n}")import random
random.seed(3)
TRIALS = 100_000
disagreed = sum(1 for _ in range(TRIALS)
if random.choice(DEALS) != random.choice(DEALS))
measured = record("measured_gini", round(disagreed / TRIALS, 3))
print(f"{disagreed} of {TRIALS} pairs disagreed -> {measured}")def gini(pile):
"""1 - the chance two draws agree."""
if not pile:
return 0.0
shares = [pile.count(k) / len(pile) for k in set(pile)]
return 1 - sum(s * s for s in shares)
by_hand = 1 - (6 / 10) ** 2 - (4 / 10) ** 2
print(f"by hand {by_hand:.3f}")
print(f"by function {gini(DEALS):.3f}")
print(f"by playing {measured:.3f}")
record("gini_by_hand", round(by_hand, 3))
record("gap_to_simulation", round(abs(by_hand - measured), 3))piles = [(w, gini(["won"] * w + ["lost"] * (10 - w))) for w in range(11)]
for w, g in piles:
print(f"{w:>2} won, {10 - w:>2} lost {g:.2f} {'#' * round(g * 40)}")
record("gini_5_5", round(gini(["won"] * 5 + ["lost"] * 5), 3))
record("gini_9_1", round(gini(["won"] * 9 + ["lost"]), 3))
record("gini_10_0", round(gini(["won"] * 10), 3))
record("gap_5_5_to_6_4", round(gini(["won"] * 5 + ["lost"] * 5) - by_hand, 3))
def plot(ax):
ps = [i / 200 for i in range(201)]
ax.plot(ps, [1 - p * p - (1 - p) ** 2 for p in ps], color="#9aa0aa", lw=1.4, zorder=1)
ax.scatter([w / 10 for w, _ in piles], [g for _, g in piles], s=34, color="#e2574c", zorder=3)
for w, g in [(0, 0.0), (5, 0.5), (6, by_hand)]:
label = {0: "0 won\nnot mixed at all", 5: "5 and 5\nas mixed as it gets",
6: "our pile\n6 won, 4 lost"}[w]
ax.annotate(label, (w / 10, g), textcoords="offset points",
xytext=(0, 14 if w == 0 else -40), ha="center", fontsize=9, color="#77777f")
ax.set_xlabel("Share of the pile that were won")
ax.set_ylabel("Gini impurity")
ax.set_ylim(-0.16, 0.62)
save_fig("every-pile-of-ten", plot, figsize=(7, 4.0))for k in range(2, 7):
even = [str(i) for i in range(k)] * 12
print(f"{k} outcomes, evenly spread {gini(even):.3f} (= 1 - {k} x (1/{k})^2)")
record("max_two", round(gini(["a", "b"] * 12), 3))
record("max_three", round(gini(["a", "b", "c"] * 12), 3))
three = ["won"] * 5 + ["lost"] * 3 + ["broke even"] * 2
record("gini_three_way", round(gini(three), 3))
print(f"\n5 won, 3 lost, 2 broke even -> {gini(three):.3f}")SPLIT = {
"budget signed off": ["won"] * 4,
"no budget": ["won"] * 2 + ["lost"] * 4,
}
after = 0.0
for side, pile in SPLIT.items():
w = len(pile) / n
print(f"{side:<20} {len(pile):>2} deals impurity {gini(pile):.2f} weight {w:.1f}")
after += w * gini(pile)
record("after_split", round(after, 3))
record("plain_average", round(sum(gini(p) for p in SPLIT.values()) / len(SPLIT), 3))
print(f"\nweighted {after:.3f} <- what a tree compares against")
print(f"plain mean {sum(gini(p) for p in SPLIT.values()) / len(SPLIT):.3f} <- wrong: it lets 4 spotless deals count as much as 6 messy ones")
def plot2(ax):
labels = ["before the split\n10 deals", "budget signed off\n5 deals", "no budget\n5 deals"]
vals = [gini(DEALS), gini(SPLIT["budget signed off"]), gini(SPLIT["no budget"])]
ax.bar(labels, vals, color=["#9aa0aa", "#e2574c", "#e2574c"], width=0.55)
for i, v in enumerate(vals):
ax.text(i, v + 0.015, f"{v:.2f}", ha="center", fontsize=10)
ax.axhline(after, ls="--", lw=1.2, color="#3b6fd4")
ax.text(2.42, after + 0.015, f"weighted\n{after:.2f}", ha="center", fontsize=9, color="#3b6fd4")
ax.set_ylabel("Gini impurity")
ax.set_ylim(0, 0.58)
ax.grid(axis="x", visible=False)
save_fig("before-and-after-a-split", plot2, figsize=(7, 3.9))
