R/snreg.R

#' Linear Regression with Skew-Normal Errors
#'
#' @title Linear Regression with Skew-Normal Errors
#'
#' @description
#' \code{snreg} fits a linear regression model where the disturbance term follows
#' a skew-normal distribution. The function supports multiplicative
#' heteroskedasticity of the noise variance via a log-linear specification
#' (\code{ln.var.v}) and allows the skewness parameter to vary linearly with
#' exogenous variables (\code{skew.v}).
#'
#' @param formula
#' an object of class \code{formula} specifying the regression:
#' typically \code{y ~ x1 + ...}, where \code{y} is the dependent variable
#' and \code{x}'s are regressors.
#'
#' @param data
#' an optional \code{data.frame} containing the variables in \code{formula}.
#' If not found in \code{data}, variables are taken from \code{environment(formula)}.
#'
#' @param subset
#' an optional logical or numeric vector specifying the subset of observations
#' to be used in estimation.
#'
#' @param init.sk
#' numeric. Initial value for the (global) skewness parameter of the noise;
#' can be \code{NULL} if \code{skew.v} is supplied with its own coefficients to initialize.
#'
#' @param ln.var.v
#' optional one-sided formula; e.g. \code{ln.var.v ~ z1 + z2}. Specifies
#' exogenous variables entering the (log) variance of the random noise component.
#' If \code{NULL}, the noise variance is homoskedastic.
#'
#' @param skew.v
#' optional one-sided formula; e.g. \code{skew.v ~ z3 + z4}. Specifies exogenous
#' variables determining the skewness of the noise via a linear index; if
#' \code{NULL}, the skewness is constant (scalar).
#'
#' @param start.val
#' optional numeric vector of starting values for all free parameters
#' (regression coefficients, variance/heteroskedasticity parameters, skewness parameters).
#'
#' @param technique
#' character vector giving the preferred maximization routine(s) in order of
#' preference. Currently recognized keywords include \code{"nr"} (Newton–Raphson),
#' \code{"bhhh"}, \code{"nm"} (Nelder–Mead), \code{"bfgs"}, \code{"cg"}.
#' This scaffold does not implement them yet, but records the choice.
#'
#' @param vcetype
#' character specifying the variance-covariance estimator type:
#' \code{"aim"} for the approximated information matrix or \code{"opg"}
#' for the outer product of gradients. Default is \code{"aim"}.
#'
#' @param lmtol
#' numeric. Convergence tolerance based on the scaled gradient (if applicable).
#' Default is \code{1e-5}.
#'
#' @param reltol
#' numeric. Relative convergence tolerance for likelihood maximization.
#' Default is \code{1e-12}.
#'
#' @param maxit
#' integer. Maximum number of iterations for the optimizer. Default is \code{199}.
#'
#' @param optim.report
#' integer. Verbosity for reporting progress (if implemented). Default is \code{1}.
#'
#' @param optim.trace
#' integer. If positive, tracing information is printed (if implemented).
#' Default is \code{1}.
#'
#' @param print.level
#' integer. Printing level for summaries: \code{1}—print estimation results;
#' \code{2}—print optimization details; \code{3}—print compact summary. Default \code{3}.
#'
#' @param digits
#' integer. Number of digits for printing. Default \code{4}.
#'
#' @param only.data
#' logical. If \code{TRUE}, the function returns only the constructed model
#' matrices and design sets (no estimation). Default \code{FALSE}.
#'
#' @param ...
#' additional arguments reserved for future methods (e.g., box constraints).
#'

#' @details
#' The model is
#' \deqn{y_i = x_i^\top \beta + \varepsilon_i,\quad \varepsilon_i \sim SN(0, \sigma_i^2, \alpha_i),}
#' where \eqn{SN} denotes the skew-normal distribution (Azzalini).
#'
#' Heteroskedasticity in the noise variance (if specified via \code{ln.var.v}) is modeled as
#' \deqn{\log(\sigma_i^2) = w_i^\top \gamma_v,}
#' and the (optional) covariate-driven skewness (if specified via \code{skew.v}) as
#' \deqn{\alpha_i = s_i^\top \delta.}
#'
#' This function constructs the model frame and design matrices for
#' \eqn{\beta}, \eqn{\gamma_v}, and \eqn{\delta}, and is designed to be paired with
#' a maximum likelihood routine to estimate parameters and (optionally) their
#' asymptotic covariance via either AIM or OPG.
#'

