EVERYTHING AIAI engineering, made visual
0/18 complete
LESSON 18 · MACHINE LEARNING × AI · BUILD

More features is not better.
The right features is better.

Relevance without redundancy. Rank forty candidates, prune the tail, and watch validation rise — then level off. Three families of methods, one rule: select inside the fold.

75 MIN · 7 CHAPTERSPREREQ · PHASE 2, LESSONS 01–09
FIG. 18 / RANK, THEN PRUNE THE TAIL
KEEPING 40 / 40 · VALIDATION kept next to prune pruned
LESSON 18TYPE · BUILD~75 MINPREREQ · PHASE 2 · LESSONS 01–09, 08ORIGINAL LESSON ↗
01 / RELEVANCE, NOT COUNT

Every extra feature is another chance to fit noise.

More columns do not add information — they add volume. Distances stretch, data thins out, and with enough candidates some of them correlate with the target by pure chance. Redundancy is the other half of the problem: two copies of the same feature tell the model exactly one thing, but split the weight and double the variance.

relevance − redundancy = useful columns
02 / THREE FAMILIES

Filters score, wrappers retrain, embedded select while fitting.

Filter methods judge each feature alone with statistics — fast, but blind to interactions. Wrapper methods train a model, rank features, remove and repeat — they see interactions but pay for every round. Embedded methods fold selection into training: L1 drives weights to exactly zero and trees spend their best splits. Same goal, three price points.

statistic · model · penalty
03 / SELECT INSIDE THE FOLD

If the selector saw the test data, the score is fiction.

Choosing features on the full dataset uses every label — including the labels you are about to be tested on. The model looks better than it is, and production pays the difference. Run the whole selection procedure inside each training fold, and judge the selected set on data that had no vote in choosing it.

selection is part of the model
MENTAL MODEL IN ONE SENTENCE

Feature selection is modeling the feature set itself: score relevance, subtract redundancy, keep the smallest set that validates — and treat the selector as part of the model, so it never touches the test fold.

By the end you will be able to rank features with variance, correlation and mutual information; run RFE and read a Lasso path; choose a feature count with a validation curve and the one-standard-error rule; stress a selection with stability resampling; and explain why selecting before cross-validation quietly overstates every score.

RELEVANCE AND REDUNDANCY

More columns,
less signal.

Two things make a feature worth keeping: it is related to the target, and it is not a copy of a feature you already have. Everything in this lesson is a different way of measuring those two things.

Suppose you have 500 candidate features and 100 labeled rows. The training set fits beautifully — a flexible model can always find something to memorize — and then the model meets new data and collapses. It was not learning a pattern; it was learning which of the 500 columns happened to line up with the labels in those particular 100 rows. Adding features does not add information; it adds opportunities to be fooled.

The first failure is irrelevance: a feature that has nothing to do with the target still has some correlation with it in a finite sample, and a model will happily spend capacity on it. The second is redundancy: two columns that carry the same signal give the model one piece of information but two knobs to turn. The signal gets split between them, and small changes in the data swing the weights wildly — the coefficient for either one alone is nearly meaningless.

Worked check: why chance correlations show up

In a unit cube with side 0.9, the volume lands below 0.9^d. In 10 dimensions, 0.9^10 = 0.349 — a third of the space sits in the outer shell. In 50 dimensions, 0.9^50 = 0.005: 99.5% of the volume is shell, and “typical” points are all far apart. Data thins out exponentially while the number of columns grows linearly.

max |corr| from pure noise ≈ sqrt( 2·ln(p) / n ) p = 1000 candidate features, n = 100 rows: sqrt(2 × ln 1000 / 100) = sqrt(13.816 / 100) = 0.372 p = 100, n = 100: sqrt(9.210 / 100) = 0.303 p = 50, n = 100: sqrt(7.824 / 100) = 0.280 So with 1000 candidates and 100 rows, chance alone produces a feature correlated ≈ 0.37 with the target.

The numeric check is the whole first chapter: relevance filters look for the columns whose evidence beats that noise floor, and redundancy filters remove the copies that make the evidence look like it comes from more places than it does.

Quick check

You add 200 random noise columns to a model trained on 100 rows. Training accuracy jumps to 99%. The test set behaves no better (and quietly worse). What happened?

FILTERS: SCORE WITHOUT A MODEL

Judge each column
on its own.

Filter methods are statistics, not models: score every feature by how much it varies, how linearly it tracks the target, or how much uncertainty about the target it removes. Fast, simple, and blind to teamwork between features.

Variance threshold. If a column barely changes from row to row, it cannot separate anything. Drop features whose variance falls below a threshold — a common default is 0.01. This is the cheapest possible pass, and it never looks at the target at all.

