ML Atlas

Machine learning from zero · Lesson 4.4 · 23 of 33 lessons ready · 12 min

Feature engineering

In short

Cultivar 2 sits mid-axis and no threshold cuts it out. A new column x² gives 77.4% instead of 60.4%; 200 noise columns drag kNN from 98.1% to 77.4%.

Warm-up Two questions from earlier lessons. A minute, and it never blocks the lesson.
From lesson 4.3: Categories and scaling Why does kNN without scaling score only 0.695 on the wines?

Show the answer

Correct answer: Because proline has a huge spread and rules the whole distance

Proline has a standard deviation of 314, the least variable measurement 0.124. Without scaling the distance is almost only the difference in proline. After standardising the accuracy rises to 0.963.

From lesson 4.2: Missing data BMI is missing for the 110 heaviest patients. What does filling in the mean of the others do?

Show the answer

Correct answer: It weakens the BMI–disease link, because the sickest land at an average BMI

The correlation falls from 0.586 to 0.352. 110 patients with a lot of progression sit in one point: at the mean of 24.3.

In lesson 4.3 we changed how features are written: the scale of numbers and the code of categories. Today we make new features out of the ones we already have. Feature engineering means adding columns that show the model more clearly what it is looking for: a square, a ratio, a difference, the number of days since an event. One good feature can turn a hard problem into an easy one. Many weak ones can spoil a model that worked.

Predict, then check Wines of three cultivars. A linear model has to tell cultivar 2 from the others by flavanoid content alone. On one axis a linear model puts down one threshold: “cultivar 2” on one side, “another” on the other. Cultivar 2 averages 2.08, cultivar 1 has 2.98 and cultivar 3 has 0.78. Will one threshold separate cultivar 2?
How sure are you?

Interactive lab

This interactive lab runs in a browser with JavaScript enabled. The text around it describes what the lab shows.

Show the answer

Correct answer: No, because cultivar 2 sits in the middle and has wines of other cultivars on both sides

No. In the middle of the axis one threshold always leaves one of the two other cultivars on the “cultivar 2” side. The best threshold on the training wines gets 61.6%, barely more than 60.0% for saying “another cultivar” for every wine, and on the test wines exactly as much: 60.4%. The lab's linear SVM predicts cultivar 2 nowhere. Only a new column x² lets it cut out the middle interval.

Try it

  • Start with x alone. Where does the model predict cultivar 2? What is the test accuracy, and where does it come from?
  • Add the column x². What does the boundary look like in the plane (x, x²), and on the flavanoid axis alone?
  • Switch to noise. Move the slider down from 200 columns. When does kNN recognise the held-out wines best?
  • Watch the distance contrast. What happens to the gap between the nearest and the farthest wine as columns pile up?

Once you have checked it in the lab, let's name what you see.

The idea in plain words

A linear model on one axis can do only one thing: put down a threshold. Cultivar 2 sits in the middle of the flavanoid axis, so a threshold cannot cut it out from either side: it cuts off at most one of the neighbouring cultivars. The best such threshold cuts off cultivar 3 and gets 61.6% of the training wines, while saying “another cultivar” for every wine gets 60.0%. The gain is tiny, and on the test it vanishes. The lab's linear SVM predicts cultivar 2 nowhere and gets 60.4%, the share of the other cultivars among the 53 test wines. Let's add the column x², the square of the standardised flavanoids. Wines from both ends of the axis have a large x², wines from the middle a small one. The same linear model now lays a straight line in the plane (x, x²). On the flavanoid axis that is two thresholds, an interval from 1.11 to 2.82. Accuracy rises to 77.4%. The model did not change; what it gets did.

More features do not mean better, though. kNN (k = 5) on 13 chemical measurements recognises 98.1% of the held-out wines. We add columns of pure noise, random numbers on the same scale. With 200 such columns accuracy falls to 77.4%. Every noise column adds as much to the distance as a real measurement, so the “nearest” wines stop being similar. This is the curse of dimensionality: in many dimensions all points lie almost equally far from one another.

Jak to policzono
  • Wines: Wine (Aeberhard and Forina, 1992; UCI, licence CC BY 4.0), 178 wines, 13 chemical measurements, 3 cultivars. Stratified split: 125 wines to learn from, 53 to test.
  • New feature: a linear model (a linear support vector machine, C = 1) on the standardised flavanoids, cultivar 2 against the rest. Mean flavanoids computed on all 178 wines.
  • Noise: kNN with k = 5 on the 13 standardised measurements and 0 to 200 columns of numbers from a normal distribution (mean 0, standard deviation 1), drawn with a fixed seed.

Why it works

A linear model splits space with a straight line (a plane in higher dimensions). On one axis a straight line is one point, a threshold. The column x² adds a second axis. Wines from both ends of the flavanoid axis land high on the parabola, wines from the middle low. Now a straight line can cut cultivar 2 off from above. On 20 other splits of the wines, without x² the linear SVM never once predicts cultivar 2, and the best single threshold from training beats “always other” on the test in only 1 of 20. With x² it always cuts out one interval: from 67.9 to 84.9% on the test.

With noise it is the other way round. The distance in kNN is a sum of squared differences over all columns. With 13 measurements the farthest training wine is on average 296.5% farther than the nearest. With 200 noise columns only 25.7%. The differences in the noise are random, and there are many of them, so they drown the 13 real differences. Over 20 draws of the noise, 200 columns give from 64.2 to 83.0%, always below 98.1% without noise.

A common mistake

True or false: “more features make a better model”. False. A feature helps when it carries information in a shape the model can use, like x² for cultivar 2. Columns without information hurt: 200 noise columns drag kNN from 98.1% down to 77.4%. In the code below the accuracy of kNN falls from 0.962 without noise to 0.689 with 200 columns.