#' @return
#' An object of class \code{"snreg"} containing the maximum-likelihood results and,
#' depending on the optimization routine, additional diagnostics:
#'
#' \describe{
#'   \item{\code{par}}{Numeric vector of parameter estimates at the optimum.}
#'   \item{\code{coef}}{Named numeric vector equal to \code{par}.}
#'
#'   \item{\code{vcov}}{Variance–covariance matrix of the estimates.}
#'   \item{\code{sds}}{Standard errors, computed as \code{sqrt(diag(vcov))}.}
#'   \item{\code{ctab}}{Coefficient table with columns:
#'         \code{Estimate}, \code{Std.Err}, \code{Z value}, \code{Pr(>z)}.}
#'
#'   \item{\code{RSS}}{Residual sum of squares.}
#'   \item{\code{esample}}{Logical vector indicating which observations were used in estimation.}
#'   \item{\code{n}}{Number of observations used in the estimation sample.}
#'   \item{\code{skewness}}{Vector of the fitted skewness index.}
#'
#'   \item{\code{hessian}}{(BFGS only) Observed Hessian at the optimum. If \code{vcetype == "opg"},
#'         this is set to the negative outer product of the individual gradients;
#'         otherwise a numerical Hessian is computed.}
#'   \item{\code{value}}{(BFGS only) Objective value returned by \code{optim}. With
#'         \code{control$fnscale = -1}, this equals the maximized log-likelihood.}
#'   \item{\code{counts}}{(BFGS only) Number of iterations / function evaluations returned by \code{optim}.}
#'   \item{\code{convergence}}{(BFGS only) Convergence code from \code{optim}.}
#'   \item{\code{message}}{(BFGS only) Additional \code{optim} message, if any.}
#'
#'   \item{\code{ll}}{Maximized log-likelihood value.}
#'   \item{\code{gradient}}{(NR only) Gradient at the solution.}
#'   \item{\code{gg}}{(NR only) Optional gradient-related diagnostic.}
#'   \item{\code{gHg}}{(NR only) Optional Newton-step diagnostic.}
#'   \item{\code{theta_rel_ch}}{(NR only) Relative parameter change metric across iterations.}
#' }
#'
#' The returned object has class \code{"snreg"}.
#'
#' @references
#' Azzalini, A. (1985).
#' \emph{A Class of Distributions Which Includes the Normal Ones}.
#' Scandinavian Journal of Statistics, 12(2), 171–178.
#'
#' Azzalini, A., & Capitanio, A. (2014).
#' \emph{The Skew-Normal and Related Families}.
#' Cambridge University Press.
#' 
#' @seealso
#' \code{\link{snsf}}, \code{\link{lm.mle}}
#' 
#' @examples
#' library(snreg)
#' 
#' data("banks07")
#' head(banks07)
#' 
#' # Translog cost function specification
#' 
#' spe.tl <- log(TC) ~ (log(Y1) + log(Y2) + log(W1) + log(W2))^2 +
#'   I(0.5 * log(Y1)^2) + I(0.5 * log(Y2)^2) +
#'   I(0.5 * log(W1)^2) + I(0.5 * log(W2)^2)
#' 
#' # Specification 1: homoskedastic noise and skewness
#' 
#' formSV <- NULL   # variance equation; constant variance
#' formSK <- NULL   # skewness equation; constant skewness
#' 
#' m1 <- snreg(
#'   formula  = spe.tl,
#'   data     = banks07,
#'   ln.var.v = formSV,
#'   skew.v   = formSK
#' )
#' 
#' summary(m1)
#' 
#' # Specification 2: heteroskedastic
#' 
#' formSV <- ~ log(TA)      #' variance equation; heteroskedastic noise (variance depends on TA)
#' formSK <- ~ ER           #' skewness equation; with determinants (skewness is determined by ER)
#' 
#' m2 <- snreg(
#'   formula  = spe.tl,
#'   data     = banks07,
#'   prod     = myprod,
#'   ln.var.v = formSV,
#'   skew.v   = formSK
#' )
#' 
#' summary(m2)
#'
#' @keywords regression skew-normal heteroskedasticity maximum-likelihood
#' @export
snreg <- function (
  formula, data, subset,
  init.sk  = NULL,#.5*(2*prod-1),
  ln.var.v  = NULL,
  skew.v    = NULL,
  start.val = NULL,
  technique = c('nr'),   #,'bhhh','nm', 'bfgs', 'cg'),
  vcetype   = c('aim'),  #, 'opg'), # `approximated information matrix` or `outer product of gradients`
  lmtol     = 1e-5,  
  reltol    = 1e-12, 
  maxit     = 199, 
  optim.report    = 1,
  optim.trace     = 1,
  print.level = 0, 
  digits    = 4,
  only.data = FALSE,
  ...){
  
  # threads <- 1
  
  # handle technique
  
  technique <- technique[1]
  

  if( !technique %in% c('nr','bhhh','nm', 'bfgs','cg') ){
    stop("'technique' is invalid")
  }
  
  if( !vcetype %in% c('aim', 'opg') ){
    stop("'vcetype' is invalid")
  }
  
  if(technique == 'nm') technique <- 'Nelder-Mead'
  if(technique == 'bfgs') technique <- 'BFGS'
  if(technique == 'cg') technique <- 'CG'
  
  mf0 <- match.call(expand.dots = FALSE, call = sys.call(sys.parent(n = 0)))
  if(is.null(ln.var.v)){
    ln.var.v <- ~ 1
    ksv <- 1
    # cat.print(ksv)
  } else {
    ksv <- 17
  }
  if(is.null(skew.v)){
    skew.v <- ~ 1
    ksk <- 1
    # cat.print(ksk)
  } else {
    ksk <- 17
  }

  form1 <- as.Formula(formula, ln.var.v, skew.v)

  data.order <- as.numeric(rownames(data)) # seq_len(nrow(data))
  
  mf <- mf0
  mf$formula <- form1 #formula( form )
  # cat("05\n")
  m <- match(c("formula", "data", "subset"), names(mf), 0L)
  # cat("06\n")
  # cat.print(m)
  mf <- mf[c(1L, m)]
  # cat("07\n")
  # cat.print(mf)
  mf[[1L]] <- as.name("model.frame")
  # cat("08\n")
  # cat.print(mf)
  # cat.print(needed.frame)
  # mf <- eval(mf, sys.frame(sys.parent(n = needed.frame+2)))
  mf <- eval(mf, parent.frame())
  # cat.print(mf)
  # cat("09\n")
  # cat.print(mf)
  mt <- attr(mf, "terms")
  x <- as.matrix(model.matrix(mt, mf))
  # print(x)
  # now get the names in the entire data
  # esample <- seq_len( nrow(data) ) %in% as.numeric(rownames(x))
  esample <- rownames(data) %in% rownames(x)
  # cat("10\n")
  # cat.print(mt)
  X <- model.matrix(mt, mf)
  # cat("11\n")
  # cat.print(X)
  esample.nu <- as.numeric(rownames(X))
  esample <- data.order %in% esample.nu
  
  Y <- as.matrix(model.part(form1, data = mf, lhs = 1, drop = FALSE))
  # print(length(Y))
  # print(Y)
  X <- as.matrix( model.matrix(formula(form1, lhs = 0, rhs = 1), data = mf))
  # print(X)
  n <- nrow(Y)
  k <- ncol(X)
  # ksv:  noise, scale/variance
  # zsv
  if(ksv == 1){
    zsv <- matrix(1, nrow = sum(esample), ncol = 1)
  }
  else {
    zsv <- as.matrix( model.matrix(formula(form1, lhs = 0, rhs = 2), data = mf))
    ksv <- ncol(zsv)
  }
  # zsk
  if(ksk == 1){
    zsk <- matrix(1, nrow = sum(esample), ncol = 1)
  }
  else {
    zsk <- as.matrix( model.matrix(formula(form1, lhs = 0, rhs = 3), data = mf))
    ksk <- ncol(zsk)
  }

  # from `sf`
  
  colnames(X)[1]   <- "(Intercept)"
  colnames(zsv)[1] <- "(Intercept)"
  colnames(zsk)[1] <- "(Intercept)"

  abbr.length <- 34
  names_x <- abbreviate(colnames(X), abbr.length+6, strict = TRUE, dot = FALSE, method = "both.sides")
  # names_zu <-  abbreviate(colnames(Zu), 9, strict = TRUE, dot = FALSE)
  # names_zv <-  abbreviate(colnames(Zv), 9, strict = TRUE, dot = FALSE)
  # Zu_colnames <- abbreviate(colnames(Zu), abbr.length, strict = TRUE, dot = FALSE, method = "both.sides")
  names_zv <- paste0("lnVARv0i_", abbreviate(colnames(zsv), abbr.length, strict = TRUE, dot = FALSE, method = "both.sides"))
  names_zk <- paste0("Skew_v0i_", abbreviate(colnames(zsk), abbr.length, strict = TRUE, dot = FALSE, method = "both.sides"))
  

  coef.names.full <- c(
    names_x,
    paste("lnVARv0i_",c("(Intercept)", names_zv[-1]),"", sep = ""),
    paste("Skew_v0i_",c("(Intercept)", names_zk[-1]),"", sep = "")
  )
  # cat.print(coef.names.full)
  # coef.names.full <- c(
  #   names_x,  names_zv, names_zk
  # )

  
  time.05 <- proc.time()
  
  # starting values ---------------------------------------------------------
  
  # OLS
  tymch2 <- lm(Y ~ X - 1)
  olsres  <- resid(tymch2)
  shat <- mean(olsres^2)
  
  gamma1.0 <- min(.99, mean(olsres^3)/shat )
  
  r  <- sign(gamma1.0)*(2*abs(gamma1.0)/(4-pi))^(1/3)
  delta <- r/(sqrt(2/pi) *sqrt(1+r^2))
  if(abs(delta) > 1){
    alpha.0 <- delta/sqrt(1-delta^2)
  } else {
    alpha.0 <- sign(delta) * 3
  }
  
  # cat.print(alpha.0)
  
  # b <- sqrt(2/pi) 
  # sd <- 1.3069693
  # gamma1 <- 0.6670236
  # r  <- sign(gamma1)*(2*abs(gamma1)/(4-pi))^(1/3)
  # delta <- r/(b*sqrt(1+r^2))
  # ( alpha <- delta/sqrt(1-delta^2) )
  # (mu.z <- b*delta )
  # (sd.z <- sqrt(1-mu.z^2) )
  # ( omega <- sd/sd.z )

  if(is.null(init.sk)){
    # sk.initial <- tymch1$par[(k+ksv+1):(k+ksv+ksk)] * .5
    if(ksk==1){
      sk.initial <- alpha.0
    } else {
      sk.initial <- c(alpha.0, rep(0,ksk-1))
    }
  } else {
    if(ksk==1){
      sk.initial <- init.sk
    } else {
      sk.initial <- c(init.sk, rep(0,ksk-1))
    }
  }
  


  lnsv00 <- log(mean(olsres^2))
  if(ksv==1){
    sv.initial <- lnsv00
  } else {
    sv.initial <- c(lnsv00, rep(0,ksv-1))
  }
  
  theta0 <- c( coef(tymch2), sv.initial, sk.initial)
  names(theta0) <- coef.names.full
  
  if(print.level > 0) {
    cat.print(theta0)
  }
  
  if(print.level >= 1){
    max.name.length <- max(nchar(names(theta0) ))
    est.rez.left <- floor( (max.name.length+42-33) / 2 )
    est.rez.right <- max.name.length+42-33 - est.rez.left
    cat("\n",rep("-", est.rez.left)," Regression with skewed errors: ",rep("-", est.rez.right),"\n\n", sep ="")
  }
  # mytrace <- ifelse(print.level < 2, 0, 1)
  if(print.level <= 2){
    trace <- trace1 <- 0
  } else {
    trace1 <- optim.trace
  }
  
  # * optimization ------------------------------------------------------------
  
  if(technique == 'BFGS'){
    tymch <- optim(
      par = theta0, fn = .ll.sn, gr = .gr.sn, y = Y, x = X, zsv = zsv, zsk = zsk, k = k, ksv = ksv, ksk = ksk,
      method = technique, control = list(fnscale = -1, trace = trace1, REPORT = optim.report, maxit = 10000), 
      hessian = FALSE)
    
    if(vcetype == 'opg'){
      gri <- .gr.sn.by.i(tymch$par, y = Y, x = X, zsv = zsv, zsk = zsk, k = k, ksv = ksv, ksk = ksk)
      tymch$hessian <- -crossprod(gri)
    } else {
      tymch$hessian <- hessian2(funcg = .gr.sn, at = tymch$par, y = Y, x = X, zsv = zsv, zsk = zsk, k = k, ksv = ksv, ksk = ksk)
    }
    tymch$coef <- tymch$par
    tymch$ll <- tymch$value
    tymch$vcov <- tryCatch(solve(-tymch$hessian), tol = .Machine$double.xmin * 10, error = function(e) e )
    
    if (!is.null(tymch$value) && is.null(tymch$ll)) tymch$ll <- unname(tymch$value)
    
    
  } else if (technique == 'nr'){
    tymch <- .mlmaximize(theta0, ll = .ll.sn, gr = .gr.sn, hess = .hess.sn, gr.hess = .hess.gr.sn, alternate = NULL, BHHH = FALSE, level = 0.99, step.back = .Machine$double.eps^.5, reltol =  sqrt(.Machine$double.eps), lmtol =  1e-4, steptol =  .Machine$double.eps, digits = 4, when.backedup = sqrt(.Machine$double.eps), max.backedup = 7, print.level = print.level, only.maximize = FALSE, maxit = 250, 
                               y = Y, x = X, zsv = zsv, zsk = zsk, k = k, ksv = ksv, ksk = ksk                   )
    tymch$coef <- tymch$par
    # print(tymch)
  }
  
  # cat.print(names(tymch))
  
  
  
  # cat.print(tymch1)
  # table with coefs
  # tymch$par  <- c(tymch$par, sv = unname(sqrt(exp(tymch$par[k+1]))) )
  # tymch$sds  <- suppressWarnings( sqrt(diag(solve(-tymch$hessian))) )
  # tymch$sds  <- c(tymch$sds, tymch$sds[k+1]*exp(tymch$par[k+1]))
  
  tymch$sds  <- suppressWarnings( sqrt(diag(tymch$vcov )) )
  
  time.06 <- proc.time()
  est.time.sec <- (time.06-time.05)[3]

  tymch$ctab <- cbind(tymch$par, tymch$sds, tymch$par/tymch$sds,2*(1-pnorm(abs(-tymch$par/tymch$sds))))
  colnames(tymch$ctab) <- c("Estimate", "Std.Err", "Z value", "Pr(>z)")
  
  if(print.level >= 1){
    printCoefmat(tymch$ctab)
    cat("",rep("_", max.name.length+42-1),"", "\n", sep = "")
  }
  # cat.print(tymch1)
  # return(tymch1)
  

  # cat('Auxiliary parameters \n')
  # Auxiliary parameters
  
  # tymch $ resid   <- as.vector(unname(eps0))
  # shat2           <- var( tymch$resid )
  # tymch $ shat2   <- shat2
  # tymch $ shat    <- sqrt(shat2)
  eps0  <- Y - X %*%    tymch$par[1                        :k]
  tymch $ resid   <- as.vector(unname(eps0))
  tymch $ RSS     <- crossprod(eps0)
  # tymch $ aic     <- log((n-1)/n*shat2)+1+2*(k.all+1)/n
  # tymch $ bic     <- log((n-1)/n*shat2)+1+(k.all+1)*log(n)/n
  # tymch $ aic     <- 2*k.all - 2*tymch$ll
  # tymch $ bic     <- log(n)*k.all - 2*tymch$ll
  # tymch $ Mallows <- tymch $ RSS/shat2 - n + 2*k.all
  # tymch $ coef    <- tymch$par
  tymch $ esample <- esample
  # tymch $ sv      <- as.vector(unname(sv))
  tymch $ n       <- n
  # if(distribution == "e"){
  #   tymch$ su    <- 1/as.vector(unname(lam))
  # } else {
  #   tymch$ su    <- unname(su)
  # }
  tymch$ skewness   <- as.vector(unname(zsk %*%      tymch$par[(k + ksv + 1)            :(k + ksv + ksk)]))
  # tymch$ u     <- as.vector(unname(u))
  # tymch$ eff   <- exp(-tymch$ u)

  tymch$K <- k
  tymch$Ksv <- ksv
  tymch$Ksk <- ksk
  
  # cat.print(tymch$coef)
  # cat.print(tymch$par)
  # cat.print(tymch$vcov)
  # cat.print(coef.names.full)
  
  # cat.print(names(tymch$coef) )
  # cat.print(names(tymch$par))
  # cat.print(colnames(tymch$vcov))
  # cat.print(rownames(tymch$vcov))
  # cat.print(coef.names.full)
  tymch$esttime <- est.time.sec
  if(technique != 'nr'){
    names(tymch$coef) <- names(tymch$par) <- colnames(tymch$vcov) <- rownames(tymch$vcov) <-
      coef.names.full
  }

  
  class(tymch) <- "snreg"
  return(tymch)
  
}




#

Try the snreg package in your browser

Any scripts or data that you put into this service are public.

snreg documentation built on Feb. 6, 2026, 5:08 p.m.