Classification: Predicting Categories
Sorting new things using old examples
In this lesson
The other half of supervised machine learning. Classification puts a new example into one of a fixed set of buckets: spam or not spam, which species, will this customer renew.
You are going to write a working classifier in about five lines. Doing it by hand once removes most of the mystery from everything that follows.
Explain it like I’m 5
Classification is sorting new examples into buckets based on old ones — “this new flower looks most like the ones we called setosa, so it is probably a setosa”.
The simplest classifier that works
Here is an idea so obvious it feels like cheating: to classify something new, find the example it most resembles and copy that answer. This is nearest neighbor, and it is a genuine algorithm people use.
“Most resembles” just means closest. With two features per example you can measure distance exactly as you would on a map: the difference in each direction, squared, summed, square-rooted.
import numpy as np
# Two features per flower: petal length and petal width.
X = np.array([[1.4, 0.2], [1.3, 0.2], [1.5, 0.4],
[4.7, 1.4], [4.5, 1.5], [4.9, 1.5]])
y = np.array(["setosa", "setosa", "setosa",
"versicolor", "versicolor", "versicolor"])
def nearest_neighbour(point):
"""Predict by finding the closest example we have already seen."""
distances = np.sqrt(((X - point) ** 2).sum(axis=1))
return y[distances.argmin()]
print(nearest_neighbour(np.array([1.4, 0.3])))
print(nearest_neighbour(np.array([4.6, 1.4])))
predictions = np.array([nearest_neighbour(row) for row in X])
print("accuracy on training data:", (predictions == y).mean())
setosa versicolor accuracy on training data: 1.0
Accuracy, and why 100% should worry you
Accuracy is the fraction of predictions that were right, measured here against the training data. It is the obvious measure and the right place to start.
But look at that result: 100% accurate. Before celebrating, notice what was measured. The classifier predicted the labels of the same six flowers it was given, and its rule is “find the closest example”, so for any training row, the closest example is itself, at distance zero. It scored perfectly by memorizing.
That number tells you nothing about a flower it has never seen, which is the only thing you actually want to know. The fix has its own lesson, immediately after this one.
Use the nearest-neighbor classifier on three flowers it has never seen. The predict function is written for you. Call it on each row of new_flowers, then count how many of the known examples it gets right.
import numpy as np
X = np.array([[1.4, 0.2], [1.3, 0.2], [4.7, 1.4], [4.5, 1.5]])
y = np.array(["setosa", "setosa", "versicolor", "versicolor"])
def predict(point):
distances = np.sqrt(((X - point) ** 2).sum(axis=1))
return y[distances.argmin()]
new_flowers = np.array([[1.5, 0.3], [4.6, 1.5], [1.2, 0.1]])
# TODO: print each new flower and its prediction
# TODO: count how many known examples are predicted correctly
correct = 0
print("correct on known examples:", correct, "of", len(X))
Loop with for flower in new_flowers: and print list(flower), "->", predict(flower). For the count, sum(predict(row) == label for row, label in zip(X, y)), where True counts as 1, the trick from Unit 12.
import numpy as np
X = np.array([[1.4, 0.2], [1.3, 0.2], [4.7, 1.4], [4.5, 1.5]])
y = np.array(["setosa", "setosa", "versicolor", "versicolor"])
def predict(point):
distances = np.sqrt(((X - point) ** 2).sum(axis=1))
return y[distances.argmin()]
new_flowers = np.array([[1.5, 0.3], [4.6, 1.5], [1.2, 0.1]])
for flower in new_flowers:
print(list(flower), "->", predict(flower))
correct = sum(predict(row) == label for row, label in zip(X, y))
print("correct on known examples:", correct, "of", len(X))
[1.5, 0.3] -> setosa
[4.6, 1.5] -> versicolor
[1.2, 0.1] -> setosa
correct on known examples: 4 of 4
Confidence is not correctness
Most classifiers can report how sure they are, usually as a probability per class. It is a useful number and a dangerous one.
A model saying “95% confident this is spam” is reporting how strongly its learned pattern matches, not the probability that it is right. A model trained on bad data can be confidently, consistently wrong, and its confidence will not waver.
The nearest-neighbor classifier above makes this vivid: hand it a photograph of a car encoded as two numbers and it will return “setosa” with no hesitation, because setosa was the closest thing it had ever seen. A model can only answer within the world of its examples.
Common mistake: Celebrating accuracy measured on the training data
It is the data in front of you, and the number comes out high.
That measures memorization. Hold some examples back and score on those instead. The next lesson shows how.
Common mistake: Trusting accuracy when the classes are unbalanced
90% sounds good in isolation.
Work out what always guessing the commonest class would score. If that is also 90%, your model has learned nothing.
Common mistake: Treating a confidence score as a probability of being right
They are both percentages and both sound authoritative.
Confidence describes how well the input matched the learned pattern. On data unlike the training set it can be high and wrong.
Common mistake: Using classification where the answer is really a number
Buckets feel simpler than a continuous value.
Splitting price into “cheap” and “expensive” throws away information. If the answer is a number, use regression.
What does a classifier predict?
Spam or not, which species, which grade band. Numbers are regression's job.
Why is 100% accuracy on the training data not impressive?
Nearest neighbor finds each training row's closest match: itself. That says nothing about unseen data.
A model labels every email 'not spam' and scores 99% accuracy. What does that tell you?
If 99% of emails genuinely are not spam, guessing that every time scores 99% while catching no spam at all.
Mini exercise (hard)
Build a confusion matrix by hand: the four counts that show how a classifier is wrong, not just how often. Given the true labels and the predictions for eight emails, count the true positives, true negatives, false positives and false negatives, then compute accuracy from them.
Have a go. Finish the code and press Run to see the result immediately, right on this page.
actual = ["spam", "spam", "ok", "ok", "spam", "ok", "ok", "spam"]
predicted = ["spam", "ok", "ok", "ok", "ok", "ok", "spam", "spam"]
# TODO: count the four outcomes
true_pos = true_neg = false_pos = false_neg = 0
print("true positives: ", true_pos)
print("true negatives: ", true_neg)
print("false positives:", false_pos)
print("false negatives:", false_neg)
print("accuracy:", round((true_pos + true_neg) / len(actual), 2))
Walk the pairs with zip(actual, predicted). A true positive is predicted spam and actually spam; a false positive is predicted spam but actually not. Accuracy is (true_pos + true_neg) / len(actual).
actual = ["spam", "spam", "ok", "ok", "spam", "ok", "ok", "spam"]
predicted = ["spam", "ok", "ok", "ok", "ok", "ok", "spam", "spam"]
true_pos = true_neg = false_pos = false_neg = 0
for real, guess in zip(actual, predicted):
if real == "spam" and guess == "spam":
true_pos += 1
elif real == "ok" and guess == "ok":
true_neg += 1
elif real == "ok" and guess == "spam":
false_pos += 1
else:
false_neg += 1
print("true positives: ", true_pos)
print("true negatives: ", true_neg)
print("false positives:", false_pos)
print("false negatives:", false_neg)
print("accuracy:", round((true_pos + true_neg) / len(actual), 2))
true positives: 2
true negatives: 3
false positives: 1
false negatives: 2
accuracy: 0.62
assert true_pos == 2, "two spam emails were correctly caught"
assert false_neg == 2, "two real spam emails slipped through"
assert false_pos == 1, "one safe email was wrongly flagged"
assert true_pos + true_neg + false_pos + false_neg == len(actual), "every email must land in exactly one box"
print("✓ Looks good!")