variance(x) = (1/n) · Σᵢ (xᵢ − x̄)² "average squared distance from the mean" drop when variance(x) < threshold

Correlation. Pearson’s r measures how well a straight line explains the pair (feature, target). It is fast and interpretable, and it only sees straight lines.

r = Σ (xᵢ − x̄)(yᵢ − ȳ) / sqrt( Σ (xᵢ − x̄)² · Σ (yᵢ − ȳ)² ) |r| near 1: strong line · |r| near 0: no line (but maybe a curve!)

Chi-squared. For categorical or count features — word counts, flags — compare each observed count with the count expected if the feature and class were independent. Large χ² means the counts lean toward one class.

χ² = Σ (observed − expected)² / expected expected = (row total × column total) / n

Mutual information. MI asks a more general question: how many bits of uncertainty about the target does the feature remove? If feature and target are independent, the answer is exactly 0; unlike correlation, a curve or a cycle can score high.

I(X;Y) = Σₓ Σᵧ p(x,y) · log₂( p(x,y) / (p(x) · p(y)) ) independent → p(x,y) = p(x)p(y) → log(1) = 0 → MI = 0 continuous features are binned first (equal-count bins work well)
The displayed dataset: 24 rows, 12 candidates, one binary target. Every number in the filter board is computed from this table.
#ABCDEFGHIJKLy
1-0.260.22-0.23-0.34-0.93-0.570.97-0.572.49-1.78-0.820.520
2-0.68-0.160.03-0.55-0.530.65-1.21-0.312.50-4.920.69-0.300
3-0.03-1.01-1.33-0.16-1.00-1.870.38-0.432.50-0.640.26-0.401
41.261.73-1.121.480.581.931.010.732.580.89-0.771.390
50.83-0.421.300.70-0.310.230.69-0.092.47-3.991.11-0.951
60.89-0.100.850.86-0.21-1.111.581.732.51-0.10-0.361.871
70.210.241.480.02-0.950.210.800.822.57-2.000.73-0.091
8-1.611.00-1.08-1.811.600.230.270.702.554.78-1.090.180
91.28-0.18-2.471.300.63-0.701.650.272.512.90-0.21-0.571
100.12-0.890.300.19-0.98-0.61-1.32-0.542.530.501.88-0.371
110.18-0.81-0.280.31-0.97-0.250.891.172.562.46-0.330.411
12-2.310.12-0.10-2.114.34-0.840.061.072.463.43-0.29-0.280
13-0.651.39-2.18-0.78-0.571.25-0.64-0.022.491.261.481.250
14-1.691.92-0.69-1.761.852.100.511.002.542.76-1.310.580
150.21-0.120.080.29-0.96-0.04-1.530.192.562.190.75-0.841
162.13-0.560.062.033.53-0.26-1.130.862.46-0.33-0.06-0.231
17-0.150.011.01-0.20-0.981.261.720.312.51-2.26-0.921.781
180.440.080.560.35-0.810.33-0.81-0.192.50-3.350.240.411
190.581.070.400.55-0.670.830.082.172.562.421.35-0.580
202.19-1.34-0.392.153.81-0.83-0.381.662.450.00-0.37-1.261
21-0.471.89-0.86-0.35-0.781.03-0.23-0.282.53-3.660.04-1.120
22-1.12-0.650.33-1.040.26-1.03-0.14-0.572.527.810.121.380
23-0.821.070.31-0.69-0.320.79-1.910.332.57-2.280.960.290
240.890.111.040.84-0.21-0.78-0.09-0.702.521.480.18-1.191
A, B and C are linear signals for y; D is a copy of A; E is A²; F is a weak signal; I is near-constant; J is high-variance noise; G, H, K and L are plain noise. The reader does not need to know that — the filters below have to discover it from the numbers.

The filter board: score first, model later

Twelve candidates, three cheap statistics, no model. Switch the metric and watch the ranking change — the nonlinear feature falls off the correlation list but stays on the mutual-information list.

dataset: 24 samples, 12 candidates, y has 13 positives metric: mutual information (bits) kept (5): B A D E C ⚠ redundancy: D ~ A (r = 0.994) — keep one top of the table B 0.420 A 0.420 D 0.420 E 0.332 C 0.286 E is A²: corr 0.124 but MI 0.332 — nonlinear, invisible to correlation I is near-constant: variance 0.0014 J is noise with the largest variance: 8.896

A filter never trains a model, so it cannot see that B makes F redundant. That interaction is what wrapper methods pay for.

Worked check: every filter number on the displayed rows

The lab’s board recomputes these from all 24 rows; here is what the arithmetic says, and what each statistic misses.

I (near-constant): values 2.5 ± 0.04 variance = 0.0014 → killed by the 0.01 threshold but |corr| = 0.207: pointless variation still points somewhere E (A²): |corr| = 0.124, MI = 0.332 bits correlation ranks E 10th of 12; MI ranks it 4th B (signal): |corr| = 0.686, MI = 0.420 A ~ D: corr(A, D) = 0.994 — the pair is one feature wearing two badges B ~ F: corr(B, F) = 0.789 — weaker, but the same warning

The chi-squared check is a 2×2 word-count table over 100 emails: the word “free” appears in 20 of 25 spam and 10 of 75 inbox mails.

cells spam inbox expected spam inbox free = yes 20 10 free = yes 7.5 22.5 free = no 5 65 free = no 17.5 52.5 χ² = (20−7.5)²/7.5 + (5−17.5)²/17.5 + (10−22.5)²/22.5 + (65−52.5)²/52.5 = 20.83 + 8.93 + 6.94 + 2.98 = 39.68 MI = 0.20·log₂(0.20/0.075) + 0.05·log₂(0.05/0.175) + 0.10·log₂(0.10/0.225) + 0.65·log₂(0.65/0.525) = 0.283 − 0.090 − 0.117 + 0.200 = 0.276 bits

Both statistics agree the word is informative — and they disagree on nothing here because a 2×2 table is monotone. The disagreement appears with curves: E would score near zero correlation and a strong MI.

Quick check

A feature has |correlation| = 0.02 with the target but mutual information of 0.41 bits. Which relationship is most likely?

WRAPPERS: TRAIN, RANK, PRUNE

Let the model
vote a feature off the island.

Wrapper methods treat feature selection as a search problem: train a model, ask it which feature matters least, remove that one, and repeat. It costs a training run per round — and it is the only family that sees interactions.

Recursive Feature Elimination (RFE) is the canonical wrapper:

1. train a model on the features that remain 2. rank them by importance (|coefficient| for linear models, impurity for trees) 3. remove the least important feature 4. repeat until the target count is reached

The reason to remove one at a time is that importances are relative. When two features carry the same signal, each gets roughly half the credit while both are present. Delete one and the other’s importance jumps — the model redistributes. Remove all the low-importance features in one sweep and you can delete a feature that would have become essential the moment its twin left.

Forward selection is RFE’s mirror image: start empty, add the feature that improves validation the most, stop when additions stop helping. It is often faster when the final set is small, and it can find features that only work in combination — at the price of never reconsidering an early choice. Both wrappers cost many training runs: stripping 500 features down to 10 is at least 490 fits, which is why practitioners often remove the bottom 10% per round, or pre-filter with statistics before calling a wrapper.

RFE: train, rank, prune, repeat

Each round trains a logistic model, drops the feature with the smallest coefficient, and re-scores. Step through the rounds and watch cross-validation accuracy rise while the tail disappears.

keeping 12: A B C D E F G H I J K L next to eliminate: H cv accuracy 0.630 ± 0.254 train accuracy 1.000 overfit gap 0.370 importances (|w|) B 4.124 A 2.732 D 2.474 C 1.738 I 0.655 E 0.604 J 0.582 F 0.496

Round 1 removes H (|w| 0.145) — a noise column. Later rounds remove E, whose quadratic information a linear model cannot spend, and keep A B C D. In production the whole loop runs inside every training fold.

Worked check: RFE round 1, sorted by |coefficient|

The model is logistic regression on the 24 displayed rows, all 12 features standardized. A coefficient of 4.124 means one standard deviation up in that feature moves the log-odds by 4.124.

round 1 |w| for all 12 candidates B 4.124 A 2.732 D 2.474 C 1.738 I 0.655 E 0.604 J 0.582 F 0.496 G 0.405 K 0.385 L 0.210 H 0.145 weakest → H removed. validation after each removal: 12 features 0.630 11 → 0.670 10 → 0.630 9 → 0.710 8 → 0.790 7 → 0.790 6 → 0.840 5 → 0.880 4 → 0.880 (stable, stop) final set: A B C D watch F: |corr| 0.465 looked useful to the filter, but it left at 7 features because B already knows it (corr(B, F) = 0.789). RFE sees the overlap; filters do not.

Every value above is computed live in the stepper: the importances come from the same fits, and the accuracy numbers are 5-fold cross-validation on the displayed rows, not a training score. Training accuracy is 1.000 at every round — the model memorizes 24 rows trivially — which is exactly why the wrapper ranks by validation.

Quick check

Why remove one feature per round instead of deleting every feature whose importance is below some cutoff?

EMBEDDED: SELECT WHILE FITTING

Build the pruning
into the training loss.

Embedded methods do not select features as a separate step: the penalty on the weights, or the way a tree spends its splits, decides which features survive while the model is being fit.

L1 / Lasso. Add the absolute values of the weights to the loss:

loss = prediction_error + α · Σᵢ |wᵢ|

The plain-English version: every weight now has to pay rent proportional to its size, and the rent is charged linearly — no volume discount. A weight that does not buy enough error reduction to cover its rent is pushed all the way to exactly zero, and a zero weight is a deleted feature. α sets the rent: larger α, fewer survivors.

Why exactly zero? The constraint region created by the absolute-value penalty is a diamond, and its corners lie on the axes. The first point where the loss contours touch the diamond is usually a corner, where one or more coordinates are zero. The L2 penalty (ridge) makes a circle, which has no corners — weights shrink smoothly toward zero but almost never land on it. That geometric difference is the entire reason Lasso selects and ridge does not.

Tree importance. Every split in a decision tree reduces impurity (Gini or entropy for classification, variance for regression). Sum, over all nodes splitting on a feature, the number of samples times the impurity decrease; average over the forest. Features that produce larger, more common reductions score higher — and trees capture curves and interactions automatically.

importance(j) = (1/T) · Σ_trees Σ_nodes on j ( n_samples · impurity_decrease ) plain English: "how much confusion did this feature clear, weighted by how many rows felt it, averaged over the forest"

Permutation importance audits any trained model without touching its internals: record validation performance, shuffle one column, and measure how much the score drops. No drop means the model does not depend on that column. It avoids the tree bias toward high-cardinality columns (an ID column that perfectly tags every row), at the cost of one full evaluation per feature. Correlated columns share credit: shuffle one and the model quietly leans on its twin.

L1 · DIAMONDw₁w₂corner ⇒ w₂ = 0 exactlyL2 · CIRCLEw₁w₂smooth point ⇒ weights shrink, rarely zero
A simplified 2-weight picture (teaching model, not a scale drawing). The loss wants to move toward the unconstrained optimum; the penalty keeps it inside the shaded region. A diamond has corners that sit on the axes, so the first touch is usually a corner — one weight is exactly zero. A circle has no corners, so both weights shrink together.

The Lasso path: λ turns features off

Drag λ down and coefficients leave the path one by one; drag it back up and they snap to exactly zero. The penalty is the sum of absolute weights — no model retraining between steps.

λmax = max |xⱼ·y| / n = 0.3420 λ now = 0.0912 nonzero = 3 selected features B -0.191 A 0.152 C 0.003 D is A + 0.12·noise, so the solver pays for A first and only lets the near-copy D in at λ = 0, where it pushes D to −0.8 against A's +1.0 and almost cancels it: the split is the model shrugging about which copy to believe.

Soft-threshold: a coefficient with partial correlation ρ is set to sign(ρ)·max(|ρ| − λ, 0). At λ = 0.20, ρ = 0.35 shrinks to 0.15 and ρ = 0.12 becomes exactly 0.

Worked check: soft-threshold, the path, and one Gini split

Coordinate descent solves the Lasso one weight at a time with the soft-threshold rule — “subtract λ, but never cross zero”:

S(ρ, λ) = sign(ρ) · max(|ρ| − λ, 0) ρ = 0.35, λ = 0.20 → 0.35 − 0.20 = 0.15 (shrank, survived) ρ = 0.12, λ = 0.20 → max(0.12 − 0.20, 0) = 0 (rent too high, gone) the lab's path on the displayed 24 rows: λmax = max |xⱼ·y|/n = 0.342 → everything zero B enters first at λ ≈ 0.336, A at λ ≈ 0.302 at λ = 0.20 only A (0.074) and B (−0.113) survive C enters at λ ≈ 0.091; at λ = 0 all 12 coefficients exist at λ = 0: A ≈ 1.0, D ≈ −0.8 — the solver pays for A first, then lets the near-copy D in with the opposite sign to cancel it

One Gini check by hand, on a 10-row node with 5 positives: parent impurity 1 − (0.5² + 0.5²) = 0.5. A split sends 6 rows (4 positive, 2 negative) left — 1 − (0.667² + 0.333²) = 0.444 — and 4 rows (1, 3) right — 1 − (0.25² + 0.75²) = 0.375. The gain is 0.5 − (6/10)·0.444 − (4/10)·0.375 = 0.083, and this node credits 0.083 × 10 = 0.83 to the feature it split on. Sum that over every node and tree, normalize, and you have the importance bar chart.

HOW MANY FEATURES?

The best score is not
the right number.

Selection methods rank features; they do not say where to stop. Answer that with a validation curve, and read it through a one-standard-error band instead of chasing the peak.

