Generalizing this recipe to GLMs indeed is not difficult as GLMs are usually fit using iteratively reweighted least squares. Hence, within each iteration one can subsitute the regular weighted least squares step with a ridge penalized weighted least squares step to get a ridge penalized GLM. In fact, in combination with adaptive ridge penalties this recipe is used to fit L0 penalized GLMs (aka best subset, ie GLMs where the total number of nonzero coefficients are penalized). This has been implemented for example in the l0ara package, see this paper and this one for details.
It's also worth noting that the fastest closed-form way of solving a regular ridge regression is using
lmridge_solve = function (X, y, lambda, intercept = TRUE) {
if (intercept) {
lambdas = c(0, rep(lambda, ncol(X)))
X = cbind(1, X)
} else { lambdas = rep(lambda, ncol(X)) }
solve(crossprod(X) + diag(lambdas), crossprod(X, y))[, 1]
}
for the case where n>=p
, or using
lmridge_solve_largep = function (X, Y, lambda) (t(X) %*% solve(tcrossprod(X)+lambda*diag(nrow(X)), Y))[,1]
when p>n
and for a model without intercept.
This is faster than using the row augmentation recipe, i.e. doing
lmridge_rbind = function (X, y, lambda, intercept = TRUE) {
if (intercept) {
lambdas = c(0, rep(lambda, ncol(X)))
X = cbind(1, X)
} else { lambdas = rep(lambda, ncol(X)) }
qr.solve(rbind(X, diag(sqrt(lambdas))), c(y, rep(0, ncol(X))))
}
If you would happen to need nonnegativity constraints on your fitted coefficients then you can just do
library(nnls)
nnlmridge_solve = function (X, y, lambda, intercept = TRUE) {
if (intercept) {
lambdas = c(0, rep(lambda, ncol(X)))
X = cbind(1, X)
} else { lambdas = rep(lambda, ncol(X)) }
nnls(A=crossprod(X)+diag(lambdas), b=crossprod(X,Y))$x
}
which then gives a slightly more accurate result btw than
nnlmridge_rbind = function (X, y, lambda, intercept = TRUE) {
if (intercept) {
lambdas = c(0, rep(lambda, ncol(X)))
X = cbind(1, X)
} else { lambdas = rep(lambda, ncol(X)) }
nnls(A=rbind(X,diag(sqrt(lambdas))), b=c(Y,rep(0,ncol(X))))$x
}
(and strictly speaking only the solution nnls(A=crossprod(X)+diag(lambdas), b=crossprod(X,Y))$x
is then the correct one).
I haven't yet figured out how the nonnegativity constrained case could be further optimized for the p > n
case - let me know if anyone would happen to know how to do this... [lmridge_nnls_largep = function (X, Y, lambda) t(X) %*% nnls(A=tcrossprod(X)+lambda*diag(nrow(X)), b=Y)$x
doesn't work]