The table a person would write
Each row is one example. The last column is the label: the answer we'd want a model to learn. Everything else is a feature.
| Fruit | Weight (g) | Diameter (cm) | Colour | Sweetness | Label |
|---|---|---|---|---|---|
| Fuji apple | 190 | 8.0 | red | 8 | apple |
| Gala apple | 160 | 7.3 | red | 7 | apple |
| Granny Smith apple | 175 | 7.8 | green | 4 | apple |
| Small red apple | 130 | 6.8 | red | 6 | apple |
| Golden apple | 170 | 7.5 | yellow | 7 | apple |
| Navel orange | 220 | 8.5 | orange | 6 | orange |
| Valencia orange | 180 | 7.6 | orange | 6 | orange |
| Mandarin | 90 | 6.0 | orange | 7 | orange |
| Blood orange | 160 | 7.2 | red | 6 | orange |
| Large orange | 260 | 9.2 | orange | 5 | orange |
| Lemon | 110 | 6.2 | yellow | 1 | sour citrus |
| Meyer lemon | 95 | 5.8 | yellow | 3 | sour citrus |
| Lime | 65 | 5.0 | green | 1 | sour citrus |
| Large lemon | 150 | 7.0 | yellow | 1 | sour citrus |
| Calamansi | 25 | 3.0 | green | 2 | sour citrus |
The same table, as the computer sees it
Colour became four 0/1 columns (one-hot encoding). Scaling: min–max, every column squeezed into 0 to 1. Blue is positive, red is negative.
| weight | diameter | sweetness | color=green | color=orange | color=red | color=yellow | |
|---|---|---|---|---|---|---|---|
| Fuji apple | 0.70 | 0.81 | 1.00 | 0.00 | 0.00 | 1.00 | 0.00 |
| Gala apple | 0.57 | 0.69 | 0.86 | 0.00 | 0.00 | 1.00 | 0.00 |
| Granny Smith apple | 0.64 | 0.77 | 0.43 | 1.00 | 0.00 | 0.00 | 0.00 |
| Small red apple | 0.45 | 0.61 | 0.71 | 0.00 | 0.00 | 1.00 | 0.00 |
| Golden apple | 0.62 | 0.73 | 0.86 | 0.00 | 0.00 | 0.00 | 1.00 |
| Navel orange | 0.83 | 0.89 | 0.71 | 0.00 | 1.00 | 0.00 | 0.00 |
| Valencia orange | 0.66 | 0.74 | 0.71 | 0.00 | 1.00 | 0.00 | 0.00 |
| Mandarin | 0.28 | 0.48 | 0.86 | 0.00 | 1.00 | 0.00 | 0.00 |
| Blood orange | 0.57 | 0.68 | 0.71 | 0.00 | 0.00 | 1.00 | 0.00 |
| Large orange | 1.00 | 1.00 | 0.57 | 0.00 | 1.00 | 0.00 | 0.00 |
| Lemon | 0.36 | 0.52 | 0.00 | 0.00 | 0.00 | 0.00 | 1.00 |
| Meyer lemon | 0.30 | 0.45 | 0.29 | 0.00 | 0.00 | 0.00 | 1.00 |
| Lime | 0.17 | 0.32 | 0.00 | 1.00 | 0.00 | 0.00 | 0.00 |
| Large lemon | 0.53 | 0.65 | 0.00 | 0.00 | 0.00 | 0.00 | 1.00 |
| Calamansi | 0.00 | 0.00 | 0.14 | 1.00 | 0.00 | 0.00 | 0.00 |
Settings
Two features, drawn as a picture
Hover a dot to see which fruit it is. The dashed line points to the nearest fruit using all features, not just the two drawn.
Which fruit is the mystery closest to?
| Rank | Fruit | Label | Distance |
|---|---|---|---|
| #1 | Large lemon | sour citrus | 0.166 |
| #2 | Lemon | sour citrus | 0.320 |
| #3 | Meyer lemon | sour citrus | 0.400 |
| #4 | Golden apple | apple | 0.719 |
| #5 | Granny Smith apple | apple | 1.449 |
Where that distance comes from
Distance adds up the squared difference in every column. This is each column's share for the nearest fruit.
Same mystery fruit, three ways of scaling
| Scaling | Nearest fruit | So it's probably… |
|---|---|---|
| No scaling | Golden apple | apple |
| Min–max (0 to 1) | Large lemon | sour citrus |
| Standardised | Large lemon | sour citrus |
What to notice. Without scaling, weight is measured in hundreds while sweetness only goes from 1 to 10. A 20 g difference counts 400 in the distance, but the difference between "very sour" and "very sweet" counts at most 81. So the raw model mostly compares weights.
With the default mystery (170 g, yellow, sweetness 2), raw distance says it's closest to the Golden apple, just because they weigh the same. Once every column is scaled, sourness gets a fair vote and it lands next to the lemons.
Every model in this course, including LLMs, has the same problem. Neural networks rescale the numbers inside every layer for exactly this reason (you'll meet layer normalisation in lesson 13).
The code that just ran
Everything on this page came from calling this code, which lives at the top of this same file.
Show or hide the source
final class Featurizer
{
private array $vocab = []; // for each word column: every value we've seen
private array $min = [], $max = [], $mean = [], $std = [];
public function __construct(private array $numeric, private array $categorical) {}
// Learn what's needed to encode: the list of possible words, and the
// min/max/mean/spread of every column (needed for scaling).
public function fit(array $rows): void
{
foreach ($this->categorical as $col) {
$values = array_values(array_unique(array_column($rows, $col)));
sort($values);
$this->vocab[$col] = $values;
}
$vectors = array_map(fn($r) => $this->encode($r), $rows);
foreach (array_keys($this->names()) as $j) {
$column = array_column($vectors, $j);
$this->min[$j] = min($column);
$this->max[$j] = max($column);
$this->mean[$j] = array_sum($column) / count($column);
$var = 0.0;
foreach ($column as $v) $var += ($v - $this->mean[$j]) ** 2;
$this->std[$j] = sqrt($var / count($column)) ?: 1.0;
}
}
// Names of the columns in the vector, e.g. "weight", "color=red"
public function names(): array
{
$names = $this->numeric;
foreach ($this->categorical as $col) {
foreach ($this->vocab[$col] as $value) $names[] = "$col=$value";
}
return $names;
}
// ONE-HOT ENCODING: a word column becomes several 0/1 columns,
// one per possible word. "orange" → color=orange: 1, every other colour: 0.
// (Why not red=1, orange=2, yellow=3? Because then yellow would be
// "more" than red, and red–yellow would be "farther" than red–orange.)
public function encode(array $row): array
{
$v = [];
foreach ($this->numeric as $col) $v[] = (float) $row[$col];
foreach ($this->categorical as $col) {
foreach ($this->vocab[$col] as $value) $v[] = $row[$col] === $value ? 1.0 : 0.0;
}
return $v;
}
// SCALING: put every column on a similar range so no column dominates.
public function scale(array $v, string $how): array
{
foreach ($v as $j => $x) {
if ($how === 'minmax') { // squeeze into 0..1
$range = $this->max[$j] - $this->min[$j];
$v[$j] = $range ? ($x - $this->min[$j]) / $range : 0.0;
} elseif ($how === 'standard') { // "how many spreads from average?"
$v[$j] = ($x - $this->mean[$j]) / $this->std[$j];
}
}
return $v;
}
}
function euclidean(array $a, array $b): float
{
$sum = 0.0;
foreach ($a as $j => $x) $sum += ($x - $b[$j]) ** 2;
return sqrt($sum);
}