A validation curve plots model score against a knob — here, the number of features kept. Typical shape: accuracy climbs quickly, flattens once the real signal is in, then drifts down as noise features let the model start memorizing. The peak is a candidate, not a verdict.

The reason is that every point on the curve is an estimate with error bars, and the maximum of noisy estimates is biased upward: some k just got a lucky fold split. The one-standard-error rule cuts through this. Take the best mean, subtract one standard error, and choose the simplest model (fewest features) whose mean is still at or above that bar. You trade a fraction of a percent of measured score for a smaller, cheaper, more stable model — a trade that almost always pays out of sample.

How many features? Read the curve, then the error bars

Every k is scored honestly: mutual information ranks features inside each training fold, then logistic regression is tested on the held-out fold. The raw maximum is not the answer — the one-standard-error band is.

k = 3 cv mean 0.960 std ± 0.089 folds [1.00 1.00 1.00 0.80 1.00] curve at a glance k= 1 0.610 ±0.225 k= 2 0.880 ±0.110 k= 3 0.960 ±0.089 ← best ← one-SE pick k= 4 0.960 ±0.089 k= 5 0.830 ±0.264 k= 6 0.830 ±0.264 k= 7 0.790 ±0.246 k= 8 0.750 ±0.328 k= 9 0.750 ±0.218 k=10 0.670 ±0.172 k=11 0.670 ±0.172 k=12 0.630 ±0.254 best k = 3 mean 0.960 standard error = std/√5 = 0.040 one-SE threshold = 0.920 smallest k at or above it = 3

The curve peaks at k = 3–4 and falls off past k = 5: the extra columns are noise, and the model spends capacity fitting it. The one-SE rule asks “how much worse is a simpler model than the best, once fold noise is counted?”

Worked check: the one-SE rule on the displayed curve
honest 5-fold curve (MI ranked inside each training fold) k=1 0.610 k=5 0.830 k=9 0.750 k=2 0.880 k=6 0.830 k=10 0.670 k=3 0.960 ← k=7 0.790 k=11 0.670 k=4 0.960 k=8 0.750 k=12 0.630 best mean = 0.960 at k = 3, fold scores [1.00, 1.00, 1.00, 0.80, 1.00] std = 0.089, standard error = 0.089 / √5 = 0.040 one-SE bar = 0.960 − 0.040 = 0.920 k=2 (0.880) is below the bar → rejected k=3 (0.960) clears it → chosen all 12 features score 0.630, three tenths below k=3

The curve is honest in a second sense: the mutual-information ranking is recomputed inside every training fold, so the choice of which single feature to keep at k=1 differs from fold to fold — that is why k=1 is so unstable (0.610). The one-SE bar is not a formality; it is the smallest useful significance test on a curve made of noisy estimates.

Quick check

Your curve peaks at k=12 with 0.94; the standard error is 0.03 and k=4 scores 0.92. Your colleague wants to ship k=12 because it is 'the best'. What does the one-SE rule say?

STABLE, AND HONEST

One dataset gives you
one opinion. Resample it.

A selection that changes when you nudge the data is not a discovery. Stability selection reruns the selector on many resamples and counts how often each feature survives — and then the same discipline has to hold for your cross-validation split.

Stability selection turns a selector into a measurement. Run it — L1, RFE, MI, anything — many times, each time on a random subsample of the rows, with a slightly different hyperparameter. Count how often each feature is selected. A feature that survives 80% of runs is stable; one that appears in one run and vanishes in the next is a fluctuation, not a finding. The idea comes from genomics, where p is enormous, n is small, and reproducibility is the whole game.

The same logic applies to evaluation. Feature selection is part of the model. If the selector sees the test fold’s labels — because it was run once on the full dataset — the cross-validation score is contaminated. The honest protocol runs the entire selection procedure inside each training fold and evaluates the result on rows that had no vote in choosing the features.

Stability selection: resample, then count

Run L1 selection 40 times, each time on a random 80% subsample with a slightly different λ. A feature worth trusting survives most runs; a fluke does not.

A100%
B100%
C60%
D0%
E25%
F8%
G25%
H0%
I0%
J25%
K3%
L13%

Each bar is the fraction of 40 runs in which L1 kept the feature. ⧉ marks D, the copy of A (r = 0.994).

runs 40 · subsample 19/24 · λ ∈ [21%, 29%] of λmax stable (≥ 80%): A B unstable (20–79%): C E G J rejected (< 20%): D F H I K L per feature A 100% B 100% C 60% D 0% E 25% F 8% G 25% H 0% I 0% J 25% K 3% L 13% A and B: A 100% · B 100% D (copy of A) was picked in 0% of runs C was picked in 60% of runs

