Skip to content

Regularization - Ridge and Lasso Regression

What you’ll learn

  • why shrinking coefficients toward zero can improve predictions — the bias-variance trade
  • the Ridge closed form, and the exact shrinkage factor it applies
  • the Lasso soft-thresholding rule, and why it produces genuine zeros
  • both computed by hand on the orthogonal design from the Multiple Regression page
  • the geometric reason a diamond selects features and a circle does not
  • Elastic Net, and how to choose α\alpha honestly with cross-validation

Intuition

Ordinary least squares has exactly one goal: make the training residuals small. Given enough features it will happily grow enormous coefficients that cancel each other out — a coefficient of +4,000 on one column and −3,950 on a nearly identical one. The training fit looks superb, and the first new row destroys it.

Regularisation adds a second goal to the objective: keep the coefficients small. The model now has to justify every unit of coefficient with a real reduction in error. Noise cannot pay that price, so noise gets squeezed out. You trade a little bias for a large reduction in variance, and on held-out data the trade usually pays.

diagram Diagram mermaid

The math

The three objectives

Jridge(θ)=yXθ22+λj=1nθj2J_{\text{ridge}}(\boldsymbol{\theta}) = \lVert\mathbf{y} - \mathbf{X}\boldsymbol{\theta}\rVert_2^2 + \lambda\sum_{j=1}^{n}\theta_j^2
Jlasso(θ)=yXθ22+λj=1nθjJ_{\text{lasso}}(\boldsymbol{\theta}) = \lVert\mathbf{y} - \mathbf{X}\boldsymbol{\theta}\rVert_2^2 + \lambda\sum_{j=1}^{n}\lvert\theta_j\rvert
Jenet(θ)=yXθ22+λ(ρj=1nθj+1ρ2j=1nθj2)J_{\text{enet}}(\boldsymbol{\theta}) = \lVert\mathbf{y} - \mathbf{X}\boldsymbol{\theta}\rVert_2^2 + \lambda\left(\rho\sum_{j=1}^{n}\lvert\theta_j\rvert + \frac{1-\rho}{2}\sum_{j=1}^{n}\theta_j^2\right)

Note the sums start at j=1j = 1: the intercept is never penalised. Shrinking θ0\theta_0 would tie the model’s predictions to zero rather than to the data’s mean, which is meaningless.

Ridge has a closed form

Differentiating JridgeJ_{\text{ridge}} and setting the gradient to zero gives a modified Normal Equation:

θridge=(XX+λI)1Xy\boldsymbol{\theta}_{\text{ridge}} = \left(\mathbf{X}^\top\mathbf{X} + \lambda\mathbf{I}\right)^{-1}\mathbf{X}^\top\mathbf{y}

Adding λI\lambda\mathbf{I} to the diagonal does something valuable beyond shrinkage: it makes the matrix invertible even when XX\mathbf{X}^\top\mathbf{X} is singular. Ridge works when features outnumber samples, and when two columns are perfectly duplicated. Plain OLS does neither.

For an orthogonal design where XX=cI\mathbf{X}^\top\mathbf{X} = c\mathbf{I}, the formula collapses to pure per-coefficient shrinkage:

θjridge=cc+λθjOLS\theta_j^{\text{ridge}} = \frac{c}{c + \lambda}\,\theta_j^{\text{OLS}}

Every coefficient is multiplied by the same factor below 1. That factor never reaches zero for finite λ\lambda — which is the whole story of why Ridge cannot select features.

Lasso has no closed form, but it has a rule

The absolute value is not differentiable at zero, so there is no matrix formula. On an orthogonal design, though, the coordinate-wise solution is exact and beautifully simple — soft thresholding:

θjlasso=sign ⁣(θjOLS)max ⁣(θjOLSλ2c,  0)\theta_j^{\text{lasso}} = \operatorname{sign}\!\left(\theta_j^{\text{OLS}}\right)\max\!\left(\left\lvert\theta_j^{\text{OLS}}\right\rvert - \frac{\lambda}{2c},\; 0\right)

