TAI BUI
Back to curriculum
BuildPython

Feature Selection

More features isn't better. The right features are.

TIME: ~75 minutesPREREQUISITES: Phase 2, Lessons 01-09, 08 (feature engineering)

Learning Objectives

  • Implement filter methods (variance threshold, mutual information) and wrapper methods (RFE, forward selection) from scratch
  • Explain why mutual information captures nonlinear feature-target relationships that correlation misses
  • Compare L1 regularization (embedded selection) with RFE (wrapper selection) and evaluate their computational tradeoffs
  • Build a feature selection pipeline that combines multiple methods and demonstrate improved generalization on held-out data

The Problem

Say you have 500 features. The model trains slowly, overfits constantly, and nobody on the team can explain what it actually learned. You throw more features at it hoping that helps. It gets worse.

That's the curse of dimensionality doing its job. As the feature count grows, the feature space's volume explodes. Data points get sparser. Distances between them start to converge toward meaningless. Finding a real pattern in that emptiness takes exponentially more data than you have. Every noise feature you added along the way drowns out the signal from the ones that actually mattered. Overfitting stops being a risk and becomes the default.

Feature selection fixes that by cutting what doesn't help: strip the noise, remove the redundancy, keep only the features carrying real information about the target. The payoff is faster training, better generalization, and a model you can actually explain to someone else.

The goal isn't to use all the information available. It's to use the right information.

Warm-up0/3
1. A column in your dataset has the same value in every row. What can a model learn from that column about telling rows apart?Warm-up
A five-row table with one column, col_A, that holds 3.2 in every row, next to an unknown for what the column says about which row is which.
Illustrative values, not from a real dataset.
2. Which would you expect to be cheaper to compute: a statistic about one feature on its own, or retraining an entire model many times on different feature subsets?Warm-up
Two panels. Left: three features each turned into a single number by a statistic. Right: a model fitted again on each of three feature subsets. Each panel has an unknown compute cost.
Schematic of the question's two options; subsets are placeholders.
3. Why do you keep a test set that the model never sees while it is being trained?Warm-up
A dataset bar split into a large training part feeding a model and a smaller dashed test part the model never sees while training, with an unknown purpose for the test rows.
Split drawn 80/20 as in the lesson's code; proportions schematic.
Check: The Problem0/2
1. What is the curse of dimensionality?Define
The same ten random points drawn on a line, inside a square and inside a cube, one extra axis each time.
Schematic: random points, not tied to a dataset.
2. Why can adding more features actually make a model perform worse?Explain
Line chart of held-out accuracy against the number of leading dataset columns used, rising from 0.75 at one column to a peak of about 0.92 and falling to 0.87 at twenty columns.
Lesson's logistic regression and data generator, 40 training rows, mean of 200 datasets; captured.

The Concept

Three Categories of Feature Selection

Feature selection is choosing which subset of your available features a model should actually use, and discarding the rest. Every method for doing that falls into one of three categories:

Mermaid diagram

Filter methods score each feature on its own, using a statistical measure and no model at all. That makes them fast, but blind to how features interact with each other.

Wrapper methods go the other way: they train an actual model on different feature subsets and use its performance as the score. Results are usually better, at the cost of retraining the model over and over.

Embedded methods fold selection into training itself, rather than running it as a separate step. L1 regularization drives irrelevant weights to zero as it fits; decision trees split on whichever feature is most useful at each node. Either way, you get the selection for free from a model you were already training.

Variance Threshold

The simplest filter there is. If a feature barely moves across your samples, it isn't carrying much information to begin with.

Take a feature that's 0.0 for 999 out of 1000 samples. Its variance is essentially zero, and no model can use a value that never changes to tell one class from another. Remove it.

variance(x) = mean((x - mean(x))^2)

Pick a threshold (0.01 is a common start) and drop every feature whose variance falls below it. That clears out constant and near-constant features without even glancing at the target.

When to use it: as a cheap preprocessing step before running anything heavier. It catches the obviously useless features at close to zero cost.

Limitation: high variance doesn't guarantee a feature is useful, only that it isn't constant. Variance threshold is necessary, not sufficient.

Mutual Information

Mutual information measures how much knowing feature X reduces your uncertainty about target Y.

I(X; Y) = sum_x sum_y p(x, y) * log(p(x, y) / (p(x) * p(y)))

If X and Y are independent, p(x, y) = p(x) * p(y), the log term collapses to zero, and I(X; Y) = 0. The more X tells you about Y, the higher the score climbs.

Its advantage over correlation: mutual information catches nonlinear relationships too. A feature can sit at zero correlation with the target and still carry a high mutual information score, because the real relationship is quadratic or periodic rather than linear.

For continuous features, discretize into bins first (histogram-based estimation). The number of bins affects the estimate: too few and you lose information, too many and you add noise. A common choice is sqrt(n) bins, or Sturges' rule, 1 + log2(n).

Mermaid diagram

Recursive Feature Elimination (RFE)

RFE is a wrapper method that leans on a model's own feature importance to prune, one round at a time:

  1. Train the model with all features
  2. Rank features by importance (coefficients for linear models, impurity reduction for trees)
  3. Remove the least important feature(s)
  4. Repeat until the desired number of features remains

Mermaid diagram

Because the model sees every remaining feature at once, RFE picks up on interactions that filter methods miss entirely: remove one feature and the importance of the others shifts, and RFE lets the model reassess on every round instead of judging everything up front.

The cost is real: you're training the model N minus target times. With 500 features and a target of 10, that's 490 training runs, which gets slow fast for anything expensive to train. Removing a chunk of the bottom features per round (say, the bottom 10%) instead of one at a time is the usual fix.

L1 (Lasso) Regularization

L1 regularization adds the sum of the absolute weight values to the loss function:

loss = prediction_error + alpha * sum(|w_i|)

The alpha parameter sets how aggressively features get pruned: push it higher and more weights land at exactly zero.

Why exactly zero, and not just small? The L1 penalty carves out a diamond-shaped constraint region in weight space, and the optimal solution tends to land on a corner of that diamond, where one or more weights are exactly zero. Ridge (L2) carves out a circle instead, so weights shrink toward zero but almost never actually hit it.

That's embedded selection in practice: the model decides which features to ignore as it trains, and a feature with zero weight is gone in every way that matters.

Advantages: one training run, handles correlated features by picking one and zeroing the rest, and it's already built into most linear model libraries.

Limitation: it only works for linear models. Nonlinear feature importance is invisible to it.

Tree-Based Feature Importance

Decision trees and their ensembles (random forests, gradient boosting) rank features as a side effect of how they're built. Every split reduces impurity (Gini or entropy for classification, variance for regression), and whichever features drive the biggest reductions come out looking most important.

For a random forest with T trees:

importance(feature_j) = (1/T) * sum over all trees of
    sum over all nodes splitting on feature_j of
        (n_samples * impurity_decrease)

That gives a normalized importance score per feature, and it picks up nonlinear relationships and interactions without any extra work.

Caution: tree-based importance is biased toward high-cardinality features, ones with lots of unique values. A random ID column will look important because it can split every sample perfectly, even though it's meaningless. Run permutation importance as a sanity check before trusting the ranking.

Permutation Importance

A model-agnostic method that doesn't care what's inside the model:

  1. Train the model and record baseline performance on validation data
  2. For each feature, shuffle its values randomly and measure the drop in performance
  3. The bigger the drop, the more important the feature

Shuffle a feature and performance barely moves? The model wasn't leaning on it. Performance collapses instead, and that feature was doing real work.

It sidesteps the cardinality bias that trips up tree-based importance, but it's slow: one full evaluation per feature, repeated several times for stability.

Comparison Table

