🚀 Alguni Start learning

Unit 4: Right and Wrong

One number is never enough.

Unit 4 of 13 in AI and machine learning for kids. Its 4 lessons are Two Ways To Be Wrong, Precision and Recall, Moving the Line and Which Mistake Is Worse? — below is everything each one explains, and a question or two from it to try.

Every sample on this page is plain Python with no libraries, run before it shipped, and prints exactly what it says it prints.

This unit opens with a fortnight’s trial of everything — no card needed — or with a family plan, bought in the iPhone app. The first two units of every track are free for ever. Try it in the app.

⚠️ Two Ways To Be Wrong

A berry sorter

A model looks at berries and says poison or safe. There are two ways for it to be right, and two quite different ways for it to be wrong:

- a false alarm — it said poison, the berry was fine;
- a miss — it said safe, and the berry was not.

One of those wastes a berry. The other sends somebody to hospital. Any score that adds them together has thrown away the only thing worth knowing.

Four boxes, not one number

Count each kind separately. The four counts together are called a confusion matrix, which is a grand name for "keep the mistakes apart".

Python

berries = [
    ["poison", "poison"],
    ["poison", "poison"],
    ["poison", "safe"],
    ["safe", "poison"],
    ["safe", "safe"],
    ["safe", "safe"],
]

hits = 0
alarms = 0
misses = 0
clears = 0
for said, truth in berries:
    if said == "poison" and truth == "poison":
        hits = hits + 1
    if said == "poison" and truth == "safe":
        alarms = alarms + 1
    if said == "safe" and truth == "poison":
        misses = misses + 1
    if said == "safe" and truth == "safe":
        clears = clears + 1

print("caught:      ", hits)
print("false alarms:", alarms)
print("missed:      ", misses)
print("all clear:   ", clears)

It prints

caught:       2
false alarms: 1
missed:       1
all clear:    2

Accuracy adds up the wrong things

Accuracy is (caught + all clear) / everything. It counts a wasted berry and a poisoned child as the same event.

It is still worth knowing. It is simply never the whole answer, and on its own it can hide the only mistake that mattered.

Try it yourself

The model says "safe" about a poisonous berry. What is that called?

  • A false alarm
  • A miss
  • An all clear
  • A hit

How many misses does this print?

Python

berries = [
    ["safe", "poison"],
    ["poison", "poison"],
    ["safe", "poison"],
    ["safe", "safe"],
]

misses = 0
for said, truth in berries:
    if said == "safe" and truth == "poison":
        misses = misses + 1
print(misses)

Answer them in the app

🎯 Precision and Recall

Two questions worth asking

Precision — of the berries it flagged, how many really were poisonous? *When it shouts, should you believe it?*

Recall — of the poisonous berries there were, how many did it catch? *Does anything get past it?*

Both are counts divided by counts. Neither is optional.

Both from the same four boxes

Precision divides the hits by everything it flagged. Recall divides the hits by everything that was really poisonous.

Python

hits = 3
alarms = 1
misses = 1

print("precision", round(hits / (hits + alarms), 2))
print("recall   ", round(hits / (hits + misses), 2))

It prints

precision 0.75
recall    0.75

The cheat that gets perfect recall

Shout "poison" at every single berry. Nothing gets past you, so recall is a perfect 1.0 — and precision collapses, because most of what you flagged was fine.

This is unit 1's lazy baseline again, wearing the other hat. Either score is easy to max out on its own, which is exactly why you quote both.

Python

truths = ["poison", "safe", "safe", "poison", "safe", "safe"]

hits = 0
alarms = 0
for truth in truths:
    if truth == "poison":
        hits = hits + 1
    else:
        alarms = alarms + 1

print("recall   ", round(hits / len([t for t in truths if t == "poison"]), 2))
print("precision", round(hits / (hits + alarms), 2))

It prints

recall    1.0
precision 0.33

Try it yourself

A model flags 10 berries and 9 of them really are poisonous. That is:

  • Recall of 0.9
  • Precision of 0.9
  • Accuracy of 0.9
  • A miss rate of 0.9

There were 8 poisonous berries and the model caught 6. That is:

  • Precision of 0.75
  • Recall of 0.75
  • Accuracy of 0.75
  • Two false alarms

Answer them in the app

🎚️ Moving the Line

Models do not really say yes or no

Every model in this track ends in a number — the sigmoid gave 0.97, the neighbours voted 2 to 1. The yes or no comes from you, when you decide how big that number has to be.

Here are ten berries with the model's score for each, and whether it really was poisonous.

Python

berries = [
    [0.95, 1],
    [0.9, 1],
    [0.8, 0],
    [0.75, 1],
    [0.6, 0],
    [0.5, 1],
    [0.4, 0],
    [0.3, 0],
    [0.2, 0],
    [0.1, 0],
]

print("berries:", len(berries))
print("really poisonous:", sum(b[1] for b in berries))

It prints

berries: 10
really poisonous: 4

The same model, three different machines

Raise the bar and it only shouts when it is sure. Lower it and it shouts at anything. Same scores, same model — a dial that you set.

Python

berries = [
    [0.95, 1], [0.9, 1], [0.8, 0], [0.75, 1], [0.6, 0],
    [0.5, 1], [0.4, 0], [0.3, 0], [0.2, 0], [0.1, 0],
]

