Easier to read with cream paper, a tint, more contrast or larger text? Choose your colours and text, once, for every page.

Free lesson

Judging a classifier honestly

After this lesson: I can judge a classifier with a confusion matrix, precision and recall, and not by accuracy alone.

60 min lesson2 commandsJupyterLab data science lab: pandas, Matplotlib and scikit-learn in the browser

Ninety-six percent, and useless

A spam filter is tested on 1000 messages. It is right 960 times, so it scores 96 percent. The team is pleased.

Then somebody finds the filter says "not spam" to every single message. Forty of the 1000 really were spam. One score hid the whole fault.

By the end of this lesson

Note

I can judge a classifier with a confusion matrix, precision and recall, and not by accuracy alone.

Key words

WordWhat it means
Accuracythe share of all messages the model got right
Imbalancedone label is far more common than the other
Confusion matrixa table of right and wrong answers for each label
Precisionof the messages flagged, the share that really were spam
Recallof the real spam, the share that was flagged
Thresholdthe score at or above which a message is flagged

The cells students fill in are left blank in this preview.

Accuracy is one number for two mistakes

Accuracy counts every right answer the same. That is fine when the labels are about equally common.

It fails when the table is imbalanced. If 40 messages in 1000 are spam, a model that never says "spam" is right 960 times. It is right 96 percent of the time and finds no spam at all.

Score a model that never says spam

Command
docker exec jupyter python3 -c "t=[1]*40+[0]*960; g=[0]*1000; print(sum(a==b for a,b in zip(t,g))/1000)"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Count the four kinds of answer

Spam is the thing we look for, so call it positive. Each message lands in one of four boxes.

Caught: spam, flagged. False alarm: not spam, flagged. Missed: spam, not flagged. Right rejection: not spam, not flagged.

The four counts together are the confusion matrix.

Worked example: a filter on 1000 messages

Flagged as spamNot flagged
Really spam (40)Caught: 30Missed: 10
Not spam (960)False alarms: 20Right rejections: 940

The cells students fill in are left blank in this preview.

Print the matrix, precision and recall

Command
docker exec jupyter python3 -c "from sklearn.metrics import confusion_matrix, precision_score, recall_score; t=[1]*40+[0]*960; g=[1]*30+[0]*10+[1]*20+[0]*940; print(confusion_matrix(t,g)); print(round(precision_score(t,g),3), round(recall_score(t,g),3), (30+940)/1000)"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Precision and recall ask two different questions

Precision asks: when the filter says spam, how often is it right? Here that is 30 of the 50 flagged, which is 0.6.

Recall asks: of the real spam, how much did the filter find? Here that is 30 of 40, which is 0.75. The accuracy was 0.97. The filter is better than the one that never says spam, but accuracy hardly shows how much.

Moving the threshold trades one for the other

Most models give a score and a threshold turns it into a label. Lower the threshold and more messages are flagged. More spam is caught, but more good mail is flagged too.

So precision and recall pull against each other, and a person has to choose. Run the code and watch the counts move.

Four thresholds on ten messages

Code: python
# Ten messages: the score a model gave (0 to 1) and whether it really was spam.
messages = [
    (0.95, 1), (0.9, 1), (0.8, 0), (0.7, 1), (0.6, 0),
    (0.5, 1), (0.4, 0), (0.3, 0), (0.2, 0), (0.1, 0),
]
for limit in [0.9, 0.7, 0.5, 0.3]:
    caught = 0
    false_alarms = 0
    missed = 0
    for score, spam in messages:
        flagged = score >= limit
        if flagged and spam == 1:
            caught = caught + 1
        if flagged and spam == 0:
            false_alarms = false_alarms + 1
        if not flagged and spam == 1:
            missed = missed + 1
    print("limit", limit, "caught", caught, "false alarms", false_alarms, "missed", missed)
# Which limit would you choose for a mailbox? Which for a bank?

Worked example: what each limit gives

LimitCaughtFalse alarmsMissedPrecisionRecall
0.92021.00.5
0.73110.750.75
0.54200.6671.0
0.34400.51.0

The cells students fill in are left blank in this preview.

A common mistake

Warning

Many people think the best limit is the one with the highest score on one number. There is no best limit without knowing the cost. A mailbox can live with some spam. A fraud check cannot afford to miss any.

Going further: the cost of each mistake

Say a missed spam costs 1 minute of reading and a false alarm costs 5 minutes looking for lost mail. Use the table of four limits.

Work out the minutes lost at each limit. Which limit costs least? Would it change if a false alarm cost only 1 minute?

About this lab and its source: JupyterLab data science lab: pandas, Matplotlib and scikit-learn in the browser

Teaching a class? Set this module for your students with a teacher guide, marked assignments and a lab for every student.