MethodTypeSpeedNonlinearFeature Interactions
Variance thresholdFilterVery fastNoNo
Mutual informationFilterFastYesNo
Correlation filterFilterFastNoNo
RFEWrapperSlowDepends on modelYes
L1 / LassoEmbeddedFastNo (linear)No
Tree importanceEmbeddedMediumYesYes
Permutation importanceModel-agnosticSlowYesYes

Decision Flowchart

Mermaid diagram

Check: The Concept0/29
1. What is feature selection?Define
A small table of 4 rows and 6 columns (f1 to f6) with rows labelled as samples and columns as features, feeding into a dashed box for feature selection with an unknown result.
Made-up numbers, for shape only.
2. What defines a filter method?Define
Twenty feature cells grouped as informative, correlated and noise features, an arrow into a box labelled filter method, and an unknown for what defines it.
Feature groups as built by the lesson's data generator.
3. What is a wrapper method?Define
Twenty feature cells grouped as informative, correlated and noise features, an arrow into a box labelled wrapper method, and an unknown for what it uses as the score.
Feature groups as built by the lesson's data generator.
4. What makes a selection method embedded?Define
Twenty feature cells grouped as informative, correlated and noise features, an arrow into a box labelled embedded method, and an unknown for when the selection happens.
Feature groups as built by the lesson's data generator.
5. What does mutual information between a feature X and a target Y measure?Define
Paired bar chart of rows per bin of info_0, split by target class 0 and 1, next to an unknown value for the mutual information.
Lesson's data generator, 400 training rows; counts computed.
6. What is Recursive Feature Elimination (RFE)?Define
Twenty labelled feature cells and, below them, twenty dashed unknown cells for the rankings that RFE returns when keeping 5.
Feature groups as built by the lesson's data generator.
7. What does L1 (Lasso) regularization add to the loss function?Define
The text loss equals prediction_error plus alpha times a bracket with an unknown term, and three example weights 0.8, -0.2 and -0.5.
Weights are illustrative.
8. What does a variance threshold do?Define
Three dot strips of 24 values each. Feature A spreads from about -2 to 2, feature B stays within a few hundredths of 0, and feature C is exactly 0 in every row. Variances are printed beside each.
Illustrative random values; variances computed with numpy.
9. What does a feature importance score tell you?Define
A table of six features (info_0, info_1, info_2, corr_0, noise_0, noise_1) each with an unknown importance score, next to a box for a logistic regression fitted on all 20 features.
Features from the lesson's data generator.
10. How is permutation importance computed?Define
A trained model with validation accuracy 0.92, the first six validation values of the info_0 column, and an unknown for the importance of info_0.
Lesson's logistic model and data; values captured.
11. Mutual information can detect relationships that Pearson correlation cannot. What kind?Explain
Two boxes for Pearson correlation and mutual information, the lesson's mutual information formula, and an unknown for what mutual information can detect that correlation cannot.
Formula as given in the lesson.
12. Why does mutual information come out as zero when a feature and the target are independent?Explain
The lesson's mutual information formula, a statement that independent X and Y give I(X; Y) equal to 0, and an unknown for why.
Formula as given in the lesson.
13. Why does the number of bins matter when estimating mutual information for a continuous feature?Explain
Three histograms of the same feature, info_0, with 3, 10 and 50 bins, each with an unknown mutual information estimate beneath.
Histograms computed from the lesson's data generator, 400 rows.
14. RFE removes the least important feature and retrains. Why is this better than just removing all low-importance features at once?Explain
Bar chart of 20 absolute weights from the first RFE round, with the smallest, noise_0, drawn in red as the feature to remove.
Lesson's simple_logistic_importance on all 20 standardised features; captured.
15. Why do L1 weights land at exactly zero while ridge (L2) weights only shrink toward it?Explain
Two rows of 20 signed weight bars on the same data: under the L1 penalty twelve bars are flat at zero, under the L2 penalty all twenty bars are small but nonzero.
L1: lesson's code, alpha 0.05. L2: the same loop with a squared-weight penalty, 0.05. Captured.
16. What is the computational tradeoff between L1 (Lasso) selection and RFE?Explain
Two boxes, L1 (Lasso) selection and RFE, each with an unknown number of training runs, for N features of which k are kept.
Symbolic setup from the question.
17. What caution applies to tree-based feature importance?Explain
The lesson's formula for tree-based importance as an average over trees of summed impurity decreases at splits on the feature, and an unknown caution.
Formula as given in the lesson.
18. How does permutation importance measure a feature, and what bias does it avoid compared to tree-based importance?Explain
Bar chart of tree-based importance for the 20 features, and two unknown boxes for how permutation importance is measured and which bias it avoids.
Lesson's tree_importance, 100 trees, depth 6; captured.
19. Which methods in the comparison can capture feature interactions?Explain
Seven methods listed with type and speed, each with an unknown in the feature interactions column.
Method, type and speed as listed in the lesson's comparison table.
20. You estimate mutual information on n = 1024 samples with Sturges' rule, 1 + log2(n). How many bins does it suggest?Apply
A chain from 1024 samples through Sturges' rule, bins equals 1 plus log2 of n, to an unknown number of bins.
Setup from the question.
21. A random forest ranks a randomly generated ID column near the top of its importances. Which explanation fits, and what is the sensible next step?Analyze
Horizontal bars of tree importance with the random_id column at the top with 0.42, then c with 0.28, a with 0.17 and b with 0.12.
Forest from the lesson's best_split and tree_importance on constructed data, 400 rows; captured.
22. A teammate drops a feature because its correlation with the target is 0.01, but its mutual information score is among the highest. What is the most likely situation?Analyze
Two sets of bars for five features f1 to f5. Absolute correlation: f1 0.48, f2 0.27, f3 0.01, f4 0.00, f5 0.09. Mutual information: f3 is the largest at 0.20.
Constructed data, 500 rows, binary target; correlation from numpy, mutual information from the lesson's code.
23. Two features are near-copies of each other. After L1 selection one has a nonzero weight and the other has weight 0, and a teammate calls the zeroed feature useless. What is wrong with that conclusion?Analyze
Signed weight bars for 20 features from a Lasso fit; info_1 and info_4 are highlighted, with info_1 at about -0.17 and info_4 at 0.
Coordinate-descent Lasso (own numpy solver) on the lesson's data with a 0/1 target, alpha 0.02; captured.
24. You fit a Lasso model and a feature whose effect on the target is purely quadratic gets weight 0. Why, and what should you do?Analyze
Bars of Lasso weights: f1 0.90, f2 -0.53, q 0, and noise features n1 and n2 0. The q feature acts on the target only through its square.
Constructed data, 5000 rows, q symmetric around 0; own numpy Lasso solver, alpha 0.1; captured.
25. After a variance threshold of 0.01 removes only 20 of 500 features, the model still overfits badly. What is the best reading of this?Analyze
A bar for 500 features with 20 coloured red as removed by the variance threshold and 480 blue as remaining, and an unknown about why the model still overfits.
Counts from the question; bar drawn to scale.
26. You run mutual information scoring on your full dataset, keep the top features, and only then split into train and test sets. Validation accuracy looks great. What went wrong?Analyze
Two rows of steps: all rows, score features with mutual information, keep top features; then split into train and test, fit model, validation accuracy that looks great.
Order of steps as described in the question.
27. You have 500 features and an expensive model, and you want the result to generalize well. Which plan fits the lesson's ideas?Design
Fifty blocks standing for 500 features, a box for an expensive model, and an unknown for the selection plan.
Setup from the question; blocks are schematic.
28. You will use 5-fold cross-validation and need a feature selection step. Which design gives an honest score?Design
Five rows of five blocks, one validation block per row in a different position and the other four marked as training, with an unknown for what the feature selector is fitted on.
Standard 5-fold layout.
29. A gradient boosting model has strong feature interactions, nonlinear effects, and a few ID-like high-cardinality columns. You can afford slow computation. Which ranking should you trust most?Design
A gradient boosting model box with three property chips (feature interactions, nonlinear effects, ID-like high-cardinality columns), a note that slow computation is affordable, and an unknown for the ranking to trust.
Facts from the question.

Build It

Step 1: Generate synthetic data with known feature structure

python
import numpy as np
 
 
def make_feature_selection_data(n_samples=500, seed=42):
    rng = np.random.RandomState(seed)
 
    x1 = rng.randn(n_samples)
    x2 = rng.randn(n_samples)
    x3 = rng.randn(n_samples)
    x4 = x1 + 0.1 * rng.randn(n_samples)
    x5 = x2 + 0.1 * rng.randn(n_samples)
 
    informative = np.column_stack([x1, x2, x3, x4, x5])
 
    correlated = np.column_stack([
        x1 * 0.9 + 0.1 * rng.randn(n_samples),
        x2 * 0.8 + 0.2 * rng.randn(n_samples),
        x3 * 0.7 + 0.3 * rng.randn(n_samples),
        x1 * 0.5 + x2 * 0.5 + 0.1 * rng.randn(n_samples),
        x2 * 0.6 + x3 * 0.4 + 0.1 * rng.randn(n_samples),
    ])
 
    noise = rng.randn(n_samples, 10) * 0.5
 
    X = np.hstack([informative, correlated, noise])
    y = (2 * x1 - 1.5 * x2 + x3 + 0.5 * rng.randn(n_samples) > 0).astype(int)
 
    feature_names = (
        [f"info_{i}" for i in range(5)]
        + [f"corr_{i}" for i in range(5)]
        + [f"noise_{i}" for i in range(10)]
    )
 
    return X, y, feature_names

We built this with the ground truth baked in: features 0-4 are informative (features 3 and 4 are just correlated copies of 0 and 1), features 5-9 correlate with the informative ones, and features 10-19 are pure noise. Any selection method worth using should rank 0-4 at the top and 10-19 at the bottom.

Step 2: Variance threshold

python
def variance_threshold(X, threshold=0.01):
    variances = np.var(X, axis=0)
    mask = variances > threshold
    return mask, variances

Step 3: Mutual information (discrete)

python
def discretize(x, n_bins=10):
    min_val, max_val = x.min(), x.max()
    if max_val == min_val:
        return np.zeros_like(x, dtype=int)
    bin_edges = np.linspace(min_val, max_val, n_bins + 1)
    binned = np.digitize(x, bin_edges[1:-1])
    return binned
 
 
def mutual_information(X, y, n_bins=10):
    n_samples, n_features = X.shape
    mi_scores = np.zeros(n_features)
 
    y_vals, y_counts = np.unique(y, return_counts=True)
    p_y = y_counts / n_samples
 
    for f in range(n_features):
        x_binned = discretize(X[:, f], n_bins)
        x_vals, x_counts = np.unique(x_binned, return_counts=True)
        p_x = dict(zip(x_vals, x_counts / n_samples))
 
        mi = 0.0
        for xv in x_vals:
            for yi, yv in enumerate(y_vals):
                joint_mask = (x_binned == xv) & (y == yv)
                p_xy = np.sum(joint_mask) / n_samples
                if p_xy > 0:
                    mi += p_xy * np.log(p_xy / (p_x[xv] * p_y[yi]))
        mi_scores[f] = mi
 
    return mi_scores

Step 4: Recursive Feature Elimination

python
def simple_logistic_importance(X, y, lr=0.1, epochs=100):
    n_samples, n_features = X.shape
    w = np.zeros(n_features)
    b = 0.0
 
    for _ in range(epochs):
        z = X @ w + b
        pred = 1.0 / (1.0 + np.exp(-np.clip(z, -500, 500)))
        error = pred - y
        w -= lr * (X.T @ error) / n_samples
        b -= lr * np.mean(error)
 
    return w, b
 
 
def rfe(X, y, n_features_to_select=5, lr=0.1, epochs=100):
    n_total = X.shape[1]
    remaining = list(range(n_total))
    rankings = np.ones(n_total, dtype=int)
    rank = n_total
 
    while len(remaining) > n_features_to_select:
        X_subset = X[:, remaining]
        w, _ = simple_logistic_importance(X_subset, y, lr, epochs)
        importances = np.abs(w)
 
        least_idx = np.argmin(importances)
        original_idx = remaining[least_idx]
        rankings[original_idx] = rank
        rank -= 1
        remaining.pop(least_idx)
 
    for idx in remaining:
        rankings[idx] = 1
 
    selected_mask = rankings == 1
    return selected_mask, rankings

Step 5: L1 feature selection

python
def soft_threshold(w, alpha):
    return np.sign(w) * np.maximum(np.abs(w) - alpha, 0)
 
 
def l1_feature_selection(X, y, alpha=0.1, lr=0.01, epochs=500):
    n_samples, n_features = X.shape
    w = np.zeros(n_features)
    b = 0.0
 
    for _ in range(epochs):
        z = X @ w + b
        pred = 1.0 / (1.0 + np.exp(-np.clip(z, -500, 500)))
        error = pred - y
 
        gradient_w = (X.T @ error) / n_samples
        gradient_b = np.mean(error)
 
        w -= lr * gradient_w
        w = soft_threshold(w, lr * alpha)
        b -= lr * gradient_b
 
    selected_mask = np.abs(w) > 1e-6
    return selected_mask, w

Step 6: Tree-based importance (simple decision tree)

python
def gini_impurity(y):
    if len(y) == 0:
        return 0.0
    classes, counts = np.unique(y, return_counts=True)
    probs = counts / len(y)
    return 1.0 - np.sum(probs ** 2)
 
 
def best_split(X, y, feature_idx):
    values = np.unique(X[:, feature_idx])
    if len(values) <= 1:
        return None, -1.0
 
    best_threshold = None
    best_gain = -1.0
    parent_gini = gini_impurity(y)
    n = len(y)
 
    for i in range(len(values) - 1):
        threshold = (values[i] + values[i + 1]) / 2.0
        left_mask = X[:, feature_idx] <= threshold
        right_mask = ~left_mask
 
        n_left = np.sum(left_mask)
        n_right = np.sum(right_mask)
 
        if n_left == 0 or n_right == 0:
            continue
 
        gain = parent_gini - (n_left / n) * gini_impurity(y[left_mask]) - (n_right / n) * gini_impurity(y[right_mask])
 
        if gain > best_gain:
            best_gain = gain
            best_threshold = threshold
 
    return best_threshold, best_gain
 
 
def tree_importance(X, y, n_trees=50, max_depth=5, seed=42):
    rng = np.random.RandomState(seed)
    n_samples, n_features = X.shape
    importances = np.zeros(n_features)
 
    for _ in range(n_trees):
        sample_idx = rng.choice(n_samples, size=n_samples, replace=True)
        feature_subset = rng.choice(n_features, size=max(1, int(np.sqrt(n_features))), replace=False)
 
        X_boot = X[sample_idx]
        y_boot = y[sample_idx]
 
        tree_imp = _build_tree_importance(X_boot, y_boot, feature_subset, max_depth)
        importances += tree_imp
 
    total = importances.sum()
    if total > 0:
        importances /= total
 
    return importances
 
 
def _build_tree_importance(X, y, feature_subset, max_depth, depth=0):
    n_features = X.shape[1]
    importances = np.zeros(n_features)
 
    if depth >= max_depth or len(np.unique(y)) <= 1 or len(y) < 4:
        return importances
 
    best_feature = None
    best_threshold = None
    best_gain = -1.0
 
    for f in feature_subset:
        threshold, gain = best_split(X, y, f)
        if gain > best_gain:
            best_gain = gain
            best_feature = f
            best_threshold = threshold
 
    if best_feature is None or best_gain <= 0:
        return importances
 
    importances[best_feature] += best_gain * len(y)
 
    left_mask = X[:, best_feature] <= best_threshold
    right_mask = ~left_mask
 
    importances += _build_tree_importance(X[left_mask], y[left_mask], feature_subset, max_depth, depth + 1)
    importances += _build_tree_importance(X[right_mask], y[right_mask], feature_subset, max_depth, depth + 1)
 
    return importances

