Ridge regression in marketing mix modeling: what the penalty does and doesn't fix

Robyn fits every candidate model with ridge regression. That penalty keeps correlated channel estimates from flying apart, but it does so by adding an assumption, and it's worth knowing exactly which one.

Mike Taylor By Mike Taylor · Updated · 9 min read

Ridge regression is linear regression with a penalty on the squared size of the coefficients, so the model has to balance fitting the data against keeping its estimates small. In a marketing mix model that penalty stops correlated channels from getting wildly large positive and negative coefficients, trading a little bias for a big reduction in variance, which is why Meta’s open-source Robyn fits every candidate model with ridge regression.

When I taught Robyn in the earlier Vexpower MMM path, ridge was the part people were most likely to treat as a black box: it was “the regression” inside a lot of hyperparameter machinery. That’s a shame, because it’s a simple idea with a clear interpretation, and knowing what it does tells you a lot about how to read Robyn’s output. It also tells you what it can’t do, which matters more for budget decisions. I’ll start with the objective itself and finish with how Robyn chooses the penalty.

The ridge regression objective

Ordinary least squares (OLS) picks the coefficients that minimize the sum of squared errors. Ridge adds a second term that charges for every coefficient’s squared size, weighted by a penalty λ\lambda. In the form used by glmnet, the R package Robyn relies on, with nn weeks of data and pp predictors, the ridge objective is:

min⁡β0, β  12n∑t=1n(yt−β0−∑j=1pxtjβj)2  +  λ2∑j=1pβj2\min_{\beta_0,\,\beta}\; \frac{1}{2n}\sum_{t=1}^{n}\Big(y_t - \beta_0 - \sum_{j=1}^{p} x_{tj}\beta_j\Big)^2 \;+\; \frac{\lambda}{2}\sum_{j=1}^{p}\beta_j^2

The intercept isn’t penalized, and with λ=0\lambda = 0 you’re back to OLS. As λ\lambda grows, the cheapest way to lower the objective is to shrink the coefficients, so they’re pulled towards zero, and correlated coefficients are pulled towards each other, because splitting the credit evenly between two linked channels costs less penalty than giving it all to one. The solution even has a closed form, which shows where the name comes from:

β^ridge=(X⊤X+nλI)−1X⊤y\hat\beta_{\text{ridge}} = \left(X^\top X + n\lambda I\right)^{-1} X^\top y

Adding a constant to the diagonal of X⊤XX^\top X is exactly what Arthur Hoerl and Robert Kennard proposed in their 1970 Technometrics paper, written for “nonorthogonal” problems, which is the statisticians’ term for correlated predictors. When two channels move together, X⊤XX^\top X is close to singular and inverting it amplifies noise enormously; the added diagonal makes it well conditioned again. They also introduced the “ridge trace”, a plot of every coefficient across a range of penalties, which is still the best way to see what ridge is doing.

Why standardization matters before you penalize

The penalty treats every coefficient the same, but coefficients depend on units. A channel measured in dollars of weekly spend has a coefficient around 1 or 2, a 0/1 promotion flag might have a coefficient of 5,000, and impressions measured in raw counts might have a coefficient of 0.0001. Penalise those on their raw scales and the promotion is crushed while the impressions channel barely notices. The fix is to standardize each predictor (subtract its mean, divide by its standard deviation) before fitting, so the penalty is applied on a common scale, then convert the coefficients back. glmnet does this by default (standardize = TRUE) and, as its vignette notes, always returns the coefficients on the original scale, so you never see the standardized versions unless you go looking.

It’s worth keeping in mind that standardization is itself a modeling choice. A channel with very little variation (an always-on display line, say) gets a small standard deviation, so on the standardized scale its coefficient is large relative to how much it moved, and the penalty bites it harder. That’s not a bug, since a channel that barely varied really is weakly identified, but it’s one reason small, steady channels often come out of ridge-based models looking unremarkable.

The bias-variance trade-off in one slider

