Penalized Regression in R (glmnet)

Context: FIT2086_MOC Β· Studio 8 Β· fit lasso βž” choose by Cross-Validation βž” score on test with the Studio 7 my.pred.stats (Logistic Regression in R (glm, pROC, step)) Β· the stepwise rival βž” Model Selection and Information Criteria (AIC, BIC) Problem it solves: many candidate predictors (or close to ) βž” a shrunk, possibly sparse model whose penalty strength is chosen by held-out error instead of all-or-nothing step().

Quick Revision

  • 🎯 Trigger: many predictors / transformations, or small βž” cv.glmnet.f(y ~ …, df, family = "binomial") βž” coefficients(fit, s = "lambda.min") βž” predict.glmnet.f(fit, test, type = "response", s = "lambda.min").
  • ⚠️ Key Constraint: glmnet takes a matrix, not a formula βž” use the .f wrappers; alpha = 1 (default) is lasso, alpha = 0 is ridge; forget s = "lambda.min" and you get the whole path.

πŸ”§ Minimal Working Example

library(glmnet); library(pROC)       # install.packages("glmnet") once
source("wrappers.R")                 # glmnet.f, cv.glmnet.f, predict.glmnet.f, my.make.formula
source("my.prediction.stats.R")      # my.pred.stats(prob, target)
pima.train = read.csv("pima.train.csv", stringsAsFactors = T)   # 668 x 9
pima.test  = read.csv("pima.test.csv",  stringsAsFactors = T)   # 100 x 9
 
# lambda = 0: no penalty = plain ML, matches glm()
fit0 = glmnet.f(DIABETES ~ ., pima.train, family = "binomial", lambda = 0)  # "binomial" IN QUOTES
 
# hand-picked lambda: how sparse?
fit = glmnet.f(DIABETES ~ ., pima.train, family = "binomial", lambda = 0.05)
coefficients(fit)                    # "." = exactly zero
sum(coefficients(fit)[-1, ] != 0)    # non-zero predictors, intercept excluded -> 4
 
# whole path, then CV for lambda
path = glmnet.f(DIABETES ~ ., pima.train, family = "binomial")
plot(path, "lambda", label = T)      # coefficient paths vs log(lambda)
set.seed(1)
cvfit = cv.glmnet.f(DIABETES ~ ., pima.train, family = "binomial")   # 10 folds
plot(cvfit); min(cvfit$cvm); cvfit$lambda.min
coefficients(cvfit, s = "lambda.min")
my.pred.stats(predict.glmnet.f(cvfit, pima.test, type = "response", s = "lambda.min"),
              pima.test$DIABETES)
 
# ridge: same calls + alpha = 0
ridge = cv.glmnet.f(DIABETES ~ ., pima.train, family = "binomial", alpha = 0)

Expected output (rerun on the studio files; CV rows use set.seed(1)):

(lasso)Non-zero of 8Dropped
INS
BP, INS
(PREG, PLAS, BMI, AGE)BP, SKIN, INS, PED
Model on Pima ()TermsCASensSpecAUCLog-loss
lasso ( glm)
lasso CV, lambda.min (all, shrunk)
BIC stepwise (PREG, PLAS, BMI, PED)
ridge CV, lambda.min
  • Path βž” the default grid stopped at values (–), not βž” glmnet ends early once the deviance stops changing.
  • Lasso CV equation βž” .
  • Lasso vs BIC βž” with and only predictors, CV barely penalises (all kept); BIC’s 4-term model predicts slightly better and is far easier to read.
  • Ridge path βž” : every coefficient shrinks smoothly (PED ) and none reaches zero β€” unlike lasso at , which zeroed four.
  • Seed check βž” over seeds 1–5, lambda.min ranged – and always kept all 8 βž” the CV choice moves, the conclusion doesn’t.

πŸ”€ Variations

  • All transformations βž” f = my.make.formula("DIABETES", pima.train, use.interactions = T, use.logs = T, use.squares = T, use.cubics = T) βž” . + .*. + log() of each strictly positive predictor + I(x^2) + I(x^3) ⟹ columns (PREG has zeros ⟹ no log).
, 59 columnsTerms keptFit timeCAAUCLog-loss
lasso CV s
BIC stepwise (both) s
  • Reading βž” lasso is ~ faster; with plenty of data it does not beat stepwise, but is not much worse.
  • Swap train/test βž” pima.train = read.csv("pima.test.csv", …) ⟹ to fit, to test, :
modelTermsCAAUCLog-loss
glm, 8 raw predictors
glm, all 59
backward BIC from 59
lasso CV (min CV dev. )
ridge CV, same folds (min CV dev. )
  • Reading βž” 59 terms on 100 rows overfits catastrophically; stepwise barely rescues it; lasso and ridge degrade gracefully and beat even the raw 8-predictor glm βž” the lasso’s real strength is small relative to . CV preferred lasso (); the test set slightly favoured ridge β€” CV is an estimate, not a guarantee.
  • Lasso’s equation βž” .
  • Same folds for a fair CV comparison βž” foldid = sample(rep(1:10, length.out = nrow(df))), then pass foldid = foldid to both cv.glmnet.f calls.
  • Continuous target βž” omit family (Gaussian default) βž” lasso/ridge linear regression.

✍️ Practice

⚠️ Common Mistakes

  • πŸ’‘ family = binomial unquoted βž” glm takes the object, glmnet needs the string "binomial".
  • πŸ’‘ Calling glmnet with a formula βž” it only accepts X, y; the wrappers build model.matrix and drop the intercept column for you.
  • πŸ’‘ Counting the intercept as a predictor βž” coefficients(fit)[-1, ] != 0 excludes row 1.
  • πŸ’‘ roc() warning β€œDeprecated use a matrix as predictor” βž” harmless β€” predict.glmnet.f returns a 1-column matrix; wrap in as.numeric() to silence it.