← Teacher guide

The nearest penguin

Students write a nearest-neighbour model from scratch, measuring the distance between penguins with Pythagoras and copying the species of the closest one, then ask for measurements, add a "not sure" answer and guess five mystery penguins.

Machine learning with Gradio · Models by hand · about 45 minutes · Open the student lesson

Classroom resources

Load these into Waypoints Prepare: choose Load URL and paste the copied address, or download the file and import it. In Prepare, the worksheet's Teacher edition shows the answers; the student copy hides them.

Students will be able to

Timing

Predict and Run 10 min · Investigate 10 min · Modify 15 min · Make 10 min

Before class

Read the unit primer first. Nearest neighbour keeps every example and answers with the species of the closest one; the distance is Pythagoras on bill and flipper length (** 2 squares, ** 0.5 is the square root). nearest() is Data detective's "smallest so far" pattern (the bug task uses >, finding the furthest penguin). A big distance means nothing similar is known, so the guess is weak, but the model still answers: the "Not sure" task and the optional last lesson come back to this. The info question raises scale: flippers (around 200) outweigh bills (around 45) in the distance.

Running the lesson

  1. Predict: draw the right-angled triangle with sides 3 and 4.
  2. Run: plot three penguins and the mystery on a sketch graph (bill across, flipper up).
  3. Investigate: ask which guess students would bet on, and why.
  4. Make: discuss the 60, 240 penguin: what is wrong with its guess?

The hard part (on purpose)

The smallest-so-far loop inside nearest(). Ask students to trace it for three penguins on paper before they fix the bug.

When students get stuck

What you will seeA question to askThe fix
The guess is always the same species.Does nearest() compare with < and update best?if distance(p, new) < distance(best, new): best = p.
TypeError: unsupported operand type(s) for -: 'str' and 'float'.Was the typed answer turned into a number?float(input(...)).
Distance 0.0 for a mystery penguin.Is that penguin's exact measurement in the data?Nothing is wrong; it matched a real penguin exactly.

Answers to the checks

  1. Predict: What does it print?
    5.0. The differences are 3 and 4; 3 squared plus 4 squared is 25, and the square root of 25 is 5.0.
  2. Where the learning is: Where is the "learning" in this model?
    The examples are the knowledge: it answers from the penguins it has seen, not from rules someone wrote
    Nearest neighbour learns by remembering. Give it different examples and it gives different answers, with no rules changed.
  3. How sure?: A mystery penguin's nearest match is 0.2 away; another's is 10 away. Which guess would you trust more, and why?
    The 0.2 one: a penguin in the data is almost exactly like it
    A small distance means a very similar penguin is known. A big one means nothing in the data is like it, so the guess is weak, even though the model still answers.
  4. Spot the bug: The mystery penguin (47, 215) is guessed as an Adelie 44 away, though a Gentoo 0.2 away is in the data. Click the line that is wrong.
    line 16
    > keeps the biggest distance so far, which finds the furthest penguin. < keeps the smallest: the nearest.
  5. Where this leads: Flipper lengths are around 200 and bill lengths around 45. What does that do to the distance?
    Differences in flippers count for more, because the numbers are bigger
    Measurements with bigger spreads dominate the distance. Real machine learning often rescales each measurement first so each counts fairly.

Solutions

One possible solution for each task. Other correct programs are fine.

Modify › Ask for the measurements

import csv

penguins = []
with open("penguins.csv") as f:
    for row in csv.DictReader(f):
        penguins.append({"species": row["species"], "bill": float(row["bill_length_mm"]), "flipper": float(row["flipper_length_mm"])})


def distance(a, b):
    return ((a["bill"] - b["bill"]) ** 2 + (a["flipper"] - b["flipper"]) ** 2) ** 0.5


def nearest(new):
    best = penguins[0]
    for p in penguins:
        if distance(p, new) < distance(best, new):
            best = p
    return best


bill = float(input("Bill length (mm)? "))
flipper = float(input("Flipper length (mm)? "))
mystery = {"bill": bill, "flipper": flipper}
match = nearest(mystery)
print("Nearest penguin: " + match["species"] + ", bill " + str(match["bill"]) + ", flipper " + str(match["flipper"]))
print("Distance: " + str(round(distance(match, mystery), 1)))
print("Guess: " + match["species"])

Modify › Not sure

import csv

penguins = []
with open("penguins.csv") as f:
    for row in csv.DictReader(f):
        penguins.append({"species": row["species"], "bill": float(row["bill_length_mm"]), "flipper": float(row["flipper_length_mm"])})


def distance(a, b):
    return ((a["bill"] - b["bill"]) ** 2 + (a["flipper"] - b["flipper"]) ** 2) ** 0.5


def nearest(new):
    best = penguins[0]
    for p in penguins:
        if distance(p, new) < distance(best, new):
            best = p
    return best


mystery = {"bill": 47.0, "flipper": 215.0}
match = nearest(mystery)
print("Nearest penguin: " + match["species"] + ", bill " + str(match["bill"]) + ", flipper " + str(match["flipper"]))
print("Distance: " + str(round(distance(match, mystery), 1)))
if distance(match, mystery) > 10:
    print("Not sure: no penguin in the data is close")
else:
    print("Guess: " + match["species"])

five mystery penguins

import csv

penguins = []
with open("penguins.csv") as f:
    for row in csv.DictReader(f):
        penguins.append({"species": row["species"], "bill": float(row["bill_length_mm"]), "flipper": float(row["flipper_length_mm"])})


def distance(a, b):
    return ((a["bill"] - b["bill"]) ** 2 + (a["flipper"] - b["flipper"]) ** 2) ** 0.5


def nearest(new):
    best = penguins[0]
    for p in penguins:
        if distance(p, new) < distance(best, new):
            best = p
    return best


mysteries = [
    {"bill": 38.0, "flipper": 185.0},
    {"bill": 49.5, "flipper": 196.0},
    {"bill": 46.0, "flipper": 218.0},
    {"bill": 44.0, "flipper": 201.0},
    {"bill": 60.0, "flipper": 240.0},
]

for m in mysteries:
    match = nearest(m)
    print(str(m["bill"]) + ", " + str(m["flipper"]) + ": " + match["species"] + " (distance " + str(round(distance(match, m), 1)) + ")")

Stretch and support

Stretch: Make the three nearest penguins vote (sort by distance with sorted(penguins, key=lambda p: distance(p, mystery))[:3]).

Support: Give students the Not sure if, and ask them only to choose the number.

Exit ticket

How does a nearest-neighbour model guess a new penguin's species?

It finds the penguin in its data with the closest measurements and gives that penguin's species.

What each step is for

Where it leads

Next lesson tests the model fairly, on penguins it has not seen. scikit-learn's KNeighborsClassifier (Lesson 4) is this model.