Step 7: Run all methods and compare

The code file runs all five methods against the same synthetic dataset and prints a comparison table showing which features each one keeps.

Check: Build It0/6
1. The code keeps features with mask = variances > threshold, and threshold is 0.01. A feature has a variance of exactly 0.01. What happens to it?Apply
Three lines of code from the question: threshold 0.01, feature j with variance 0.01, and mask equal to variances greater than threshold, with an unknown for mask[j].
Code as in the question and the lesson's variance_threshold.
2. In the from-scratch mutual_information, a feature column holds the same value in every row. What score does it get?Apply
A table of eight rows with column f equal to 3.0 in every row and target y with four 0s then four 1s, the call mutual_information(X, y), and an unknown score for f.
Illustrative values. Only the shape matters: f never changes.
3. With 20 features and n_features_to_select=5, how many times does the rfe function call simple_logistic_importance?Apply
The call rfe(X, y, n_features_to_select=5) on a 20-column X, the 20 feature cells, and an unknown for how many times simple_logistic_importance is called.
Setup from the question.
4. soft_threshold(w, alpha) is np.sign(w) * np.maximum(np.abs(w) - alpha, 0). What does it return for w = [0.8, -0.2, -0.5] and alpha = 0.3?Apply
Signed bars for the input w = [0.8, -0.2, -0.5] with alpha 0.3, an arrow, and three dashed unknown slots for the returned vector.
Values from the question.
5. What does gini_impurity return for labels y = [0, 0, 1, 1]?Apply
Four label cells, two blue zeros then two orange ones, next to an unknown gini_impurity value for them.
Values from the question.
6. In tree_importance, each tree considers int(np.sqrt(n_features)) randomly chosen features. With the 20-feature synthetic dataset, how many features does each tree consider?Apply
The 20 feature cells, the lesson's feature_subset line using int(np.sqrt(n_features)) with n_features equal to 20, and an unknown count of features per tree.
Code as in the lesson's tree_importance.

Use It

With scikit-learn, feature selection is already built into the pipeline:

python
from sklearn.feature_selection import (
    VarianceThreshold,
    mutual_info_classif,
    RFE,
    SelectFromModel,
)
from sklearn.linear_model import Lasso, LogisticRegression
from sklearn.ensemble import RandomForestClassifier
 
vt = VarianceThreshold(threshold=0.01)
X_filtered = vt.fit_transform(X)
 
mi_scores = mutual_info_classif(X, y)
top_k = np.argsort(mi_scores)[-10:]
 
rfe_selector = RFE(LogisticRegression(), n_features_to_select=10)
rfe_selector.fit(X, y)
X_rfe = rfe_selector.transform(X)
 
lasso_selector = SelectFromModel(Lasso(alpha=0.01))
lasso_selector.fit(X, y)
X_lasso = lasso_selector.transform(X)
 
rf = RandomForestClassifier(n_estimators=100)
rf.fit(X, y)
importances = rf.feature_importances_

The from-scratch versions above show exactly what's happening inside each method. Variance threshold is just var(X, axis=0) with a mask applied on top. Mutual information is counting joint and marginal frequencies in a contingency table. RFE is a loop that trains, ranks, and prunes. L1 is gradient descent with a soft-thresholding step bolted on. Tree importance accumulates impurity reductions across every split. No magic. Just statistics and loops.

What sklearn adds on top: robustness (mutual_info_classif uses k-NN density estimation instead of binning), speed (C implementations under the hood), and pipeline integration you don't have to write yourself.