Lower λ admits more features (and more noise); higher λ makes only the strongest survive. A feature that appears at one λ and vanishes at the next is a feature you should not build a story around.

The leak: select once, then cross-validate

Same features, same folds, two protocols — one picks the top 5 using every label, the other picks inside each training fold. The gap between the lines is the optimism you get for free when the selector cheats.

0.50.60.70.80.91.0● naive · selection saw all labels● honest · selection inside each fold12 random 5-fold splits →

Every dot is one random 5-fold split. Selection for the orange protocol happens once, on all 24 rows; the teal protocol re-ranks with only the training rows in view.

naive (pick once on all labels) 0.897 honest (pick inside each fold) 0.839 optimism gap 0.058 all 12 features (reference) 0.723 naive beat honest in 10 / 12 repeats naive picks once: B A D F C junk in that set: 0 (noise or near-constant) inside the folds: features chosen in ≥ 50% of training folds A 100% B 100% C 55% D 100% F 98% flickering in and out: E G I J K L

The naive protocol chose its set once, using all 24 labels. Cross-validating that choice cannot expose the mistake, because the labels that shaped it are inside the test folds. Drag k toward 8: the naive set starts admitting near-constant I and noise L, while the honest bars for those columns stay low. That is why selection belongs in the pipeline, not before it.

Worked check: the stability counts and the optimistic gap
stability (40 runs, 80% subsample, λ ∈ [21%, 29%] of λmax) A 100% B 100% C 60% D 0% E 25% F 8% G 25% H 0% I 0% J 25% K 3% L 13% stable (≥80%): A, B unstable (20–79%): C, E, G, J rejected (<20%): D, F, H, I, K, L D is A's near-copy. A survives every run; D survives none, because whenever both are available the L1 solution pays for one of them, not two. Redundancy is not just waste — it is the reason a solo ranking would keep both copies where one carries all the information. leakage experiment (12 repeats, 5 folds, k = 5) naive: pick top-5 by |corr| once, on all 24 rows → 0.897 honest: pick top-5 inside each training fold → 0.839 gap → +0.058 naive > honest in 10 of 12 repeats reference: all 12 features under the same folds → 0.723 inside the folds, A, B, D are chosen in 100% of runs and F in 98%, but C only 55% — its apparent edge in the naive set is partly a property of these 24 labels, which is exactly what the test folds are supposed to measure.

The gap is small in absolute terms because this dataset is small and mostly friendly. Its size is not the point: the naive protocol is structurally unable to detect its own mistake, and the gap grows with the number of candidates and shrinks with the number of rows — the exact regime — 500 features, 100 rows — where selection frameworks are usually deployed.

THE RECIPE

Which method,
in which order.

The families are tools, not rivals. A working recipe picks the cheap statistic first, the model-aware method second, and the honest evaluation protocol always.

The source lesson’s decision flow compresses to one idea: filters narrow, embedded or wrapper methods decide, and the validation protocol polices the whole thing. The table is the cheat sheet — use it by asking what your final model is and how many candidates you actually have.

MethodFamilySpeedNonlinearInteractions
Variance thresholdfiltervery fastnono
Correlation filterfilterfastnono
Mutual informationfilterfastyesno
RFE / forward selectionwrapperslowmodel-dependentyes
L1 / Lassoembeddedfastnono
Tree importanceembeddedmediumyesyes
Permutation importanceauditslowyesyes
The comparison table from the source lesson. “Interactions” means the method accounts for features being useful together; only wrappers and trees do.
  1. Count first. Under 50 candidates: variance threshold, then mutual information or correlation, then validate. 50–500: variance threshold, then L1 or tree importance. Over 500: variance threshold, then an MI filter to a few hundred, then RFE on the survivors. Filters exist to make wrappers affordable.
  2. Screen with the cheapest thing that could work. Drop near-constant columns without ceremony. Keep the statistic simple enough that you can explain the ranking to a colleague, and remember that variance never looks at the target.
  3. Choose the family by your final model. A linear model pairs naturally with L1; a tree ensemble with tree importance plus a permutation audit; anything else with RFE. The selector should speak the model's language, because importance is relative to the model.
  4. Pick the count on a validation curve. Sweep the number of features, look at the curve and its error bars, and take the simplest model inside one standard error of the best. Write the number down before you look at the test set.
  5. Stress the choice with resampling. Run the whole selector on many subsamples and count how often each feature survives. Anything below 80% is a hypothesis, not a commitment. Two candidates that swap in and out are redundant with each other.
  6. Run everything inside the fold. Selection, scaling, imputing, encoding — all of it belongs in the pipeline. Cross-validate the pipeline, not the reduced matrix. Compare selected-features vs all-features on the same splits, and ship only if the smaller set is not worse.
  7. Keep the door open. Feature relevance drifts: a sensor is recalibrated, a product changes, a season turns. Re-run the stability check on fresh data periodically, log which features the model actually leans on, and retire columns the way you retire code — deliberately, with evidence.