Subtract a fixed amount from every coefficient’s magnitude, and clamp at zero. Any coefficient smaller than the threshold becomes exactly zero, not merely small. That is feature selection falling out of the algebra.

Worked example by hand

The orthogonal design from Multiple Linear Regression, where XX=4I\mathbf{X}^\top\mathbf{X} = 4\mathbf{I}, Xy=[17,9,5]\mathbf{X}^\top\mathbf{y} = [17, 9, 5]^\top and the OLS answer was θ=[4.25, 2.25, 1.25]\boldsymbol{\theta} = [4.25,\ 2.25,\ 1.25]^\top.

Ridge at λ=4\lambda = 4. With c=4c = 4, the shrinkage factor is 4/(4+4)=0.54/(4+4) = 0.5:

θ1=94+4=1.125,θ2=54+4=0.625\theta_1 = \frac{9}{4+4} = 1.125, \qquad \theta_2 = \frac{5}{4+4} = 0.625

Ridge at λ=10\lambda = 10. Factor 4/140.2864/14 \approx 0.286:

θ1=9140.643,θ2=5140.357\theta_1 = \frac{9}{14} \approx 0.643, \qquad \theta_2 = \frac{5}{14} \approx 0.357

Lasso at λ=4\lambda = 4. The threshold is λ/(2c)=4/8=0.5\lambda/(2c) = 4/8 = 0.5, subtracted from each OLS coefficient:

θ1=2.250.5=1.75,θ2=1.250.5=0.75\theta_1 = 2.25 - 0.5 = 1.75, \qquad \theta_2 = 1.25 - 0.5 = 0.75

Lasso at λ=10\lambda = 10. Threshold 10/8=1.2510/8 = 1.25:

θ1=2.251.25=1.00,θ2=max(1.251.25, 0)=0\theta_1 = 2.25 - 1.25 = 1.00, \qquad \theta_2 = \max(1.25 - 1.25,\ 0) = \mathbf{0}

There it is. At λ=10\lambda = 10 Lasso has deleted the weekend feature entirely, while Ridge at the same λ\lambda merely shrank it to 0.357. The intercept stays at 4.25 throughout, because it is never penalised.

OLSRidge λ=4\lambda{=}4Ridge λ=10\lambda{=}10Lasso λ=4\lambda{=}4Lasso λ=10\lambda{=}10
intercept4.254.254.254.254.25
θ1\theta_1 promotion2.251.1250.6431.751.00
θ2\theta_2 weekend1.250.6250.3570.750
features used22221

The geometry

figureWhy the shape of the penalty decides whether coefficients reach zeromatplotlib
Two panels. Left: elliptical cost contours expanding from the OLS solution until they touch the corner of a diamond on the vertical axis. Right: the same contours touching a circle on a smooth part of its edge.Two panels. Left: elliptical cost contours expanding from the OLS solution until they touch the corner of a diamond on the vertical axis. Right: the same contours touching a circle on a smooth part of its edge.
Both problems are 'find the smallest cost inside the amber region'. The diamond has corners on the axes; the circle has none. Corners are where coefficients equal zero.

Both penalties can be restated as a constrained problem: minimise the squared error subject to the coefficient vector staying inside a region of fixed size. The solution is where the growing elliptical cost contours first touch that region.

  • The 1\ell_1 ball is a diamond, and its corners sit exactly on the axes. A corner is a point where one coordinate is zero. Corners stick out, so contours tend to hit them first.
  • The 2\ell_2 ball is a circle with no corners anywhere. The touch point is generically a smooth spot where every coordinate is non-zero.

That is the entire explanation, and it generalises: in nn dimensions the 1\ell_1 ball has low-dimensional faces where many coefficients vanish at once.

See it move

Watch both penalties act on five coefficients as λ\lambda sweeps upward. The Ridge bars shrink smoothly and never disappear; the Lasso bars hit zero one after another.

sketch Ridge versus Lasso shrinking coefficients p5.js
As lambda increases, Ridge shrinks all weights smoothly while Lasso pushes the smaller weights to exactly zero.

From scratch

Both rules, verified against scikit-learn on the four-row design:

shrinkage.py
import numpy as np
from sklearn.linear_model import Ridge, Lasso
 
X = np.array([[-1.0, -1.0], [-1.0, 1.0], [1.0, -1.0], [1.0, 1.0]])
y = np.array([1.0, 3.0, 5.0, 8.0])
c = 4.0                                  # X.T @ X = 4 * I for this design
ols = np.array([2.25, 1.25])             # from the Multiple Regression page
 
 
def ridge_closed_form(theta_ols, lam, c):
    return c / (c + lam) * theta_ols
 
 
def lasso_soft_threshold(theta_ols, lam, c):
    shrink = lam / (2 * c)
    return np.sign(theta_ols) * np.maximum(np.abs(theta_ols) - shrink, 0.0)
 
 
for lam in (4.0, 10.0):
    mine_r = ridge_closed_form(ols, lam, c)
    mine_l = lasso_soft_threshold(ols, lam, c)
 
    sk_r = Ridge(alpha=lam).fit(X, y).coef_
    sk_l = Lasso(alpha=lam / (2 * len(y)), max_iter=100000).fit(X, y).coef_
 
    print(f"lambda = {lam}")
    print(f"  ridge  mine {mine_r.round(4)}  sklearn {sk_r.round(4)}")
    print(f"  lasso  mine {mine_l.round(4)}  sklearn {sk_l.round(4)}")
 
# lambda = 4.0
#   ridge  mine [1.125 0.625]  sklearn [1.125 0.625]
#   lasso  mine [1.75 0.75  ]  sklearn [1.75 0.75  ]
# lambda = 10.0
#   ridge  mine [0.6429 0.3571]  sklearn [0.6429 0.3571]
#   lasso  mine [1. 0.]         sklearn [1. 0.]
shrinkage.py
import numpy as np
from sklearn.linear_model import Ridge, Lasso
 
X = np.array([[-1.0, -1.0], [-1.0, 1.0], [1.0, -1.0], [1.0, 1.0]])
y = np.array([1.0, 3.0, 5.0, 8.0])
c = 4.0                                  # X.T @ X = 4 * I for this design
ols = np.array([2.25, 1.25])             # from the Multiple Regression page
 
 
def ridge_closed_form(theta_ols, lam, c):
    return c / (c + lam) * theta_ols
 
 
def lasso_soft_threshold(theta_ols, lam, c):
    shrink = lam / (2 * c)
    return np.sign(theta_ols) * np.maximum(np.abs(theta_ols) - shrink, 0.0)
 
 
for lam in (4.0, 10.0):
    mine_r = ridge_closed_form(ols, lam, c)
    mine_l = lasso_soft_threshold(ols, lam, c)
 
    sk_r = Ridge(alpha=lam).fit(X, y).coef_
    sk_l = Lasso(alpha=lam / (2 * len(y)), max_iter=100000).fit(X, y).coef_
 
    print(f"lambda = {lam}")
    print(f"  ridge  mine {mine_r.round(4)}  sklearn {sk_r.round(4)}")
    print(f"  lasso  mine {mine_l.round(4)}  sklearn {sk_l.round(4)}")
 
# lambda = 4.0
#   ridge  mine [1.125 0.625]  sklearn [1.125 0.625]
#   lasso  mine [1.75 0.75  ]  sklearn [1.75 0.75  ]
# lambda = 10.0
#   ridge  mine [0.6429 0.3571]  sklearn [0.6429 0.3571]
#   lasso  mine [1. 0.]         sklearn [1. 0.]

Coefficient paths on real data

