EduFlow

Section 2 · Regression

Regularisation with Ridge and Lasso

20 min4 min readPython exercise

Lesson at a glance

Control overfitting by penalising large coefficients with Ridge and Lasso, scale features so the penalty is fair, and use Lasso’s zeros as automatic feature selection.

  1. 1A penalty on the loss
  2. 2Scale first, then penalise
  3. 3Lasso picks features for you
  4. 4Taming correlated coefficients
  5. 5Ridge or Lasso?

No video here — just the real thing: a written lesson, runnable examples, a quiz and practice.

Last lesson, degree-2 polynomial features gave the diabetes model 65 columns, and the test score dropped from 0.49 to 0.42. You could throw the extra columns away. Regularisation offers something better: keep them all, but make the model pay for every unit of coefficient it uses. Columns that earn their keep get weight; the rest get shrunk.

A penalty on the loss

Ordinary least squares minimises the squared error and nothing else. Regularised regression minimises the squared error plus a penalty that grows with the size of the coefficients:

  • Ridge adds alpha * sum(coef ** 2), the L2 penalty.
  • Lasso adds alpha * sum(abs(coef)), the L1 penalty (scikit-learn also scales the error term slightly differently, which is why good alpha values for the two models differ).

alpha sets the exchange rate. At 0 you’re back to least squares. As it grows, a big coefficient must buy a big reduction in error to be worth it. Coefficients that only fit noise can’t, so they shrink. Squared values make Ridge shrink everything smoothly; absolute values make Lasso push weak coefficients all the way to zero.

Scale first, then penalise

The penalty adds up coefficients as if they were comparable. They only are if the features share a scale. Measure spend in cents instead of dollars and its coefficient becomes 100 times smaller, so the penalty barely notices it. Always put a StandardScaler in front of a penalised model.

Python
from sklearn.datasets import load_diabetes
from sklearn.linear_model import LinearRegression, Ridge, Lasso
from sklearn.model_selection import train_test_split
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import PolynomialFeatures, StandardScaler

X, y = load_diabetes(return_X_y=True, as_frame=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=42)

def degree2(model):
    return make_pipeline(PolynomialFeatures(degree=2, include_bias=False), StandardScaler(), model)

print("no penalty:", round(degree2(LinearRegression()).fit(X_train, y_train).score(X_test, y_test), 3))
for alpha in [1, 10, 100, 1000]:
    m = degree2(Ridge(alpha=alpha)).fit(X_train, y_train)
    print(f"Ridge alpha={alpha:<5} train {m.score(X_train, y_train):.3f}  test {m.score(X_test, y_test):.3f}")

As alpha rises the training score falls steadily (the model is allowed less freedom) while the test score climbs from 0.42 to 0.50 at alpha=100, better than the plain ten-feature model. Push to 1,000 and both scores drop: now the model underfits. Regularisation turns the overfitting/underfitting trade-off into a dial.

Lasso picks features for you

Run the same experiment with Lasso and count the coefficients that end up at exactly zero.

Python
from sklearn.datasets import load_diabetes
from sklearn.linear_model import Lasso
from sklearn.model_selection import train_test_split
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import PolynomialFeatures, StandardScaler

X, y = load_diabetes(return_X_y=True, as_frame=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=42)

for alpha in [0.1, 1, 2, 5]:
    m = make_pipeline(PolynomialFeatures(degree=2, include_bias=False), StandardScaler(),
                      Lasso(alpha=alpha)).fit(X_train, y_train)
    zeros = (m[-1].coef_ == 0).sum()
    print(f"Lasso alpha={alpha:<4} test R2 {m.score(X_test, y_test):.3f}  zero coefficients: {zeros} of 65")

names = m[0].get_feature_names_out()
kept = [n for n, c in zip(names, m[-1].coef_) if c != 0]
print("kept at alpha=5:", kept)

At alpha=2 Lasso reaches a test R² of 0.53, the best regression score in this section, while ignoring 39 of the 65 columns. At alpha=5 it keeps 11 columns: BMI, blood pressure and s5 from the earlier models, plus a few interactions such as bmi bp. m[-1] reaches the last step of the pipeline, and m[0].get_feature_names_out() names the generated columns.

