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.
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.
Choose an answer and how sure you are.
Answer to unlock the lab.
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.
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.
Download the notebook (.ipynb). Open it in Jupyter or upload it to Google Colab; it fetches any missing data itself.
For practitioners
- In scikit-learn:
PolynomialFeatures(2)adds squares and products of feature pairs,SplineTransformersmooth bends,FunctionTransformer(np.log1p)a logarithm for features with a long tail. Put them inmake_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 (
SelectKBestin 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.
Was this helpful?
Thanks! This helps us improve the Atlas.
Go deeper in the Atlas
Encyclopedia entries on the ideas in this lesson: