Skip to content
Expedify
12 min

Entropy — how many questions is the answer worth?

Sixteen animals take four questions, and the first lesson never said where the four came from. It came from entropy, and this lesson proves it by building the best strategy there is and counting.

Twenty questions used a number it never explained. Sixteen animals, and four questions is the best anyone could do. So a tree averaging 4.1 was close to perfect.

Where did the four come from?

It came from entropy. Entropy is the second way to measure how mixed a pile is, and it answers a very concrete question.

Entropy is how many yes/no questions an answer is worth

Suppose I know an outcome and you do not. How many yes-or-no questions would you need to get it out of me?

One bit is one yes-or-no question's worth of answer.

2

Questions needed
1
Entropy
1 bit

4

Questions needed
2
Entropy
2 bits

16

Questions needed
4
Entropy
4 bits

Sixteen equally likely answers cost four questions. That is the number Twenty questions used and never explained.

log₂ of a share is how many halvings it takes to reach that share

How mixed is this pile? scored ten deals with gini and got 0.48, which is a probability. Entropy asks a different question about the same pile and answers in questions.

entropy=kpklog2pk\text{entropy} = -\sum_{k} p_k \log_2 p_k
Same shape as gini, with a logarithm where gini has a square.

An animal that comes up one time in sixteen is four halvings away, so naming it costs four questions. The formula averages that cost over every outcome, weighted by how often each one comes up.

The ten deals are worth 0.971 questions

Under one question, because the pile already leans one way. Knowing a deal was won is the less surprising answer, so it costs less to tell you.

  • won: 0.6 × log₂(1 / 0.6) = 0.442
  • lost: 0.4 × log₂(1 / 0.4) = 0.529
  • together: 0.971 bits

A pile that is split down the middle costs a whole question. A pile that agrees costs none.

The same ten deals, and the two extremes for comparison.

5 won, 5 lost

Entropy
1 — a whole question, because you know nothing

6 won, 4 lost

Entropy
0.971

10 won, 0 lost

Entropy
0 — no question needed, because you already know

Entropy falls as a pile agrees with itself, exactly as gini does. The two measures move together. Only the unit is different.

Sixteen animals are worth exactly 4 questions

Entropy claims to be a floor. It says no questioning strategy can beat it on average. That is a strong claim, so the notebook builds the best possible strategy and counts.

Three numbers from three different places.

what entropy says is possible

Questions
4

the best strategy there is, built and measured

Questions
4

lesson 1's tree, measured

Questions
4.1

Entropy predicted the floor before any tree existed, and the best strategy hit it exactly. Lesson 1's tree paid 4.1, a tenth of a question above the minimum.

If one animal is picked half the time, entropy falls to 2.95

All of that assumed the sixteen animals were equally likely. Suppose half the people who play think of the dolphin.

The same sixteen animals, played two ways.decision-trees/how-many-questions-is-it-worth.ipynb

Then “is it the dolphin?” becomes the best first question after all. Lesson 1 dismissed that opening, and it was only a bad opening while every animal was equally likely.

Entropy is the number that knows this. It falls from 4 to 2.953, and the best strategy falls with it. Nothing about the animals changed. Only how often each one comes up.

When 99% of rows share one outcome, there is very little for a model to earn

A pile that almost agrees with itself has a very low entropy. There is very little to tell anybody. So a model that is right 99% of the time by always saying the same thing has learned nothing.

Keep that in mind. Perfect on what it has seen meets it in a real file, where 86.5% of the people share one outcome.

Gini and entropy pick the same best split in all 33 splits of the ten deals

Two measures of the same disagreement, in different units.decision-trees/how-many-questions-is-it-worth.ipynb

Gini is a probability. Entropy is a count of questions. Across all 33 possible splits of those ten deals, they pick the same best split and the same top three.

This path uses gini, because it has no logarithms

Gini is the measure from here on, and not because it is better. It uses squares instead of logarithms, so the arithmetic in every later lesson stays checkable on paper. On real splits the two measures agree anyway.

You now have both numbers and know what each one means. The next lesson spends them on twenty-four films and five questions.

Entropy, the best possible strategy, and both curves

content/notebooks/decision-trees/how-many-questions-is-it-worth.ipynb

Give one animal 0.9 of the probability and watch entropy fall below a single question. Worth trying: attempt a strategy for the equally likely sixteen that averages under four questions. You cannot, and that is the only thing entropy claims.

Show the code5 cells
Entropy of the ten deals, by hand and by function
from math import log2

DEALS = ["won"] * 6 + ["lost"] * 4

def entropy(pile):
    """The average number of yes/no questions the answer is worth, in bits."""
    if not pile:
        return 0.0
    shares = [pile.count(k) / len(pile) for k in set(pile)]
    return -sum(s * log2(s) for s in shares if s > 0)

by_hand = -0.6 * log2(0.6) - 0.4 * log2(0.4)
print(f"  0.6 x log2(1/0.6) = {-0.6 * log2(0.6):.3f}")
print(f"  0.4 x log2(1/0.4) = {-0.4 * log2(0.4):.3f}")
print(f"                      -----")
print(f"                      {by_hand:.3f} bits")

record("deals_entropy", round(by_hand, 3))
record("even_entropy", round(entropy(["won", "lost"]), 3))
# abs(): -0.0 is a true statement and an ugly one to print in a lesson.
record("pure_entropy", abs(round(entropy(["won"] * 10), 3)))
print(f"\nfive and five  {entropy(['won'] * 5 + ['lost'] * 5):.3f}   (a whole question — you know nothing)")
print(f"all won        {entropy(['won'] * 10):.3f}   (no question needed — you already know)")
Sixteen equally likely animals — where the four came from
uniform = [f"animal {i}" for i in range(16)]
print(f"entropy of 16 equally likely animals = {entropy(uniform):.3f} bits")
print(f"log2(16)                             = {log2(16):.3f}")

record("animals_entropy", round(entropy(uniform), 3))
record("animals_measured", 4.1)   # what the greedy tree in lesson 1 actually averaged
The best possible questioning strategy, built and measured
import heapq

def best_tree_cost(weights):
    """Average questions under the shortest possible strategy (Huffman)."""
    if len(weights) == 1:
        return 0.0
    heap = [(w, i, 0) for i, w in enumerate(weights)]   # (weight, tiebreak, depth-so-far)
    heapq.heapify(heap)
    total = 0.0
    nxt = len(weights)
    while len(heap) > 1:
        a = heapq.heappop(heap)
        bq = heapq.heappop(heap)
        merged = a[0] + bq[0]
        total += merged          # every merge adds one question to everything below it
        heapq.heappush(heap, (merged, nxt, 0))
        nxt += 1
    return total

equal = [1 / 16] * 16
print(f"entropy of the sixteen        {entropy(uniform):.3f} questions")
print(f"best possible strategy        {best_tree_cost(equal):.3f} questions")

record("animals_best_strategy", round(best_tree_cost(equal), 3))
A skewed game — entropy falls, and so does the number of questions
def entropy_of(weights):
    return -sum(w * log2(w) for w in weights if w > 0)

skewed = [0.5] + [0.5 / 15] * 15   # one animal picked half the time
print(f"{'':22} {'entropy':>9} {'best strategy':>15}")
print(f"{'all equally likely':22} {entropy_of(equal):>9.3f} {best_tree_cost(equal):>15.3f}")
print(f"{'one picked half the time':22} {entropy_of(skewed):>9.3f} {best_tree_cost(skewed):>15.3f}")

record("skewed_entropy", round(entropy_of(skewed), 3))
record("skewed_best", round(best_tree_cost(skewed), 3))
record("questions_saved", round(best_tree_cost(equal) - best_tree_cost(skewed), 1))

def plot(ax):
    labels = ["all sixteen\nequally likely", "one animal picked\nhalf the time"]
    ent = [entropy_of(equal), entropy_of(skewed)]
    best = [best_tree_cost(equal), best_tree_cost(skewed)]
    xs = range(len(labels))
    ax.bar([x - 0.19 for x in xs], ent, width=0.36, color="#e2574c", label="entropy — the floor")
    ax.bar([x + 0.19 for x in xs], best, width=0.36, color="#9aa0aa", label="best strategy — measured")
    for x, (e, bb) in enumerate(zip(ent, best)):
        ax.text(x - 0.19, e + 0.07, f"{e:.2f}", ha="center", fontsize=9.5)
        ax.text(x + 0.19, bb + 0.07, f"{bb:.2f}", ha="center", fontsize=9.5)
    ax.set_xticks(list(xs))
    ax.set_xticklabels(labels)
    ax.set_ylabel("Questions to name the animal")
    ax.set_ylim(0, 5.0)
    ax.legend(frameon=False, fontsize=9)
    ax.grid(axis="x", visible=False)

save_fig("entropy-is-the-floor", plot, figsize=(7, 3.9))
The two curves, and whether they ever disagree about a split
def gini(pile):
    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)

# Every way of splitting ten deals into two sides, scored both ways: does the
# ranking ever change? This is the honest version of "they mostly agree".
from itertools import combinations
rows = []
for size in range(1, 10):
    for left_won in range(0, min(size, 6) + 1):
        left_lost = size - left_won
        if left_lost > 4:
            continue
        left = ["won"] * left_won + ["lost"] * left_lost
        right = ["won"] * (6 - left_won) + ["lost"] * (4 - left_lost)
        w = len(left) / 10
        rows.append((
            gini(DEALS) - (w * gini(left) + (1 - w) * gini(right)),
            entropy(DEALS) - (w * entropy(left) + (1 - w) * entropy(right)),
        ))

by_gini = sorted(range(len(rows)), key=lambda i: -rows[i][0])
by_entropy = sorted(range(len(rows)), key=lambda i: -rows[i][1])
record("n_splits_compared", len(rows))
record("same_best_split", by_gini[0] == by_entropy[0])
record("same_top_three", by_gini[:3] == by_entropy[:3])
print(f"{len(rows)} possible splits of the ten deals")
print(f"same best split      {by_gini[0] == by_entropy[0]}")
print(f"same top three       {by_gini[:3] == by_entropy[:3]}")
print(f"identical ranking    {by_gini == by_entropy}")

def plot2(ax):
    ps = [i / 200 for i in range(201)]
    ax.plot(ps, [1 - p * p - (1 - p) ** 2 for p in ps], color="#e2574c", label="gini — chance two draws disagree")
    ax.plot(ps, [0 if p in (0, 1) else -p * log2(p) - (1 - p) * log2(1 - p) for p in ps],
            color="#3b6fd4", ls="--", label="entropy — questions the answer is worth")
    ax.set_xlabel("Share of the pile that were won")
    ax.set_ylabel("Impurity")
    ax.legend(frameon=False, fontsize=9)

save_fig("gini-and-entropy", plot2, figsize=(7, 3.6))

Related lessons