Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

A bug fix in the forecast.gts function #53

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
3 changes: 2 additions & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,6 @@ URL: http://pkg.earo.me/hts
BugReports: https://github.com/earowang/hts/issues
License: GPL (>= 2)
VignetteBuilder: knitr
RoxygenNote: 6.0.1
RoxygenNote: 7.1.0
Roxygen: list(markdown = TRUE, roclets=c('rd', 'collate', 'namespace'))
Encoding: UTF-8
278 changes: 170 additions & 108 deletions R/MinT.R
Original file line number Diff line number Diff line change
Expand Up @@ -35,30 +35,16 @@ shrink.estim <- function(x, tar)
round(lambda, digits = 4))))
}

## Arguments

#fcasts: Matrix of forecasts for all levels of the hierarchical time series.
# Each row represents one forecast horizon and each column represents one time series from the hierarchy

# nodes: If the object class is hts, a list contains the number of child nodes referring to hts.
# groups: If the object is gts, a gmatrix is required, which is the same as groups in the function gts.
# residuals: Matrix of insample residuals for all time series in the hierarchy. Each column referring to one time series.
# covariance: Type of the covariance matrix to be used. Sample covariance matrix ("sam") or shrinking towards a diagonal unequal variances ("shr").
# algorithms: Algorithm used to compute inverse of the matrices.
# keep: Return a gts object or the reconciled forecasts at the bottom level.


# MinT - Trace minimization approach