Taming correlated coefficients

Remember s1 at -918 and s2 at +508 in the plain regression, two correlated blood measurements pulling against each other? With standardised features the same story shows up on a smaller scale, and the penalty settles it.

Coefficients for s1 and s2 on standardised features. Least squares: s1 minus 44, s2 plus 25. Ridge with alpha 10: s1 minus 13, s2 near zero. Lasso with alpha 1: s1 minus 8, s2 exactly zero. 0 Least squares s1 −44 s2 +25 Ridge, alpha=10 s1 −13 s2 +0.5 Lasso, alpha=1 s1 −8 s2 0 (dropped)
What happens to two correlated coefficients

Ridge spreads weight sensibly between correlated features instead of letting them cancel each other out; Lasso tends to keep one and drop the other. Either way the coefficients become stable enough that a small change in the training data won’t flip them.

Ridge or Lasso?

Both cure the same disease, so the choice comes down to what you want from the final model. Ridge usually wins when many features each carry a little signal, which is common with measurements that overlap, like the six blood readings here. Lasso wins when a few features matter and the rest are noise, and it hands you a shorter shopping list of inputs, which matters when every feature has to be collected, cleaned and monitored in production. When features come in correlated groups, Lasso’s habit of keeping one member, somewhat arbitrarily, can be unstable from one training run to the next. ElasticNet mixes the two penalties for exactly that case. On a dataset this size, the honest answer is to try both on validation data and keep whichever wins, then prefer the simpler one when they tie.

That closes regression. The same penalty idea comes straight back in the next section: LogisticRegression is regularised by default, with a parameter C that works as the inverse of alpha. Section 3 switches the target back to churn, yes or no.

Key takeaways

  • Regularisation adds a penalty on coefficient size to the loss, so the model trades a little training fit for simpler, more stable coefficients.
  • Ridge penalises squared coefficients and shrinks them all; Lasso penalises absolute values and sets some exactly to zero.
  • alpha is the strength of the penalty: 0 is plain least squares, very large values flatten the model into underfitting.
  • Scale features before a penalised model, or the penalty punishes features for their units rather than their usefulness.
  • Choose alpha on validation data or with cross-validation, the same way you choose any hyperparameter.

Further reading

Check your understanding

3 questions

Question 1 of 3

You raise Ridge’s alpha from 1 to 100,000. What happens to the model?

Question 2 of 3

Why put StandardScaler before Lasso in the pipeline?

Question 3 of 3

You need a churn model that uses as few of 60 candidate features as possible. Which is the natural first choice?

Practice

Python exercise

Let Lasso choose the features

The starter pipeline forgets something important. Fix it, then inspect what Lasso did.

  1. lasso_model: degree-2 PolynomialFeatures(include_bias=False), then StandardScaler, then Lasso(alpha=1), fitted on the training data.
  2. lasso_r2: its test R². n_zero: how many of its 65 coefficients are exactly zero.
  3. kept: a list with the names of the generated columns whose coefficient is not zero.
from sklearn.datasets import load_diabetes
from sklearn.linear_model import Lasso, Ridge
from sklearn.model_selection import train_test_split
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import PolynomialFeatures, StandardScaler

X, y = load_diabetes(return_X_y=True, as_frame=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=42)

# 1. Pipeline: degree-2 PolynomialFeatures (include_bias=False) -> StandardScaler -> Lasso(alpha=1)
lasso_model = make_pipeline(PolynomialFeatures(degree=2, include_bias=False), Lasso(alpha=1))
lasso_model.fit(X_train, y_train)

# 2. Test R2 and the number of coefficients that are exactly zero
lasso_r2 = None
n_zero = None

# 3. Names of the generated columns Lasso kept (non-zero coefficient), as a list of strings
kept = None
Loading the editor…

Ctrl/⌘ + Enter to run · Runs in a sandbox in your browser. Nothing leaves your device.

(no output)

Checks

  • The pipeline is PolynomialFeatures → StandardScaler → Lasso(alpha=1)
  • lasso_r2 is the test R²
  • n_zero counts the zero coefficients
  • kept lists the names of the non-zero columns