Check: Use It0/1
1. How does scikit-learn's mutual_info_classif differ from the from-scratch mutual information code?Explain
Two code lines, the lesson's from-scratch mutual_information and scikit-learn's mutual_info_classif, with the from-scratch scores for info_0 to info_2 and an unknown for what differs.
From-scratch scores captured from the lesson's code. scikit-learn is not installed here, so its output is not shown.

Ship It

This lesson produces outputs/skill-feature-selector.md, a quick-reference decision tree for picking the right feature selection method for a given dataset and model.

Author's Note

The mistake I see most with feature selection isn't picking the wrong method. It's running the method on the whole dataset before the train/test split.

Say you compute mutual information scores, or fit RFE, on all your data, then split into train and test afterward. You just let the test set influence which features you kept. Every feature you selected is now biased toward doing well on data it technically hasn't seen yet, and your validation score is quietly lying to you about how the model will perform in production.

The fix is boring but non-negotiable: split first, fit your selector on the training fold only, then apply that same selection to the test fold without refitting. If you're doing k-fold cross-validation, the selection step belongs inside the fold loop, not outside it. I've watched this leak add two or three points of phantom accuracy to a validation score that evaporated the moment the model saw real data.

Exercises

  1. Forward selection: implement the opposite of RFE. Start with zero features. At each step, add the feature that improves model performance the most. Stop when adding features no longer helps. Compare the selected features against RFE results. Which is faster? Which gives better results?

  2. Stability selection: run L1 feature selection 50 times, each time on a random 80% subsample of the data, with slightly different alpha values. Count how often each feature is selected. Features selected in > 80% of runs are "stable." Compare stable features against single-run L1 selection. Which is more reliable?

  3. Multicollinearity detection: compute the correlation matrix for all features. Implement a function that, given a correlation threshold (e.g., 0.9), removes one feature from each highly-correlated pair (keeping the one with higher mutual information with the target). Test on the synthetic dataset and verify it removes the redundant correlated features.

  4. Feature selection pipeline: chain variance threshold, mutual information filter, and RFE into a single pipeline. First remove near-zero-variance features, then keep the top 50% by mutual information, then run RFE on the survivors. Compare this pipeline against running RFE alone on all features. Is the pipeline faster? Is it equally accurate?

  5. Permutation importance from scratch: implement permutation importance. For each feature, shuffle its values 10 times, measure the average drop in F1 score. Compare the ranking against tree-based importance. Find cases where they disagree and explain why (hint: correlated features).

Key Terms

TermWhat people sayWhat it actually means
Feature selection"Picking the important columns"Choosing which subset of available features a model should actually use, and discarding the rest
Filter method"Score features independently"A feature selection approach that ranks features using a statistical measure without training a model, evaluating each feature in isolation
Wrapper method"Use the model to pick features"A feature selection approach that evaluates feature subsets by training a model and using its performance as the selection criterion
Embedded method"The model selects features during training"Feature selection that happens as part of model fitting, such as L1 regularization driving weights to zero
Mutual information"How much one variable tells you about another"A measure of the reduction in uncertainty about Y given knowledge of X, capturing both linear and nonlinear dependencies
Recursive Feature Elimination"Train, rank, prune, repeat"An iterative wrapper method that trains a model, removes the least important feature(s), and repeats until a target count is reached
L1 / Lasso regularization"Penalty that kills features"Adding the sum of absolute weight values to the loss function, which drives unimportant feature weights to exactly zero
Variance threshold"Remove constant features"Dropping features whose variance across samples falls below a specified threshold, filtering out features that carry no information
Feature importance"Which features matter most"A score indicating how much each feature contributes to model predictions, computed from split gains (trees) or coefficient magnitudes (linear)
Permutation importance"Shuffle and measure the damage"Evaluating feature importance by randomly shuffling each feature's values and measuring the resulting drop in model performance
Curse of dimensionality"Too many features, not enough data"The phenomenon where adding features increases the volume of the feature space exponentially, making data sparse and distances meaningless

Further Reading

Code

Ships With