#' Trace minimization for hierarchical or grouped time series
#'
#' Using the method of Wickramasuriya et al. (2015), this function combines the
#' Using the method of Wickramasuriya et al. (2019), this function combines the
#' forecasts at all levels of a hierarchical or grouped time series. The
#' \code{\link{forecast.gts}} calls this function when the \code{MinT} method
#' is selected.
#'
#'
#' @param fcasts Matrix of forecasts for all levels of a hierarchical or
#' grouped time series. Each row represents one forecast horizon and each
#' column represents one time series of aggregated or disaggregated forecasts.
Expand All @@ -70,23 +56,36 @@ shrink.estim <- function(x, tar)
#' disaggregated time series. The columns must be in the same order as
#' \code{fcasts}.
#' @param covariance Type of the covariance matrix to be used. Shrinking
#' towards a diagonal unequal variances ("shr") or sample covariance matrix
#' ("sam").
#' towards a diagonal unequal variances (\code{"shr"}) or sample covariance matrix
#' (\code{"sam"}).
#' @param nonnegative Logical. Should the reconciled forecasts be non-negative?
#' @param algorithms Algorithm used to compute inverse of the matrices.
#' @param keep Return a gts object or the reconciled forecasts at the bottom
#' level.
#' @param parallel Logical. Import parallel package to allow parallel processing.
#' @param num.cores Numeric. Specify how many cores are going to be used.
#' @param control.nn A list of control parameters to be passed on to the
#' block principal pivoting algorithm. See 'Details'.
#' @return Return the reconciled \code{gts} object or forecasts at the bottom
#' level.
#' @details
#' The \code{control.nn} argument is a list that can supply any of the following components:
#' \describe{
#' \item{\code{ptype}}{Permutation method to be used: \code{"fixed"} or \code{"random"}. Defaults to \code{"fixed"}.}
#' \item{\code{par}}{The number of full exchange rules that may be tried. Defaults to 10.}
#' \item{\code{gtol}}{The tolerance of the convergence criteria. Defaults to \code{sqrt(.Machine$double.eps)}.}
#' }
#' @author Shanika L Wickramasuriya
#' @seealso \code{\link[hts]{hts}}, \code{\link[hts]{gts}},
#' \code{\link[hts]{forecast.gts}}, \code{\link[hts]{combinef}}
#' @references Wickramasuriya, S. L., Athanasopoulos, G., & Hyndman, R. J.
#' (2015). Forecasting hierarchical and grouped time series through trace
#' minimization. \emph{Working paper 15/15, Department of Econometrics &
#' Business Statistics, Monash University.}
#' \url{http://robjhyndman.com/working-papers/mint/}
#' @references Wickramasuriya, S. L., Athanasopoulos, G., & Hyndman, R. J. (2019).
#' Optimal forecast reconciliation for hierarchical and grouped time series through trace minimization.
#' \emph{Journal of the American Statistical Association}, \bold{114}(526), 804--819. \url{http://robjhyndman.com/working-papers/mint/}
#'
#' Hyndman, R. J., Lee, A., & Wang, E. (2015). Fast computation of reconciled
#' Wickramasuriya, S. L., Turlach, B. A., & Hyndman, R. J. (to appear). Optimal non-negative forecast reconciliation.
#' \emph{Statistics and Computing}. \url{https://robjhyndman.com/publications/nnmint/}
#'
#' Hyndman, R. J., Lee, A., & Wang, E. (2016). Fast computation of reconciled
#' forecasts for hierarchical and grouped time series. \emph{Computational
#' Statistics and Data Analysis}, \bold{97}, 16--32.
#' \url{http://robjhyndman.com/papers/hgts/}
Expand All @@ -113,6 +112,20 @@ shrink.estim <- function(x, tar)
#' y.f_cg <- MinT(allf, get_nodes(htseg1), residual = res, covariance = "shr",
#' keep = "all", algorithms = "cg")
#' }
#'
#' \dontrun{h <- 12
#' ally <- abs(aggts(htseg2))
#' allf <- matrix(NA, nrow = h, ncol = ncol(ally))
#' res <- matrix(NA, nrow = nrow(ally), ncol = ncol(ally))
#' for(i in 1:ncol(ally)) {
#' fit <- auto.arima(ally[, i], lambda = 0, biasadj = TRUE)
#' allf[,i] <- forecast(fit, h = h)$mean
#' res[,i] <- na.omit(ally[, i] - fitted(fit))
#' }
#' b.f <- MinT(allf, get_nodes(htseg2), residual = res, covariance = "shr", keep = "bottom", algorithms = "lu")
#' b.nnf <- MinT(allf, get_nodes(htseg2), residual = res, covariance = "shr", keep = "bottom", algorithms = "lu",
#' nonnegative = TRUE, parallel = TRUE)
#' }
#'
#' # gts example
#' \dontrun{abc <- ts(5 + matrix(sort(rnorm(200)), ncol = 4, nrow = 50))
Expand All @@ -136,126 +149,175 @@ shrink.estim <- function(x, tar)
#' plot(y.f)}
#'
#' @export MinT
MinT <- function (fcasts, nodes, groups, residual, covariance = c("shr", "sam"),
algorithms = c("lu", "cg", "chol"), keep = c("gts", "all", "bottom"))
MinT <- function (fcasts, nodes = NULL, groups = NULL, residual, covariance = c("shr", "sam"),
nonnegative = FALSE, algorithms = c("lu", "cg", "chol"),
keep = c("gts", "all", "bottom"), parallel = FALSE, num.cores = 2, control.nn = list())
{
if (is.null(nodes) && is.null(groups)) {
stop("Please specify the hierarchical or the grouping structure.", call. = FALSE)
}

if (!xor(is.null(nodes), is.null(groups))) {
stop("Please specify either nodes or groups argument, not both.", call. = FALSE)
}

alg <- match.arg(algorithms)
keep <- match.arg(keep)
covar <- match.arg(covariance)
res <- residual
fcasts <- stats::as.ts(fcasts)
tspx <- stats::tsp(fcasts)
cnames <- colnames(fcasts)

if(missing(residual))
{
stop("MinT needs insample residuals.", call. = FALSE)
}
if(covar=="sam")
{
n <- nrow(res)
w.1 <- crossprod(res) / n
if(is.positive.definite(w.1)==FALSE)

if (!nonnegative) {
if (missing(residual))
{
stop("MinT needs covariance matrix to be positive definite.", call. = FALSE)
stop("MinT needs insample residuals.", call. = FALSE)
}
}else{
tar <- lowerD(res)
shrink <- shrink.estim(res, tar)
w.1 <- shrink[[1]]
lambda <- shrink[[2]]
if(is.positive.definite(w.1)==FALSE)
if (covar=="sam")
{
stop("MinT needs covariance matrix to be positive definite.", call. = FALSE)
}
}

if (missing(groups)) { # hts class
totalts <- sum(Mnodes(nodes))
if (!is.matrix(fcasts)) {
fcasts <- t(fcasts)
}
h <- nrow(fcasts)
if (ncol(fcasts) != totalts) {
stop("Argument fcasts requires all the forecasts.", call. = FALSE)
n <- nrow(res)
w.1 <- crossprod(res) / n
if(is.positive.definite(w.1)==FALSE)
{
stop("MinT needs covariance matrix to be positive definite.", call. = FALSE)
}
} else {
tar <- lowerD(res)
shrink <- shrink.estim(res, tar)
w.1 <- shrink[[1]]
lambda <- shrink[[2]]
if (is.positive.definite(w.1)==FALSE)
{
stop("MinT needs covariance matrix to be positive definite.", call. = FALSE)
}
}
gmat <- GmatrixH(nodes)
fcasts <- t(fcasts)
if (alg == "chol") {
smat <- Smatrix(gmat)
if (!is.null(w.1)) {
w.1 <- as.matrix.csr(w.1)

if (is.null(groups)) { # hts class
totalts <- sum(Mnodes(nodes))
if (!is.matrix(fcasts)) {
fcasts <- t(fcasts)
}
allf <- CHOL(fcasts = fcasts, S = smat, weights = w.1)
h <- nrow(fcasts)
if (ncol(fcasts) != totalts) {
stop("Argument fcasts requires all the forecasts.", call. = FALSE)
}
else {
smat <- SmatrixM(gmat)
gmat <- GmatrixH(nodes)
fcasts <- t(fcasts)
if (alg == "chol") {
smat <- Smatrix(gmat)
if (!is.null(w.1)) {
w.1 <- as.matrix.csr(w.1)
}
allf <- CHOL(fcasts = fcasts, S = smat, weights = w.1, allow.changes = FALSE)
}
else {
smat <- SmatrixM(gmat)
if (!is.null(w.1)) {
weights <- methods::as(w.1, "sparseMatrix")
}
if (alg == "lu") {
allf <- LU(fcasts = fcasts, S = smat, weights = weights)
allf <- LU(fcasts = fcasts, S = smat, weights = weights, allow.changes = FALSE)
}
else if (alg == "cg") {
allf <- CG(fcasts = fcasts, S = smat, weights = weights)
allf <- CG(fcasts = fcasts, S = smat, weights = weights, allow.changes = FALSE)
}
}

if (keep == "all") {
if (keep == "all") {
out <- t(allf)
}
else {
bottom <- totalts - (ncol(smat):1L) + 1L
bf <- t(allf[bottom, ])
if (keep == "gts") {
bf <- ts(bf, start = tspx[1L], frequency = tspx[3L])
out <- suppressMessages(hts(bf, nodes = nodes))
}
else {
out <- bf
bottom <- totalts - (ncol(smat):1L) + 1L
bf <- t(allf[bottom, ])
if (keep == "gts") {
bf <- ts(bf, start = tspx[1L], frequency = tspx[3L])
out <- suppressMessages(hts(bf, nodes = nodes))
}
else {
out <- bf
}
}
}
}
else if (missing(nodes)) {
rownames(groups) <- NULL
gmat <- GmatrixG(groups)
totalts <- sum(Mlevel(gmat))
if (ncol(fcasts) != totalts) {
stop("Argument fcasts requires all the forecasts.", call. = FALSE)
}
fcasts <- t(fcasts)
if (alg == "chol") {
smat <- Smatrix(gmat)
if (!is.null(w.1)) {
weights <- as.matrix.csr(w.1)
else if (is.null(nodes)) {
rownames(groups) <- NULL
gmat <- GmatrixG(groups)
totalts <- sum(Mlevel(gmat))
if (ncol(fcasts) != totalts) {
stop("Argument fcasts requires all the forecasts.", call. = FALSE)
}
allf <- CHOL(fcasts = fcasts, S = smat, weights = weights)
}
else {
smat <- SmatrixM(gmat)
if (!is.null(w.1)) {
weights <- methods::as(w.1, "sparseMatrix")
fcasts <- t(fcasts)
if (alg == "chol") {
smat <- Smatrix(gmat)
if (!is.null(w.1)) {
weights <- as.matrix.csr(w.1)
}
allf <- CHOL(fcasts = fcasts, S = smat, weights = weights, allow.changes = FALSE)
}
if (alg == "lu") {
allf <- LU(fcasts = fcasts, S = smat, weights = weights)
else {
smat <- SmatrixM(gmat)
if (!is.null(w.1)) {
weights <- methods::as(w.1, "sparseMatrix")
}
if (alg == "lu") {
allf <- LU(fcasts = fcasts, S = smat, weights = weights, allow.changes = FALSE)
}
else if (alg == "cg") {
allf <- CG(fcasts = fcasts, S = smat, weights = weights, allow.changes = FALSE)
}
}
else if (alg == "cg") {
allf <- CG(fcasts = fcasts, S = smat, weights = weights)
if (keep == "all") {
out <- t(allf)
}
else {
bottom <- totalts - (ncol(smat):1L) + 1L
bf <- t(allf[bottom, ])
if (keep == "gts") {
colnames(bf) <- cnames[bottom]
bf <- ts(bf, start = tspx[1L], frequency = tspx[3L])
out <- suppressMessages(gts(bf, groups = groups))
}
else {
out <- bf
}
}
}
if (keep == "all") {
out <- t(allf)
} else {
if (any(fcasts < 0)) {
fcasts[fcasts < 0] <- 0
warning("Negative base forecasts are truncated to zero.")
}
else {
bottom <- totalts - (ncol(smat):1L) + 1L
bf <- t(allf[bottom, ])
if (keep == "gts") {
colnames(bf) <- cnames[bottom]
bf <- ts(bf, start = tspx[1L], frequency = tspx[3L])
out <- suppressMessages(gts(bf, groups = groups))

lst.fc <- split(fcasts, row(fcasts))
if (parallel) {
if (is.null(num.cores)) {
num.cores <- detectCores()
}
else {
cl <- makeCluster(num.cores)
bf <- parSapplyLB(cl = cl, X = lst.fc, MinTbpv, nodes = nodes, groups = groups, res = res, covar = covar, alg = alg, control.nn = control.nn, simplify = TRUE)
stopCluster(cl = cl)
} else {
bf <- sapply(lst.fc, MinTbpv, nodes = nodes, groups = groups, res = res, covar = covar, alg = alg, control.nn = control.nn)
}
bf <- ts(t(bf), start = tspx[1L], frequency = tspx[3L])
if (is.null(groups)) {
if (keep == "bottom") {
out <- bf
} else {
out <- suppressMessages(hts(bf, nodes = nodes))
if (keep == "all") {
out <- aggts(out)
}
}
} else {
if (keep == "bottom") {
out <- bf
} else {
colnames(bf) <- tail(cnames, ncol(bf))
out <- suppressMessages(gts(bf, groups = groups))
if (keep == "all") {
out <- aggts(out)
}
}
}
}
Expand Down
Loading