Lesson 44 · Practical · 19 min

Python model evaluation and accuracy

A score is only useful when you know what it measured.

train/test splitaccuracyprecisionrecallconfusion matrix

The classifier reports 92% accuracy, and the 8% it misses are all the complaints. Accuracy hid the failure. Which metric would have shown it?

A model that predicts well on the data it memorized has not proved anything. Evaluation needs a separate test set, a clear positive class, and metrics that match the cost of mistakes. Accuracy is a starting point, not the whole report. You will also combine precision and recall with F1 and inspect subgroup results.

Hold out a test set

Split examples before fitting or tuning anything. The test set should not influence feature selection, scaling, or threshold choice. For small datasets, use several folds and report the variation instead of trusting one lucky split.

rows = [("a", 1), ("b", 0), ("c", 1), ("d", 0), ("e", 1)]
cut = 3
train = rows[:cut]
test = rows[cut:]
print(len(train), len(test))
print(test)

A visible, fixed split

Count the four outcomes

For a binary classifier, every prediction falls into true positive, true negative, false positive, or false negative. Build the confusion matrix explicitly. The names depend on which class you call positive, so state that choice in the report.

actual = [1, 1, 0, 0]
predicted = [1, 0, 0, 1]
tp = sum(a == 1 and p == 1 for a, p in zip(actual, predicted))
tn = sum(a == 0 and p == 0 for a, p in zip(actual, predicted))
fp = sum(a == 0 and p == 1 for a, p in zip(actual, predicted))
fn = sum(a == 1 and p == 0 for a, p in zip(actual, predicted))
print(tp, tn, fp, fn)

Four counts, not one

Compute precision and recall

Precision asks how many predicted positives were correct. Recall asks how many real positives were found. Both can be gamed by a degenerate classifier, so report them together with the class balance and the decision threshold.

tp, fp, fn = 8, 2, 4
precision = tp / (tp + fp)
recall = tp / (tp + fn)
print(round(precision, 2))
print(round(recall, 2))

Two different questions

Beware accuracy on imbalance

If only five of one hundred examples are positive, predicting negative every time gives 95% accuracy and finds none of the positives. Always compare against a trivial baseline and inspect per-class results before celebrating a headline number.

actual = [0] * 95 + [1] * 5
predicted = [0] * 100
accuracy = sum(a == p for a, p in zip(actual, predicted)) / len(actual)
print(round(accuracy, 2))
print(sum(1 for a, p in zip(actual, predicted) if a == 1 and p == 1))

High accuracy, zero recall

F1 balances precision and recall

F1 is the harmonic mean of precision and recall. It is high only when both are high, so it is useful when neither false positives nor false negatives can be ignored. Report the component metrics too; F1 hides which side is failing.

tp, fp, fn = 8, 2, 4
precision = tp / (tp + fp)
recall = tp / (tp + fn)
f1 = 2 * precision * recall / (precision + recall)
print(round(precision, 2), round(recall, 2), round(f1, 2))

One number, two ingredients

Check slices, not only the overall score

An aggregate can look acceptable while one group fails badly. Group results by a relevant slice, but keep groups large enough to avoid overreading noise. Slicing is a diagnostic, not permission to collect unnecessary personal data.

rows = [
    ("new", 1, 1), ("new", 1, 0), ("new", 0, 1),
    ("returning", 1, 1), ("returning", 0, 0), ("returning", 0, 0),
]
for group in sorted({row[0] for row in rows}):
    subset = [row for row in rows if row[0] == group]
    correct = sum(actual == predicted for _, actual, predicted in subset)
    print(group, correct, "/", len(subset), round(correct / len(subset), 2))

A slice can hide inside a good average

Key points

  • Evaluate on data that was held out from training and tuning.
  • A confusion matrix exposes the four possible outcomes.
  • Precision measures correct positives; recall measures found positives.
  • Accuracy can look high when the classes are imbalanced.
  • Report class balance, threshold, and a trivial baseline with any score.
  • F1 summarizes the precision/recall trade-off; subgroup checks expose hidden failures.

Common mistakes

  • Tuning on the test set until it stops being a test set.
  • Reporting accuracy without saying which class is positive.
  • Ignoring false negatives when missing a positive case is costly.
  • Reporting one aggregate accuracy and ignoring a small but important failing segment.
Your turn

Given actual and predicted binary labels, compute the confusion matrix, accuracy, precision, and recall, using zero-safe division.

Try it yourself

Change the values, predict the output, then run the code. The Python runtime is downloaded the first time you press Run, so the first run can take a few seconds.

In-browser PythonRuns locally · no account
First run downloads Pyodide from a CDN. Use Run code after editing.

Practice this lesson

12 questions with immediate explanations.

See all 12 answersStart practice
← Nearest neighborPrompt validation →