#--- Main function -------------------------------------------------------------
#' @export
#' @title
#' Stacked Elastic Net Regression
#'
#' @description
#' Implements stacked elastic net regression.
#'
#' @param y
#' response\strong{:}
#' numeric vector of length \eqn{n}
#'
#' @param X
#' covariates\strong{:}
#' numeric matrix with \eqn{n} rows (samples)
#' and \eqn{p} columns (variables)
#'
#' @param family
#' character "gaussian", "binomial" or "poisson"
#'
#' @param nalpha
#' number of \code{alpha} values
#'
#' @param alpha
#' elastic net mixing parameters\strong{:}
#' vector of length \code{nalpha} with entries
#' between \eqn{0} (ridge) and \eqn{1} (lasso);
#' or \code{NULL} (equidistance)
#'
#' @param nfolds
#' number of folds
#'
#' @param foldid
#' fold identifiers\strong{:}
#' vector of length \eqn{n} with entries between \eqn{1} and \code{nfolds};
#' or \code{NULL} (balance)
#'
#' @param type.measure
#' loss function\strong{:}
#' character "deviance", "class", "mse" or "mae"
#' (see \code{\link[glmnet]{cv.glmnet}})
#'
#' @param alpha.meta
#' meta-learner\strong{:}
#' value between \eqn{0} (ridge) and \eqn{1} (lasso)
#' for elastic net regularisation;
#' \code{NA} for convex combination
#'
#' @param intercept,upper.limit,unit.sum
#' settings for meta-learner\strong{:} logical,
#' or \code{NULL}
#' (\code{intercept=!is.na(alpha.meta)},
#' \code{upper.limit=TRUE},
#' \code{unit.sum=is.na(alpha.meta)})
#'
#' @param penalty.factor
#' differential shrinkage\strong{:}
#' vector of length \eqn{n} with entries
#' between \eqn{0} (include) and \eqn{Inf} (exclude),
#' or \code{NULL} (all \eqn{1})
#'
#' @param ...
#' further arguments passed to \code{\link[glmnet]{glmnet}}
#'
#' @inherit starnet-package references
#'
#' @details
#' Post hoc feature selection\strong{:} consider
#' argument \code{nzero} in functions
#' \code{\link{coef}} and \code{\link{predict}}.
#'
#' @return
#' Object of class \code{\link[starnet]{starnet}}.
#' The slots \code{base} and \code{meta}
#' contain \code{\link[glmnet]{cv.glmnet}}-like objects,
#' for the base and meta learners, respectively.
#'
#' @examples
#' \dontshow{
#' if(!grepl('SunOS',Sys.info()['sysname'])){
#' set.seed(1)
#' n <- 50; p <- 100
#' y <- rnorm(n=n)
#' X <- matrix(rnorm(n*p),nrow=n,ncol=p)
#' object <- starnet(y=y,X=X,family="gaussian")}}
#' \donttest{
#' set.seed(1)
#' n <- 50; p <- 100
#' y <- rnorm(n=n)
#' X <- matrix(rnorm(n*p),nrow=n,ncol=p)
#' object <- starnet(y=y,X=X,family="gaussian")}
#'
starnet <- function(y,X,family="gaussian",nalpha=21,alpha=NULL,nfolds=10,foldid=NULL,type.measure="deviance",alpha.meta=1,penalty.factor=NULL,intercept=NULL,upper.limit=NULL,unit.sum=NULL,...){
if(is.na(alpha.meta) && (!"CVXR" %in% .packages(all.available=TRUE))){
stop("Install CVXR from CRAN for alpha.meta=NA.",call.=FALSE)
}
#--- temporary ---
# family <- "gaussian"; nalpha <- 21; alpha <- NULL; nfolds <- 10; foldid <- NULL; type.measure <- "deviance"; alpha.meta <- 0; penalty.factor <- NULL; intercept <- TRUE; upper.limit=TRUE; unit.sum=FALSE
#--- default ---
#if(all(is.null(intercept),is.null(upper.limit),is.null(unit.sum))){
# if(is.na(alpha.meta)){
# intercept <- FALSE
# upper.limit <- unit.sum <- TRUE
# } else if(alpha.meta==1){
# intercept <- TRUE
# upper.limit <- unit.sum <- FALSE
# } else if(alpha.meta==0){
# intercept <- upper.limit <- TRUE
# unit.sum <- FALSE
# }
#}
if(is.null(intercept)){intercept <- !is.na(alpha.meta)}
if(is.null(upper.limit)){upper.limit <- TRUE}
if(is.null(unit.sum)){unit.sum <- is.na(alpha.meta)}
#--- checks ---
cornet:::.check(x=y,type="vector")
cornet:::.check(x=X,type="matrix")
cornet:::.check(x=family,type="string",values=c("gaussian","binomial","poisson"))
cornet:::.check(x=nalpha,type="scalar",min=0)
cornet:::.check(x=alpha,type="vector",min=0,max=1,null=TRUE)
if(length(y)!=nrow(X)){stop("Contradictory sample size.",call.=FALSE)}
cornet:::.check(x=nfolds,type="scalar",min=3)
cornet:::.check(x=foldid,type="vector",values=seq_len(nfolds),null=TRUE)
cornet:::.check(x=type.measure,type="string",values=c("deviance","class","mse","mae")) # not auc (min/max confusion)
if(any(!is.na(alpha.meta))){cornet:::.check(x=alpha.meta[!is.na(alpha.meta)],type="vector",min=0,max=1)}
if(!is.null(c(list(...)$lower.limits,list(...)$upper.limits))){
stop("Reserved arguments \"lower.limits\" and \"upper.limits\".",call.=FALSE)
}
if(!is.null(list(...)$intercept)){
stop("Reserved argument \"intercept\".",call.=FALSE)
}
if(is.null(penalty.factor)){
penalty.factor <- rep(x=1,times=ncol(X))
# never pass NULL to glmnet/cv.glmnet!
}
#--- fold identifiers ---
if(is.null(foldid)){
foldid <- .folds(y=y,nfolds=nfolds)
}
nfolds <- max(foldid)
#--- alpha sequence ---
if(is.null(alpha)){
alpha <- seq(from=0,to=1,length.out=nalpha)
} else {
alpha <- alpha
nalpha <- length(alpha)
}
#--- full fit ---
base <- lapply(alpha,function(x) list())
names(base) <- paste0("alpha",alpha)
nlambda <- numeric()
for(i in seq_len(nalpha)){
base[[i]]$glmnet.fit <- glmnet::glmnet(y=y,x=X,family=family,alpha=alpha[i],penalty.factor=penalty.factor,...)
base[[i]]$lambda <- base[[i]]$glmnet.fit$lambda
nlambda[i] <- length(base[[i]]$glmnet.fit$lambda)
}
#--- predictions ---
n <- length(y)
link <- list()
for(i in seq_len(nalpha)){
link[[i]] <- matrix(data=NA,nrow=n,ncol=nlambda[i])
}
#--- base cross-validation ---
for(k in seq_len(nfolds)){
y0 <- y[foldid!=k]
y1 <- y[foldid==k]
X0 <- X[foldid!=k,,drop=FALSE]
X1 <- X[foldid==k,,drop=FALSE]
for(i in seq_len(nalpha)){
object <- glmnet::glmnet(y=y0,x=X0,family=family,alpha=alpha[i],penalty.factor=penalty.factor,...)
temp <- stats::predict(object=object,newx=X1,type="link",
s=base[[i]]$glmnet.fit$lambda)
link[[i]][foldid==k,seq_len(ncol(temp))] <- temp
}
}
#--- tune base lambdas ---
for(i in seq_len(nalpha)){
fit <- .mean.function(link[[i]],family=family)
base[[i]]$cvm <- apply(fit,2,function(x) .loss(y=y,x=x,family=family,type.measure=type.measure,foldid=foldid))
base[[i]]$id.min <- which.min(base[[i]]$cvm)
base[[i]]$lambda.min <- base[[i]]$lambda[base[[i]]$id.min]
}
#--- predictions ---
hat <- matrix(NA,nrow=nrow(X),ncol=nalpha)
for(i in seq_len(nalpha)){
hat[,i] <- link[[i]][,base[[i]]$id.min]
}
meta <- list()
#--- meta cross-validation ---
for(i in seq_along(alpha.meta)){
if(is.na(alpha.meta[i])){next}
meta[[i]] <- glmnet::cv.glmnet(y=y,x=hat,
lower.limits=0,
upper.limits=ifelse(upper.limit,1,Inf), # was 1
foldid=foldid,
family=family,
type.measure=type.measure,
intercept=intercept,
alpha=alpha.meta[i],...)
if(unit.sum){
warning("inequality constraint",call.=FALSE)
cond <- Matrix::colSums(meta[[i]]$glmnet.fit$beta)>1
meta[[i]]$cvm[cond] <- Inf
meta[[i]]$lambda.min <- meta[[i]]$lambda[which.min(meta[[i]]$cvm)]
}
}
#--- convex combination ---
for(i in seq_along(alpha.meta)){
if(!is.na(alpha.meta[i])){next}
glm <- .glm(y=y,X=hat,family=family,intercept=intercept,lower.limit=TRUE,upper.limit=upper.limit,unit.sum=unit.sum)
meta[[i]] <- list()
meta[[i]]$glmnet.fit <- list()
meta[[i]]$glmnet.fit$a0 <- rep(x=glm$alpha,times=2)
meta[[i]]$glmnet.fit$beta <- cbind(0,glm$beta) # matrix(data=rep(glm$beta,times=2),ncol=2)
meta[[i]]$glmnet.fit$lambda <- meta[[i]]$lambda <- c(99e99,0) # was c(1,0)
meta[[i]]$glmnet.fit$offset <- FALSE
if(length(alpha.meta)>1){
link <- rep(NA,times=length(y))
for(k in seq_len(nfolds)){
glm <- .glm(y=y[foldid!=k],X=hat[foldid!=k,],family=family,intercept=intercept,lower.limit=TRUE,upper.limit=upper.limit,unit.sum=unit.sum)
link[foldid==k] <- glm$alpha + hat[foldid==k,] %*% glm$beta
}
y_hat <- .mean.function(x=link,family=family)
cvm <- .loss(y=y,x=y_hat,family=family,type.measure=type.measure,foldid=foldid)
} else {
cvm <- 0
}
meta[[i]]$cvm <- c(Inf,cvm)
meta[[i]]$lambda.min <- 0
class(meta[[i]]) <- "cv.glmnet"
class(meta[[i]]$glmnet.fit) <- "glmnet"
}
names(meta) <- paste0("alpha",alpha.meta)
#--- tune meta alpha ---
cvm_meta <- sapply(meta,function(x) min(x$cvm))
#message(paste0(names(cvm_meta)," ",round(cvm_meta,digits=2)," "))
id_meta <- which.min(cvm_meta)
#--- message ---
meta <- meta[[names(id_meta)]]
#message(paste(paste(round(stats::coef(meta,s="lambda.min")[1],digits=3)),"_",
# paste(round(stats::coef(meta,s="lambda.min")[-1],digits=3),collapse=" ")))
if(FALSE){
## debugging: stacking returns same output as tuning
# cvm <- sapply(base,function(x) x$cvm[x$id.min])
# id <- which.min(cvm)
# meta$glmnet.fit$a0[] <- 0
# meta$glmnet.fit$beta[,] <- 0
# meta$glmnet.fit$beta[id,] <- 1
}
#--- return ---
info <- list(id_meta=id_meta,
type.measure=type.measure,
family=family,
mean=mean(y),
foldid=foldid,X=X)
list <- list(base=base,meta=meta,info=info)
class(list) <- "starnet"
return(list)
}
#--- Methods for class "starnet" ------------------------------------------------
#' @export
#' @title
#' Makes Predictions
#'
#' @description
#' Predicts outcome from features with stacked model.
#'
#' @param object
#' \link[starnet]{starnet} object
#'
#' @param newx
#' covariates\strong{:}
#' numeric matrix with \eqn{n} rows (samples)
#' and \eqn{p} columns (variables)
#'
#' @param type
#' character "link" or "response"
#'
#' @param nzero
#' maximum number of non-zero coefficients\strong{:}
#' positive integer, or \code{NULL}
#'
#' @param ...
#' further arguments (not applicable)
#'
#' @return
#' Matrix of predicted values, with samples in the rows,
#' and models in the columns. Included models are
#' \code{alpha} (fixed elastic net),
#' \code{ridge} (i.e. \code{alpha0}),
#' \code{lasso} (i.e. \code{alpha1}),
#' \code{tune} (tuned elastic net),
#' \code{stack} (stacked elastic net),
#' and \code{none} (intercept-only model).
#'
#' @examples
#' \dontshow{
#' if(!grepl('SunOS',Sys.info()['sysname'])){
#' set.seed(1)
#' n <- 50; p <- 100
#' y <- rnorm(n=n)
#' X <- matrix(rnorm(n*p),nrow=n,ncol=p)
#' object <- starnet(y=y,X=X)
#' y_hat <- predict(object,newx=X[c(1),,drop=FALSE])}}
#' \donttest{
#' set.seed(1)
#' n <- 50; p <- 100
#' y <- rnorm(n=n)
#' X <- matrix(rnorm(n*p),nrow=n,ncol=p)
#' object <- starnet(y=y,X=X)
#' y_hat <- predict(object,newx=X[c(1),,drop=FALSE])}
#'
predict.starnet <- function(object,newx,type="response",nzero=NULL,...){
if(length(list(...))!=0){warning("Ignoring argument.",call.=FALSE)}
x <- object
cornet:::.check(x=newx,type="matrix")
# all alphas
nalpha <- length(x$base)
link <- matrix(NA,nrow=nrow(newx),ncol=nalpha)
colnames(link) <- names(x$base)
for(i in seq_len(nalpha)){
link[,i] <- as.numeric(stats::predict(object=x$base[[i]]$glmnet.fit,
newx=newx,s=x$base[[i]]$lambda.min,type="link"))
}
# elastic net
cvm <- sapply(x$base[seq_len(nalpha)],function(x) x$cvm[x$id.min])
id <- which.min(cvm)
frame <- as.data.frame(link)
frame$ridge <- frame$alpha0
frame$lasso <- frame$alpha1
frame$tune <- frame[[names(id)]]
if(any(frame$tune!=frame[[id]])){stop("Invalid.")}
if(is.null(nzero)){
frame$stack <- as.numeric(stats::predict(object=x$meta,newx=link,type="link",s="lambda.min"))
} else {
coef <- coef.starnet(object=x,nzero=nzero)
frame$stack <- as.numeric(coef$alpha + newx %*% coef$beta)
}
frame$none <- as.numeric(stats::predict(object=x$base$alpha1$glmnet.fit,
newx=newx,s=Inf,type="link"))
if(type=="link"){
return(frame)
} else if(type=="response"){
frame <- lapply(frame,function(y) .mean.function(y,family=x$info$family))
return(as.data.frame(frame))
} else {
stop("Invalid type.",call.=FALSE)
}
}
#' @export
#' @title
#' Extract Coefficients
#'
#' @description
#' Extracts pooled coefficients.
#' (The meta learners weights the coefficients from the base learners.)
#'
#' @inheritParams predict.starnet
#'
#' @return
#' List of scalar \code{alpha} and vector \code{beta},
#' containing the pooled intercept and the pooled slopes,
#' respectively.
#'
#' @examples
#' \dontshow{
#' if(!grepl('SunOS',Sys.info()['sysname'])){
#' set.seed(1)
#' n <- 50; p <- 100
#' y <- rnorm(n=n)
#' X <- matrix(rnorm(n*p),nrow=n,ncol=p)
#' object <- starnet(y=y,X=X)
#' coef <- coef(object)}}
#' \donttest{
#' set.seed(1)
#' n <- 50; p <- 100
#' y <- rnorm(n=n)
#' X <- matrix(rnorm(n*p),nrow=n,ncol=p)
#' object <- starnet(y=y,X=X)
#' coef <- coef(object)}
#'
coef.starnet <- function(object,nzero=NULL,...){
if(length(list(...))!=0){warning("Ignoring argument.",call.=FALSE)}
# base coefficients
base <- list()
coef <- sapply(object$base,function(x) stats::coef(object=x$glmnet.fit,s=x$lambda.min))
base$alpha <- sapply(coef,function(x) x[1,])
base$beta <- sapply(coef,function(x) x[-1,])
# meta coefficients
meta <- list()
weights <- weights.starnet(object)
meta$alpha <- weights[1]
meta$beta <- weights[-1]
# pooled coefficients
pool <- list()
pool$alpha <- meta$alpha + sum(meta$beta * base$alpha)
pool$beta <- base$beta %*% meta$beta
# post hoc selection
if(!is.null(nzero)){
eta <- as.numeric(pool$alpha + object$info$X %*% pool$beta)
if(stats::sd(eta)==0){return(list(alpha=pool$alpha,beta=0*pool$beta))}
model <- .cv.glmnet(y=eta,x=object$info$X,family="gaussian",
alpha=1,penalty.factor=1/abs(pool$beta),
foldid=object$info$foldid,nzero=nzero)
coef <- stats::coef(model,s="lambda.min")
return(list(alpha=coef[1],beta=coef[-1]))
# alternatives: lasso-like elastic net (alpha=0.95);
# logistic regression of predicted probabilities on X
}
return(pool)
}
#' @export
#' @importFrom stats weights
#' @title
#' Extract Weights
#'
#' @description
#' Extracts coefficients from the meta learner,
#' i.e. the weights for the base learners.
#'
#' @param object
#' \link[starnet]{starnet} object
#'
#' @param ...
#' further arguments (not applicable)
#'
#' @return
#' Vector containing intercept and slopes from the meta learner.
#'
#' @examples
#' \dontshow{
#' if(!grepl('SunOS',Sys.info()['sysname'])){
#' set.seed(1)
#' n <- 50; p <- 100
#' y <- rnorm(n=n)
#' X <- matrix(rnorm(n*p),nrow=n,ncol=p)
#' object <- starnet(y=y,X=X)
#' weights(object)}}
#' \donttest{
#' set.seed(1)
#' n <- 50; p <- 100
#' y <- rnorm(n=n)
#' X <- matrix(rnorm(n*p),nrow=n,ncol=p)
#' object <- starnet(y=y,X=X)
#' weights(object)}
#'
weights.starnet <- function(object,...){
if(length(list(...))!=0){warning("Ignoring argument.",call.=FALSE)}
x <- object$meta
coef <- stats::coef(object=x,s=x$lambda.min)
names <- rownames(coef)
coef <- as.numeric(coef)
names(coef) <- names
return(coef)
}
#' @export
#' @title
#' Print Values
#'
#' @description
#' Prints object of class \link[starnet]{starnet}.
#'
#' @param x
#' \link[starnet]{starnet} object
#'
#' @param ...
#' further arguments (not applicable)
#'
#' @return
#' Prints "stacked gaussian/binomial/poisson elastic net".
#'
#' @examples
#' \dontshow{
#' if(!grepl('SunOS',Sys.info()['sysname'])){
#' set.seed(1)
#' n <- 50; p <- 100
#' y <- rnorm(n=n)
#' X <- matrix(rnorm(n*p),nrow=n,ncol=p)
#' object <- starnet(y=y,X=X)
#' print(object)}}
#' \donttest{
#' set.seed(1)
#' n <- 50; p <- 100
#' y <- rnorm(n=n)
#' X <- matrix(rnorm(n*p),nrow=n,ncol=p)
#' object <- starnet(y=y,X=X)
#' print(object)}
#'
print.starnet <- function(x,...){
cat(paste0("stacked \"",x$info$family,"\" elastic net"),"\n")
}
#--- Manuscript functions ------------------------------------------------------
#' @export
#' @title
#' Model comparison
#'
#' @description
#' Compares stacked elastic net, tuned elastic net, ridge and lasso.
#'
#' @inheritParams starnet
#'
#' @param nzero
#' number of non-zero coefficients\strong{:}
#' scalar/vector including positive integer(s) or \code{NA};
#' or \code{NULL} (no post hoc feature selection)
#'
#' @param nfolds.ext,nfolds.int,foldid.ext,foldid.int
#' number of folds (\code{nfolds})\strong{:}
#' positive integer;
#' fold identifiers (\code{foldid})\strong{:}
#' vector of length \eqn{n} with entries between \eqn{1} and \code{nfolds},
#' or \code{NULL},
#' for hold-out (single split) instead of cross-validation (multiple splits)\strong{:}
#' set \code{foldid.ext} to \eqn{0} for training and to \eqn{1} for testing samples
#'
#' @param ...
#' further arguments (not applicable)
#'
#' @return
#' List containing the cross-validated loss
#' (or out-of sample loss if \code{nfolds.ext} equals two,
#' and \code{foldid.ext} contains zeros and ones).
#' The slot \code{meta} contains the loss from the stacked elastic net
#' (\code{stack}), the tuned elastic net (\code{tune}), ridge, lasso,
#' and the intercept-only model (\code{none}).
#' The slot \code{base} contains the loss from the base learners.
#' And the slot \code{extra} contains the loss from the restricted
#' stacked elastic net (\code{stack}), lasso, and lasso-like elastic net
#' (\code{enet}),
#' with the maximum number of non-zero coefficients shown in the column name.
#'
#' @examples
#' \dontshow{
#' if(!grepl('SunOS',Sys.info()['sysname'])){
#' set.seed(1)
#' n <- 50; p <- 20
#' X <- matrix(rnorm(n*p),nrow=n,ncol=p)
#' y <- rnorm(n=n,mean=rowSums(X[,1:20]))
#' loss <- cv.starnet(y=y,X=X,nfolds.ext=2,nfolds.int=3)}}
#' \donttest{
#' loss <- cv.starnet(y=y,X=X)}
#'
cv.starnet <- function(y,X,family="gaussian",nalpha=21,alpha=NULL,nfolds.ext=10,nfolds.int=10,foldid.ext=NULL,foldid.int=NULL,type.measure="deviance",alpha.meta=1,nzero=NULL,intercept=NULL,upper.limit=NULL,unit.sum=NULL,...){
# family <- "gaussian"; nfolds.ext <- nfolds.int <- 10; foldid.ext <- foldid.int <- NULL; type.measure <- "deviance"; alpha.meta <- 0; alpha = NULL; nalpha <- 21; nzero <- NULL; intercept <- upper.limit <- unit.sum <- NULL
#--- fold identifiers ---
if(is.null(foldid.ext)){
fold <- .folds(y=y,nfolds=nfolds.ext)
} else {
fold <- foldid.ext
}
nfolds.ext <- max(fold)
# start trial
if(is.null(foldid.int)){
foldid.int <- .folds(y=y,nfolds=nfolds.int)
}
nfolds.int <- max(foldid.int)
# end trial
#--- alpha sequence ---
if(is.null(alpha)){
alpha <- seq(from=0,to=1,length.out=nalpha)
} else {
nalpha <- length(alpha)
}
#--- cross-validated loss ---
meta <- c("stack","tune","ridge","lasso","none")
base <- paste0("alpha",alpha)
cols <- c(meta,base)
if(!is.null(nzero)){cols <- c(cols,paste0(rep(c("stack","lasso","enet"),times=length(nzero)),rep(nzero,each=3)))}
pred <- matrix(data=NA,nrow=length(y),ncol=length(cols),
dimnames=list(NULL,cols))
for(i in seq_len(nfolds.ext)){
# dense models
fit <- starnet(y=y[fold!=i],X=X[fold!=i,],alpha=alpha,nalpha=nalpha,nfolds=nfolds.int,foldid=foldid.int[fold!=i],family=family,type.measure=type.measure,alpha.meta=alpha.meta,intercept=intercept,upper.limit=upper.limit,unit.sum=unit.sum,...) # INSERT ,...
temp <- predict.starnet(fit,newx=X[fold==i,,drop=FALSE])
for(j in c(meta,base)){
pred[fold==i,j] <- unlist(temp[[j]])
}
# sparse models
if(!is.null(nzero)){
for(j in seq_along(nzero)){
pred[fold==i,paste0("stack",nzero[j])] <- predict.starnet(fit,newx=X[fold==i,,drop=FALSE],nzero=nzero[j])[,"stack"] # added drop=FALSE 2020-04-02
temp <- .cv.glmnet(y=y[fold!=i],x=X[fold!=i,],alpha=1,family=family,type.measure=type.measure,foldid=fit$info$foldid,nzero=nzero[j],...)
pred[fold==i,paste0("lasso",nzero[j])] <- stats::predict(temp,
newx=X[fold==i,,drop=FALSE],type="response",s="lambda.min") # added drop=FALSE 2020-04-02
temp <- .cv.glmnet(y=y[fold!=i],x=X[fold!=i,],alpha=0.95,family=family,type.measure=type.measure,foldid=fit$info$foldid,nzero=nzero[j],...)
pred[fold==i,paste0("enet",nzero[j])] <- stats::predict(temp,
newx=X[fold==i,,drop=FALSE],type="response",s="lambda.min") # added drop=FALSE 2020-04-02
}
}
}
if(length(type.measure)!=1){stop("Implement multiple type measures!")}
loss <- apply(pred[fold!=0,],2,function(x) .loss(y=y[fold!=0],x=x,family=family,type.measure=type.measure,foldid=fold))
ylim <- range(c(loss[base],loss[c("tune","stack")]))
tryCatch(graphics::plot(y=loss[base],x=as.numeric(substring(text=base,first=6)),xlab="alpha",ylab="loss",ylim=ylim),error=function(x) NULL)
tryCatch(graphics::abline(h=loss["tune"],lty=2,col="grey"),error=function(x) NULL)
tryCatch(graphics::abline(h=loss["stack"],lty=2,col="black"),error=function(x) NULL)
meta <- loss[names(loss) %in% meta]
base <- loss[names(loss) %in% base]
type <- c("stack","lasso","enet")
extra <- matrix(NA,nrow=3,ncol=length(nzero),dimnames=list(type,nzero))
for(i in seq_len(3)){
for(j in seq_along(nzero)){
extra[i,j] <- loss[paste0(type[i],nzero[j])]
}
}
return(list(meta=meta,base=base,extra=extra))
}
#' @name .simulate
#'
#' @title
#' Simulation
#'
#' @description
#' Functions for simulating data
#'
#' @param n
#' sample size\strong{:}
#' positive integer
#'
#' @param p
#' dimensionality\strong{:}
#' positive integer
#'
#' @param mode
#' character \code{"sparse"}, \code{"dense"} or \code{"mixed"}
#'
#' @param family
#' character \code{"gaussian"}, \code{"binomial"} or \code{"poisson"}
#'
#' @return
#' List of vector \code{y} and matrix \code{X}.
#'
#' @examples
#' NA
#'
.simulate.block <- function(n,p,mode,family="gaussian"){
Z <- matrix(data=stats::rnorm(n*3),nrow=n,ncol=3)
y <- rowSums(Z)
#if(family=="binomial"){y <- round(1/(1+exp(-y)))}
#if(family=="poisson"){y <- round(exp(y))}
y <- switch(family,gaussian=y,binomial=round(1/(1+exp(-y))),
poisson=round(exp(y)),stop("Invalid."))
X <- matrix(data=stats::rnorm(n*p),nrow=n,ncol=p)
if(mode=="sparse"){
X[,1] <- sqrt(0.1)*X[,1] + sqrt(0.9)*Z[,1]
X[,p] <- sqrt(0.1)*X[,p] + sqrt(0.9)*Z[,2]
} else if(mode=="dense"){
X[,1:250] <- sqrt(0.9)*X[,1:250] + sqrt(0.1)*Z[,1]
X[,(p-250+1):p] <- sqrt(0.9)*X[,(p-250+1):p] + sqrt(0.1)*Z[,2]
} else if(mode=="mixed"){
X[,1:25] <- sqrt(0.5)*X[,1:25] + sqrt(0.5)*Z[,1]
X[,(p-25+1):p] <- sqrt(0.5)*X[,(p-25+1):p] + sqrt(0.5)*Z[,2]
} else {
stop("Invalid.",.call=FALSE)
}
return(list(y=y,X=X))
}
#' @rdname .simulate
#' @param rho
#' correlation\strong{:}
#' numeric between \eqn{0} and \eqn{1}
#' @param pi
#' effects\strong{:}
#' numeric between \eqn{0} (sparse) and \eqn{1} (dense)
#'
.simulate.grid <- function(n,p,rho,pi,family="gaussian"){
if(!"mvtnorm" %in% .packages(all.available=TRUE)){
stop("Install mvtnorm from CRAN for simulation.",call.=FALSE)
}
mean <- rep(x=0,times=p)
sigma <- matrix(data=NA,nrow=p,ncol=p)
sigma <- rho^abs(row(sigma)-col(sigma))
diag(sigma) <- 1
X <- mvtnorm::rmvnorm(n=n,mean=mean,sigma=sigma)
nzero <- round(pi*p)
beta <- abs(stats::rnorm(p))*sample(x=rep(x=c(0,1),times=c(p-nzero,nzero)))
eta <- as.numeric(X %*% beta)
y <- eta + stats::rnorm(n=n,sd=0.5*stats::sd(eta))
y <- switch(family,gaussian=y,binomial=round(1/(1+exp(-y))),stop("Invalid."))
return(list(y=y,X=X,beta=beta))
}
#' @rdname .simulate
#'
.simulate.mode <- function(n,p,mode,family="gaussian"){
if(!"mvtnorm" %in% .packages(all.available=TRUE)){
stop("Install mvtnorm from CRAN for simulation.",call.=FALSE)
}
mean <- rep(x=0,times=p)
sigma <- matrix(data=0.1,nrow=p,ncol=p)
diag(sigma) <- 1
X <- mvtnorm::rmvnorm(n=n,mean=mean,sigma=sigma)
q <- switch(mode,sparse=5,dense=50,mixed=20,stop("Invalid.",.call=FALSE))
# mixed: 15 is too lasso-friendly
beta <- sample(rep(c(0,1),times=c(p-q,q)))
eta <- as.numeric(X %*% beta)
y <- eta + stats::rnorm(n=n,sd=0.5*stats::sd(eta))
y <- switch(family,gaussian=y,binomial=round(1/(1+exp(-y))),stop("Invalid."))
return(list(y=y,X=X,beta=beta))
}
#' @title
#' Loss
#'
#' @description
#' Calculate loss from predicted and observed values
#'
#' @param y
#' observed values\strong{:}
#' numeric vector of length \eqn{n}
#'
#' @param x
#' predicted values\strong{:}
#' numeric vector of length \eqn{n}
#'
#' @param family
#' character \code{"gaussian"}, \code{"binomial"}, \code{"poisson"},
#' \code{"mgaussian"}, or \code{"multinomial"} (to implement: \code{"cox"})
#'
#' @param type.measure
#' character \code{"deviance"}, \code{"mse"}, \code{"mae"}, \code{"class"},
#' or \code{"auc"}
#'
#' @param foldid
#' fold identifiers\strong{:}
#' integer vector of length \eqn{n},
#' or \code{NULL}
#'
#' @param grouped
#' logical (for \code{"cox"} only)
#'
#' @examples
#' NA
#'
.loss <- function(y,x,family,type.measure,foldid=NULL,grouped=TRUE){
if(family=="cox" & grouped){stop("Implement \"grouped Cox\"! See unit tests.",call.=FALSE)}
if(is.null(foldid)&(family=="cox"|type.measure=="auc")){
stop("Missing foldid.",call.=FALSE)
}
if(family=="multinomial"){
Y <- matrix(0,nrow=length(y),ncol=length(unique(y)))
Y[cbind(seq_along(y),as.numeric(as.factor(y)))] <- 1
}
if(family=="mgaussian"){
Y <- y
}
if(type.measure=="deviance"){
if(family=="gaussian"){
type.measure <- "mse"
} else if(family=="binomial"){
limit <- 1e-05
x[x<limit] <- limit
x[x>1-limit] <- 1-limit
return(mean(-2*(y*log(x)+(1-y)*log(1-x))))
} else if(family=="poisson"){
return(mean(2*(ifelse(y==0,0,y*log(y/x))-y+x)))
} else if(family=="cox"){
cvraw <- numeric()
for(i in seq_along(unique(foldid))){
if(grouped){
stop("Invalid \"grouped Cox\"! See unit tests!",call.=FALSE)
full <- glmnet::coxnet.deviance(pred=x,y=y)
mink <- glmnet::coxnet.deviance(pred=x[foldid!=i],y=y[foldid!=i])
cvraw[i] <- full-mink
} else {
cvraw[i] <- glmnet::coxnet.deviance(pred=x[foldid==i],y=y[foldid==i])
}
}
weights <- tapply(X = y[, "status"], INDEX = foldid, FUN = sum)
return(stats::weighted.mean(x = cvraw/weights, w = weights, na.rm = TRUE))
} else if(family=="multinomial"){
limit <- 1e-05
x[x<limit] <- limit
x[x>1-limit] <- limit
lp <- Y * log(x)
ly <- Y * log(Y)
ly[Y==0] <- 0
return(mean(rowSums(2*(ly-lp))))
}
}
if(type.measure=="mse" & family %in% c("gaussian","binomial","poisson")){
return((1 + (family=="binomial"))*mean((x-y)^2))
}
if(type.measure %in% c("deviance","mse") & family %in% c("mgaussian","multinomial")){
return(mean(rowSums((Y-x)^2)))
}
if(type.measure=="mae" & family %in% c("mgaussian","multinomial")){
return(mean(rowSums(abs(Y-x))))
}
if(type.measure=="mae" & family %in% c("gaussian","binomial","poisson")){
return((1 + (family=="binomial"))*mean(abs(x-y)))
}
if(type.measure=="class" & family=="binomial"){
return(mean(y!=round(x)))
}
if(type.measure=="class" & family=="multinomial"){
temp <- colnames(x)[apply(x,1,which.max)]
return(mean(y!=temp))
}
if(type.measure=="auc" & family=="binomial"){
weights <- table(foldid)
cvraw <- rep(x=NA,times=length(weights))
for(k in seq_along(weights)){
cvraw[k] <- glmnet.auc(y=y[foldid==k],prob=x[foldid==k])
}
return(stats::weighted.mean(x=cvraw,w=weights,na.rm=TRUE))
}
stop(paste0("type measure \"",type.measure,"\" not implemented for family \"",family,"\"."))
}
#' @title
#' glmnet:::auc
#'
#' @description
#' Import of \code{\link[glmnet]{auc}} (internal function)
#'
#' @param y
#' observed classes
#'
#' @param prob
#' predicted probabilities
#'
#' @param w
#' (ignored here)
#'
#' @return
#' area under the ROC curve
#'
#' @examples
#' NA
#'
glmnet.auc <- get("auc",envir=asNamespace("glmnet"))
# same as cv.glmnet but with nzero (similar to dfmax and pmax)
#' @title
#' glmnet::cv.glmnet
#'
#' @description
#' Wrapper for \code{\link[glmnet]{cv.glmnet}},
#' with different handling of sparsity constraints.
#'
#' @param ...
#' see \code{\link[glmnet]{cv.glmnet}}
#'
#' @param nzero
#' maximum number of non-zero coefficients\strong{:}
#' positive integer
#'
#' @return
#' Object of class \code{\link[glmnet]{cv.glmnet}}.
#'
#' @examples
#' NA
#'
.cv.glmnet <- function(...,nzero){
model <- glmnet::cv.glmnet(...)
#cat(unique(model$nzero),"\n")
for(i in seq_len(3)){ # original: 2
# change from 1.05*min to min(1.05*)
from <- 1.05*min(model$lambda[model$nzero==0],max(model$lambda),na.rm=TRUE) # original: 1.05*min
if(is.na(nzero)||is.infinite(nzero)){ # unlimited model
if(model$lambda.min==rev(model$lambda)[1]){
to <- 0.01*min(model$lambda)
} else {
to <- max(model$lambda[model$lambda<model$lambda.min])
}
} else { # sparsity constraint
if(all(model$nzero>=nzero)){
to <- 0.01*min(model$lambda)
} else {
to <- min(model$lambda[model$nzero<=(nzero+1)],na.rm=TRUE)
to <- max(model$lambda[model$lambda<model$lambda.min],to)
}
}
lambda <- exp(seq(from=log(from),to=log(to),length.out=100))
model <- glmnet::cv.glmnet(...,lambda=lambda)
#cat(unique(model$nzero),"\n")
}
if(is.na(nzero)){return(model)}
cond <- model$nzero<=nzero
model$lambda <- model$lambda[cond]
model$cvm <- model$cvm[cond]
model$cvsd <- model$cvsd[cond]
model$cvup <- model$cvup[cond]
model$cvlo <- model$cvlo[cond]
model$nzero <- model$nzero[cond]
model$lambda.min <- model$lambda[which.min(model$cvm)]
model$lambda.1se <- max(model$lambda[model$cvm<min(model$cvup)])
model$glmnet.fit$a0 <- model$glmnet.fit$a0[cond]
model$glmnet.fit$beta <- model$glmnet.fit$beta[,cond,drop=FALSE]
model$glmnet.fit$df <- model$glmnet.fit$df[cond]
model$glmnet.fit$lambda <- model$glmnet.fit$lambda[cond]
model$glmnet.fit$dev.ratio <- model$glmnet.fit$dev.ratio[cond]
model$glmnet.fit$call <- NULL
model$glmnet.fit$dim[2] <- sum(cond)
#cat(unique(model$nzero),"\n")
return(model)
}
.glm <- function(y,X,family,intercept=TRUE,lower.limit=FALSE,upper.limit=FALSE,unit.sum=FALSE){
alpha <- CVXR::Variable(1)
beta <- CVXR::Variable(ncol(X))
if(family=="gaussian"){
objective <- CVXR::Minimize(mean((y-alpha-X%*%beta)^2))
} else if(family=="binomial"){
objective <- CVXR::Minimize(sum(alpha+X[y<=0,]%*%beta)+sum(CVXR::logistic(-alpha-X%*%beta)))
} else if(family=="poisson"){
stop("Family not implemented.")
#objective <- CVXR::Minimize(mean(2*(y[y>=1]*log(y[y>=1]/(alpha+X[y>=1,]%*%beta)))-y[y>=1]+(alpha-X[y>=1,]%*%beta)))
} else {
stop("Family not implemented.")
}
constraints <- list(alpha==0,beta>=0,beta<=1,sum(beta)==1)[c(!intercept,lower.limit,upper.limit,unit.sum)]
# either sum(beta)==1 or sum(beta)<=1
problem <- CVXR::Problem(objective,constraints=constraints)
result <- CVXR::solve(problem)
alpha <- round(result$getValue(alpha),digits=6)
beta <- round(result$getValue(beta),digits=6)
return(list(alpha=alpha,beta=beta))
}
# This function should replace the one in palasso!
.folds <- function (y, nfolds, foldid = NULL) {
if(nfolds > length(y)){nfolds <- length(y); warning("too many folds!")} # added
if (!is.null(foldid)) {
return(foldid)
}
if (survival::is.Surv(y)) {
y <- y[,"status"]
}
if (all(y %in% c(0,1))) {
foldid <- rep(x=NA,times=length(y))
foldid[y==0] <- sample(x=rep(x=seq_len(nfolds),length.out=sum(y==0)))
#foldid[y==1] <- sample(x=rep(x=seq(from=nfolds,to=1),length.out=sum(y==1))) # balanced size
# balanced proportions
max <- max(foldid,na.rm=TRUE)
if(max==nfolds){
seq <- seq_len(nfolds)
} else {
seq <- c(seq(from=max+1,to=nfolds,by=1),seq(from=1,to=max,by=1))
}
foldid[y==1] <- sample(x=rep(x=seq,length.out=sum(y==1)))
} else {
foldid <- sample(x=rep(x=seq_len(nfolds),length.out=length(y)))
}
return(foldid)
}
# This function is a copy from joinet!
.mean.function <- function(x,family){
if(family %in% c("gaussian","cox")){
return(x)
} else if(family=="binomial"){
return(1/(1+exp(-x)))
} else if(family=="poisson"){
return(exp(x))
} else {
stop("Family not implemented.",call.=FALSE)
}
}
#y <- c(0,0,0,0,0,0,0,0,1,1)
#for(nfolds in 1:10){
# folds <- .folds(y=y,nfolds=nfolds)
# table <- table(folds,y)
# print(table)
#}
#y <- c(0,0,0,0,0,0,0,0,1,1)
#fold <- rep(NA,times=length(y))
#fold[y==0] <- rep(1:min(sum(y==0),nfolds),length.out=sum(y==0))
### plot matrix
# #x <- matrix(c(-(12:1),1:12),nrow=4,ncol=6)
# plot.matrix <- function(x,cutoff=0,main="",xlab="col",ylab="row",xscale=NULL,yscale=NULL,xlas=1,ylas=3,...){
#
# if(is.null(xscale)){xscale <- seq_len(ncol(x))}
# if(is.null(yscale)){yscale <- rev(seq_len(nrow(x)))}
#
# if(length(list(...))!=0){warning("Ignoring arguments.")}
#
# #k <- 100
# #breaks <- stats::quantile(x,probs=seq(from=0,to=1,length.out=k+1))
#
# # start original
# #step <- 0.005
# #min <- pmin(-0.05,min(x,na.rm=TRUE))
# #max <- pmax(+0.05,max(x,na.rm=TRUE))
# #breaks <- c(min,rev(seq(from=0,to=min,by=-step)),seq(from=step,max,by=step),max)
# #k <- length(breaks)
#
# # start trial
# step <- 0.01
# min <- pmin(-1,min(x,na.rm=TRUE)) # was -1
# max <- pmax(+1,max(x,na.rm=TRUE)) # was +1
# breaks <- c(min,rev(seq(from=0,to=-0.5,by=-step)),seq(from=step,+0.5,by=step),max) # was -0.2 and +0.2
# #k <- length(breaks)
#
# if("RColorBrewer" %in% .packages(all.available=TRUE)){
# blue <- RColorBrewer::brewer.pal(n=9,name="Blues")
# blue <- grDevices::colorRampPalette(colors=blue)(sum(breaks<cutoff)) # was k
# #white <- ifelse(any(breaks==cutoff),"white",NULL)
# red <- RColorBrewer::brewer.pal(n=9,name="Reds")
# red <- grDevices::colorRampPalette(colors=red)(sum(breaks>cutoff)) # was k
# col <- c(rev(blue[seq_len(sum(breaks<cutoff)-1)]),
# rep("white",times=sum(breaks==cutoff)+1),
# red[seq_len(sum(breaks>cutoff)-1)])
# } else {
# stop("install.packages(\"RColorBrewer\")")
# }
#
# nrow <- nrow(x)
# ncol <- ncol(x)
#
# graphics::plot.new()
# graphics::par(xaxs="i",yaxs="i")
# graphics::plot.window(xlim=c(1-0.5,ncol+0.5),ylim=c(1-0.5,nrow+0.5))
#
# graphics::title(main=main,line=0.5)
# graphics::title(xlab=xlab,ylab=ylab,cex.lab=1,line=2.5)
# graphics::image(x=seq_len(ncol),y=seq_len(nrow),z=t(x),breaks=breaks,col=col,add=TRUE) # t(x[rev(seq_len(nrow)),])
# graphics::box()
#
# at <- unique(round(seq(from=1,to=ncol,by=ceiling(ncol/15))))
# graphics::axis(side=1,at=seq_len(ncol)[at],labels=signif(xscale,digits=2)[at],las=xlas)
# at <- unique(round(seq(from=1,to=nrow,by=ceiling(nrow/15))))
# graphics::axis(side=2,at=seq_len(nrow)[at],labels=signif(yscale,digits=2)[at],las=ylas)
#
# }
### simulate data
# .simulate <- function(n,p,rho,pi,family,factor){
# if(pi==base::pi){stop("Invalid pi!")}
# mu <- rep(x=0,times=p)
# Sigma <- matrix(data=NA,nrow=p,ncol=p)
# Sigma <- rho^abs(row(Sigma)-col(Sigma))
# diag(Sigma) <- 1
# X <- MASS::mvrnorm(n=n,mu=mu,Sigma=Sigma)
# mean <- rowSums(X[,seq_len(pi*p),drop=FALSE])
# # consider "rowMeans" or "scale(mean)"
# if(family=="gaussian"){
# y <- stats::rnorm(n=n,mean=mean,sd=factor*stats::sd(mean))
# } else if(family=="binomial"){
# y <- stats::rbinom(n=n,size=1,prob=1/(1+exp(-mean)))
# } else if(family=="poisson"){
# y <- stats::rpois(n=n,lambda=exp(mean))
# y[is.na(y)] <- exp(20)
# }
# list <- list(y=y,X=X)
# return(list)
# }
### test significant difference
# .test <- function (y,X){
# fold <- palasso:::.folds(y=y,nfolds=5)
# fold <- ifelse(fold==1,1,0)
# fit <- starnet(y=y[fold==0],X=X[fold==0,],family="binomial")
# pred <- predict.starnet(fit,newx=X[fold==1,])
# if(any(pred < 0 | pred > 1)) {
# stop("Outside unit interval.",call.=FALSE)
# }
# limit <- 1e-05
# pred[pred < limit] <- limit
# pred[pred > 1 - limit] <- 1 - limit
# res <- -2*(y[fold==1]*log(pred)+(1-y[fold==1])*log(1 - pred))
# old <- as.data.frame(res[,c("ridge","lasso","tune")])
# new <- res[,"stack"]
# pvalue <- apply(old,2,function(x) stats::wilcox.test(x=x,y=new,paired=TRUE,alternative="greater")$p.value)
# return(pvalue)
# }
# Adapt code to family="cox", which has no intercept,
# i.e. coef.glmnet and coef.starnet do not yet work.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.