The point of the penalty is the bias-variance trade-off. OLS is unbiased (right on average over many hypothetical datasets) but with correlated channels and only a year or two of weekly data, the estimate from your one dataset can be far from the truth, as the multicollinearity guide shows. Ridge accepts some bias, pulling every estimate towards zero and towards its correlated neighbors, in exchange for estimates that move much less from one dataset to the next. If the variance you remove is larger than the bias you add, the model predicts new weeks better.

The explorer below fits three correlated channels on one simulated year. The truth is fixed: search returns $3.00 per $1, social $0.50 and video $1.50. Start at a small penalty, then slide λ to the right and watch the ridge trace on the left and the error curves on the right.

Interactive
Slide the ridge penalty and watch the coefficients shrink

Three channels with known returns, fitted on one year of weekly data. Move λ to the right to penalise large coefficients. The left chart is the ridge trace (each channel's estimate across every λ); the right chart compares error on the training weeks with error on 500 new weeks from the same process.

Ridge trace: estimated return per $1 (dashed = truth)
Error in and out of sample (RMSE)
Search (truth 3.0)
–
Social (truth 0.5)
–
Video (truth 1.5)
–
Avg. distance from true returns
–
Test RMSE vs OLS
–
λ with lowest test error
–

Training data: 52 seeded weeks; spend around $10,000 per channel with a shared driver; sales = 20,000 + 3.0 × search + 0.5 × social + 1.5 × video + noise. Channels and sales are standardised before fitting, as glmnet does by default, using the objective on this page; coefficients are converted back to sales per $1. Test error is RMSE on 500 fresh weeks; average distance is the root-mean-square gap between the three estimated returns and the truth. Synthetic data.

Three things are worth noticing. First, the training error only ever goes up as λ grows, because the penalty stops the model fitting the training weeks as closely as it could, while the error on new weeks usually falls a little before rising, and the bottom of that curve is the λ you’d want. Second, the lines in the ridge trace converge before they fall: the penalty first pushes the three channels towards each other, so the large search effect gets pulled down and the small social effect gets pulled up. Third, at the best λ the estimates are more stable, but they aren’t the truth. Social is usually overstated and search understated, because ridge’s idea of a plausible answer is “correlated channels have similar effects”, and that’s an assumption, not something the data told it. Push the correlation down towards zero and the best λ usually falls towards zero too, since there’s little variance left to remove.

It’s also worth comparing the last two stats. In the default year, OLS gives social a negative return, and the best λ brings the average distance from the true returns down by more than half while improving out-of-sample error by only a few percent. That gap is typical: noise in weekly sales dominates prediction error, so a model can predict almost equally well with very different channel splits, which is exactly why fit statistics are a poor guide to whether the coefficients are right. Across 40 simulated training years at these settings, the best-λ ridge model understated search on average (around 2.8 against 3.00) and overstated social (around 0.7 against 0.50), while cutting the typical coefficient error by roughly a fifth to a quarter.

How Robyn uses ridge regression

Robyn’s documentation says it uses “the L2-form of the regularisation, also known as Ridge regression”, fitted with glmnet, and explains why the team chose it over lasso: lasso tends to set some channels’ coefficients to exactly zero, and a model that concludes several media channels have no effect at all was considered unrealistic. In the source code (R/R/model.R), each candidate model is a call to glmnet with alpha = 0, which is glmnet’s setting for pure ridge. Here’s how the pieces fit together, as of the version on CRAN in October 2026 (3.12.1):

  • λ is a hyperparameter, not a separate cross-validation step. Alongside the adstock and saturation hyperparameters for every channel, Robyn samples a value of lambda between 0 and 1, and Nevergrad’s evolutionary optimizer (TwoPointsDE by default) searches over all of them together.
  • That 0-1 value is rescaled from the data. Robyn computes a glmnet-style maximum penalty from the data, uses 10 percent of it as lambda_max, sets lambda_min to lambda_max × 0.0001 (glmnet’s default ratio), and maps the sampled value linearly between the two.
  • Models are scored on more than fit. Each candidate is evaluated on NRMSE (prediction error), DECOMP.RSSD (how far each channel’s share of effect strays from its share of spend) and, if you calibrate with experiments, MAPE.LIFT. Robyn keeps the Pareto-optimal models across these objectives and clusters them to suggest a few to choose from.
  • Signs are constrained. By default (paid_media_signs = "positive") paid media coefficients get a lower limit of zero through glmnet’s lower.limits, so a channel can be shrunk to nothing but not made negative. If the intercept comes out negative, Robyn by default drops it and refits.

The practical upshot is that the λ in a Robyn model wasn’t chosen to minimize out-of-sample error alone. It was chosen together with the adstock and saturation shapes, partly on how well the decomposition matches spend shares. That’s a reasonable design for a tool that has to produce believable budgets, but it means the penalty is doing double duty as a business-plausibility prior, which is worth knowing when you compare a Robyn model with one fitted another way. The Robyn guide covers the rest of the workflow, including picking a model from the Pareto front.

Choosing λ outside Robyn

If you’re fitting ridge yourself, in Python or R, the standard approach is cross-validation: fit the model on part of the data for a grid of λ values, score each on the held-out part, and pick the λ with the lowest error. For weekly MMM data, the folds must respect time. Randomly shuffled folds let the model train on next week to predict last week, which flatters it, so use an expanding window instead (train on weeks 1 to 52, test on 53 to 60, then train on 1 to 60, test on 61 to 68, and so on). Many practitioners then take the “one standard error” choice, the largest λ whose error is within one standard error of the best, which gives a slightly more conservative model for very little loss of accuracy.

In R the core call is short. This is the glmnet interface Robyn wraps, shown on its own for a matrix x of transformed media and controls and a revenue vector y:

library(glmnet)
fit <- glmnet(x, y, alpha = 0)              # ridge path over glmnet's default lambda grid
cv  <- cv.glmnet(x, y, alpha = 0, foldid = fold_ids)  # pass time-ordered folds, don't use random ones
coef(cv, s = "lambda.1se")                  # coefficients at the one-standard-error lambda

The notebook below does the same from scratch in numpy on the practice dataset, with fixed adstock, a time-series cross-validation loop and a comparison of OLS and ridge contributions, so you can see every step rather than trusting a library. Be clear about what the practice data can show: it was generated with no noise, no carryover and almost uncorrelated channels (the true returns are 1.4, 0.8 and 0.6 per $1), so ridge changes the coefficients only slightly, and the notebook’s fixed adstock rates are illustrative rather than discovered. Swap in your own data, where the channels move together and sales are noisy, and the difference is usually much larger.

ipynb
Ridge regression MMM notebook (Jupyter)Standardise, fit the ridge path, choose λ with expanding-window CV, and compare OLS vs ridge ROIs. numpy, pandas, matplotlib.
Download

Why ridge regression isn’t a causal fix

The most common misunderstanding I see is that regularization makes a model “more accurate” in the sense that matters for budget decisions. It makes the estimates more stable and often improves prediction, but it doesn’t address any of the reasons a channel’s coefficient might be causally wrong. If a confounder such as seasonality or promotions is missing, ridge shrinks the biased coefficient a little; it’s still biased. If brand search is in the model as a control, ridge doesn’t know it’s a mediator. The confounding variables guide covers those problems, and they need to be solved before the penalty is worth discussing.

Ridge also has a direction. It shrinks towards zero, which is a statement that, absent strong evidence, channels probably don’t work. For a small channel that’s often a sensible default, but for a big channel with a genuinely large effect it means the model systematically understates it, and the larger the penalty, the more so. In Bayesian terms, ridge is equivalent to putting a normal prior centered at zero on every standardized coefficient, with the same width for all of them. Seen that way, the alternative is obvious: a Bayesian MMM can center each channel’s prior on something informed, such as an experiment result or a benchmark ROI, rather than on zero, and can make the prior tighter for channels you know more about. That’s more work to justify, but it’s an assumption you chose for a reason rather than one built into the method.

None of this is an argument against ridge. With 104 weeks and a dozen correlated channels, OLS estimates are often so noisy that they’re useless, and some form of regularization, whether a ridge penalty or a prior, is unavoidable. The point is to know which assumption is settling the answer, say so when you present the results, and use experiments to calibrate the channels where the assumption matters most. The model validation guide covers how to check the out-of-sample side properly.

Ridge, lasso and elastic net compared

Since the question comes up whenever someone opens the glmnet documentation, here’s how the three penalties differ in practice for MMM:

  • Ridge (L2, glmnet alpha = 0): shrinks all coefficients smoothly, never to exactly zero; spreads credit across correlated channels. Robyn’s choice.
  • Lasso (L1, alpha = 1): can set coefficients exactly to zero, which selects variables; among correlated channels it tends to keep one and drop the others somewhat arbitrarily, which is a poor fit for media you know you spent money on.
  • Elastic net (0 < alpha < 1): a mix of both; useful for large sets of control variables, less often for media.

For most marketing mix models I’d use ridge or priors on the media channels and be cautious with anything that can zero out a channel the business is actively buying. If you want to see where all of this sits in the bigger picture, the pillar guide to marketing mix modeling is the place to start, and the Vexpower MMM course works through linked channels, priors and calibration on the invented Northstar case.

Downloads

ipynb
Ridge regression MMM notebook (Jupyter)numpy-only ridge MMM on the practice CSV: standardization, ridge trace, time-series cross-validation and OLS vs ridge contributions.
Download
csv
Practice weekly dataset (CSV)104 synthetic weeks of revenue, three channels and a promotion flag.
Download

Frequently asked questions

What is ridge regression in simple terms?

Ridge regression is linear regression with an extra cost for large coefficients. It finds the coefficients that balance fitting the data against keeping the squared size of the coefficients small, which makes estimates much more stable when predictors are correlated, at the price of shrinking them towards zero.

Why does Robyn use ridge regression?

Robyn uses ridge regression through the glmnet package to handle correlated media variables, which are common in marketing data. Its documentation says ridge was preferred over lasso because lasso tends to set some channels to exactly zero, while ridge only shrinks them.

How is lambda chosen in Robyn?

In Robyn lambda is treated as a hyperparameter alongside the adstock and saturation parameters and searched by Nevergrad's evolutionary optimizer. Robyn samples a value between 0 and 1 and scales it between a minimum and maximum penalty derived from the data, then keeps the models that trade off prediction error and decomposition error best.

Is ridge regression better than ordinary least squares for MMM?

It usually predicts new weeks better when channels are correlated and data is short, because it cuts the variance of the estimates. But it adds bias towards zero and towards equal credit for correlated channels, so its coefficients are not more causal than OLS ones.

What is the difference between ridge and lasso regression?

Ridge penalizes the sum of squared coefficients and shrinks them smoothly without reaching zero, while lasso penalizes the sum of absolute values and can set some coefficients exactly to zero. Elastic net mixes the two penalties.

References

  1. Robyn documentation: features L2 regularisation via glmnet, lambda as a hyperparameter tuned by Nevergrad, NRMSE and DECOMP.RSSD objectives, why ridge over lasso.
  2. Robyn source code, R/R/model.R (GitHub) glmnet(alpha = 0), lambda hyperparameter in [0, 1] scaled between lambda_min and lambda_max, lambda_min_ratio = 0.0001, lower.limits for signs.
  3. glmnet vignette (Hastie, Qian & Tay) Gaussian objective, alpha = 0 is ridge, standardize = TRUE by default, coefficients returned on the original scale.
  4. Hoerl & Kennard (1970), Ridge Regression: Biased Estimation for Nonorthogonal Problems, Technometrics Original paper; adding small positive quantities to the diagonal of X'X and the ridge trace.
  5. Robyn on CRAN Version 3.12.1, published 2025-07-02 (checked October 2026).
Mike Taylor
Mike Taylor

I've built marketing mix models for Monzo Bank, Booking.com, Time Out, The AA and others, advised Google, Appsflyer and Recast on MMM, and teach marketing mix modeling on LinkedIn Learning. Before that I co-founded a 50-person growth marketing agency. I write the Vexpower MMM course.

Keep reading

Learn MMM by doing itThe Vexpower course walks through thirty lessons on one invented brand, with simulators, Python labs and a decision case you can defend.
See the course