def judge(threshold):
    hits = 0
    alarms = 0
    misses = 0
    for score, poison in berries:
        said = 1 if score >= threshold else 0
        if said == 1 and poison == 1:
            hits = hits + 1
        if said == 1 and poison == 0:
            alarms = alarms + 1
        if said == 0 and poison == 1:
            misses = misses + 1
    return hits, alarms, misses

for threshold in [0.85, 0.7, 0.5]:
    hits, alarms, misses = judge(threshold)
    print(threshold, "caught", hits, "alarms", alarms, "missed", misses)

It prints

0.85 caught 2 alarms 0 missed 2
0.7 caught 3 alarms 1 missed 1
0.5 caught 4 alarms 2 missed 0

Now the important bit

All three settings have exactly the same accuracy: 0.8. The first one lets half the poisonous berries through and the last one catches every single one, and accuracy cannot tell them apart at all.

Precision and recall can, and they move in opposite directions as the dial turns.

Python

berries = [
    [0.95, 1], [0.9, 1], [0.8, 0], [0.75, 1], [0.6, 0],
    [0.5, 1], [0.4, 0], [0.3, 0], [0.2, 0], [0.1, 0],
]

def judge(threshold):
    hits = 0
    alarms = 0
    misses = 0
    clears = 0
    for score, poison in berries:
        said = 1 if score >= threshold else 0
        if said == 1 and poison == 1:
            hits = hits + 1
        if said == 1 and poison == 0:
            alarms = alarms + 1
        if said == 0 and poison == 1:
            misses = misses + 1
        if said == 0 and poison == 0:
            clears = clears + 1
    return hits, alarms, misses, clears

for threshold in [0.85, 0.7, 0.5]:
    hits, alarms, misses, clears = judge(threshold)
    print(threshold,
          "accuracy", round((hits + clears) / 10, 2),
          "precision", round(hits / (hits + alarms), 2),
          "recall", round(hits / (hits + misses), 2))

It prints

0.85 accuracy 0.8 precision 1.0 recall 0.5
0.7 accuracy 0.8 precision 0.75 recall 0.75
0.5 accuracy 0.8 precision 0.67 recall 1.0

You cannot have both

Every step you take towards catching everything drags in more berries that were fine. Every step towards never crying wolf lets more real ones past.

This is not a flaw to be fixed by better code. It is a genuine choice, and the code cannot make it.

Try it yourself

You lower the threshold. What happens?

  • Recall goes up, precision goes down
  • Precision goes up, recall goes down
  • Both go up
  • Nothing — the model is the same

How many berries does a threshold of 0.75 flag?

Python

berries = [
    [0.95, 1], [0.9, 1], [0.8, 0], [0.75, 1], [0.6, 0],
    [0.5, 1], [0.4, 0], [0.3, 0], [0.2, 0], [0.1, 0],
]

flagged = 0
for score, poison in berries:
    if score >= 0.75:
        flagged = flagged + 1
print(flagged)

Answer them in the app

🏆 Which Mistake Is Worse?

Same maths, opposite answers

Berries. A false alarm throws away a good berry. A miss makes somebody ill. Turn the dial down: catch everything, put up with the waste. Recall matters.

A junk mail filter. A miss means an advert in your inbox, which is annoying for a second. A false alarm means your grandmother's letter is in the bin and you never know it was sent. Turn the dial up: only bin what you are sure about. Precision matters.

The model cannot tell which world it is in. You can.

Put a price on each mistake

If a miss is ten times worse than a false alarm, say so — then add up what each threshold would cost and pick the cheapest. The same search as always, over a score that finally knows what the job is.

Python

berries = [
    [0.95, 1], [0.9, 1], [0.8, 0], [0.75, 1], [0.6, 0],
    [0.5, 1], [0.4, 0], [0.3, 0], [0.2, 0], [0.1, 0],
]

MISS_COST = 10
ALARM_COST = 1

def cost(threshold):
    total = 0
    for score, poison in berries:
        said = 1 if score >= threshold else 0
        if said == 0 and poison == 1:
            total = total + MISS_COST
        if said == 1 and poison == 0:
            total = total + ALARM_COST
    return total

for threshold in [0.95, 0.9, 0.75, 0.5, 0.1]:
    print(threshold, cost(threshold))

It prints

0.95 30
0.9 20
0.75 11
0.5 2
0.1 6

Cheapest at 0.5

Not the highest accuracy, not the highest precision — the lowest cost, given what we said the mistakes were worth.

And notice 0.1, where it flags every berry in the basket: no misses at all, and still worse, because six wasted berries add up.

Nothing in the maths chose 10 and 1

A person did. Change those two numbers and the best threshold changes with them.

This is where a model stops being arithmetic and starts being a decision about people. Somebody decides what a mistake costs, and often the person who pays for it is not the person who chose.

Try it yourself

For the junk mail filter, which mistake should be priced higher?

  • A miss — an advert reaching the inbox
  • A false alarm — a real letter thrown away
  • They are equal
  • Neither matters much

Someone tells you their model is "95% accurate". What is the best next question?

  • How long did it train?
  • What does it get wrong, and what does each of those mistakes cost?
  • How many weights does it have?
  • Which language is it in?

Answer them in the app