figureTen standardised diabetes features, penalised two waysmatplotlib
Two panels of coefficient paths against alpha on a log scale for the ten standardised diabetes features. Left, Ridge: all ten curves decay smoothly toward but never reach zero. Right, Lasso: curves snap to exactly zero at different alphas until none are left.Two panels of coefficient paths against alpha on a log scale for the ten standardised diabetes features. Left, Ridge: all ten curves decay smoothly toward but never reach zero. Right, Lasso: curves snap to exactly zero at different alphas until none are left.
Read left to right as the penalty tightens. Ridge curves approach the axis asymptotically; Lasso curves meet it and stop, one feature at a time.

Reading the plots

  1. Both start at the OLS solution. At the far left the penalty is negligible and both panels reproduce ordinary least squares.
  2. Ridge shrinks in proportion. The largest coefficients fall fastest in absolute terms, but the ordering is broadly preserved and nothing ever reaches the axis.
  3. Lasso eliminates in sequence. Each curve hits zero at its own α\alpha and stays there. The order in which features drop out is a crude but genuinely useful importance ranking.
  4. Both end at zero. Push α\alpha high enough and every coefficient vanishes; the model becomes “always predict the mean”.

With scikit-learn

On the raw ten diabetes features, regularisation changes almost nothing — 442 samples over 10 well-behaved columns is simply not an overfitting situation. Expand to degree 2 (65 features) and the picture changes completely:

regularised_pipelines.py
from sklearn.datasets import load_diabetes
from sklearn.linear_model import LinearRegression, Ridge, Lasso
from sklearn.model_selection import cross_val_score
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import PolynomialFeatures, StandardScaler
 
X, y = load_diabetes(return_X_y=True)
 
 
def cv_mse(estimator):
    pipe = make_pipeline(
        PolynomialFeatures(2, include_bias=False),   # 10 features -> 65
        StandardScaler(),                            # penalties need a common scale
        estimator,
    )
    return -cross_val_score(
        pipe, X, y, cv=5, scoring="neg_mean_squared_error"
    ).mean()
 
 
print(f"OLS              {cv_mse(LinearRegression()):.1f}")   # OLS              3495.3
print(f"Ridge alpha=10   {cv_mse(Ridge(alpha=10)):.1f}")      # Ridge alpha=10   3165.1
print(f"Ridge alpha=100  {cv_mse(Ridge(alpha=100)):.1f}")     # Ridge alpha=100  3063.0
print(f"Lasso alpha=1    {cv_mse(Lasso(alpha=1, max_iter=200000)):.1f}")
#                                                             # Lasso alpha=1    3016.1
regularised_pipelines.py
from sklearn.datasets import load_diabetes
from sklearn.linear_model import LinearRegression, Ridge, Lasso
from sklearn.model_selection import cross_val_score
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import PolynomialFeatures, StandardScaler
 
X, y = load_diabetes(return_X_y=True)
 
 
def cv_mse(estimator):
    pipe = make_pipeline(
        PolynomialFeatures(2, include_bias=False),   # 10 features -> 65
        StandardScaler(),                            # penalties need a common scale
        estimator,
    )
    return -cross_val_score(
        pipe, X, y, cv=5, scoring="neg_mean_squared_error"
    ).mean()
 
 
print(f"OLS              {cv_mse(LinearRegression()):.1f}")   # OLS              3495.3
print(f"Ridge alpha=10   {cv_mse(Ridge(alpha=10)):.1f}")      # Ridge alpha=10   3165.1
print(f"Ridge alpha=100  {cv_mse(Ridge(alpha=100)):.1f}")     # Ridge alpha=100  3063.0
print(f"Lasso alpha=1    {cv_mse(Lasso(alpha=1, max_iter=200000)):.1f}")
#                                                             # Lasso alpha=1    3016.1

Regularisation cuts cross-validated error by roughly 14%, from 3495 to 3016 — and the Lasso model gets there using 34 of the 65 columns.