Selection inside the pipeline, so cross-validation is honestpython
from sklearn.feature_selection import SelectKBest, mutual_info_classif
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler

pipe = Pipeline([
    ("scale", StandardScaler()),
    ("select", SelectKBest(mutual_info_classif, k=5)),
    ("model", LogisticRegression(max_iter=2000)),
])

# Every fold re-fits the scaler, re-ranks the features,
# and trains only on that fold's rows.
scores = cross_val_score(pipe, X, y, cv=5, scoring="accuracy")
print(scores.mean(), scores.std())

# Compare against keeping everything, on the SAME folds:
full = Pipeline([("scale", StandardScaler()), ("model", LogisticRegression(max_iter=2000))])
print(cross_val_score(full, X, y, cv=5, scoring="accuracy").mean())

# For L1 selection instead:
#   LogisticRegression(penalty="l1", solver="liblinear", C=0.5)
# For tree importance instead:
#   from sklearn.ensemble import RandomForestClassifier
#   select = SelectFromModel(RandomForestClassifier(n_estimators=300), threshold="median")
Run at the prototype stage on your own data: the selector must live inside the pipeline, never before the split.
CHECK YOURSELF

Six questions.
Then the terms worth keeping.

Answer before you look. The mutual-information question and the leakage question are the two that separate a memorized definition from a working instinct.

0 / 6 answered · 0 correct

01Why can adding more features actually make a model perform worse?

02What is the key difference between filter and wrapper feature selection methods?

03Mutual information can detect relationships that Pearson correlation cannot. What kind?

04L1 (Lasso) regularization performs feature selection as part of training. How?

05RFE removes the least important feature and retrains. Why is this better than just removing all low-importance features at once?

06You rank features by correlation with the target using all 24 rows, keep the top 5, then run 5-fold cross-validation. The score looks excellent. What is the flaw?

Key terms, demystified

Click a card to swap the lazy description for what it actually means.

Exercises from the lesson

Four problems with exact numbers — forward selection, stability selection, multicollinearity and a full pipeline. Try first; a worked answer is one click away.

  1. Implement forward selection: start with zero features, add the feature that improves 5-fold cross-validated accuracy the most, and stop when additions stop helping. Report the order features enter, compare the final set with RFE's, and count the training runs.
    Show one worked answer

    On the lesson's 24 × 12 dataset the entry order is A (cv 0.870), then B (0.910), then E (1.000), then D (1.000 — the only add that keeps the score at 1.000). A fifth addition drops the score: the best candidate is I at 0.950, and every other add is worse, so the procedure stops at {A, B, E, D}. RFE on the same data stops at {A, B, C, D} — it keeps C and drops E, because a linear model's coefficients cannot spend E's quadratic information, while forward selection evaluated E in the presence of A and found the gain. Cost: forward selection evaluated 12 + 11 + 10 + 9 + 8 = 50 candidate sets, each with 5 folds, about 250 fits before the final scoring — RFE needed only 9 full-data fits plus its cross-validation. Forward selection is typically faster when the final set is tiny and the candidate pool is large; RFE is faster when you are cutting many features from a large pool. Neither is uniformly better, and both must run inside the evaluation fold.

  2. Run your L1 selector 40 times, each time on a random 80% subsample of the rows with a slightly different α, and count how often each feature is selected. Which features are stable at an 80% threshold? Compare the count with a single L1 run.
    Show one worked answer

    The lesson's stability lab gives A 100%, B 100%, C 60%, D 0%, E 25%, F 8%, G 25%, H 0%, I 0%, J 25%, K 3%, L 13% (40 runs, 19-of-24 subsamples, λ between 0.214 and 0.285 of each subsample's λmax). Stable at 80%: A and B — the two strongest linear signals. C sits on the fence at 60%: real enough to appear often, not reliable enough to promise. D is selected 0% of the time: it is A's near-copy (r = 0.994), and whenever the solver can afford one of the pair it pays for A, so redundancy reads as instability. Single-run comparison: at λ = 0.114 the one-shot L1 solution keeps only {A, B}; at smaller λ it admits C and then E, G, J, L — so the single run understates the set at high λ and overstates it at low λ, and gives no way to tell which. That is the argument for resampling: stability is not a property any single run can observe.

  3. Compute the correlation matrix of all features. Write a function that, given a threshold (for example 0.9), removes one feature from each highly correlated pair while keeping the one with more mutual information with the target. Run it on the displayed dataset and verify it removes the redundant copy.
    Show one worked answer

    The only pair at or above 0.9 is A~D with r = 0.994. Their MI scores tie at 0.420 bits (to three decimals), so the tie-break goes to the higher correlation with the target: A at 0.638 versus D at 0.611 — keep A, drop D. The next-largest pair is B~F at r = 0.789, below the threshold, so both stay; a stricter 0.75 threshold would drop F (MI 0.190) in favour of B (MI 0.420). After removing D the correlation matrix has no pair above 0.9. Two cautions: pairwise filtering only catches two-feature redundancy, not a feature reconstructed from three others (that is what variance inflation factors or a full correlation-matrix rank check are for), and the removal rule must be executed inside the training fold when it feeds a cross-validated pipeline.

  4. Chain variance threshold, a mutual-information filter, and RFE into one pipeline. First remove near-zero-variance features, keep the top half by MI, then run RFE on the survivors down to three features. Compare the result and the cost against running RFE alone on all 12 candidates.
    Show one worked answer

    Step 1: variance threshold 0.01 removes only I (variance 0.0014), leaving 11 candidates. Step 2: MI ranks the survivors B, A, D, E, C, F, G, J, L, K, H — keeping the top half leaves {B, A, D, E, C, F}. Step 3: RFE on those six eliminates F first, then E, then C, ending at {B, A, D} with 5-fold CV 0.910. RFE alone on all 12 stops at four features {A, B, C, D} with CV 0.880 and needed 9 full-data fits (plus cross-validation at each step); the pipeline needed 3 elimination fits after two statistics that cost no model training. So on this dataset the pipeline is both cheaper and marginally better — because the MI filter removed the obvious noise before the wrapper paid to discover it. The caveat is the filter's blind spot: if a feature were useful only in combination with another, the MI half-filter could drop it before RFE ever sees it. Pre-filtering trades a small risk of lost interactions for a large saving in training runs; on high-dimensional data that trade is almost always worth it, and it must all happen inside the training fold.

