k-Nearest Neighbors — Visual Lab

Every calculation on this page is done by the ~70-line PHP class at the bottom. Click the plot to place a point and watch KNN classify it.

0 0 2 2 4 4 6 6 8 8 10 10 x y point 0 (3.44, 7.14) — Red point 1 (3.16, 6.09) — Red point 2 (1.00, 8.72) — Red point 3 (0.46, 6.70) — Red point 4 (3.69, 6.47) — Red point 5 (5.87, 6.95) — Red point 6 (3.62, 8.94) — Red point 7 (5.03, 5.50) — Red point 8 (1.70, 8.25) — Red point 9 (4.41, 5.29) — Red point 10 (2.12, 9.09) — Red point 11 (4.70, 7.17) — Red point 12 (4.16, 6.44) — Red point 13 (1.54, 8.26) — Red point 14 (1.90, 8.74) — Red point 15 (2.53, 5.03) — Red point 16 (5.56, 7.33) — Red point 17 (2.73, 6.46) — Red point 18 (4.69, 7.28) — Red point 19 (3.05, 6.43) — Red point 20 (5.26, 7.65) — Blue point 21 (8.32, 6.63) — Blue point 22 (7.75, 7.03) — Blue point 23 (5.50, 7.33) — Blue point 24 (5.90, 8.98) — Blue point 25 (7.15, 6.25) — Blue point 26 (6.69, 7.46) — Blue point 27 (6.84, 6.79) — Blue point 28 (4.81, 5.03) — Blue point 29 (9.66, 7.92) — Blue point 30 (6.97, 5.28) — Blue point 31 (6.42, 8.54) — Blue point 32 (5.63, 6.80) — Blue point 33 (6.38, 7.37) — Blue point 34 (9.49, 6.84) — Blue point 35 (6.38, 6.47) — Blue point 36 (9.80, 7.22) — Blue point 37 (6.78, 7.22) — Blue point 38 (6.38, 9.25) — Blue point 39 (6.08, 8.97) — Blue point 40 (5.35, 1.98) — Green point 41 (6.35, 1.22) — Green point 42 (4.18, 2.17) — Green point 43 (4.27, 2.84) — Green point 44 (4.50, 2.00) — Green point 45 (7.23, 3.53) — Green point 46 (4.86, 3.10) — Green point 47 (4.97, 4.26) — Green point 48 (4.69, 4.10) — Green point 49 (2.23, 3.69) — Green point 50 (7.29, 1.97) — Green point 51 (3.46, 2.57) — Green point 52 (5.31, 1.55) — Green point 53 (3.58, 3.24) — Green point 54 (6.77, 0.97) — Green point 55 (4.41, 4.54) — Green point 56 (4.70, 2.80) — Green point 57 (5.83, 2.44) — Green point 58 (5.14, 1.26) — Green point 59 (5.39, 2.45) — Green point 60 (5.00, 6.90) — Green point 61 (3.60, 4.20) — Blue
Red Blue Green ◆ query point shaded = what KNN would predict there

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 y

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

5
↶ Undo last point

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.

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)
03.447.14 Red
13.166.09 Red
21.008.72 Red
30.466.70 Red
43.696.47 Red
55.876.95 Red
63.628.94 Red
75.035.50 Red
81.708.25 Red
94.415.29 Red
102.129.09 Red
114.707.17 Red
124.166.44 Red
131.548.26 Red
141.908.74 Red
152.535.03 Red
165.567.33 Red
172.736.46 Red
184.697.28 Red
193.056.43 Red
205.267.65 Blue
218.326.63 Blue
227.757.03 Blue
235.507.33 Blue
245.908.98 Blue
257.156.25 Blue
266.697.46 Blue
276.846.79 Blue
284.815.03 Blue
299.667.92 Blue
306.975.28 Blue
316.428.54 Blue
325.636.80 Blue
336.387.37 Blue
349.496.84 Blue
356.386.47 Blue
369.807.22 Blue
376.787.22 Blue
386.389.25 Blue
396.088.97 Blue
405.351.98 Green
416.351.22 Green
424.182.17 Green
434.272.84 Green
444.502.00 Green
457.233.53 Green
464.863.10 Green
474.974.26 Green
484.694.10 Green
492.233.69 Green
507.291.97 Green
513.462.57 Green
525.311.55 Green
533.583.24 Green
546.770.97 Green
554.414.54 Green
564.702.80 Green
575.832.44 Green
585.141.26 Green
595.392.45 Green
605.006.90 Green
613.604.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; }
}