figureChoosing alpha by cross-validationmatplotlib
Cross-validated MSE plotted against alpha on a log scale for ridge regression on 65 polynomial features. The curve is flat and high at small alpha, dips to a minimum near alpha 66, then rises steeply.Cross-validated MSE plotted against alpha on a log scale for ridge regression on 65 polynomial features. The curve is flat and high at small alpha, dips to a minimum near alpha 66, then rises steeply.
Flat on the left because a tiny penalty is no penalty; a clear minimum around 66; a steep climb on the right as the penalty overwhelms the signal. Never pick alpha by eye — pick it here.

Choosing α\alpha properly

tuning_alpha.py
import numpy as np
from sklearn.datasets import load_diabetes
from sklearn.linear_model import RidgeCV, LassoCV
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
 
X, y = load_diabetes(return_X_y=True)
alphas = np.geomspace(1e-3, 1e3, 60)
 
ridge = make_pipeline(StandardScaler(), RidgeCV(alphas=alphas)).fit(X, y)
lasso = make_pipeline(
    StandardScaler(), LassoCV(alphas=alphas, cv=5, max_iter=100000, random_state=0)
).fit(X, y)
 
print(f"RidgeCV alpha = {ridge[-1].alpha_:.4f}")           # RidgeCV alpha = 1.7957
print(f"LassoCV alpha = {lasso[-1].alpha_:.4f}")           # LassoCV alpha = 0.0732
print(f"Lasso kept {(lasso[-1].coef_ != 0).sum()} of 10")  # Lasso kept 9 of 10
tuning_alpha.py
import numpy as np
from sklearn.datasets import load_diabetes
from sklearn.linear_model import RidgeCV, LassoCV
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
 
X, y = load_diabetes(return_X_y=True)
alphas = np.geomspace(1e-3, 1e3, 60)
 
ridge = make_pipeline(StandardScaler(), RidgeCV(alphas=alphas)).fit(X, y)
lasso = make_pipeline(
    StandardScaler(), LassoCV(alphas=alphas, cv=5, max_iter=100000, random_state=0)
).fit(X, y)
 
print(f"RidgeCV alpha = {ridge[-1].alpha_:.4f}")           # RidgeCV alpha = 1.7957
print(f"LassoCV alpha = {lasso[-1].alpha_:.4f}")           # LassoCV alpha = 0.0732
print(f"Lasso kept {(lasso[-1].coef_ != 0).sum()} of 10")  # Lasso kept 9 of 10

RidgeCVRidgeCV and LassoCVLassoCV do the sweep internally and are considerably faster than a manual GridSearchCVGridSearchCV because they reuse the solution path.

Which penalty

Ridge (L2)Lasso (L1)Elastic Net
Coefficients reach zeroNeverYesYes
Closed formYesNoNo
With correlated featuresSplits weight between themPicks one arbitrarilyKeeps the group together
When n>mn > mWorksSelects at most mm featuresWorks, no cap
InterpretabilityGoodBest — fewer featuresGood
Default choice whenAll features plausibly matterYou suspect most are noiseCorrelated groups, or unsure
scikit-learnRidgeRidge, RidgeCVRidgeCVLassoLasso, LassoCVLassoCVElasticNetElasticNet, ElasticNetCVElasticNetCV

Elastic Net exists because Lasso has a specific failure mode: with two nearly identical features it picks one at random and zeroes the other, and which one it picks flips with a different random seed. Adding a little L2 to the L1 makes correlated features enter or leave together.

algorithmRidge and Lasso RegressionSupervised · Regression · Regularised

APIsklearn.linear_model.Ridge / Lasso / ElasticNet

Assumes

  • The same linearity assumptions as ordinary least squares
  • Features are standardised — the penalty is meaningless otherwise
  • Shrinking coefficients toward zero is a reasonable prior for this problem

Cost

train
Ridge O(m·n² + n³) closed form; Lasso O(m·n) per coordinate sweep
predict
O(n)
memory
O(n)

m = samples, n = features

Hyperparameters that matter

  • alphadefault 1.0Penalty strength. Zero recovers OLS; large values drive every coefficient to zero. Always cross-validate it.
  • l1_ratiodefault 0.5 (ElasticNet)Mix between L1 and L2. 1.0 is pure Lasso, 0.0 is pure Ridge.
  • max_iterdefault 1000Lasso and Elastic Net iterate; raise it when you see a convergence warning.
  • solverdefault auto (Ridge)'saga' handles sparse data and very large problems; 'cholesky' is fastest for dense wide-but-short data.

Reach for it when

  • Features outnumber samples, or nearly do
  • Features are correlated and OLS coefficients look unstable
  • You expanded features (polynomials, one-hot, interactions) and now overfit
  • You want automatic feature selection (Lasso)

Look elsewhere when

  • You have far more samples than features and OLS already generalises well
  • You cannot standardise the features
  • You need unbiased coefficient estimates for statistical inference

Pitfalls

quizCheck yourself
  1. Why can Lasso set a coefficient to exactly zero while Ridge cannot?

    Show answer

    B — The L1 constraint region is a diamond with corners on the axes, and the cost contours tend to touch a corner first — It is geometry. A corner of the diamond is a point where one coordinate is zero; the circle of the L2 ball has no corners, so its touch point generically has every coordinate non-zero.

  2. What does adding lambda times the identity to X transpose X accomplish beyond shrinkage?

    Show answer

    B — It makes the matrix invertible even when the features are collinear or outnumber the samples — A singular X transpose X becomes non-singular once you add a positive constant to its diagonal, which is why Ridge has a solution in cases where OLS has none.

  3. Why must features be standardised before regularising?

    Show answer

    B — Because the penalty charges by coefficient size, so a feature's units silently decide how much it is penalised — Rescaling a feature rescales its coefficient inversely, which changes the penalty that coefficient incurs. Without a common scale you are penalising your choice of units.

  4. Your two most predictive features have a correlation of 0.98. Which penalty is the best fit?

    Show answer

    B — Elastic Net, so the correlated pair enters or leaves together — Pure Lasso picks one of a correlated pair essentially at random. The L2 component in Elastic Net keeps the group together, which is both more stable and more interpretable.

🧪 Try It Yourself

Exercise 1 – The Ridge shrinkage factor

Exercise 2 – Soft thresholding by hand

Exercise 3 – Ridge keeps everything, Lasso does not

Exercise 4 – Regularisation earns its keep on wide data

Exercise 5 – Let cross-validation pick alpha

Recap

  • Regularisation adds a penalty on coefficient size, trading a little bias for a lot less variance.
  • Ridge has the closed form (XX+λI)1Xy(\mathbf{X}^\top\mathbf{X} + \lambda\mathbf{I})^{-1}\mathbf{X}^\top\mathbf{y}, which also fixes singular designs.
  • On an orthogonal design Ridge multiplies each coefficient by c/(c+λ)c/(c+\lambda); Lasso subtracts λ/2c\lambda/2c from each magnitude and clamps at zero.
  • Worked by hand at λ=10\lambda = 10: Ridge gives [0.643,0.357][0.643, 0.357], Lasso gives [1.0,0][1.0, 0] — one feature deleted.
  • The diamond has corners on the axes and the circle does not. That is the whole geometric story.
  • On 65 polynomial features, cross-validated MSE drops from 3495 (OLS) to 3063 (Ridge) to 3016 (Lasso, using 34 columns). On the raw 10 features it changes nothing.
  • Always standardise, never penalise the intercept, and always cross-validate α\alpha.

Exercise 6 – Watch each penalty act on the same coefficients

Next

Continue to Metrics - R-Squared and Adjusted R-Squared — how to report all of this to someone who was not in the room.

If this helped you, consider buying me a coffee ☕

Buy me a coffee

Was this page helpful?

Let us know how we did