A good new feature usually comes from understanding the problem: what should matter, and in what shape. Throwing in everything you can is an invitation to noise.

Quiz

Three questions to check yourself. Pick an answer, or open the answer under a question. Keyboard: arrow keys choose, Enter or Space confirms.

1. Why can one threshold on flavanoids alone not separate cultivar 2?
Show the answer

Correct answer: Because cultivar 2 sits in the middle and one threshold cannot cut a middle interval

On average 2.08 against 2.98 and 0.78 for the others. Every threshold leaves another cultivar on the “cultivar 2” side too. The best threshold gets 61.6% of the training wines against 60.0% for “always other”, and the linear SVM predicts cultivar 2 nowhere: 60.4%.

2. You add 200 columns of random noise to the 13 wine measurements. What happens to kNN?
Show the answer

Correct answer: Accuracy falls, because the noise drowns the real differences in the distance

Accuracy falls from 98.1% to 77.4%. Every noise column weighs as much in the distance as a real measurement.

3. True or false: “more features make a better model”.
Show the answer

Correct answer: False, because features without information add noise and can lower accuracy

200 noise columns drag kNN from 98.1% down to 77.4%. What helps is a feature with information in a good shape, like x² for cultivar 2.

Key takeaways

  • One feature in a good shape turns a hard problem into an easy one: x² lifts accuracy from 60.4% to 77.4%.
  • A linear model with new columns is still linear; what changes is what it gets as input.
  • Columns without information hurt: 200 noise columns drag kNN from 98.1% down to 77.4%.
  • In many dimensions distances even out, so distance-based models lose the most.

Going further: formula, code, practice

The math

The new feature: z = (x − x̄) / s, and the linear model gets (z, z²). The boundary w₁ · z + w₂ · z² + b = 0 is a quadratic equation in z, so it has up to two roots: two thresholds on the x axis and an interval between them.

The distance in kNN: d(a, b)² = Σⱼ (aⱼ − bⱼ)² over all columns. For m noise columns from N(0, 1) each adds 2 to d² on average (the variance of the difference of two independent numbers), whatever the cultivar. With a large m the noise sum drowns the 13 real terms, and the contrast (d_max − d_min) / d_min shrinks.

In code (Python)

The same experiment in scikit-learn: a linear model on flavanoids without and with x² in 10 × 5 cross-validation, then kNN on the 13 measurements and a growing number of noise columns.

import numpy as np
from sklearn.datasets import load_wine
from sklearn.model_selection import cross_val_score, RepeatedStratifiedKFold
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler, PolynomialFeatures
from sklearn.svm import SVC
from sklearn.neighbors import KNeighborsClassifier

wine = load_wine(as_frame=True)
x, is2 = wine.data[["flavanoids"]], (wine.target == 1).astype(int)
cv = RepeatedStratifiedKFold(n_splits=5, n_repeats=10, random_state=0)
line = make_pipeline(StandardScaler(), SVC(kernel="linear", C=1))
squared = make_pipeline(StandardScaler(), PolynomialFeatures(2, include_bias=False), StandardScaler(), SVC(kernel="linear", C=1))  # the new column x²
a, b = cross_val_score(line, x, is2, cv=cv), cross_val_score(squared, x, is2, cv=cv)
print(f"cultivar 2 vs the rest from flavanoids, 10 x 5 CV: x only {a.mean():.3f}, x and x^2 {b.mean():.3f}; always 'other' {1 - is2.mean():.3f}")
print(f"x only predicts cultivar 2 for {line.fit(x, is2).predict(x).sum()} of {len(x)} wines; x and x^2 wins in {(b > a).sum()} of 50 folds")
X, y = wine.data.to_numpy(), wine.target.to_numpy()
noise = np.random.default_rng(0).normal(size=(len(X), 200))  # columns of pure noise
for m in [0, 10, 50, 200]:
    acc = cross_val_score(make_pipeline(StandardScaler(), KNeighborsClassifier(5)), np.hstack([X, noise[:, :m]]), y, cv=cv)
    print(f"kNN on 13 measurements + {m} noise columns: {acc.mean():.3f}")

Output

cultivar 2 vs the rest from flavanoids, 10 x 5 CV: x only 0.601, x and x^2 0.755; always 'other' 0.601
x only predicts cultivar 2 for 0 of 178 wines; x and x^2 wins in 49 of 50 folds
kNN on 13 measurements + 0 noise columns: 0.962
kNN on 13 measurements + 10 noise columns: 0.940
kNN on 13 measurements + 50 noise columns: 0.842
kNN on 13 measurements + 200 noise columns: 0.689

The splits and the noise differ from the lab's, so the numbers differ a little too. The picture is the same: without x² the model never once predicts cultivar 2 (0.601, as much as “always another”), with x² it gets 0.755 and wins in 49 of 50 validation folds. The noise drags kNN from 0.962 down to 0.689.

For practitioners
  • In scikit-learn: PolynomialFeatures(2) adds squares and products of feature pairs, SplineTransformer smooth bends, FunctionTransformer(np.log1p) a logarithm for features with a long tail. Put them in make_pipeline, so cross-validation sees the whole path.
  • The best features come from knowing the problem: a ratio (price per square metre), a difference (days since the last purchase), a part of a date (day of the week). The model will not invent them unless it gets the raw ingredients in a good form.
  • When there are many features, select them inside each split (SelectKBest in the pipeline) or use a model with regularisation (lesson 3.4). Selecting features on all the data before validation is leakage (lesson 4.5).
  • The kernel methods you will meet with support vector machines compute the result as if they had added such columns, but without building them explicitly.

Go deeper in the Atlas

Encyclopedia entries on the ideas in this lesson: