#' A block coordinate descent function
#'
#' \code{block.cd} implements the block-coordinate descent algorithm of Radchenko (2015) to optimize the (active) single index coefficients.
#'
#' @param w.a a warm start (initial) value of the current nonzero single index coefficients.
#' @param ind.m a value of the index used in defining each coordinate-wise block.
#' @param ind.old a vector of the indices corresponding to the current nonzero coefficients.
#' @param u a vector of the current singe index variable, u = alpha'X.
#' @param y treatment outcomes, a n-by-1 vector.
#' @param Tr treatment indicators, a n-by-1 vector; each element represents one of the K available treatment options.
#' @param X.a a matrix of the pre-treatment covarates, associated with the current nonzero single index coefficients.
#' @param d.link.fn.obj an object of the class \code{d.link.fn.obj} passed from the previous iteration.
#' @param it.max an integer value specifying the maximum number of iterations for each coordinate.
#' @param eps a value specifying the converge criterion of algorithm.
#' @param trace if \code{TRUE}, show the trace of the fitting procedure; the default is \code{FALSE}.
#' @param nbasis.t a length K+1 vector; each element specifies the number of B-spline basis funtions for approximating the treatment-specific link function; the last element is for the "main effect" link function; the default is \code{nbasis.t=NULL}, and will be determined depending on the sample size.
#' @param rho.grid a grid vector of (ridge-type) smoothing parameters for approximating the link functions.
#' @param linear.link if \code{TRUE}, restrict the link functions to be linear functions; the default is \code{FALSE}.
#' @param ortho.constr the constraint that separates the interaction effects from the main effect (without this, the interaction effect can be confounded by the main effect); the default is \code{TRUE}.
#'
#' @return a list of information of the optimized single index coefficients including
#' \item{w.a}{a vector of the optimized single index coefficients associated with the active index set.} \item{u}{a vector of the optimized single index variable.} \item{it}{the number of iterations.}
#'
#' @seealso \code{pred.csim}, \code{fit.csim}, \code{fit.csim.cv}
#'
######################################################################
# a function to implement block-coordinate descent given the current index u = alpha'X;
# return: w.a, the updated (active) coefficients; u, the updated single index; it, the number of iterations
######################################################################
block.cd <- function(w.a, ind.m, ind.old,
u, y, Tr, X.a, d.link.fn.obj,
it.max =60, eps=10^-4, trace=F,
nbasis.t = c(6,6,8),
rho.grid = c(0, 0.25, 0.5),
linear.link= FALSE,
ortho.constr = TRUE)
{
w.a.old <- w.a + 5
it <- 0;
chng.fl <- F;
w.a.start <- w.a;
while((max(abs(w.a- w.a.old))>eps)&(max(abs(w.a- w.a.start))>eps/100)|(it==0))
{
w.a.old <- w.a;
it <- it + 1
for(j in 1:length(w.a))
{
gr.m <- crossprod(d.link.fn.obj$d.link.fn1, X.a[,ind.m]);
gr1 <- crossprod(d.link.fn.obj$d.link.fn1, X.a[,j])
tmp1 <- apply(as.matrix(X.a[,ind.old]), 2, function(v){crossprod(d.link.fn.obj$d.link.fn1, v)})
tmp2 <- -sign(w.a[ind.old]);
thr1 <- tmp1*tmp2
thr2 <- -sign(w.a[ind.m])*crossprod(d.link.fn.obj$d.link.fn1, X.a[,ind.m]);
rm(tmp1);
rm(tmp2)
thr.entr <- max(c(thr1,thr2))
# it all begins when the gr1 exceeds the threshold...
if( ( (abs(gr1)>=thr.entr) | (w.a[j]!=0) ) & (j!=ind.m) )
{
if(w.a[j]==0) {sn= -sign(gr1)} else{sn= sign(w.a[j])}
gr.cmb <- crossprod(d.link.fn.obj$d.link.fn1, (X.a[,j] - sign(w.a[ind.m])*sn*X.a[,ind.m]))
hes.cmb <- crossprod((X.a[,j] - sign(w.a[ind.m])*sn*X.a[,ind.m])^2, d.link.fn.obj$d.link.fn2)
# compute how much it needs to be updated..
dj <- -gr.cmb/hes.cmb
old.w.aj <- w.a[j];
old.w.am <- w.a[ind.m]
w.a[j] <- w.a[j]+dj
if(sign(w.a[j]*old.w.aj) < 0)
{
dj <- -old.w.aj;
w.a[j] <- 0;
w.a[ind.m] <- w.a[ind.m]*(abs(w.a[ind.m]) + abs(old.w.aj))/abs(w.a[ind.m]);
dm <- w.a[ind.m]- old.w.am
}
else{
dm <- -dj*sign(w.a[ind.m])*sign(w.a[j]);
w.a[ind.m] <- w.a[ind.m] - dj*sign(w.a[ind.m])*sign(w.a[j])
}
if(sign(w.a[ind.m]*old.w.am)<=0)
{
dm <- -old.w.am;
w.a[ind.m] <- 0;
chng.fl <- T;
if(trace) cat("Ch")
if(old.w.aj!=0)
{
w.a[j] <- old.w.aj*(abs(old.w.aj) + abs(old.w.am))/abs(old.w.aj)
}
else{
w.a[j] <- sign(w.a[j])*abs(old.w.am)
}
dj <- w.a[j] - old.w.aj
}
u <- u + dj*X.a[,j]+ dm*X.a[,ind.m]
# update the gt and their 1st derivatives
link.fn.obj <- fit.link.fn.gcv(y, Tr, u= u, nbasis.t = nbasis.t, rho.grid = rho.grid, linear.link = linear.link, ortho.constr = ortho.constr)
d.link.fn.obj <- deriv.link.fn(link.fn.obj)
if(trace) cat("wa1=",w.a,"\n");
if(trace) cat("grads",gr1, gr.m,"\n")
if(chng.fl)
{
ind.m <- which.max(abs(w.a));
chng.fl <- F
}
}
}
if(it > it.max) break; if(trace) cat("\n","DID NOT CONVERGE","\n"); break
}
results <- list(w.a=w.a, u=u, it=it)
return(results)
}
#' A wrapper function for the link function estimator
#'
#' This is a wrapper function that fits the (B-spline approximated) link functions, given the current single index u = alpha'X. An optimal smoothing parameter, \code{rho.opt}, is chosen by minimizing the generalized cross validation (GCV) for prediction errors.
#'
#' @param y treatment outcomes, n-by-1 vector.
#' @param Tr treatment indicators, n-by-1 vector; each element represents one of the K available treatment options.
#' @param u a vector of the current singe index variable, u = alpha'X.
#' @param nbasis.t a length K+1 vector; each element specifies the number of B-spline basis funtions for approximating the treatment-specific link function; the last element is for the "main effect" link function; the default is \code{nbasis.t=NULL}, and will be determined depending on the sample size.
#' @param rho.grid a grid vector of (ridge-type) smoothing parameters for approximating the link functions.
#' @param linear.link if \code{TRUE}, restrict the link functions to be linear funtions; the default is \code{FALSE}.
#' @param ortho.constr the constraint that separates the interaction effects from the main effect (without this, the interaction effect can be confounded by the main effect); the default is \code{TRUE}.
#' @param ini if \code{TRUE}, calculate the MSE using the interaction-effect component only.
#'
#'@return
#' \item{beta.t.coef}{a vector of the estimated treatment-specific B-spline coefficients vectors.} \item{beta.0.coef}{a vector of the estimated B-spline coefficients fitted regardless of the treatment indicators.} \item{smoother}{the \code{smoother} object obtained from \code{smoother.fn} used in fitting the link functions.} \item{resid}{a vector of the residuals from the current fitted constrained single index model.} \item{working.resid}{a vector of the residuals from the current fitted constrained single index model, which accounts for the fitted main effects implied by \code{beta.0.coef}.} \item{Q}{the criterion value (a MSE estimate) of the current fitted constrained single index model.} \item{rho.opt}{the optimized smoothing parameter used in fitting the link functions.}
#'
#' @seealso \code{smoother.fn}, \code{fit.link.fn}, \code{fit.csim}, \code{block.cd}
######################################################################
# a wrapper function to fit the (B-spline approximated) link functions, given the current index u = alpha'X;
# an optimal smoothing parameter, rho.opt, is chosen by minimizing GCV.
# return: beta.t.coef, beta.0.coef, smoother, resid, and MSE.
######################################################################
fit.link.fn.gcv <- function(y, Tr, u, nbasis.t = NULL, rho.grid = c(0, 0.25, 0.5), linear.link = FALSE, ortho.constr=TRUE, ini=FALSE)
{
if(is.null(nbasis.t))
{
n <- length(y);
nt <- summary(as.factor(Tr));
K <- length(nt);
for(t in 1:K)
{
nbasis.t[t] <- floor(nt[t]^{1/5.5}) + 4
}
nbasis.t[K+1] <- floor(n^{1/5.5}) + 6
}
if(length(rho.grid) >1)
{
smoother <- smoother.fn(Tr, u, nbasis.t = nbasis.t, rho.grid = rho.grid, linear.link = linear.link)
rho.opt <- fit.link.fn(y, smoother, ortho.constr=ortho.constr, ini=ini)$rho.opt
}else{
rho.opt <- rho.grid
}
smoother <- smoother.fn(Tr, u, nbasis.t=nbasis.t, rho.grid = rho.opt, linear.link =linear.link)
link.fn.obj <- fit.link.fn(y, smoother, ortho.constr=ortho.constr, ini=ini)
return(link.fn.obj)
}
#' A smoother constructor function
#'
#' Given both a vector of the current single index variable u = alpha'X and a vector of the treatment indicators \code{Tr}, \code{smoother.fn} constructs a set of B-spline smoother matrices, used in estimating the treatment-specific link functions (see \code{fit.link.fn}).
#'
#' The function returns a set of the QR decomposed design matrices (and the knot sequences used in constructing the B-spline design matrices), over the values of the (ridge-type) smoothing parameters, \code{rho.grid}. Since the ridge-type smoothing is equivalent to a regular least squares estimation with added observations, some psedo observations are added to the design matrices for the case of the nonzero values of \code{rho.grid}.
#'
#' @param Tr treatment indicators, n-by-1 vector; each element represents one of the K available treatment options.
#' @param u a vector of the current singe index variable, u = alpha'X.
#' @param nbasis.t a length K+1 vector; each element specifies the number of B-spline basis funtions used in approximating the treatment-specific link function; the last element is for the "main effect" link function.
#' @param rho.grid a grid vector of (ridge-type) smoothing parameters for approximating the link functions.
#' @param linear.link if \code{TRUE}, restrict the link functions to be linear funtions; the default is \code{FALSE}.
#'
#' @return a list of information of the smoother matrices given the current single index variable including
#' \item{Bt.qr}{the QR decomposed design matrix constructed based on both the single index variable and the treatment indicator.} \item{B0.qr}{the QR decomposed design matrix based on the single index variable only.} \item{Bt}{the design matrix based on both the single index variable and the treatment indicator.} \item{B0}{the design matrix based on the single index variable only.}\item{knots.t}{the knot sequences used in constructing the treatment-specific B-spline design matrices.}
#'
#' @seealso \code{fit.link.fn}, \code{fit.link.fn.gcv}, \code{fit.csim}
######################################################################
# a subfunction to construct (B-spline) smoother matrices, given the current index u = alpha'X.
# return: the QR decomposed design matrices (and the knot sequences used in constructing the B-spline design matrices);
######################################################################
smoother.fn <- function(Tr, u, nbasis.t=c(6,6,8), rho.grid = c(0, 0.25, 0.5), linear.link = FALSE)
{
K <- length(unique(Tr));
u.t = design.t <- vector("list", K+1);
# create a list, dat.list, grouped by the treatment indicator
dat <- data.frame(Tr=Tr, u =u);
dat_list <- dlply(dat, .(dat$Tr), function(dat) as.matrix(dat[,-1]));
for(t in 1:K)
{
u.t[[t]] <- dat_list[[t]][,1]; # data points from the tth treatment group
}
u.t[[K+1]] <- u;
u.min <- max(sapply(u.t, min));
u.max <- min(sapply(u.t, max));
# construct treatment group-specific design matrices
design.t = knots.t <- vector("list", K+1)
for(t in 1:(K+1))
{
if(linear.link) # if linear.link==TRUE, construct the linear model design matrix
{
nbasis.t[t] <- 2
design.t[[t]] <- cbind(1, u.t[[t]])
}else{
#knots.t[[t]] <- c(rep(u.min, 3), quantile(u.t[[t]], probs = seq(0, 1, length = nbasis.t[t] -2)), rep(u.max,3))
knots.t[[t]] <- seq(u.min, u.max, length.out= nbasis.t[t]+4);
design.t[[t]] <- splineDesign(knots.t[[t]], x= u.t[[t]], outer.ok = TRUE)
}
}
# construct the block-diagonal matrix consist of the treatment-specific design matrices, to approximate E(Y| u, T)
design.t.block <- NULL;
for(t in 1:K)
{
design.t.block <- c( design.t.block, list(design.t[[t]]))
}
Bt <- Reduce(adiag, design.t.block) # Bt is the block-diagonal design matrix
# QR decomposition of the design matrix Bt, given each value of the smoothness tuning parameter, rho
Bt.qr <- vector("list", length(rho.grid))
ncol.Bt <- ncol(Bt)
D <- diff(diag(ncol.Bt), differences = 2)
for(r in seq_along(rho.grid)) # a ridge-type smoothing (equivalent to a regular least squares with added observations)
{
#Bt.qr[[r]] <- qr(rbind(Bt, diag(sqrt(rho.grid[r]), ncol.Bt)))
Bt.qr[[r]] <- qr(rbind(Bt, sqrt(rho.grid[r])*D))
}
# compute effective degrees of freedom of smoothers, so that later we use GCV to select an optimal smoothing parameter
edf <- vector("list", length=length(rho.grid))
svd.Bt <- svd(Bt)
for(r in seq_along(rho.grid))
{
edf[[r]] <- sum(svd.Bt$d[svd.Bt$d>0]^2/(svd.Bt$d[svd.Bt$d>0]^2 +rho.grid[r] )) #/K
}
# QR decomposition of the design matrix B0
B0 <- design.t[[K+1]]
B0.qr <- qr(B0)
results <- list(Bt.qr= Bt.qr, B0.qr= B0.qr, Bt=Bt, B0= B0,
u.t = u.t, u.min = u.min, u.max = u.max,
edf= edf, rho.grid = rho.grid, K=K,
knots.t = knots.t, nbasis.t = nbasis.t, ncol.Bt = ncol.Bt,
linear.link=linear.link)
return(results)
}
#' A link function estimator
#'
#' Given a vector of responses \code{y} and a smoother object obtained from \code{smoother.fn}, \code{fit.link.fn} computes the treatment-specific link functions of the constrained single index model.
#'
#' This is a subfunction that fits the (B-spline approximated) link functions gt, given the current single index u = alpha'X. An optimal smoothing parameter, \code{rho.opt}, is chosen by minimizing the generalized cross validation (GCV) for prediction errors.
#'
#' @param y treatment outcomes, n-by-1 vector.
#' @param smoother a smoother object obtained from \code{smoother.fn}.
#' @param ortho.constr the constraint that separates the interaction effects from the main effect (without this, the interaction effect can be confounded by the main effect); the default is \code{TRUE}.
#' @param ini if \code{TRUE}, calculate the MSE using the interaction-effect component only.
#'
#' @return a list of information of the fitted link functions given the current single index variable including
#' \item{beta.t.coef}{a vector of the estimated treatment-specific B-spline coefficients vectors.} \item{beta.0.coef}{a vector of the estimated B-spline coefficients fitted regardless of the treatment indicators.} \item{smoother}{the \code{smoother} object obtained from \code{smoother.fn} used in fitting the link functions.} \item{resid}{a vector of the residuals from the current fitted constrained single index model.} \item{working.resid}{a vector of the residuals from the current fitted constrained single index model, which accounts for the fitted main effects implied by \code{beta.0.coef}.} \item{MSE}{the criterion value (a MSE estimate) of the current fitted constrained single index model.} \item{rho.opt}{the optimized smoothing parameter used in fitting the link functions.}
#'
#' @seealso \code{smoother.fn}, \code{fit.link.fn.gcv}, \code{fit.csim}
######################################################################
# a subfunction to fit the (B-spline approximated) link functions gt, given the current index u = alpha'X.
# an optimal smoothing parameter, rho.opt, is chosen by minimizing GCV.
# return: beta.t.coef, beta.0.coef, smoother, resid, and MSE.
######################################################################
fit.link.fn <- function(y, smoother, ortho.constr = TRUE, ini=FALSE)
{
options(warn=-1)
# a ridge-type regularization (equivalent to an OLS with added 0s)
y.aug <- c(y, rep(0, smoother$ncol.Bt-2))
n <- length(y)
# pick an optimal regularization (smoothing) paramter by GCV
if(length(smoother$rho.grid) >1)
{
GCV <- numeric()
for(s in seq_along(smoother$rho.grid))
{
GCV[s] <- sum((y - qr.fitted(smoother$Bt.qr[[s]], y.aug)[1:n] )^2) /(1 - smoother$edf[[s]] / n )^2
}
rho.index.opt <- which.min(GCV)
}else{
rho.index.opt <- 1
}
proj.Vt <- qr.fitted(smoother$Bt.qr[[rho.index.opt]], y.aug)
if(ortho.constr)
{
beta.0.coef <- qr.coef(smoother$B0.qr, proj.Vt[1:n])
proj.V0 <- drop(smoother$B0 %*% beta.0.coef)
y.hat <- proj.Vt - proj.V0
beta.t.coef <- qr.coef(smoother$Bt.qr[[1]], y.hat)
}else{
beta.0.coef <- rep(0, ncol(smoother$B0))
proj.V0 <- rep(0, n)
y.hat <- proj.Vt
beta.t.coef <- qr.coef(smoother$Bt.qr[[1]], y.hat)
}
working.resid <- y - proj.Vt[1:n]
resid <- y - y.hat[1:n]
if(ini)
{
MSE <- mean(resid^2)
}else{
MSE <- mean(working.resid^2)
}
results <- list(MSE= MSE, resid = resid, working.resid = working.resid,
y.hat = y.hat[1:n],
working.y.hat = proj.Vt[1:n],
proj.V0 = proj.V0,
smoother = smoother,
beta.t.coef = beta.t.coef, beta.0.coef = beta.0.coef,
rho.opt = smoother$rho.grid[rho.index.opt])
class(results) <- c("gt", "list")
return(results)
}
#' A first derivative of the link functions estimator
#'
#' A subfunction to compute the first derivative of the link functions, evaluated at the current single index variable u = alpha'X.
#'
#' @param link.fn.obj a fitted link function object of class \code{link.fn}, obtained from the functions \code{fit.link.fn} or \code{fit.link.fn.gcv}.
#'
#' @return a list of information of the fitted link functions given the current single index variable including
#' \item{d.link.fn}{a n x 1 vector of the 1st derivatives of the estimated link functions evaluated at the current index u = alpha'X.} \item{d.link.fn1}{an associated n x 1 vector used in computing the gradient with respect to the single index coefficients.} \item{d.link.fn2}{an associated n x 1 vector used in computing the updating rule for the single index coefficients.}
#'
#' @seealso \code{fit.link.fn}, \code{fit.link.fn.gcv}, \code{fit.csim}, \code{block.cd}
######################################################################
# a subfunction to compute the 1st derivatives of the estimated link functions, evaluated at the current index u = alpha'X.
# return: d.link.fn, a n x 1 vector of the 1st derivatives of the estimated link functions evaluated at the current index u = alpha'X; also, some other related quantities, d.gt1 and d.gt2.
######################################################################
deriv.link.fn <- function(link.fn.obj)
{
smoother <- link.fn.obj$smoother
K <- smoother$K
u.t <- smoother$u.t
knots.t <- smoother$knots.t
t.ind <- unlist(lapply(1:K, function(x) rep(x, smoother$nbasis.t[-(K+1)][x])))
beta.t.coef <- split(link.fn.obj$beta.t.coef, t.ind)
d.design.t <- vector("list", K)
d.link.fn <- NULL
for(t in 1:K)
{
if(smoother$linear.link)
{
d.link.fn <- c(d.link.fn, rep(beta.t.coef[[t]][2], length(u.t[[t]])) )
}else{
d.design.t[[t]] <- splineDesign(knots.t[[t]], x=u.t[[t]], derivs=rep(1, length(u.t[[t]])), outer.ok = TRUE) # compute the 1st derivative of the design functions
d.link.fn <- c(d.link.fn, d.design.t[[t]] %*% beta.t.coef[[t]])
}
}
rm(d.design.t)
d.link.fn1 <- - link.fn.obj$working.resid * d.link.fn
d.link.fn2 <- d.link.fn1^2
return(list(d.link.fn=d.link.fn, d.link.fn1=d.link.fn1, d.link.fn2=d.link.fn2))
}
#' A coordinate descent function
#'
#' \code{forw.cd} implements an ordinary (not a block-) coordinate descent to opitmize the single index coefficients, given an initial single index variable u = alpha'X.
#'
#' This function can be used to fit an un-regularized constrained single index model. This function is also used in obtaining the "post-selected" (re-fitted) single index coefficients, \code{cfs.pst}; see \code{fit.csim} and \code{block.cd}; see also Radchenko (2015) for the post-selected single index coefficient estimator.
#'
#'
#' @param w.a an initial estimate of the single index coefficient vector.
#' @param u a vector of the current singe index variable, u = alpha'X.
#' @param y treatment outcomes, n-by-1 vector.
#' @param Tr treatment indicators, n-by-1 vector; each element represents one of the K available treatment options.
#' @param X.a a matrix of the pre-treatment covarates, associated with the single index coefficients.
#' @param nbasis.t a length K+1 vector; each element specifies the number of B-spline basis funtions used in approximating the treatment-specific link function; the last element is for the "main effect" link function.
#' @param rho.grid a grid vector of (ridge-type) smoothing parameters for approximating the link functions.
#' @param it.max an integer value specifying the maximum number of iterations for each coordinate.
#' @param eps a value specifying the converge criterion of algorithm.
#' @param trace if \code{TRUE}, show the trace of the fitting procedure; the default is \code{FALSE}.
#' @param linear.link if \code{TRUE}, restrict the link functions to be linear functions; the default is \code{FALSE}.
#' @param ortho.constr the constraint that separates the interaction effects from the main effect (without this, the interaction effect can be confounded by the main effect); the default is \code{TRUE}.
#' @param i.fx a value of the index to be fixed throughout the estimation for model identifiability; the default is \code{NULL}, hence it is estimated.
#'
#' @return a list of information of the optimized single index coefficients including
#' \item{w.a}{a vector of the estimated single index coefficients.} \item{u}{a n x 1 vector of the estimated single index variable.} \item{it}{the number of iterations.}
#'
#' @seealso \code{pred.csim}, \code{fit.csim}, \code{fit.csim.cv}
######################################################################
# a function to implement an ordinary (not a block-) coordinate descent given the current index u = alpha'X;
# this function can be used to fit an un-regularized constrained single index model.
# return: w.a, the updated (active) coefficients; u, the updated single index; it, the number of iterations
######################################################################
forw.cd <- function(w.a, u, y, Tr, X.a, nbasis.t = c(6,6,8), rho.grid = c(0,0.25,0.5),
it.max=60, eps=10^-4, trace=F, linear.link = FALSE, ortho.constr= TRUE, i.fx=NULL)
{
i.m <- i.fx;
w.a.old <- w.a + 5;
it <- 0
while(max(abs(w.a-w.a.old)) > eps)
{
w.a.old <- w.a;
it <- it + 1;
if(is.null(i.fx)) i.m <- which.max(w.a);
for(j in 1:length(w.a))
{
if(j!=i.m)
{
link.fn.obj <- fit.link.fn.gcv(y, Tr, u, nbasis.t = nbasis.t, rho.grid = rho.grid, linear.link = linear.link, ortho.constr=ortho.constr)
d.link.fn.obj <- deriv.link.fn(link.fn.obj)
gr1 <- crossprod(d.link.fn.obj$d.link.fn1, X.a[,j]);
hes1 <- crossprod((X.a[,j])^2, d.link.fn.obj$d.link.fn2)
dj <- -gr1/hes1;
w.a[j] <- w.a[j] + dj;
u <- u + dj*X.a[,j]
if(trace) cat("w.a=",w.a,"\n")
####
if(trace) cat("predictor=", j, "grad= ", gr1, "\n")
}
}
if(it > it.max)
{
if(trace) cat("\n","DID NOT CONVERGE","\n");
break
}
}
list(w.a=w.a, u=u, it=it)
}
#######################################################################################################
######################################################################
## END OF THE FILE
######################################################################
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.