Click anywhere on the plot to place a query point ◆.
Type a word — where does it go?
Try blue first, then colours KNN has never seen: purple, teal, pink, asul. Then try hello to see what happens when there's no rule.
Type a point
x and y decide where the dot goes. The label only decides its colour. Training points have a label (we know the answer). A query point has no label, and KNN predicts it from its neighbours.
Paste spreadsheet data (raw text → points)
One point per line: x, y, label. This is exactly what a spreadsheet saved as .csv looks like.
Controls
62 training points memorised.
How it decided — step by step
No query point yet. Click anywhere on the plot. KNN will figure out which colour that spot should be.
Tip: try clicking right on the border between two colours, where the decision is hardest.
Choosing k — leave-one-out accuracy
For each point: hide it, let the other points predict it, check the answer. Repeat for every k. This is how you “tune” KNN, since there's nothing else to train.
Your k = 5: 87.1% correct. Best: ★ k = 11 (91.9%). Click a bar to try that k. Odd k values avoid ties when there are two classes.
The training data — what KNN actually memorised
Every dot on the plot is just one of these rows. Columns x and y are the features (they give the position). The label is the answer (it gives the colour).
Show all 62 rows
| # | x (feature 1) | y (feature 2) | label (answer) |
|---|---|---|---|
| 0 | 3.44 | 7.14 | Red |
| 1 | 3.16 | 6.09 | Red |
| 2 | 1.00 | 8.72 | Red |
| 3 | 0.46 | 6.70 | Red |
| 4 | 3.69 | 6.47 | Red |
| 5 | 5.87 | 6.95 | Red |
| 6 | 3.62 | 8.94 | Red |
| 7 | 5.03 | 5.50 | Red |
| 8 | 1.70 | 8.25 | Red |
| 9 | 4.41 | 5.29 | Red |
| 10 | 2.12 | 9.09 | Red |
| 11 | 4.70 | 7.17 | Red |
| 12 | 4.16 | 6.44 | Red |
| 13 | 1.54 | 8.26 | Red |
| 14 | 1.90 | 8.74 | Red |
| 15 | 2.53 | 5.03 | Red |
| 16 | 5.56 | 7.33 | Red |
| 17 | 2.73 | 6.46 | Red |
| 18 | 4.69 | 7.28 | Red |
| 19 | 3.05 | 6.43 | Red |
| 20 | 5.26 | 7.65 | Blue |
| 21 | 8.32 | 6.63 | Blue |
| 22 | 7.75 | 7.03 | Blue |
| 23 | 5.50 | 7.33 | Blue |
| 24 | 5.90 | 8.98 | Blue |
| 25 | 7.15 | 6.25 | Blue |
| 26 | 6.69 | 7.46 | Blue |
| 27 | 6.84 | 6.79 | Blue |
| 28 | 4.81 | 5.03 | Blue |
| 29 | 9.66 | 7.92 | Blue |
| 30 | 6.97 | 5.28 | Blue |
| 31 | 6.42 | 8.54 | Blue |
| 32 | 5.63 | 6.80 | Blue |
| 33 | 6.38 | 7.37 | Blue |
| 34 | 9.49 | 6.84 | Blue |
| 35 | 6.38 | 6.47 | Blue |
| 36 | 9.80 | 7.22 | Blue |
| 37 | 6.78 | 7.22 | Blue |
| 38 | 6.38 | 9.25 | Blue |
| 39 | 6.08 | 8.97 | Blue |
| 40 | 5.35 | 1.98 | Green |
| 41 | 6.35 | 1.22 | Green |
| 42 | 4.18 | 2.17 | Green |
| 43 | 4.27 | 2.84 | Green |
| 44 | 4.50 | 2.00 | Green |
| 45 | 7.23 | 3.53 | Green |
| 46 | 4.86 | 3.10 | Green |
| 47 | 4.97 | 4.26 | Green |
| 48 | 4.69 | 4.10 | Green |
| 49 | 2.23 | 3.69 | Green |
| 50 | 7.29 | 1.97 | Green |
| 51 | 3.46 | 2.57 | Green |
| 52 | 5.31 | 1.55 | Green |
| 53 | 3.58 | 3.24 | Green |
| 54 | 6.77 | 0.97 | Green |
| 55 | 4.41 | 4.54 | Green |
| 56 | 4.70 | 2.80 | Green |
| 57 | 5.83 | 2.44 | Green |
| 58 | 5.14 | 1.26 | Green |
| 59 | 5.39 | 2.45 | Green |
| 60 | 5.00 | 6.90 | Green |
| 61 | 3.60 | 4.20 | Blue |
The code that just ran
This is the KNN class from this same file, shown live. Everything above came from calling it.
Show / hide source
final class KNN
{
/** Every training point: ['x' => float, 'y' => float, 'label' => int] */
private array $points = [];
public function __construct(
private int $k = 5, // how many neighbours get a vote
private string $metric = 'euclidean', // how we measure "close"
private bool $weighted = false // should closer neighbours count more?
) {}
// ---- "TRAINING" --------------------------------------------------------
// KNN is a *lazy learner*: training is literally just remembering the data.
// There are no weights, no equations, no iterations. All the work happens
// later, at prediction time.
public function fit(array $points): void
{
$this->points = array_values($points);
}
// ---- STEP A: how far apart are two points? -----------------------------
public function distance(array $a, array $b): float
{
$dx = $a['x'] - $b['x'];
$dy = $a['y'] - $b['y'];
if ($this->metric === 'manhattan') {
return abs($dx) + abs($dy); // walk along city blocks
}
return sqrt($dx * $dx + $dy * $dy); // straight line (Pythagoras)
}
// ---- STEP B: measure the distance to EVERY training point, then sort ---
// $exclude lets us skip one point (used for leave-one-out testing below).
public function rankNeighbors(array $query, ?int $exclude = null): array
{
$ranked = [];
foreach ($this->points as $i => $p) {
if ($i === $exclude) continue;
$ranked[] = [
'index' => $i,
'label' => $p['label'],
'distance' => $this->distance($query, $p),
];
}
usort($ranked, fn($a, $b) => $a['distance'] <=> $b['distance']);
return $ranked; // nearest first
}
// ---- STEP C + D: keep the k nearest, and let them vote -----------------
// Returns [label => votes], highest first.
// Ties: PHP's sort is stable, so on a tie the label that appeared first
// (i.e. belongs to the nearer neighbour) wins.
public function vote(array $ranked, ?int $k = null): array
{
$k ??= $this->k;
$votes = [];
foreach (array_slice($ranked, 0, $k) as $n) {
$weight = $this->weighted ? 1 / ($n['distance'] + 1e-9) : 1;
$votes[$n['label']] = ($votes[$n['label']] ?? 0) + $weight;
}
arsort($votes);
return $votes;
}
// ---- Everything together: point in, label out --------------------------
public function predict(array $query): int
{
if (!$this->points) return -1;
$votes = $this->vote($this->rankNeighbors($query));
return (int) array_key_first($votes);
}
// ---- How good is each k? (Leave-One-Out cross-validation) --------------
// For every point: hide it, predict it from the others, check if we were
// right. Do that for k = 1..maxK. This is how you *choose* k.
public function leaveOneOutAccuracy(int $maxK): array
{
$n = count($this->points);
$correct = array_fill(1, $maxK, 0);
foreach ($this->points as $i => $p) {
$ranked = $this->rankNeighbors($p, exclude: $i); // sort once...
for ($k = 1; $k <= $maxK; $k++) { // ...reuse for every k
if ((int) array_key_first($this->vote($ranked, $k)) === $p['label']) {
$correct[$k]++;
}
}
}
return array_map(fn($c) => $n ? $c / $n : 0.0, $correct);
}
public function points(): array { return $this->points; }
}