Terms this lesson borrows from later lessons (or outside)

You do not need to master these here. Each one gets a proper treatment in its own lesson; the one-line meaning is enough to keep reading. Orange dotted underlines in the prose point back to this list.

  • correlation coefficientA number in [−1, 1] measuring how well a straight line describes two variables. It is the workhorse filter — and the reason a U-shaped relationship can hide from it. (Phase 1, Lesson 15)
  • entropy / bitsHow much uncertainty a distribution contains, measured in bits. Mutual information is entropy minus conditional entropy: I(X;Y) = H(Y) − H(Y|X). (Phase 1, Lesson 09)
  • cross-validationSplitting data into folds, training on all but one, and scoring the held-out fold — repeated until every row has been tested once. It is the standard honest estimate of out-of-sample performance. (Phase 2, Lesson 09)
  • bias–variance tradeoffSimple models underfit (bias), flexible models chase noise (variance). Adding noise features raises variance without lowering bias, which is why more columns can hurt. (Phase 2, Lesson 10)
  • L1 and L2 regularizationPenalties added to a loss: Σ|w| (L1) pushes weights to exactly zero; Σw² (L2) shrinks them smoothly. This lesson is the feature-selection consequence of that old contrast. (Phase 2, Lesson 03)
  • decision tree / Gini impurityA model that splits data on yes/no feature questions; Gini impurity measures how mixed the class labels are in a node. Tree importance sums the impurity each feature removes. (Phase 2, Lesson 04)
  • resampling / bootstrapDrawing many random subsets of the data to see how much a result depends on the particular sample. Stability selection is this idea applied to feature selection. (Phase 2, Lesson 09)
  • pipeline leakageInformation from outside the training fold slipping into the model — standardizing or selecting before the split. The cure is to put every learning step inside the pipeline. (Phase 2, Lesson 13)
  • high-cardinality featureA column with many unique values, like an ID. Trees can split such columns into pure leaves by luck, which is why tree importance is biased toward them and permutation importance exists as an audit. (Phase 2, Lesson 08)
KEEP GOING

A picture is a start.
Practice is the rest.

This lesson is a port of an open course. Everything here traces back to it — and the next step is running the code yourself.

Lesson text adapted from AI Engineering from Scratch (Phase 02, Lesson 18) and the Math Foundations Notebook reference build. The displayed 24 × 12 dataset, the six labs (filter board, RFE stepper, Lasso path, validation curve with one-SE rule, stability selection and the leakage experiment), the chi-squared / MI hand check, the noise-ceiling arithmetic, the Gini split check and the rounded filter scores are original to this page. Every score, frequency, coefficient and accuracy shown is computed live from the displayed data.