Nothing
#' Exponential smoothing state space model
#'
#' Returns ets model applied to `y`.
#'
#' Based on the classification of methods as described in Hyndman et al (2008).
#'
#' The methodology is fully automatic. The only required argument for ets is
#' the time series. The model is chosen automatically if not specified. This
#' methodology performed extremely well on the M3-competition data. (See
#' Hyndman, et al, 2002, below.)
#'
#' @aliases print.ets summary.ets as.character.ets coef.ets tsdiag.ets
#'
#' @inheritParams forecast.ts
#' @param y a numeric vector or univariate time series of class `ts`
#' @param model Usually a three-character string identifying method using the
#' framework terminology of Hyndman et al. (2002) and Hyndman et al. (2008).
#' The first letter denotes the error type ("A", "M" or "Z"); the second letter
#' denotes the trend type ("N","A","M" or "Z"); and the third letter denotes
#' the season type ("N","A","M" or "Z"). In all cases, "N"=none, "A"=additive,
#' "M"=multiplicative and "Z"=automatically selected. So, for example, "ANN" is
#' simple exponential smoothing with additive errors, "MAM" is multiplicative
#' Holt-Winters' method with multiplicative errors, and so on.
#'
#' It is also possible for the model to be of class `ets`, and equal to
#' the output from a previous call to `ets`. In this case, the same model
#' is fitted to `y` without re-estimating any smoothing parameters. See
#' also the `use.initial.values` argument.
#' @param damped If `TRUE`, use a damped trend (either additive or
#' multiplicative). If `NULL`, both damped and non-damped trends will be
#' tried and the best model (according to the information criterion `ic`)
#' returned.
#' @param alpha Value of alpha. If `NULL`, it is estimated.
#' @param beta Value of beta. If `NULL`, it is estimated.
#' @param gamma Value of gamma. If `NULL`, it is estimated.
#' @param phi Value of phi. If `NULL`, it is estimated.
#' @param additive.only If `TRUE`, will only consider additive models. Default is
#' `FALSE`. When `lambda` is specified, `additive.only` is set to `TRUE`.
#' @param lambda Box-Cox transformation parameter. If `lambda = "auto"`,
#' then a transformation is automatically selected using `BoxCox.lambda`.
#' The transformation is ignored if NULL. Otherwise,
#' data transformed before model is estimated.
#' @param lower Lower bounds for the parameters (alpha, beta, gamma, phi). Ignored if `bounds = "admissible"`.
#' @param upper Upper bounds for the parameters (alpha, beta, gamma, phi). Ignored if `bounds = "admissible"`.
#' @param opt.crit Optimization criterion. One of "mse" (Mean Square Error),
#' "amse" (Average MSE over first `nmse` forecast horizons), "sigma"
#' (Standard deviation of residuals), "mae" (Mean of absolute residuals), or
#' "lik" (Log-likelihood, the default).
#' @param nmse Number of steps for average multistep MSE (1<=`nmse`<=30).
#' @param bounds Type of parameter space to impose: `"usual"` indicates
#' all parameters must lie between specified lower and upper bounds;
#' `"admissible"` indicates parameters must lie in the admissible space;
#' `"both"` (default) takes the intersection of these regions.
#' @param ic Information criterion to be used in model selection.
#' @param restrict If `TRUE` (default), the models with infinite variance
#' will not be allowed.
#' @param allow.multiplicative.trend If `TRUE`, models with multiplicative
#' trend are allowed when searching for a model. Otherwise, the model space
#' excludes them. This argument is ignored if a multiplicative trend model is
#' explicitly requested (e.g., using `model = "MMN"`).
#' @param use.initial.values If `TRUE` and `model` is of class
#' `"ets"`, then the initial values in the model are also not
#' re-estimated.
#' @param ... Other arguments are ignored.
#'
#' @return An object of class `ets`.
#'
#' The generic accessor functions `fitted.values` and `residuals`
#' extract useful features of the value returned by `ets` and associated
#' functions.
#' @author Rob J Hyndman
#' @seealso [stats::HoltWinters()], [rwf()], [Arima()].
#' @references Hyndman, R.J., Koehler, A.B., Snyder, R.D., and Grose, S. (2002)
#' "A state space framework for automatic forecasting using exponential
#' smoothing methods", \emph{International J. Forecasting}, \bold{18}(3),
#' 439--454.
#'
#' Hyndman, R.J., Akram, Md., and Archibald, B. (2008) "The admissible
#' parameter space for exponential smoothing models". \emph{Annals of
#' Statistical Mathematics}, \bold{60}(2), 407--426.
#'
#' Hyndman, R.J., Koehler, A.B., Ord, J.K., and Snyder, R.D. (2008)
#' \emph{Forecasting with exponential smoothing: the state space approach},
#' Springer-Verlag. \url{https://robjhyndman.com/expsmooth/}.
#' @keywords ts
#' @examples
#' fit <- ets(USAccDeaths)
#' plot(forecast(fit))
#'
#' @export
ets <- function(
y,
model = "ZZZ",
damped = NULL,
alpha = NULL,
beta = NULL,
gamma = NULL,
phi = NULL,
additive.only = FALSE,
lambda = NULL,
biasadj = FALSE,
lower = c(rep(0.0001, 3), 0.8),
upper = c(rep(0.9999, 3), 0.98),
opt.crit = c("lik", "amse", "mse", "sigma", "mae"),
nmse = 3,
bounds = c("both", "usual", "admissible"),
ic = c("aicc", "aic", "bic"),
restrict = TRUE,
allow.multiplicative.trend = FALSE,
use.initial.values = FALSE,
...
) {
opt.crit <- match.arg(opt.crit)
bounds <- match.arg(bounds)
ic <- match.arg(ic)
seriesname <- deparse1(substitute(y))
if (inherits(y, c("data.frame", "list", "matrix", "mts"))) {
stop("y should be a univariate time series")
}
y <- as.ts(y)
ny <- length(y)
# Check if data is constant
if (missing(model) && is.constant(y)) {
return(ses(y, alpha = 0.99999, initial = "simple")$model)
}
orig.y <- y
if (inherits(model, "ets") && is.null(lambda)) {
lambda <- model$lambda
}
if (!is.null(lambda)) {
y <- BoxCox(y, lambda)
lambda <- attr(y, "lambda")
attr(lambda, "biasadj") <- biasadj
additive.only <- TRUE
}
if (nmse < 1 || nmse > 30) {
stop("nmse out of range")
}
m <- max(1, frequency(y))
if (abs(m - round(m)) > 1e-4) {
warning(
"Non-integer seasonal period. Only non-seasonal models will be considered."
)
m <- 1
} else {
m <- round(m)
}
if (any(upper < lower)) {
stop("Lower limits must be less than upper limits")
}
# If model is an ets object, re-fit model to new data
if (is.ets(model)) {
# Prevent alpha being zero (to avoid divide by zero in the C code)
alpha <- max(model$par["alpha"], 1e-10)
beta <- model$par["beta"]
if (is.na(beta)) {
beta <- NULL
}
gamma <- model$par["gamma"]
if (is.na(gamma)) {
gamma <- NULL
}
phi <- model$par["phi"]
if (is.na(phi)) {
phi <- NULL
}
modelcomponents <- paste0(
model$components[1],
model$components[2],
model$components[3]
)
damped <- (model$components[4] == "TRUE")
if (use.initial.values) {
errortype <- substr(modelcomponents, 1, 1)
trendtype <- substr(modelcomponents, 2, 2)
seasontype <- substr(modelcomponents, 3, 3)
# Recompute errors from pegelsresid.C
e <- pegelsresid.C(
y,
m,
model$initstate,
errortype,
trendtype,
seasontype,
damped,
alpha,
beta,
gamma,
phi,
nmse
)
# Compute error measures
np <- length(model$par) + 1
model$loglik <- -0.5 * e$lik
model$aic <- e$lik + 2 * np
model$bic <- e$lik + log(ny) * np
model$aicc <- model$aic + 2 * np * (np + 1) / (ny - np - 1)
model$mse <- e$amse[1]
model$amse <- mean(e$amse)
# Compute states, fitted values and residuals
tsp.y <- tsp(y)
model$states <- ts(
e$states,
frequency = tsp.y[3],
start = tsp.y[1] - 1 / tsp.y[3]
)
colnames(model$states)[1] <- "l"
if (trendtype != "N") {
colnames(model$states)[2] <- "b"
}
if (seasontype != "N") {
colnames(model$states)[
(2 + (trendtype != "N")):ncol(model$states)
] <- paste0("s", 1:m)
}
model$fitted <- ts(e$fits, frequency = tsp.y[3], start = tsp.y[1])
model$residuals <- ts(e$e, frequency = tsp.y[3], start = tsp.y[1])
model$sigma2 <- sum(model$residuals^2, na.rm = TRUE) / (ny - np)
model$x <- orig.y
model$series <- seriesname
if (!is.null(lambda)) {
model$fitted <- InvBoxCox(
model$fitted,
lambda,
biasadj,
var(model$residuals)
)
}
model$lambda <- lambda
# Return model object
return(model)
} else {
model <- modelcomponents
if (missing(use.initial.values)) {
message(
"Model is being refit with current smoothing parameters but initial states are being re-estimated.\nSet 'use.initial.values=TRUE' if you want to re-use existing initial values."
)
}
}
}
errortype <- substr(model, 1, 1)
trendtype <- substr(model, 2, 2)
seasontype <- substr(model, 3, 3)
if (!errortype %in% c("M", "A", "Z")) {
stop("Invalid error type")
}
if (!trendtype %in% c("N", "A", "M", "Z")) {
stop("Invalid trend type")
}
if (!seasontype %in% c("N", "A", "M", "Z")) {
stop("Invalid season type")
}
if (m < 1 || length(y) <= m) {
# warning("I can't handle data with frequency less than 1. Seasonality will be ignored.")
seasontype <- "N"
}
if (m == 1) {
if (seasontype %in% c("A", "M")) {
stop("Nonseasonal data")
} else {
substr(model, 3, 3) <- seasontype <- "N"
}
}
if (m > 24) {
if (seasontype %in% c("A", "M")) {
stop("Frequency too high")
} else if (seasontype == "Z") {
warning(
"I can't handle data with frequency greater than 24. Seasonality will be ignored. Try stlf() if you need seasonal forecasts."
)
substr(model, 3, 3) <- seasontype <- "N"
# m <- 1
}
}
# Check inputs
if (restrict) {
if (
(errortype == "A" && (trendtype == "M" || seasontype == "M")) ||
(errortype == "M" && trendtype == "M" && seasontype == "A") ||
(additive.only &&
(errortype == "M" || trendtype == "M" || seasontype == "M"))
) {
stop("Forbidden model combination")
}
}
data.positive <- (min(y, na.rm = TRUE) > 0)
if (!data.positive && errortype == "M") {
stop("Inappropriate model for data with negative or zero values")
}
if (!is.null(damped) && damped && trendtype == "N") {
stop("Forbidden model combination")
}
n <- sum(!is.na(y))
# Check we have enough data to fit a model
npars <- 2L # alpha + l0
if (trendtype %in% c("A", "M")) {
npars <- npars + 2L
} # beta + b0
if (seasontype %in% c("A", "M")) {
npars <- npars + m
} # gamma + s
if (!is.null(damped)) {
npars <- npars + as.numeric(damped)
}
# Produce something non-optimized for tiny data sets
if (n <= npars + 4L) {
if (!is.null(damped) && damped) {
warning("Not enough data to use damping")
}
if (seasontype %in% c("A", "M")) {
fit <- try(
HoltWintersZZ(
orig.y,
alpha = alpha,
beta = beta,
gamma = gamma,
phi = phi,
exponential = (trendtype == "M"),
seasonal = if (seasontype != "A") "multiplicative" else "additive",
lambda = lambda,
biasadj = biasadj,
warnings = FALSE
),
silent = TRUE
)
if (!inherits(fit, "try-error")) {
fit$call <- match.call()
fit$method <- as.character(fit)
fit$series <- deparse1(substitute(y))
return(fit)
} else {
warning("Seasonal component could not be estimated")
}
}
if (trendtype %in% c("A", "M")) {
fit <- try(
HoltWintersZZ(
orig.y,
alpha = alpha,
beta = beta,
gamma = FALSE,
phi = phi,
exponential = (trendtype == "M"),
lambda = lambda,
biasadj = biasadj,
warnings = FALSE
),
silent = TRUE
)
if (!inherits(fit, "try-error")) {
fit$call <- match.call()
fit$method <- as.character(fit)
fit$series <- deparse1(substitute(y))
return(fit)
} else {
warning("Trend component could not be estimated")
}
}
if (trendtype == "N" && seasontype == "N") {
fit <- try(
HoltWintersZZ(
orig.y,
alpha = alpha,
beta = FALSE,
gamma = FALSE,
lambda = lambda,
biasadj = biasadj,
warnings = FALSE
),
silent = TRUE
)
if (!inherits(fit, "try-error")) {
fit$call <- match.call()
fit$method <- as.character(fit)
fit$series <- deparse1(substitute(y))
return(fit)
}
}
# Try holt and ses and return best
fit1 <- try(
HoltWintersZZ(
orig.y,
alpha = alpha,
beta = beta,
gamma = FALSE,
phi = phi,
exponential = (trendtype == "M"),
lambda = lambda,
biasadj = biasadj,
warnings = FALSE
),
silent = TRUE
)
fit2 <- try(
HoltWintersZZ(
orig.y,
alpha = alpha,
beta = FALSE,
gamma = FALSE,
phi = phi,
exponential = (trendtype == "M"),
lambda = lambda,
biasadj = biasadj,
warnings = FALSE
),
silent = TRUE
)
if (inherits(fit1, "try-error")) {
fit <- fit2
} else if (fit1$sigma2 < fit2$sigma2) {
fit <- fit1
} else {
fit <- fit2
}
if (inherits(fit, "try-error")) {
stop("Unable to estimate a model.")
}
fit$call <- match.call()
fit$method <- as.character(fit)
fit$series <- deparse1(substitute(y))
return(fit)
}
# Fit model (assuming only one nonseasonal model)
if (errortype == "Z") {
errortype <- c("A", "M")
}
if (trendtype == "Z") {
if (allow.multiplicative.trend) {
trendtype <- c("N", "A", "M")
} else {
trendtype <- c("N", "A")
}
}
if (seasontype == "Z") {
seasontype <- c("N", "A", "M")
}
if (is.null(damped)) {
damped <- c(TRUE, FALSE)
}
best.ic <- Inf
for (i in seq_along(errortype)) {
for (j in seq_along(trendtype)) {
for (k in seq_along(seasontype)) {
for (l in seq_along(damped)) {
if (trendtype[j] == "N" && damped[l]) {
next
}
if (restrict) {
if (
errortype[i] == "A" &&
(trendtype[j] == "M" || seasontype[k] == "M")
) {
next
}
if (
errortype[i] == "M" && trendtype[j] == "M" && seasontype[k] == "A"
) {
next
}
if (
additive.only &&
(errortype[i] == "M" ||
trendtype[j] == "M" ||
seasontype[k] == "M")
) {
next
}
}
if (!data.positive && errortype[i] == "M") {
next
}
fit <- try(
etsmodel(
y,
errortype[i],
trendtype[j],
seasontype[k],
damped[l],
alpha,
beta,
gamma,
phi,
lower = lower,
upper = upper,
opt.crit = opt.crit,
nmse = nmse,
bounds = bounds,
...
),
silent = TRUE
)
if (inherits(fit, "try-error")) {
fit.ic <- Inf
} else {
fit.ic <- switch(ic, aic = fit$aic, bic = fit$bic, aicc = fit$aicc)
}
if (!is.na(fit.ic) && fit.ic < best.ic) {
model <- fit
best.ic <- fit.ic
best.e <- errortype[i]
best.t <- trendtype[j]
best.s <- seasontype[k]
best.d <- damped[l]
}
}
}
}
}
if (best.ic == Inf) {
stop("No model able to be fitted")
}
model$m <- m
model$method <- paste0(
"ETS(",
best.e,
",",
best.t,
if (best.d) "d" else "",
",",
best.s,
")"
)
model$series <- seriesname
model$components <- c(best.e, best.t, best.s, best.d)
model$call <- match.call()
model$initstate <- model$states[1, ]
np <- length(model$par)
model$sigma2 <- sum(model$residuals^2, na.rm = TRUE) / (ny - np)
model$x <- orig.y
if (!is.null(lambda)) {
model$fitted <- InvBoxCox(model$fitted, lambda, biasadj, model$sigma2)
}
model$lambda <- lambda
structure(model, class = c("fc_model", "ets"))
}
#' @export
as.character.ets <- function(x, ...) {
paste0(
"ETS(",
x$components[1],
",",
x$components[2],
if (x$components[4]) "d" else "",
",",
x$components[3],
")"
)
}
# myRequire <- function(libName) {
# req.suc <- require(libName, quietly=TRUE, character.only=TRUE)
# if(!req.suc) stop("The ",libName," package is not available.")
# req.suc
# }
# getNewBounds <- function(par, lower, upper, nstate) {
# myLower <- NULL
# myUpper <- NULL
# if("alpha" %in% names(par)) {
# myLower <- c(myLower, lower[1])
# myUpper <- c(myUpper, upper[1])
# }
# if("beta" %in% names(par)) {
# myLower <- c(myLower, lower[2])
# myUpper <- c(myUpper, upper[2])
# }
# if("gamma" %in% names(par)) {
# myLower <- c(myLower, lower[3])
# myUpper <- c(myUpper, upper[3])
# }
# if("phi" %in% names(par)) {
# myLower <- c(myLower, lower[4])
# myUpper <- c(myUpper, upper[4])
# }
# myLower <- c(myLower,rep(-1e8,nstate))
# myUpper <- c(myUpper,rep(1e8,nstate))
# list(lower=myLower, upper=myUpper)
# }
etsmodel <- function(
y,
errortype,
trendtype,
seasontype,
damped,
alpha = NULL,
beta = NULL,
gamma = NULL,
phi = NULL,
lower,
upper,
opt.crit,
nmse,
bounds,
maxit = 2000,
control = NULL,
seed = NULL,
trace = FALSE
) {
tsp.y <- tsp(y)
if (is.null(tsp.y)) {
tsp.y <- c(1, length(y), 1)
}
if (seasontype != "N") {
m <- tsp.y[3]
} else {
m <- 1
}
# Modify limits if alpha, beta or gamma have been specified.
if (!is.null(alpha)) {
upper[2] <- min(alpha, upper[2])
upper[3] <- min(1 - alpha, upper[3])
}
if (!is.null(beta)) {
lower[1] <- max(beta, lower[1])
}
if (!is.null(gamma)) {
upper[1] <- min(1 - gamma, upper[1])
}
# Initialize smoothing parameters
par <- initparam(
alpha,
beta,
gamma,
phi,
trendtype,
seasontype,
damped,
lower,
upper,
m,
bounds
)
names(alpha) <- names(beta) <- names(gamma) <- names(phi) <- NULL
par.noopt <- c(alpha = alpha, beta = beta, gamma = gamma, phi = phi)
if (!is.null(par.noopt)) {
par.noopt <- c(na.omit(par.noopt))
}
if (!is.na(par["alpha"])) {
alpha <- par["alpha"]
}
if (!is.na(par["beta"])) {
beta <- par["beta"]
}
if (!is.na(par["gamma"])) {
gamma <- par["gamma"]
}
if (!is.na(par["phi"])) {
phi <- par["phi"]
}
# if(errortype=="M" | trendtype=="M" | seasontype=="M")
# bounds="usual"
if (!check.param(alpha, beta, gamma, phi, lower, upper, bounds, m)) {
cat(
"Model: ETS(",
errortype,
",",
trendtype,
if (damped) "d" else "",
",",
seasontype,
")",
sep = ""
)
stop("Parameters out of range")
}
# Initialize state
init.state <- initstate(y, trendtype, seasontype)
nstate <- length(init.state)
par <- c(par, init.state)
lower <- c(lower, rep(-Inf, nstate))
upper <- c(upper, rep(Inf, nstate))
np <- length(par)
if (np >= length(y) - 1) {
# Not enough data to continue
return(list(
aic = Inf,
bic = Inf,
aicc = Inf,
mse = Inf,
amse = Inf,
fit = NULL,
par = par,
states = init.state
))
}
env <- etsTargetFunctionInitWrapper(
par = par,
y = y,
nstate = nstate,
errortype = errortype,
trendtype = trendtype,
seasontype = seasontype,
damped = damped,
par.noopt = par.noopt,
lowerb = lower,
upperb = upper,
opt.crit = opt.crit,
nmse = as.integer(nmse),
bounds = bounds,
m = m,
pnames = names(par),
pnames2 = names(par.noopt)
)
fred <- etsNelderMead(
par,
env,
-Inf,
sqrt(.Machine$double.eps),
1.0,
0.5,
2.0,
trace,
maxit
)
fit.par <- fred$par
names(fit.par) <- names(par)
init.state <- fit.par[(np - nstate + 1):np]
# Add extra state
if (seasontype != "N") {
init.state <- c(
init.state,
m * (seasontype == "M") - sum(init.state[(2 + (trendtype != "N")):nstate])
)
}
if (!is.na(fit.par["alpha"])) {
alpha <- fit.par["alpha"]
}
if (!is.na(fit.par["beta"])) {
beta <- fit.par["beta"]
}
if (!is.na(fit.par["gamma"])) {
gamma <- fit.par["gamma"]
}
if (!is.na(fit.par["phi"])) {
phi <- fit.par["phi"]
}
e <- pegelsresid.C(
y,
m,
init.state,
errortype,
trendtype,
seasontype,
damped,
alpha,
beta,
gamma,
phi,
nmse
)
np <- np + 1
ny <- length(y)
aic <- e$lik + 2 * np
bic <- e$lik + log(ny) * np
aicc <- aic + 2 * np * (np + 1) / (ny - np - 1)
mse <- e$amse[1]
amse <- mean(e$amse)
states <- ts(e$states, frequency = tsp.y[3], start = tsp.y[1] - 1 / tsp.y[3])
colnames(states)[1] <- "l"
if (trendtype != "N") {
colnames(states)[2] <- "b"
}
if (seasontype != "N") {
colnames(states)[(2 + (trendtype != "N")):ncol(states)] <- paste0("s", 1:m)
}
list(
loglik = -0.5 * e$lik,
aic = aic,
bic = bic,
aicc = aicc,
mse = mse,
amse = amse,
fit = fred,
residuals = ts(e$e, frequency = tsp.y[3], start = tsp.y[1]),
fitted = ts(e$fits, frequency = tsp.y[3], start = tsp.y[1]),
states = states,
par = c(fit.par, par.noopt)
)
}
etsTargetFunctionInitWrapper <- function(
par,
y,
nstate,
errortype,
trendtype,
seasontype,
damped,
par.noopt,
lowerb,
upperb,
opt.crit,
nmse,
bounds,
m,
pnames,
pnames2
) {
names(par) <- pnames
names(par.noopt) <- pnames2
alpha <- c(par["alpha"], par.noopt["alpha"])["alpha"]
if (is.na(alpha)) {
stop("alpha problem!")
}
if (trendtype != "N") {
beta <- c(par["beta"], par.noopt["beta"])["beta"]
if (is.na(beta)) {
stop("beta Problem!")
}
} else {
beta <- NULL
}
if (seasontype != "N") {
gamma <- c(par["gamma"], par.noopt["gamma"])["gamma"]
if (is.na(gamma)) {
stop("gamma Problem!")
}
} else {
m <- 1
gamma <- NULL
}
if (damped) {
phi <- c(par["phi"], par.noopt["phi"])["phi"]
if (is.na(phi)) {
stop("phi Problem!")
}
} else {
phi <- NULL
}
# determine which values to optimize and which ones are given by the user/not needed
optAlpha <- !is.null(alpha)
optBeta <- !is.null(beta)
optGamma <- !is.null(gamma)
optPhi <- !is.null(phi)
givenAlpha <- FALSE
givenBeta <- FALSE
givenGamma <- FALSE
givenPhi <- FALSE
if (!is.null(par.noopt["alpha"]) && !is.na(par.noopt["alpha"])) {
optAlpha <- FALSE
givenAlpha <- TRUE
}
if (!is.null(par.noopt["beta"]) && !is.na(par.noopt["beta"])) {
optBeta <- FALSE
givenBeta <- TRUE
}
if (!is.null(par.noopt["gamma"]) && !is.na(par.noopt["gamma"])) {
optGamma <- FALSE
givenGamma <- TRUE
}
if (!is.null(par.noopt["phi"]) && !is.na(par.noopt["phi"])) {
optPhi <- FALSE
givenPhi <- TRUE
}
if (!damped) {
phi <- 1
}
if (trendtype == "N") {
beta <- 0
}
if (seasontype == "N") {
gamma <- 0
}
# cat("alpha: ", alpha)
# cat(" beta: ", beta)
# cat(" gamma: ", gamma)
# cat(" phi: ", phi, "\n")
#
# cat("useAlpha: ", useAlpha)
# cat(" useBeta: ", useBeta)
# cat(" useGamma: ", useGamma)
# cat(" usePhi: ", usePhi, "\n")
env <- new.env(parent = emptyenv())
res <- etsTargetFunctionInit(
p_y = y,
p_nstate = nstate,
p_errortype = switch(errortype, A = 1L, M = 2L),
p_trendtype = switch(trendtype, N = 0L, A = 1L, M = 2L),
p_seasontype = switch(seasontype, N = 0L, A = 1L, M = 2L),
p_damped = damped,
p_lower = lowerb,
p_upper = upperb,
p_opt_crit = opt.crit,
p_nmse = as.integer(nmse),
p_bounds = bounds,
p_m = m,
p_optAlpha = optAlpha,
p_optBeta = optBeta,
p_optGamma = optGamma,
p_optPhi = optPhi,
p_givenAlpha = givenAlpha,
p_givenBeta = givenBeta,
p_givenGamma = givenGamma,
p_givenPhi = givenPhi,
p_alpha = alpha,
p_beta = beta,
p_gamma = gamma,
p_phi = phi,
p_rho = env
)
res
}
initparam <- function(
alpha,
beta,
gamma,
phi,
trendtype,
seasontype,
damped,
lower,
upper,
m,
bounds
) {
if (bounds == "admissible") {
lower[1L:3L] <- lower[1L:3L] * 0
upper[1L:3L] <- upper[1L:3L] * 0 + 1e-3
} else if (any(lower > upper)) {
stop("Inconsistent parameter boundaries")
}
# Select alpha
if (is.null(alpha)) {
alpha <- lower[1] + 0.2 * (upper[1] - lower[1]) / m
if (alpha > 1 || alpha < 0) {
alpha <- lower[1] + 2e-3
}
par <- c(alpha = alpha)
} else {
par <- numeric(0)
}
# Select beta
if (trendtype != "N" && is.null(beta)) {
# Ensure beta < alpha
upper[2] <- min(upper[2], alpha)
beta <- lower[2] + 0.1 * (upper[2] - lower[2])
if (beta < 0 || beta > alpha) {
beta <- alpha - 1e-3
}
par <- c(par, beta = beta)
}
# Select gamma
if (seasontype != "N" && is.null(gamma)) {
# Ensure gamma < 1-alpha
upper[3] <- min(upper[3], 1 - alpha)
gamma <- lower[3] + 0.05 * (upper[3] - lower[3])
if (gamma < 0 || gamma > 1 - alpha) {
gamma <- 1 - alpha - 1e-3
}
par <- c(par, gamma = gamma)
}
# Select phi
if (damped && is.null(phi)) {
phi <- lower[4] + .99 * (upper[4] - lower[4])
if (phi < 0 || phi > 1) {
phi <- upper[4] - 1e-3
}
par <- c(par, phi = phi)
}
par
}
check.param <- function(alpha, beta, gamma, phi, lower, upper, bounds, m) {
if (bounds != "admissible") {
if (!is.null(alpha)) {
if (alpha < lower[1] || alpha > upper[1]) {
return(0)
}
}
if (!is.null(beta)) {
if (beta < lower[2] || beta > alpha || beta > upper[2]) {
return(0)
}
}
if (!is.null(phi)) {
if (phi < lower[4] || phi > upper[4]) {
return(0)
}
}
if (!is.null(gamma)) {
if (gamma < lower[3] || gamma > 1 - alpha || gamma > upper[3]) {
return(0)
}
}
}
if (bounds != "usual") {
if (!admissible(alpha, beta, gamma, phi, m)) {
return(0)
}
}
1
}
initstate <- function(y, trendtype, seasontype) {
if (seasontype != "N") {
# Do decomposition
m <- frequency(y)
n <- length(y)
y <- na.interp(y)
if (n < 4) {
stop("You've got to be joking (not enough data).")
} else if (n < 3 * m) {
# Fit simple Fourier model
fouriery <- fourier(y, 1)
fit <- tslm(y ~ trend + fouriery)
if (seasontype == "A") {
y.d <- list(
seasonal = y - fit$coefficients[1] - fit$coefficients[2] * (1:n)
)
} else {
# seasontype=="M". Biased method, but we only need a starting point
y.d <- list(
seasonal = y / (fit$coefficients[1] + fit$coefficients[2] * (1:n))
)
}
} else {
# n is large enough to do a decomposition
y.d <- decompose(
y,
type = switch(seasontype, A = "additive", M = "multiplicative")
)
}
init.seas <- rev(y.d$seasonal[2:m]) # initial seasonal component
names(init.seas) <- paste0("s", 0:(m - 2))
# Seasonally adjusted data
if (seasontype == "A") {
y.sa <- y - y.d$seasonal
} else {
init.seas <- pmax(init.seas, 1e-2) # We do not want negative seasonal indexes
if (sum(init.seas) > m) {
init.seas <- init.seas / sum(init.seas + 1e-2)
}
y.sa <- y / pmax(y.d$seasonal, 1e-2)
}
} else {
# non-seasonal model
m <- 1
init.seas <- NULL
y.sa <- y
}
maxn <- min(max(10, 2 * m), length(y.sa))
if (trendtype == "N") {
l0 <- mean(head(y.sa, maxn))
b0 <- NULL
} else {
# Simple linear regression on seasonally adjusted data
fit <- lsfit(seq(maxn), head(y.sa, maxn))
if (trendtype == "A") {
l0 <- fit$coefficients[1]
b0 <- fit$coefficients[2]
# If error type is "M", then we don't want l0+b0=0.
# So perturb just in case.
if (abs(l0 + b0) < 1e-8) {
l0 <- l0 * (1 + 1e-3)
b0 <- b0 * (1 - 1e-3)
}
} else {
# if(trendtype=="M")
l0 <- fit$coefficients[1] + fit$coefficients[2] # First fitted value
if (abs(l0) < 1e-8) {
l0 <- 1e-7
}
b0 <- (fit$coefficients[1] + 2 * fit$coefficients[2]) / l0 # Ratio of first two fitted values
l0 <- l0 / b0 # First fitted value divided by b0
if (abs(b0) > 1e10) {
# Avoid infinite slopes
b0 <- sign(b0) * 1e10
}
if (l0 < 1e-8 || b0 < 1e-8) {
# Simple linear approximation didn't work.
l0 <- max(y.sa[1], 1e-3)
b0 <- max(y.sa[2] / y.sa[1], 1e-3)
}
}
}
names(l0) <- "l"
if (!is.null(b0)) {
names(b0) <- "b"
}
c(l0, b0, init.seas)
}
lik <- function(
par,
y,
nstate,
errortype,
trendtype,
seasontype,
damped,
par.noopt,
lowerb,
upperb,
opt.crit,
nmse,
bounds,
m,
pnames,
pnames2
) {
names(par) <- pnames
names(par.noopt) <- pnames2
alpha <- c(par["alpha"], par.noopt["alpha"])["alpha"]
if (is.na(alpha)) {
stop("alpha problem!")
}
if (trendtype != "N") {
beta <- c(par["beta"], par.noopt["beta"])["beta"]
if (is.na(beta)) {
stop("beta Problem!")
}
} else {
beta <- NULL
}
if (seasontype != "N") {
gamma <- c(par["gamma"], par.noopt["gamma"])["gamma"]
if (is.na(gamma)) {
stop("gamma Problem!")
}
} else {
m <- 1
gamma <- NULL
}
if (damped) {
phi <- c(par["phi"], par.noopt["phi"])["phi"]
if (is.na(phi)) {
stop("phi Problem!")
}
} else {
phi <- NULL
}
if (!check.param(alpha, beta, gamma, phi, lowerb, upperb, bounds, m)) {
return(Inf)
}
np <- length(par)
init.state <- par[(np - nstate + 1):np]
# Add extra state
if (seasontype != "N") {
init.state <- c(
init.state,
m * (seasontype == "M") - sum(init.state[(2 + (trendtype != "N")):nstate])
)
}
# Check states
if (seasontype == "M") {
seas.states <- init.state[-(1:(1 + (trendtype != "N")))]
if (min(seas.states) < 0) {
return(Inf)
}
}
e <- pegelsresid.C(
y,
m,
init.state,
errortype,
trendtype,
seasontype,
damped,
alpha,
beta,
gamma,
phi,
nmse
)
if (is.na(e$lik)) {
return(Inf)
}
if (e$lik < -1e10) {
# Avoid perfect fits
return(-1e10)
}
# cat("lik: ", e$lik, "\n")
# points(alpha,e$lik,col=2)
switch(
opt.crit,
lik = e$lik,
mse = e$amse[1],
amse = mean(e$amse),
sigma = mean(e$e^2),
mae = mean(abs(e$e))
)
}
#' @export
print.ets <- function(x, ...) {
cat(paste(x$method, "\n\n"))
if (!is.null(x$call)) {
cat("Call:", deparse(x$call), "", sep = "\n")
}
ncoef <- length(x$initstate)
if (!is.null(x$lambda)) {
cat(" Box-Cox transformation: lambda=", round(x$lambda, 4), "\n\n")
}
cat(" Smoothing parameters:\n")
cat(paste(" alpha =", round(x$par["alpha"], 4), "\n"))
if (x$components[2] != "N") {
cat(paste(" beta =", round(x$par["beta"], 4), "\n"))
}
if (x$components[3] != "N") {
cat(paste(" gamma =", round(x$par["gamma"], 4), "\n"))
}
if (x$components[4] != "FALSE") {
cat(paste(" phi =", round(x$par["phi"], 4), "\n"))
}
cat("\n Initial states:\n")
cat(paste(" l =", round(x$initstate[1], 4), "\n"))
if (x$components[2] != "N") {
cat(paste(" b =", round(x$initstate[2], 4), "\n"))
} else {
x$initstate <- c(x$initstate[1], NA, x$initstate[2:ncoef])
ncoef <- ncoef + 1
}
if (x$components[3] != "N") {
cat(" s = ")
if (ncoef <= 8) {
cat(round(x$initstate[3:ncoef], 4))
} else {
cat(round(x$initstate[3:8], 4))
cat("\n ")
cat(round(x$initstate[9:ncoef], 4))
}
cat("\n")
}
cat("\n sigma: ")
cat(round(sqrt(x$sigma2), 4))
if (!is.null(x$aic)) {
stats <- c(x$aic, x$aicc, x$bic)
names(stats) <- c("AIC", "AICc", "BIC")
cat("\n\n")
print(stats)
}
# cat("\n AIC: ")
# cat(round(x$aic,4))
# cat("\n AICc: ")
# cat(round(x$aicc,4))
# cat("\n BIC: ")
# cat(round(x$bic,4))
}
pegelsresid.C <- function(
y,
m,
init.state,
errortype,
trendtype,
seasontype,
damped,
alpha,
beta,
gamma,
phi,
nmse
) {
n <- length(y)
p <- length(init.state)
x <- numeric(p * (n + 1))
x[1:p] <- init.state
if (!damped) {
phi <- 1
}
if (trendtype == "N") {
beta <- 0
}
if (seasontype == "N") {
gamma <- 0
}
res <- .Call(
etscalc,
as.double(y),
as.double(x),
as.integer(m),
switch(errortype, A = 1L, M = 2L),
switch(trendtype, N = 0L, A = 1L, M = 2L),
switch(seasontype, N = 0L, A = 1L, M = 2L),
as.double(alpha),
as.double(beta),
as.double(gamma),
as.double(phi),
as.integer(nmse)
)
tsp.y <- tsp(y)
e <- ts(res$e)
tsp(e) <- tsp.y
list(
lik = res$lik,
amse = res$amse,
e = e,
fits = res$fitted,
states = matrix(res$states, nrow = n + 1, ncol = p, byrow = TRUE)
)
}
admissible <- function(alpha, beta, gamma, phi, m) {
if (is.null(phi)) {
phi <- 1
}
if (phi < 0 || phi > 1 + 1e-8) {
return(0)
}
if (is.null(gamma)) {
if (alpha < 1 - 1 / phi || alpha > 1 + 1 / phi) {
return(0)
}
if (!is.null(beta)) {
if (beta < alpha * (phi - 1) || beta > (1 + phi) * (2 - alpha)) {
return(0)
}
}
} else if (m > 1) {
# Seasonal model
if (is.null(beta)) {
beta <- 0
}
if (gamma < max(1 - 1 / phi - alpha, 0) || gamma > 1 + 1 / phi - alpha) {
return(0)
}
if (alpha < 1 - 1 / phi - gamma * (1 - m + phi + phi * m) / (2 * phi * m)) {
return(0)
}
if (beta < -(1 - phi) * (gamma / m + alpha)) {
return(0)
}
# End of easy tests. Now use characteristic equation
P <- c(
phi * (1 - alpha - gamma),
alpha + beta - alpha * phi + gamma - 1,
rep(alpha + beta - alpha * phi, m - 2),
(alpha + beta - phi),
1
)
roots <- polyroot(P)
# cat("maxpolyroots: ", max(abs(roots)), "\n")
if (max(abs(roots)) > 1 + 1e-10) {
return(0)
}
}
# Passed all tests
1
}
### PLOT COMPONENTS
#' Plot components from ETS model
#'
#' Produces a plot of the level, slope and seasonal components from an ETS
#' model.
#'
#' `autoplot` will produce an equivalent plot as a ggplot object.
#'
#' @param x Object of class \dQuote{ets}.
#' @param object Object of class \dQuote{ets}. Used for ggplot graphics (S3
#' method consistency).
#' @param range.bars Logical indicating if each plot should have a bar at its
#' right side representing relative size. If `NULL`, automatic selection
#' takes place.
#' @param ... Other plotting parameters to affect the plot.
#' @return None. Function produces a plot
#' @author Rob J Hyndman & Mitchell O'Hara-Wild
#' @seealso [ets()]
#' @keywords hplot
#' @examples
#'
#' fit <- ets(USAccDeaths)
#' plot(fit)
#' plot(fit, plot.type = "single", ylab = "", col = 1:3)
#'
#' library(ggplot2)
#' autoplot(fit)
#'
#' @export
plot.ets <- function(x, ...) {
if (!is.null(x$lambda)) {
y <- BoxCox(x$x, x$lambda)
} else {
y <- x$x
}
if (x$components[3] == "N" && x$components[2] == "N") {
plot(
cbind(observed = y, level = x$states[, 1]),
main = paste("Decomposition by", x$method, "method"),
...
)
} else if (x$components[3] == "N") {
plot(
cbind(observed = y, level = x$states[, 1], slope = x$states[, "b"]),
main = paste("Decomposition by", x$method, "method"),
...
)
} else if (x$components[2] == "N") {
plot(
cbind(observed = y, level = x$states[, 1], season = x$states[, "s1"]),
main = paste("Decomposition by", x$method, "method"),
...
)
} else {
plot(
cbind(
observed = y,
level = x$states[, 1],
slope = x$states[, "b"],
season = x$states[, "s1"]
),
main = paste("Decomposition by", x$method, "method"),
...
)
}
}
#' @export
summary.ets <- function(object, ...) {
class(object) <- c("summary.ets", class(object))
object
}
#' @export
print.summary.ets <- function(x, ...) {
NextMethod()
cat("\nTraining set error measures:\n")
print(accuracy(x))
}
#' @export
coef.ets <- function(object, ...) {
object$par
}
#' @rdname fitted.Arima
#' @export
fitted.ets <- function(object, h = 1, ...) {
if (h == 1) {
object$fitted
} else {
hfitted(object = object, h = h, FUN = "ets", ...)
}
}
#' @export
hfitted.ets <- function(object, h = 1, ...) {
n <- length(object$x)
out <- rep(NA_real_, n)
for (i in seq_len(n - h + 1)) {
out[i + h - 1] <- .Call(
etsforecast,
as.double(object$states[i, ]),
as.integer(object$m),
switch(object$components[2], N = 0L, A = 1L, M = 2L),
switch(object$components[3], N = 0L, A = 1L, M = 2L),
as.double(if (object$components[4] == "FALSE") 1 else object$par["phi"]),
as.integer(h)
)[h]
}
out
}
#' @export
logLik.ets <- function(object, ...) {
structure(object$loglik, df = length(object$par) + 1, class = "logLik")
}
#' @export
nobs.ets <- function(object, ...) {
length(object$x)
}
#' Is an object a particular model type?
#'
#' Returns true if the model object is of a particular type
#'
#' @param x object to be tested
#' @export
is.ets <- function(x) {
inherits(x, "ets")
}
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.