Nothing
#' Occurrence Model
#'
#' Function returns the occurrence part of the ADAM model with the specified
#' probability update and model types.
#'
#' The function estimates probability of demand occurrence, using the selected
#' ADAM state space model. It supports ETS, ARIMA and explanatory variables,
#' also allowing to have multiple frequencies and doing variables selection.
#' It is an ADAM analogue for the binary occurrence variable modelling.
#'
#' For the details about the model and its implementation, see the respective
#' vignette: \code{vignette("om","smooth")}
#'
#' @param data Numeric vector, time series, or data frame. Non-binary input is
#' automatically binarised: any non-zero value becomes 1.
#' @param model Three-letter ETS specification such as \code{"MNN"} or
#' \code{"AAN"}. Automatic selection with \code{"Z"} / \code{"X"} /
#' \code{"Y"} wildcards is supported.
#' @param lags Vector of seasonal lags. Defaults to \code{frequency(data)}.
#' @param orders ARIMA orders list: \code{list(ar, i, ma, select)}.
#' @param constant Logical; whether to include a constant term.
#' @param formula Optional formula for external regressors.
#' @param regressors How to handle regressors: \code{"use"},
#' \code{"select"}, or \code{"adapt"}.
#' @param occurrence Type of link function mapping state to probability:
#' \code{"fixed"} (constant), \code{"odds-ratio"}, \code{"inverse-odds-ratio"},
#' or \code{"direct"}.
#' @param loss Loss function: \code{"likelihood"} (Bernoulli) or \code{"MSE"}.
#' @param h Forecast horizon.
#' @param holdout If \code{TRUE}, a holdout sample of size \code{h} is withheld.
#' @param persistence Optional persistence (smoothing) parameter vector.
#' @param phi Optional damping parameter.
#' @param initial Initialisation method: \code{"backcasting"}, \code{"optimal"},
#' \code{"two-stage"}, or \code{"complete"}.
#' @param arma Optional fixed ARMA parameters.
#' @param ic Information criterion for model selection.
#' @param bounds Parameter bounds type.
#' @param ets Type of ETS model: \code{"conventional"} or \code{"adam"}.
#' @param silent If \code{TRUE}, suppresses output and plot.
#' @param ... Additional arguments passed to the optimiser (\code{maxeval},
#' \code{xtol_rel}, \code{algorithm}, \code{print_level}).
#'
#' @return An object of class \code{c("om","adam","smooth")}.
#'
#' @seealso \link{forecast.om}, \link{adam}
#'
#' @examples
#' set.seed(42)
#' y <- rbinom(120, 1, 0.6)
#' m <- om(y, model="MNN", occurrence="odds-ratio")
#' forecast(m, h=12)
#'
#' @rdname om
#' @export
om <- function(data,
model = "ZXZ",
lags = c(frequency(data)),
orders = list(ar=c(0), i=c(0), ma=c(0), select=FALSE),
constant = FALSE,
formula = NULL,
regressors = c("use","select","adapt"),
occurrence = c("auto","fixed","odds-ratio","inverse-odds-ratio","direct","general"),
loss = c("likelihood","MSE","MAE","HAM","LASSO","RIDGE"),
h = 0, holdout = FALSE,
persistence = NULL, phi = NULL,
initial = c("backcasting","optimal","two-stage","complete"),
arma = NULL,
ic = c("AICc","AIC","BIC","BICc"),
bounds = c("usual","admissible","none"),
ets = c("conventional","adam"),
silent = TRUE, ...){
startTime <- Sys.time();
cl <- match.call();
# Capture ellipsis early so FI / stepSize / B / lb / ub passed via ...
# are visible downstream. Mirrors adam.R.
ellipsis <- list(...);
# If a fitted om object is passed via `model`, lift its parameters out
# and set modelDo="use" so the optimiser is skipped. Mirrors
# adam.R:354-424. This is the canonical entry point for vcov(om_obj),
# which re-calls om(..., model=object, FI=TRUE, stepSize=...).
if(is.om(model)){
initial <- model$initial;
persistence <- model$persistence;
phi <- model$phi;
occurrence <- model$occurrence;
bounds <- model$bounds;
loss <- model$loss;
if(!is.null(model$ic)){
ic <- model$ic;
}
ellipsis$B <- model$B;
lags <- model$lags;
orders <- model$orders;
constant <- if(is.null(model$constant)) FALSE else model$constant;
arma <- model$arma;
if(is.null(formula)){
formula <- formula(model);
}
regressors <- model$regressors;
initialType <- model$initialType;
# When the original fit used backcasting / complete, the FI must be
# computed on the SAME objective the optimiser saw — backcasting
# active, ``nIterations=2``, and initials derived from the data by
# the C++ kernel. Pass the type as a STRING to parametersChecker so
# it keeps all ``initial*Estimate=TRUE`` flags and lets
# ``adam_creator`` seed ``matVt`` from the data; ``adam_cpp$fit``
# then runs backcasting cleanly. Pinning the converged
# ``model$profileInitial`` as the recent profile while telling C++
# to backcast produces NaN in fitted values (the two seeds disagree).
# For ``optimal`` / ``provided`` fits, the converged numeric initials
# ARE the correct seed, so keep the original behaviour for that case.
if(any(model$initialType == c("backcasting","complete"))){
initial <- model$initialType;
profilesRecentTable <- NULL;
profilesRecentProvided <- FALSE;
}
else{
profilesRecentTable <- model$profileInitial;
profilesRecentProvided <- TRUE;
}
# NOTE: do NOT propagate model$ets — that is the etsModel boolean
# flag, not the om() `ets` argument (which is a character of
# c("conventional","adam")). Leave `ets` at the user's default.
# Collapse the fitted object down to its ETS spec string so the rest
# of om() treats `model` as a normal spec.
model <- modelType(model);
modelDo_user <- "use";
} else {
modelDo_user <- NULL;
}
occurrence <- match.arg(occurrence);
# Resolve `regressors` early — both the auto.om() and omg() forwarding
# blocks below pass it through as-is, and if it's still the
# multi-element formal default at that point, downstream match.arg
# calls (notably omg.R's `match.arg(regressorsB)`) will fail.
regressors <- match.arg(regressors);
if(occurrence == "auto") {
result <- auto.om(data=data, model=model, lags=lags, orders=orders,
formula=formula, regressors=regressors,
h=h, holdout=holdout,
persistence=persistence, phi=phi,
initial=initial, arma=arma,
ic=ic, bounds=bounds, silent=silent, ets=ets,
occurrence=c("fixed","general","odds-ratio","inverse-odds-ratio","direct"),
constant=constant, loss=loss, ...)
result$call <- match.call()
return(result)
}
if(occurrence == "general") {
result <- omg(data=data, modelA=model, modelB=model,
ordersA=orders, ordersB=orders,
constantA=constant, constantB=constant,
formulaA=formula, formulaB=formula,
regressorsA=regressors, regressorsB=regressors,
persistenceA=persistence, persistenceB=persistence,
phiA=phi, phiB=phi,
armaA=arma, armaB=arma,
etsA=ets, etsB=ets,
lags=lags, h=h, holdout=holdout,
initial=initial, loss=loss, ic=ic,
bounds=bounds, silent=silent, ...)
result$call <- match.call()
return(result)
}
occurrenceType <- occurrence;
# Custom callable loss — same convention as adam() (R/adamGeneral.R:574-602):
# flip the string flag to "custom" and stash the function for the cost
# dispatch. Use a uniquely-named slot so the later
# ``list2env(checkerReturn, ...)`` blast doesn't overwrite the closure
# with the NULL ``lossFunction`` produced by ``commonParametersChecker``
# (which sees the string ``"custom"`` and has no callable to capture).
if(is.function(loss)){
omUserLossFunction <- loss;
loss <- "custom";
}
else {
omUserLossFunction <- NULL;
loss <- match.arg(loss);
}
ic <- match.arg(ic);
bounds <- match.arg(bounds);
# Regularisation weight — mirrors adam()'s LASSO/RIDGE convention
# (passed via ellipsis in adam(); promoted to a first-class arg here
# for clarity).
lambda <- if(is.null(ellipsis$lambda)) 0 else as.numeric(ellipsis$lambda);
# `regressors` is resolved earlier (above the auto.om/omg forwarding
# blocks) — no need to repeat.
ets <- match.arg(ets);
# Do not overwrite ellipsis here — it may already hold values pulled out
# of a fitted-object intake at the top of the function (ellipsis$B etc.).
if(!exists("ellipsis", inherits=FALSE)){
ellipsis <- list(...);
}
occurrenceChar <- switch(occurrence,
"odds-ratio" = "o",
"inverse-odds-ratio" = "i",
"direct" = "d",
"fixed" = "f",
"n");
#### Data preparation ####
yName <- paste0(deparse(substitute(data)), collapse="");
if(length(yName)==0 || is.null(yName)){
yName <- "y";
}
modelDo <- "estimate";
# If we lifted a fitted om object earlier, switch to the "use" path.
if(!is.null(modelDo_user)){
modelDo <- modelDo_user;
}
dataChecked <- adam_checkData(data, lags, h, holdout, yName, modelDo, formula);
list2env(dataChecked, envir=environment());
#### Force ETS(A,N,N) with persistence=0 for "fixed" occurrence ####
if(occurrence == "fixed"){
model <- "ANN";
persistence <- 0;
initial <- "optimal";
modelDo <- "use";
}
#### Call parametersChecker ####
checkerReturn <- parametersChecker(data=data, model=model, lags=lags,
formulaToUse=formula, orders=orders,
constant=constant, arma=arma,
persistence=persistence, phi=phi,
initial=initial,
distribution="plogis",
loss=if(loss=="likelihood") "likelihood" else loss,
h=h, holdout=holdout,
occurrence=occurrence,
ic=ic, bounds=bounds, regressors=regressors,
yName=yName,
silent=silent, modelDo=modelDo,
ellipsis=ellipsis, fast=FALSE);
#### Pure regression: alm was returned directly by the checker ####
if(is.alm(checkerReturn)){
obsInSample <- nobs(checkerReturn);
nParam <- length(coef(checkerReturn));
modelReturned <- list(model="Regression");
modelReturned$timeElapsed <- Sys.time() - startTime;
modelReturned$call <- cl;
if(is.null(formula)){
formula <- formula(checkerReturn);
}
if(holdout){
colnames(data) <- make.names(colnames(data), unique=TRUE);
modelReturned$holdout <- data[obsInSample + c(1:h),,drop=FALSE];
}
else{
modelReturned$holdout <- NULL;
}
responseName <- all.vars(formula)[1];
y <- data[, responseName];
yIndex <- try(time(y), silent=TRUE);
if(inherits(yIndex, "try-error")){
if(!is.data.frame(data) && !is.null(dim(data))){
yIndex <- as.POSIXct(rownames(data));
}
else if(is.data.frame(data)){
yIndex <- c(1:nrow(data));
}
else{
yIndex <- c(1:length(data));
}
}
if(inherits(y, "zoo")){
modelReturned$data <- data[1:obsInSample,,drop=FALSE];
modelReturned$fitted <- zoo(fitted(checkerReturn), order.by=yIndex[1:obsInSample]);
modelReturned$residuals <- zoo(residuals(checkerReturn), order.by=yIndex[1:obsInSample]);
if(h > 0){
if(holdout){
modelReturned$forecast <- zoo(
forecast(checkerReturn, h=h, newdata=tail(data,h), interval="none")$mean,
order.by=yIndex[obsInSample + 1:h]);
}
else{
modelReturned$forecast <- zoo(
forecast(checkerReturn, h=h, interval="none")$mean,
order.by=yIndex[obsInSample + 1:h]);
}
}
else{
modelReturned$forecast <- zoo(NA, order.by=yIndex[obsInSample + 1]);
}
modelReturned$states <- zoo(
matrix(coef(checkerReturn), obsInSample + 1, nParam, byrow=TRUE,
dimnames=list(NULL, names(coef(checkerReturn)))),
order.by=c(yIndex[1] - diff(yIndex[1:2]), yIndex[1:obsInSample]));
}
else{
yFrequency <- frequency(y);
modelReturned$data <- ts(data[1:obsInSample,,drop=FALSE], start=yIndex[1], frequency=yFrequency);
modelReturned$fitted <- ts(fitted(checkerReturn), start=yIndex[1], frequency=yFrequency);
modelReturned$residuals <- ts(residuals(checkerReturn), start=yIndex[1], frequency=yFrequency);
if(h > 0){
if(holdout){
modelReturned$forecast <- ts(
forecast(checkerReturn, h=h, newdata=tail(data,h), interval="none")$mean,
start=yIndex[obsInSample + 1], frequency=yFrequency);
}
else{
modelReturned$forecast <- ts(
as.numeric(forecast(checkerReturn, h=h, interval="none")$mean),
start=yIndex[obsInSample] + diff(yIndex[1:2]), frequency=yFrequency);
}
}
else{
modelReturned$forecast <- ts(NA, start=yIndex[obsInSample] + diff(yIndex[1:2]), frequency=yFrequency);
}
modelReturned$states <- ts(
matrix(coef(checkerReturn), obsInSample + 1, nParam, byrow=TRUE,
dimnames=list(NULL, names(coef(checkerReturn)))),
start=yIndex[1] - diff(yIndex[1:2]), frequency=yFrequency);
}
modelReturned$persistence <- rep(0, nParam);
names(modelReturned$persistence) <- paste0("delta", c(1:nParam));
modelReturned$phi <- 1;
modelReturned$transition <- diag(nParam);
modelReturned$measurement <- checkerReturn$data;
modelReturned$measurement[,1] <- 1;
colnames(modelReturned$measurement) <- colnames(modelReturned$states);
modelReturned$initial <- list(xreg=coef(checkerReturn));
modelReturned$initialType <- "optimal";
modelReturned$initialEstimated <- TRUE;
names(modelReturned$initialEstimated) <- "xreg";
modelReturned$orders <- list(ar=0, i=0, ma=0);
modelReturned$arma <- NULL;
parametersNumber <- matrix(0, 2, 5,
dimnames=list(c("Estimated","Provided"),
c("nParamInternal","nParamXreg","nParamOccurrence","nParamScale","nParamAll")));
parametersNumber[1, 2] <- nParam;
parametersNumber[1, 5] <- nParam;
modelReturned$nParam <- parametersNumber;
modelReturned$formula <- formula(checkerReturn);
modelReturned$regressors <- "use";
modelReturned$loss <- checkerReturn$loss;
modelReturned$lossValue <- checkerReturn$lossValue;
modelReturned$lossFunction <- checkerReturn$lossFunction;
modelReturned$logLik <- logLik(checkerReturn);
modelReturned$distribution <- checkerReturn$distribution;
modelReturned$scale <- checkerReturn$scale;
modelReturned$other <- checkerReturn$other;
modelReturned$B <- coef(checkerReturn);
modelReturned$lags <- 1;
modelReturned$lagsAll <- rep(1, nParam);
modelReturned$FI <- checkerReturn$FI;
modelReturned$occurrence <- occurrence;
if(holdout){
modelReturned$accuracy <- measures(modelReturned$holdout[, responseName],
modelReturned$forecast,
modelReturned$data[, responseName]);
}
else{
modelReturned$accuracy <- NULL;
}
class(modelReturned) <- c("om","adam","smooth","occurrence");
if(!silent){
plot(modelReturned, 7);
}
return(modelReturned);
}
list2env(checkerReturn, envir=environment());
# ``commonParametersChecker`` resets ``lossFunction`` to ``NULL`` for a
# string-valued loss; restore the user-provided callable captured above
# so the optimiser-side cost dispatch can call it.
if(loss == "custom"){
lossFunction <- omUserLossFunction;
}
# Delegate ARIMA order selection to auto.om() with the current occurrence type.
if(is.list(orders) && !is.null(orders$select) && isTRUE(orders$select)){
result <- auto.om(data=data, model=model, lags=lags,
orders=orders, formula=formula,
regressors=regressors, occurrence=occurrence,
h=h, holdout=holdout,
persistence=persistence, phi=phi,
initial=initial, arma=arma,
ic=ic, bounds=bounds,
silent=silent, ets=ets,
constant=constant, loss=loss, ...);
result$call <- match.call();
return(result);
}
occurrence <- occurrenceType;
# For "fixed": set initial level analytically and disable estimation
if(occurrenceType == "fixed"){
if(initialLevelEstimate){
initialLevel <- mean(ot);
}
initialType <- "provided";
initialLevelEstimate <- FALSE;
initialEstimate <- FALSE;
persistenceEstimate <- FALSE;
persistenceLevelEstimate <- FALSE;
occurrenceChar <- "d";
nParamEstimated <- 1;
modelDo <- "use";
}
# If the user supplied a complete persistence vector (or any other input
# forces modelDo="use" outside the "fixed" branch above), nParamEstimated
# has not been set yet — but downstream code (omFinalFit, IC) reads it.
if(!exists("nParamEstimated", inherits=FALSE)){
nParamEstimated <- 0;
}
# Binary indicators (ot from checker is already binary when occurrence != "none")
yInSample[] <- (yInSample!=0)*1;
if(holdout){
yHoldout[] <- (yHoldout != 0) * 1;
if(any(yClasses=="ts")){
yHoldout <- ts(yHoldout, start=yForecastStart, frequency=yFrequency);
} else {
yHoldout <- zoo(yHoldout, order.by=yForecastIndex);
}
}
# Override occurrence-related flags set by checker
occurrenceModel <- FALSE;
oesModel <- NULL;
yFitted <- matrix(rep(mean(yInSample), obsInSample), ncol=1);
refineHead <- TRUE;
adamETS <- (ets == "adam");
#### Optimiser settings ####
optimSettings <- adam_checkOptimizer(ellipsis=ellipsis, loss=loss, distribution="dnorm",
initialType=initialType, lags=lags,
arimaModel=arimaModel);
list2env(optimSettings, envir=environment());
# This is the internal variable, which should be equal to 1 everywhere
# This is to create all necessary objects correctly.
otLogicalInternal <- otLogical;
otLogicalInternal[] <- TRUE;
#### Inner estimator for a single ETS/ARIMA model ####
omEstimator <- function(etsModel, Etype, Ttype, Stype, lags,
lagsModelSeasonal, lagsModelARIMA,
obsStates, obsInSample,
yInSample, persistence, persistenceEstimate,
persistenceLevel, persistenceLevelEstimate,
persistenceTrend, persistenceTrendEstimate,
persistenceSeasonal, persistenceSeasonalEstimate,
persistenceXreg, persistenceXregEstimate,
persistenceXregProvided,
phi, phiEstimate,
initialType, initialLevel, initialTrend,
initialSeasonal, initialArima, initialEstimate,
initialLevelEstimate, initialTrendEstimate,
initialSeasonalEstimate, initialArimaEstimate,
initialXregEstimate, initialXregProvided,
arimaModel, arRequired, iRequired, maRequired,
armaParameters,
componentsNumberARIMA, componentsNamesARIMA,
formula, xregModel, xregModelInitials, xregData,
xregNumber, xregNames, regressors,
xregParametersMissing, xregParametersIncluded,
xregParametersEstimated, xregParametersPersistence,
constantRequired, constantEstimate, constantValue,
constantName,
ot, otLogical, occurrenceModel, yFitted,
bounds, loss, lossFunction, distribution,
horizon, multisteps, other, otherParameterEstimate,
lambda, B){
# omCF_local is defined at file scope (top of this file) — it is a
# pure function over its explicit arguments, so we just use the
# file-scope version. nloptr will receive it via eval_f below.
adamArchitect <- adam_architector(etsModel, Etype, Ttype, Stype, lags,
lagsModelSeasonal,
xregNumber, obsInSample, initialType,
arimaModel, lagsModelARIMA, xregModel,
constantRequired,
componentsNumberARIMA,
obsAll, yIndexAll, yClasses, adamETS);
list2env(adamArchitect, environment());
# Etype="A" is needed for the decomposition to work in case of 0/1 data
adamCreated <- adam_creator(etsModel, Etype="A", Ttype=switch(Ttype, "N"="N", "A"), Stype="A",
modelIsTrendy, modelIsSeasonal,
lags, lagsModel, lagsModelARIMA, lagsModelAll,
lagsModelMax,
profilesRecentTable, FALSE,
obsStates, obsInSample,
obsAll,
componentsNumberETS, componentsNumberETSSeasonal,
componentsNamesETS, otLogicalInternal, ot,
persistence, persistenceEstimate,
persistenceLevel, persistenceLevelEstimate,
persistenceTrend, persistenceTrendEstimate,
persistenceSeasonal, persistenceSeasonalEstimate,
persistenceXreg, persistenceXregEstimate,
persistenceXregProvided,
phi,
initialType, initialEstimate,
initialLevel, initialLevelEstimate,
initialTrend, initialTrendEstimate,
initialSeasonal, initialSeasonalEstimate,
initialArima, initialArimaEstimate,
initialArimaNumber,
initialXregEstimate, initialXregProvided,
arimaModel, arRequired, iRequired, maRequired,
armaParameters,
arOrders, iOrders, maOrders,
componentsNumberARIMA, componentsNamesARIMA,
xregModel, xregModelInitials, xregData,
xregNumber, xregNames,
xregParametersPersistence,
constantRequired, constantEstimate,
constantValue, constantName,
adamCpp,
arEstimate, maEstimate, smoother,
nonZeroARI, nonZeroMA);
adamCreated$matVt <- om_initial_transform(
adamCreated$matVt, occurrence, Etype, Ttype, Stype,
etsModel,
modelIsTrendy, modelIsSeasonal,
initialLevelEstimate, initialTrendEstimate, initialSeasonalEstimate,
componentsNumberETS,
componentsNumberETSNonSeasonal, componentsNumberETSSeasonal,
lagsModel, lagsModelMax, lagsModelSeasonal,
obsInSample, ot,
arimaModel, componentsNumberARIMA,
initialArimaEstimate, initialArimaNumber,
xregModel, xregNumber, initialXregEstimate,
constantRequired, constantEstimate);
BValues <- adam_initialiser(etsModel, Etype, Ttype, Stype,
modelIsTrendy, modelIsSeasonal,
componentsNumberETSNonSeasonal,
componentsNumberETSSeasonal,
componentsNumberETS,
lags, lagsModel, lagsModelSeasonal,
lagsModelARIMA, lagsModelMax,
adamCreated$matVt,
persistenceEstimate, persistenceLevelEstimate,
persistenceTrendEstimate,
persistenceSeasonalEstimate,
persistenceXregEstimate,
phiEstimate, initialType, initialEstimate,
initialLevelEstimate, initialTrendEstimate,
initialSeasonalEstimate,
initialArimaEstimate, initialXregEstimate,
arimaModel, arRequired, maRequired,
arEstimate, maEstimate,
arOrders, maOrders,
componentsNumberARIMA, componentsNamesARIMA,
initialArimaNumber,
xregModel, xregNumber,
xregParametersEstimated, xregParametersPersistence,
constantEstimate, constantName,
otherParameterEstimate,
adamCpp,
ets, bounds, ot, otLogicalInternal,
iOrders, armaParameters, other);
# Respect user-supplied B / lb / ub from ellipses (mirrors adam.R
# lines 1229-1361). Named B is filtered by name match against the
# initialiser's parameter set; unnamed B is taken as-is but gets the
# initialiser's names. lb / ub fall back to BValues only when NULL.
if(!is.null(B)){
if(!is.null(names(B))){
B <- B[names(B) %in% names(BValues$B)];
BValues$B[] <- B;
}
else{
BValues$B[] <- B;
names(B) <- names(BValues$B);
}
B_used <- BValues$B;
}
else{
B_used <- BValues$B;
}
if(is.null(lb)){
lb <- BValues$Bl;
}
if(is.null(ub)){
ub <- BValues$Bu;
}
# Treat the dangerous mixed models — but ONLY when the user did not
# supply their own B via ellipses. A user-provided B is treated as
# the authoritative starting point.
if(is.null(B) &&
((Etype=="A" && Ttype=="A" && Stype=="M") ||
(Etype=="A" && Ttype=="M" && Stype=="A") ||
(Etype=="M" && Ttype=="A" && Stype=="A") ||
(Etype=="M" && Ttype=="A" && Stype=="N") ||
(Etype=="A" && Ttype=="M" && Stype=="N") ||
(Etype=="M" && Ttype=="M" && Stype=="A") ||
(Etype=="M" && Ttype=="N" && Stype=="A") ||
(Etype=="A" && Ttype=="N" && Stype=="M") ||
occurrence=="direct")){
B_used[] <- 0;
B_used[1] <- 0.1;
}
# ARIMA companion matrices for bounds checking
if(arimaModel){
arPolynomialMatrix <- matrix(0, arOrders %*% lags, arOrders %*% lags);
if(nrow(arPolynomialMatrix) > 1){
arPolynomialMatrix[2:nrow(arPolynomialMatrix)-1, 2:nrow(arPolynomialMatrix)] <-
diag(nrow(arPolynomialMatrix) - 1);
}
maPolynomialMatrix <- matrix(0, maOrders %*% lags, maOrders %*% lags);
if(nrow(maPolynomialMatrix) > 1){
maPolynomialMatrix[2:nrow(maPolynomialMatrix)-1, 2:nrow(maPolynomialMatrix)] <-
diag(nrow(maPolynomialMatrix) - 1);
}
} else {
arPolynomialMatrix <- maPolynomialMatrix <- NULL;
}
# All arguments needed by omCF_local are passed explicitly to nloptr
# below, so the cost function never reads them from the surrounding
# closure (which is shared across the model-pool loop and could leak
# the original model's flags into a different submodel's evaluation).
nloptrArgs <- list(
etsModel=etsModel, Etype=Etype, Ttype=Ttype, Stype=Stype,
modelIsTrendy=modelIsTrendy, modelIsSeasonal=modelIsSeasonal,
componentsNumberETS=componentsNumberETS,
componentsNumberETSNonSeasonal=componentsNumberETSNonSeasonal,
componentsNumberETSSeasonal=componentsNumberETSSeasonal,
componentsNumberARIMA=componentsNumberARIMA,
lags=lags, lagsModel=lagsModel, lagsModelMax=lagsModelMax,
lagsModelAll=lagsModelAll,
indexLookupTable=indexLookupTable,
profilesRecentTable=profilesRecentTable,
matVt=adamCreated$matVt, matWt=adamCreated$matWt,
matF=adamCreated$matF, vecG=adamCreated$vecG,
persistenceEstimate=persistenceEstimate,
persistenceLevelEstimate=persistenceLevelEstimate,
persistenceTrendEstimate=persistenceTrendEstimate,
persistenceSeasonalEstimate=persistenceSeasonalEstimate,
persistenceXregEstimate=persistenceXregEstimate,
phiEstimate=phiEstimate,
initialType=initialType, initialEstimate=initialEstimate,
initialLevelEstimate=initialLevelEstimate,
initialTrendEstimate=initialTrendEstimate,
initialSeasonalEstimate=initialSeasonalEstimate,
initialArimaEstimate=initialArimaEstimate,
initialXregEstimate=initialXregEstimate,
initialArimaNumber=initialArimaNumber,
arimaModel=arimaModel, arEstimate=arEstimate, maEstimate=maEstimate,
arOrders=arOrders, iOrders=iOrders, maOrders=maOrders,
arRequired=arRequired, maRequired=maRequired,
armaParameters=armaParameters,
nonZeroARI=nonZeroARI, nonZeroMA=nonZeroMA,
arimaPolynomials=adamCreated$arimaPolynomials,
arPolynomialMatrix=arPolynomialMatrix,
maPolynomialMatrix=maPolynomialMatrix,
xregModel=xregModel, xregNumber=xregNumber,
xregParametersMissing=xregParametersMissing,
xregParametersIncluded=xregParametersIncluded,
xregParametersEstimated=xregParametersEstimated,
xregParametersPersistence=xregParametersPersistence,
constantRequired=constantRequired,
constantEstimate=constantEstimate,
bounds=bounds, regressors=regressors, loss=loss,
ot=ot, otLogical=otLogical, obsInSample=obsInSample,
nIterations=nIterations, refineHead=refineHead,
occurrence=occurrence, occurrenceChar=occurrenceChar,
adamCpp=adamCpp,
lambda=lambda, lossFunction=lossFunction);
# If there is nothing to estimate (e.g. a degenerate/tiny sample where
# the initialiser produced an empty parameter vector), skip nloptr —
# calling it with a zero-length x0 errors. Just evaluate the cost once
# at the empty B. Mirrors the guard in omgEstimator().
if(length(B_used) == 0){
res <- list(solution=B_used,
objective=do.call(omCF_local, c(list(B=B_used), nloptrArgs)));
}
else{
maxevalUsed <- if(is.null(maxeval)) length(B_used) * 40L else maxeval;
res <- suppressWarnings(do.call(nloptr,
c(list(x0=B_used, eval_f=omCF_local,
lb=lb, ub=ub,
opts=list(algorithm=algorithm,
xtol_rel=xtol_rel, xtol_abs=xtol_abs,
ftol_rel=ftol_rel, ftol_abs=ftol_abs,
maxeval=maxevalUsed, maxtime=maxtime,
print_level=print_level)),
nloptrArgs)));
res$call <- quote(nloptr(x0=B_used, eval_f=omCF_local, lb=lb, ub=ub, opts=opts));
# Retry from BValues$B if the first run hit the infeasibility plateau,
# but only when the user did NOT supply their own B — their B is the
# authoritative starting point and must not be silently replaced.
if(is.null(B) && (is.infinite(res$objective) || res$objective == 1e+300)){
B_used[] <- BValues$B;
B_used[] <- 0.001;
B_used[1] <- 0.01;
res <- suppressWarnings(do.call(nloptr,
c(list(x0=B_used, eval_f=omCF_local,
lb=lb, ub=ub,
opts=list(algorithm=algorithm,
xtol_rel=xtol_rel, xtol_abs=xtol_abs,
ftol_rel=ftol_rel, ftol_abs=ftol_abs,
maxeval=maxevalUsed, maxtime=maxtime,
print_level=print_level)),
nloptrArgs)));
res$call <- quote(nloptr(x0=B_used, eval_f=omCF_local, lb=lb, ub=ub, opts=opts));
}
}
B_used <- res$solution;
names(B_used) <- names(BValues$B);
CFValue <- res$objective;
nParamEstimated <- length(B_used);
logLikValue <- -CFValue;
# Fisher Information is NOT computed inside omEstimator. The canonical
# path is vcov(om_object) -> om(model=object, FI=TRUE) which routes
# via the modelDo=="use" branch where the hessian is taken at the
# fixed B. This mirrors how adam() handles FI (adam.R:2698+).
FIMatrix <- NULL;
return(list(B=B_used, CFValue=CFValue, nParamEstimated=nParamEstimated,
logLikADAMValue=logLikValue,
# Fields not present in nloptrArgs:
xregData=xregData, xregNames=xregNames,
xregModelInitials=xregModelInitials, formula=formula,
res=res, FI=FIMatrix,
iRequired=iRequired,
adamArchitect=adamArchitect, adamCreated=adamCreated,
# The exact arg-set that nloptr (and every CF call) saw.
# Downstream code reads model/component/persistence/initial
# flags from here — there is no need to also duplicate them
# into this return list.
nloptrArgs=nloptrArgs));
}
#### IC function with shared environment for nParam ####
.icEnv <- new.env(parent=emptyenv());
.icEnv$nP <- 0L;
icFunction <- function(ll){
nP <- .icEnv$nP;
llObj <- structure(ll, nobs=obsInSample, df=nP, class="logLik");
return(switch(ic,
"AIC" = AIC(llObj),
"AICc" = AICc(llObj),
"BIC" = BIC(llObj),
"BICc" = BICc(llObj)));
};
icFunctionWrap <- function(ll, nP, obsIS=obsInSample){
.icEnv$nP <- nP;
return(icFunction(ll));
};
#### Helper: build a full om object from an estimator result ####
omFinalFit <- function(res, hLocal=0, fullObject=FALSE,
yFittedOverride=NULL, yForecastOverride=NULL){
otLogicalInternal <- otLogical;
otLogicalInternal[] <- TRUE;
# Prefer the exact arg-bundle the optimiser/CF saw if it was passed
# through. This avoids any divergence between the matrices adam_filler
# is fed here vs what every CF eval was fed.
nla <- res$nloptrArgs
if(!is.null(res$adamArchitect)){
# Reuse objects built before nloptr — avoids any mismatch between the
# matrices the optimiser saw and those rebuilt here from scratch.
adamArchitect <- res$adamArchitect;
adamCreated <- res$adamCreated;
} else {
adamArchitect <- adam_architector(nla$etsModel, nla$Etype, nla$Ttype, nla$Stype,
lags, lagsModelSeasonal,
xregNumber, obsInSample, initialType,
nla$arimaModel, lagsModelARIMA,
xregModel, constantRequired,
componentsNumberARIMA,
obsAll, yIndexAll, yClasses, adamETS);
adamCreated <- adam_creator(nla$etsModel, nla$Etype, nla$Ttype, nla$Stype,
nla$modelIsTrendy, nla$modelIsSeasonal,
lags, adamArchitect$lagsModel, lagsModelARIMA,
adamArchitect$lagsModelAll, adamArchitect$lagsModelMax,
adamArchitect$profilesRecentTable, FALSE,
adamArchitect$obsStates, obsInSample,
obsAll,
adamArchitect$componentsNumberETS,
adamArchitect$componentsNumberETSSeasonal,
adamArchitect$componentsNamesETS, otLogicalInternal, ot,
persistence, nla$persistenceEstimate,
persistenceLevel, nla$persistenceLevelEstimate,
persistenceTrend, nla$persistenceTrendEstimate,
persistenceSeasonal, nla$persistenceSeasonalEstimate,
persistenceXreg, nla$persistenceXregEstimate,
persistenceXregProvided,
phi,
initialType, nla$initialEstimate,
initialLevel, nla$initialLevelEstimate,
initialTrend, nla$initialTrendEstimate,
initialSeasonal, nla$initialSeasonalEstimate,
initialArima, nla$initialArimaEstimate,
initialArimaNumber,
nla$initialXregEstimate, initialXregProvided,
nla$arimaModel, nla$arRequired, res$iRequired, nla$maRequired,
armaParameters,
nla$arOrders, nla$iOrders, nla$maOrders,
componentsNumberARIMA, componentsNamesARIMA,
xregModel, xregModelInitials, xregData,
xregNumber, xregNames,
xregParametersPersistence,
constantRequired, constantEstimate,
constantValue, constantName,
nla$adamCpp,
nla$arEstimate, nla$maEstimate, smoother,
nonZeroARI, nonZeroMA);
adamCreated$matVt <- om_initial_transform(
adamCreated$matVt, occurrence, nla$Etype, nla$Ttype, nla$Stype,
nla$etsModel,
nla$modelIsTrendy, nla$modelIsSeasonal,
nla$initialLevelEstimate, nla$initialTrendEstimate, nla$initialSeasonalEstimate,
adamArchitect$componentsNumberETS,
adamArchitect$componentsNumberETSNonSeasonal,
adamArchitect$componentsNumberETSSeasonal,
adamArchitect$lagsModel, adamArchitect$lagsModelMax, lagsModelSeasonal,
obsInSample, ot,
nla$arimaModel, componentsNumberARIMA,
nla$initialArimaEstimate, initialArimaNumber,
xregModel, xregNumber, nla$initialXregEstimate,
constantRequired, constantEstimate);
}
adamFilled <- adam_filler(res$B,
nla$etsModel, nla$Etype, nla$Ttype, nla$Stype,
nla$modelIsTrendy, nla$modelIsSeasonal,
adamArchitect$componentsNumberETS, adamArchitect$componentsNumberETSNonSeasonal,
adamArchitect$componentsNumberETSSeasonal, componentsNumberARIMA,
lags, adamArchitect$lagsModel, adamArchitect$lagsModelMax,
adamCreated$matVt, adamCreated$matWt, adamCreated$matF, adamCreated$vecG,
nla$persistenceEstimate, nla$persistenceLevelEstimate,
nla$persistenceTrendEstimate, nla$persistenceSeasonalEstimate,
nla$persistenceXregEstimate, nla$phiEstimate,
initialType, nla$initialEstimate,
nla$initialLevelEstimate, nla$initialTrendEstimate,
nla$initialSeasonalEstimate, nla$initialArimaEstimate,
nla$initialXregEstimate,
nla$arimaModel, nla$arEstimate, nla$maEstimate,
nla$arOrders, nla$iOrders, nla$maOrders,
nla$arRequired, nla$maRequired, armaParameters,
nonZeroARI, nonZeroMA, adamCreated$arimaPolynomials,
xregModel, xregNumber,
xregParametersMissing, xregParametersIncluded,
xregParametersEstimated, xregParametersPersistence,
constantEstimate, adamArchitect$adamCpp,
constantRequired, initialArimaNumber);
prof <- adamFilled$matVt[, 1:adamArchitect$lagsModelMax, drop=FALSE];
adamFitted <- adamArchitect$adamCpp$fit(adamFilled$matVt, adamFilled$matWt, adamFilled$matF, adamFilled$vecG,
adamArchitect$indexLookupTable, prof,
as.numeric(ot), as.numeric(ot),
any(initialType == c("complete","backcasting")),
nIterations, refineHead, occurrenceChar);
yFitted <- omLinkFunction(adamFitted$fitted, nla$Etype, occurrence);
# For "fixed" occurrence the optimizer never ran, so logLikADAMValue is absent.
# Compute the Bernoulli log-likelihood from the constant fitted probability.
if(is.null(res$logLikADAMValue)){
ot_vec <- as.numeric(yInSample);
yfit_vec <- as.numeric(yFitted);
ll <- sum(ot_vec * log(pmax(yfit_vec, 1e-15)) +
(1 - ot_vec) * log(pmax(1 - yfit_vec, 1e-15)));
res$logLikADAMValue <- ll;
res$CFValue <- -ll;
}
# Forecast
if(hLocal > 0){
yForecast <- adamArchitect$adamCpp$forecast(tail(adamFilled$matWt, hLocal),
adamFilled$matF,
adamArchitect$indexLookupTable[, adamArchitect$lagsModelMax +
obsInSample + 1:hLocal,
drop=FALSE],
adamFitted$profile, hLocal)$forecast;
yForecast <- omLinkFunction(yForecast, nla$Etype, occurrence);
yForecast[is.nan(yForecast)] <- 0;
}
if(!fullObject){
if(hLocal == 0){
return(yFitted);
}
return(list(fitted=yFitted, forecast=as.vector(yForecast)));
}
# States
statesRaw <- adamFitted$states[, (adamArchitect$lagsModelMax+1):ncol(adamFitted$states), drop=FALSE];
compNames <- rownames(adamCreated$matVt);
if(!is.null(compNames)){
rownames(statesRaw) <- compNames;
}
# Wrap as ts/zoo
if(any(yClasses == "ts")){
yFitted <- ts(yFitted, start=yStart, frequency=yFrequency);
errors <- ts(as.numeric(yInSample) - yFitted, start=yStart, frequency=yFrequency);
matVt <- ts(t(statesRaw), start=yStart, frequency=yFrequency);
} else {
yFitted <- zoo(yFitted, order.by=yInSampleIndex);
errors <- zoo(as.numeric(yInSample) - yFitted, order.by=yInSampleIndex);
matVt <- zoo(t(statesRaw), order.by=yInSampleIndex);
}
# Forecast ts
if(hLocal > 0 && !is.null(yForecast)){
if(any(yClasses == "ts")){
yForecast <- ts(yForecast, start=yForecastStart, frequency=yFrequency);
} else {
yForecast <- zoo(yForecast, order.by=yForecastIndex);
}
} else {
yForecast <- if(any(yClasses=="ts")){
ts(NA, start=yForecastStart, frequency=yFrequency);
} else {
zoo(NA, order.by=yForecastIndex[1]);
}
}
# Use overrides for combination, otherwise leave model-fitted values in place
if(!is.null(yFittedOverride)){
yFitted[] <- yFittedOverride;
}
if(!is.null(yForecastOverride)){
yForecast[] <- yForecastOverride;
}
# Model name
modelStr <- paste0(nla$Etype, nla$Ttype, "d"[nla$phiEstimate], nla$Stype);
modelName <- adam_model_name(nla$etsModel, modelStr, xregModel, nla$arimaModel,
nla$arOrders, nla$iOrders, nla$maOrders, lags,
regressors, constantRequired, constantName,
occurrenceType, adamArchitect$componentsNumberETSSeasonal,
prefix = "o");
# Persistence vector
vecGFinal <- adamFilled$vecG;
if(adamArchitect$componentsNumberETS > 0){
persistenceVec <- as.vector(vecGFinal)[1:adamArchitect$componentsNumberETS];
names(persistenceVec) <- rownames(vecGFinal)[1:adamArchitect$componentsNumberETS];
} else {
persistenceVec <- numeric(0);
}
# Initial values
initialCollected <- adam_initial_collector(
adamFitted$states[, 1:adamArchitect$lagsModelMax, drop=FALSE],
nla$etsModel, nla$modelIsTrendy, nla$modelIsSeasonal,
adamArchitect$lagsModel, adamArchitect$lagsModelMax,
nla$initialLevelEstimate, nla$initialTrendEstimate, nla$initialSeasonalEstimate,
adamArchitect$componentsNumberETSSeasonal,
nla$arimaModel, nla$initialArimaEstimate, initialArima, initialArimaNumber,
adamArchitect$componentsNumberETS, componentsNumberARIMA,
adamFilled$arimaPolynomials, nla$Etype,
xregModel, nla$initialXregEstimate, xregNumber);
# ARMA parameters
if(nla$arimaModel && (nla$arRequired || nla$maRequired)){
armaParametersList <- vector("list", nla$arRequired + nla$maRequired);
j <- 1L;
if(nla$arRequired && nla$arEstimate){
armaParametersList[[j]] <- res$B[nchar(names(res$B))>3 &
substr(names(res$B),1,3)=="phi"];
names(armaParametersList)[j] <- "ar";
j <- j + 1L;
} else if(nla$arRequired){
armaParametersList[[j]] <- armaParameters[substr(names(armaParameters),1,3)=="phi"];
names(armaParametersList)[j] <- "ar";
j <- j + 1L;
}
if(nla$maRequired && nla$maEstimate){
armaParametersList[[j]] <- res$B[substr(names(res$B),1,5)=="theta"];
names(armaParametersList)[j] <- "ma";
} else if(nla$maRequired){
armaParametersList[[j]] <- armaParameters[substr(names(armaParameters),1,5)=="theta"];
names(armaParametersList)[j] <- "ma";
}
} else {
armaParametersList <- NULL;
}
# Parameter counts
parNum <- parametersNumber;
parNum[1,1] <- res$nParamEstimated;
parNum[1,5] <- sum(parNum[1,1:4]);
parNum[2,5] <- sum(parNum[2,1:4]);
if(any(yClasses == "ts")){
yInSample <- ts(yInSample, start=yStart, frequency=yFrequency);
} else {
yInSample <- zoo(yInSample, order.by=yInSampleIndex);
}
subModel <- list(
model = modelName,
timeElapsed = Sys.time() - startTime,
data = yInSample,
fitted = yFitted,
residuals = errors,
forecast = yForecast,
states = matVt,
profile = adamFitted$profile,
profileInitial = prof,
persistence = persistenceVec,
phi = if(nla$phiEstimate) res$B["phi"] else phi,
transition = adamFilled$matF,
measurement = adamFilled$matWt,
initial = initialCollected$initialValue,
initialType = initialType,
initialEstimated = initialCollected$initialEstimated,
orders = list(ar=nla$arOrders, i=nla$iOrders, ma=nla$maOrders),
arma = armaParametersList,
constant = if(constantRequired) {
if(constantEstimate) res$B[constantName] else constantValue
} else NULL,
nParam = parNum,
occurrence = occurrenceType,
formula = formula,
regressors = regressors,
loss = loss,
lossValue = res$CFValue,
lossFunction = lossFunction,
logLik = res$logLikADAMValue,
distribution = "plogis",
scale = NA,
other = if(exists("otherReturned", inherits=FALSE)) otherReturned else NULL,
B = res$B,
lags = lags,
lagsAll = adamArchitect$lagsModelAll,
ets = nla$etsModel,
res = res$res,
FI = res$FI,
adamCpp = adamArchitect$adamCpp,
bounds = bounds,
call = cl
);
if(holdout){
subModel$holdout <- yHoldout;
subModel$accuracy <- measures(as.vector(yHoldout), yForecast,
as.vector(yInSample));
}
class(subModel) <- c("om","adam","smooth","occurrence");
return(subModel);
};
#### Model selection or combination ####
if(modelDo %in% c("select","combine")){
omEstimatorWrapper <- function(etsModel, Etype, Ttype, Stype, lags,
lagsModelSeasonal, lagsModelARIMA,
obsStates, obsInSample,
yInSample, persistence, persistenceEstimate,
persistenceLevel, persistenceLevelEstimate,
persistenceTrend, persistenceTrendEstimate,
persistenceSeasonal, persistenceSeasonalEstimate,
persistenceXreg, persistenceXregEstimate,
persistenceXregProvided,
phi, phiEstimate,
initialType, initialLevel, initialTrend,
initialSeasonal, initialArima, initialEstimate,
initialLevelEstimate, initialTrendEstimate,
initialSeasonalEstimate, initialArimaEstimate,
initialXregEstimate, initialXregProvided,
arimaModel, arRequired, iRequired, maRequired,
armaParameters,
componentsNumberARIMA, componentsNamesARIMA,
formula, xregModel, xregModelInitials, xregData,
xregNumber, xregNames, regressors,
xregParametersMissing, xregParametersIncluded,
xregParametersEstimated, xregParametersPersistence,
constantRequired, constantEstimate, constantValue,
constantName,
ot, otLogical, occurrenceModel, yFitted,
bounds, loss, lossFunction, distribution,
horizon, multisteps, other, otherParameterEstimate,
lambda, B){
res <- omEstimator(etsModel, Etype, Ttype, Stype, lags,
lagsModelSeasonal, lagsModelARIMA,
obsStates, obsInSample,
yInSample, persistence, persistenceEstimate,
persistenceLevel, persistenceLevelEstimate,
persistenceTrend, persistenceTrendEstimate,
persistenceSeasonal, persistenceSeasonalEstimate,
persistenceXreg, persistenceXregEstimate,
persistenceXregProvided,
phi, phiEstimate,
initialType, initialLevel, initialTrend,
initialSeasonal, initialArima, initialEstimate,
initialLevelEstimate, initialTrendEstimate,
initialSeasonalEstimate, initialArimaEstimate,
initialXregEstimate, initialXregProvided,
arimaModel, arRequired, iRequired, maRequired,
armaParameters,
componentsNumberARIMA, componentsNamesARIMA,
formula, xregModel, xregModelInitials, xregData,
xregNumber, xregNames, regressors,
xregParametersMissing, xregParametersIncluded,
xregParametersEstimated, xregParametersPersistence,
constantRequired, constantEstimate, constantValue,
constantName,
ot, otLogical, occurrenceModel, yFitted,
bounds, loss, lossFunction, "dnorm",
horizon, multisteps, other, otherParameterEstimate,
lambda, B);
.icEnv$nP <- res$nParamEstimated;
res$IC <- icFunction(res$logLikADAMValue);
return(res);
}
adamSelected <- adam_selector(omEstimatorWrapper,
model, modelsPool, allowMultiplicative,
modelDo=modelDo,
etsModel, Etype, Ttype, Stype, damped, lags,
lagsModelSeasonal, lagsModelARIMA,
obsStates, obsInSample,
yInSample, persistence, persistenceEstimate,
persistenceLevel, persistenceLevelEstimate,
persistenceTrend, persistenceTrendEstimate,
persistenceSeasonal, persistenceSeasonalEstimate,
persistenceXreg, persistenceXregEstimate,
persistenceXregProvided,
phi, phiEstimate,
initialType, initialLevel, initialTrend, initialSeasonal,
initialArima, initialEstimate,
initialLevelEstimate, initialTrendEstimate,
initialSeasonalEstimate,
initialArimaEstimate, initialXregEstimate,
initialXregProvided,
arimaModel, arRequired, iRequired, maRequired,
armaParameters,
componentsNumberARIMA, componentsNamesARIMA,
formula, xregModel, xregModelInitials, xregData,
xregNumber, xregNames, regressors,
xregParametersMissing, xregParametersIncluded,
xregParametersEstimated, xregParametersPersistence,
constantRequired, constantEstimate, constantValue,
constantName,
ot, otLogical, occurrenceModel, yFitted,
icFunction,
bounds, loss, lossFunction, "dnorm",
horizon, multisteps, other, otherParameterEstimate,
lambda, silent, B);
icSelection <- adamSelected$icSelection;
bestIdx <- which.min(icSelection)[1];
modelOriginal <- model;
estimatorResult <- adamSelected$results[[bestIdx]];
# The selected sub-model's Etype/Ttype/Stype/persistence/initial flags
# live inside nloptrArgs (single source of truth). Unpack it FIRST so
# those values land in the outer scope; then unpack the top-level keys
# (which include adamArchitect, adamCreated, B, etc.).
list2env(estimatorResult$nloptrArgs, environment());
list2env(estimatorResult, environment());
if(modelDo == "combine"){
icWeights <- adam_ic_weights(icSelection);
# temporary weights, dropping the very small ones
icWeightsTemp <- icWeights
icWeightsTemp[icWeightsTemp<1e-5] <- 0;
# Calculate sensible weights
wSum <- sum(icWeightsTemp);
# Amend the weights if they don't add up to 1
if(wSum > 0 && wSum!=1){
icWeights <- icWeights / wSum;
}
yFittedCombined <- matrix(0, obsInSample, 1);
yForecastCombined <- if(h > 0) numeric(h) else NULL;
individualModels <- vector("list", length(icWeights));
for(i in seq_along(icWeights)){
subModel <- tryCatch(omFinalFit(adamSelected$results[[i]],
hLocal=h, fullObject=TRUE),
error=function(e){
message("om(): combine: model ", i, " fitter failed (",
conditionMessage(e), "), dropping from average.");
icWeights[i] <<- 0;
NULL;
});
if(is.null(subModel)){
next;
}
individualModels[[i]] <- subModel;
# Don't add thingy if the weight is low or there are NaNs
if(icWeights[i] >= 1e-5 && !any(is.nan(subModel$fitted))){
yFittedCombined[] <- yFittedCombined +
icWeights[i] * subModel$fitted;
if(h > 0){
yForecastCombined[] <- yForecastCombined +
icWeights[i] * subModel$forecast;
}
}
}
names(individualModels) <- if(!is.null(names(icSelection))) names(icSelection) else
paste0("model", seq_along(individualModels));
}
adamArchitect <- adam_architector(etsModel, Etype, Ttype, Stype, lags, lagsModelSeasonal,
xregNumber, obsInSample, initialType,
arimaModel, lagsModelARIMA, xregModel, constantRequired,
componentsNumberARIMA,
obsAll, yIndexAll, yClasses, adamETS);
list2env(adamArchitect, environment());
}
else if(modelDo == "use"){
# No estimation needed — parameters fully specified (e.g. "fixed"
# occurrence). Build adamArchitect / adamCreated / nloptrArgs here so
# estimatorResult matches the structure returned by omEstimator and
# omFinalFit can take its "reuse" path (no rebuild).
adamArchitectUse <- adam_architector(etsModel, Etype, Ttype, Stype, lags,
lagsModelSeasonal,
xregNumber, obsInSample, initialType,
arimaModel, lagsModelARIMA, xregModel,
constantRequired,
componentsNumberARIMA,
obsAll, yIndexAll, yClasses, adamETS);
# Same Etype="A" decomposition trick as omEstimator (line 479): keeps
# the matrix structure stable on 0/1 data even when the model is M.
adamCreatedUse <- adam_creator(etsModel, Etype="A",
Ttype=switch(Ttype, "N"="N", "A"), Stype="A",
modelIsTrendy, modelIsSeasonal,
lags, adamArchitectUse$lagsModel, lagsModelARIMA,
adamArchitectUse$lagsModelAll,
adamArchitectUse$lagsModelMax,
adamArchitectUse$profilesRecentTable, FALSE,
adamArchitectUse$obsStates, obsInSample,
obsAll,
adamArchitectUse$componentsNumberETS,
adamArchitectUse$componentsNumberETSSeasonal,
adamArchitectUse$componentsNamesETS,
otLogicalInternal, ot,
persistence, persistenceEstimate,
persistenceLevel, persistenceLevelEstimate,
persistenceTrend, persistenceTrendEstimate,
persistenceSeasonal, persistenceSeasonalEstimate,
persistenceXreg, persistenceXregEstimate,
persistenceXregProvided,
phi,
initialType, initialEstimate,
initialLevel, initialLevelEstimate,
initialTrend, initialTrendEstimate,
initialSeasonal, initialSeasonalEstimate,
initialArima, initialArimaEstimate,
initialArimaNumber,
initialXregEstimate, initialXregProvided,
arimaModel, arRequired, iRequired, maRequired,
armaParameters,
arOrders, iOrders, maOrders,
componentsNumberARIMA, componentsNamesARIMA,
xregModel, xregModelInitials, xregData,
xregNumber, xregNames,
xregParametersPersistence,
constantRequired, constantEstimate,
constantValue, constantName,
adamArchitectUse$adamCpp,
arEstimate, maEstimate, smoother,
nonZeroARI, nonZeroMA);
adamCreatedUse$matVt <- om_initial_transform(
adamCreatedUse$matVt, occurrence, Etype, Ttype, Stype,
etsModel,
modelIsTrendy, modelIsSeasonal,
initialLevelEstimate, initialTrendEstimate, initialSeasonalEstimate,
adamArchitectUse$componentsNumberETS,
adamArchitectUse$componentsNumberETSNonSeasonal,
adamArchitectUse$componentsNumberETSSeasonal,
adamArchitectUse$lagsModel, adamArchitectUse$lagsModelMax,
lagsModelSeasonal,
obsInSample, ot,
arimaModel, componentsNumberARIMA,
initialArimaEstimate, initialArimaNumber,
xregModel, xregNumber, initialXregEstimate,
constantRequired, constantEstimate);
# ARIMA companion matrices (parallel to omEstimator lines 578-590).
if(arimaModel){
arPolynomialMatrixUse <- matrix(0, arOrders %*% lags, arOrders %*% lags);
if(nrow(arPolynomialMatrixUse) > 1){
arPolynomialMatrixUse[2:nrow(arPolynomialMatrixUse)-1,
2:nrow(arPolynomialMatrixUse)] <-
diag(nrow(arPolynomialMatrixUse) - 1);
}
maPolynomialMatrixUse <- matrix(0, maOrders %*% lags, maOrders %*% lags);
if(nrow(maPolynomialMatrixUse) > 1){
maPolynomialMatrixUse[2:nrow(maPolynomialMatrixUse)-1,
2:nrow(maPolynomialMatrixUse)] <-
diag(nrow(maPolynomialMatrixUse) - 1);
}
} else {
arPolynomialMatrixUse <- NULL;
maPolynomialMatrixUse <- NULL;
}
# Full nloptrArgs, identical shape to omEstimator's (line 597-642).
nloptrArgsUse <- list(
etsModel=etsModel, Etype=Etype, Ttype=Ttype, Stype=Stype,
modelIsTrendy=modelIsTrendy, modelIsSeasonal=modelIsSeasonal,
componentsNumberETS=adamArchitectUse$componentsNumberETS,
componentsNumberETSNonSeasonal=adamArchitectUse$componentsNumberETSNonSeasonal,
componentsNumberETSSeasonal=adamArchitectUse$componentsNumberETSSeasonal,
componentsNumberARIMA=componentsNumberARIMA,
lags=lags,
lagsModel=adamArchitectUse$lagsModel,
lagsModelMax=adamArchitectUse$lagsModelMax,
lagsModelAll=adamArchitectUse$lagsModelAll,
indexLookupTable=adamArchitectUse$indexLookupTable,
profilesRecentTable=adamArchitectUse$profilesRecentTable,
matVt=adamCreatedUse$matVt, matWt=adamCreatedUse$matWt,
matF=adamCreatedUse$matF, vecG=adamCreatedUse$vecG,
persistenceEstimate=persistenceEstimate,
persistenceLevelEstimate=persistenceLevelEstimate,
persistenceTrendEstimate=persistenceTrendEstimate,
persistenceSeasonalEstimate=persistenceSeasonalEstimate,
persistenceXregEstimate=persistenceXregEstimate,
phiEstimate=phiEstimate,
initialType=initialType, initialEstimate=initialEstimate,
initialLevelEstimate=initialLevelEstimate,
initialTrendEstimate=initialTrendEstimate,
initialSeasonalEstimate=initialSeasonalEstimate,
initialArimaEstimate=initialArimaEstimate,
initialXregEstimate=initialXregEstimate,
initialArimaNumber=initialArimaNumber,
arimaModel=arimaModel, arEstimate=arEstimate, maEstimate=maEstimate,
arOrders=arOrders, iOrders=iOrders, maOrders=maOrders,
arRequired=arRequired, maRequired=maRequired,
armaParameters=armaParameters,
nonZeroARI=nonZeroARI, nonZeroMA=nonZeroMA,
arimaPolynomials=adamCreatedUse$arimaPolynomials,
arPolynomialMatrix=arPolynomialMatrixUse,
maPolynomialMatrix=maPolynomialMatrixUse,
xregModel=xregModel, xregNumber=xregNumber,
xregParametersMissing=xregParametersMissing,
xregParametersIncluded=xregParametersIncluded,
xregParametersEstimated=xregParametersEstimated,
xregParametersPersistence=xregParametersPersistence,
constantRequired=constantRequired,
constantEstimate=constantEstimate,
bounds=bounds, regressors=regressors, loss=loss,
ot=ot, otLogical=otLogical, obsInSample=obsInSample,
nIterations=nIterations, refineHead=refineHead,
occurrence=occurrence, occurrenceChar=occurrenceChar,
adamCpp=adamArchitectUse$adamCpp);
# Fisher Information at the supplied / fitted parameters. Mirrors
# the FI block in adam.R:2698+ — the hessian of -logLik (which is
# what omCF_local returns) IS the observed Fisher Information.
# This is the path vcov.om uses: om(..., model=object, FI=TRUE, ...).
# The trick: omCF_local only varies the elements of B that the
# *Estimate flags say are estimated. With a fitted-model intake,
# those flags are FALSE (parameters are "provided"), so the CF
# ignores B and the hessian comes out as zero. We override the
# flags here based on names(B) so the hessian is taken over
# exactly the parameters that appear in B.
FIMatrixUse <- NULL;
if(isTRUE(ellipsis$FI)){
stepSize <- if(is.null(ellipsis$stepSize)) {
.Machine$double.eps^(1/4);
} else {
ellipsis$stepSize;
};
B_for_FI <- ellipsis$B;
if(!is.null(B_for_FI) && length(B_for_FI) > 0){
# Derive *EstimateFI flags from names(B) — adam.R:2768+
Bnames <- names(B_for_FI);
ncSeas <- adamArchitectUse$componentsNumberETSSeasonal;
pLvlFI <- any(Bnames=="alpha");
pTrdFI <- any(Bnames=="beta");
if(any(substr(Bnames,1,5)=="gamma")){
gammasMask <- substr(Bnames,1,5)=="gamma";
if(sum(gammasMask)==1){
pSeaFI <- TRUE;
}
else{
pSeaFI <- vector("logical", ncSeas);
pSeaFI[as.numeric(substr(Bnames,6,6)[gammasMask])] <- TRUE;
}
} else {
pSeaFI <- FALSE;
}
pXrgFI <- any(substr(Bnames,1,5)=="delta");
pEstFI <- any(c(pLvlFI, pTrdFI, pSeaFI, pXrgFI));
phiEstFI <- any(Bnames=="phi");
iLvlFI <- any(Bnames=="level");
iTrdFI <- any(Bnames=="trend");
if(any(substr(Bnames,1,8)=="seasonal")){
sn <- Bnames[substr(Bnames,1,8)=="seasonal"];
iSeaFI <- vector("logical", ncSeas);
if(any(substr(sn,1,9)=="seasonal_")){
iSeaFI[] <- TRUE;
} else {
iSeaFI[unique(as.numeric(substr(sn,9,9)))] <- TRUE;
}
} else {
iSeaFI <- FALSE;
}
iAriFI <- if(arimaModel) any(substr(Bnames,1,10)=="ARIMAState") else FALSE;
iXrgFI <- if(xregModel) any(colnames(xregData) %in% Bnames) else FALSE;
iEstFI <- any(c(iLvlFI, iTrdFI, iSeaFI, iAriFI, iXrgFI));
# If initials are in B, FI sees the model as initial="optimal";
# otherwise leave the user's initialType (backcasting / provided).
iTypeFI <- if(iEstFI) "optimal" else initialType;
# Patch nloptrArgsUse with the FI-specific flags and disable
# bounds so omCF_local never short-circuits with 1e+300.
nlaFI <- nloptrArgsUse;
nlaFI$persistenceEstimate <- pEstFI;
nlaFI$persistenceLevelEstimate <- pLvlFI;
nlaFI$persistenceTrendEstimate <- pTrdFI;
nlaFI$persistenceSeasonalEstimate<- pSeaFI;
nlaFI$persistenceXregEstimate <- pXrgFI;
nlaFI$phiEstimate <- phiEstFI;
nlaFI$initialType <- iTypeFI;
nlaFI$initialEstimate <- iEstFI;
nlaFI$initialLevelEstimate <- iLvlFI;
nlaFI$initialTrendEstimate <- iTrdFI;
nlaFI$initialSeasonalEstimate <- iSeaFI;
nlaFI$initialArimaEstimate <- iAriFI;
nlaFI$initialXregEstimate <- iXrgFI;
nlaFI$bounds <- "none";
if(arimaModel){
nlaFI$arPolynomialMatrix <- NULL;
nlaFI$maPolynomialMatrix <- NULL;
}
CFAtOptimum <- do.call(omCF_local,
c(list(B=B_for_FI), nlaFI));
omCF_for_FI <- function(B){
names(B) <- Bnames;
val <- tryCatch(suppressWarnings(
do.call(omCF_local,
c(list(B=B), nlaFI))),
error = function(e) CFAtOptimum + 1e6);
if(!is.finite(val)){
val <- CFAtOptimum + 1e6;
}
return(val);
}
FIMatrixUse <- try(suppressWarnings(
hessianCpp(omCF_for_FI, B_for_FI,
h=stepSize)),
silent=TRUE);
if(inherits(FIMatrixUse, "try-error") ||
any(!is.finite(FIMatrixUse))){
FIMatrixUse <- NULL;
} else {
colnames(FIMatrixUse) <- Bnames;
rownames(FIMatrixUse) <- Bnames;
}
}
}
estimatorResult <- list(
B=if(is.null(ellipsis$B)) numeric(0) else ellipsis$B,
CFValue=0, nParamEstimated=nParamEstimated,
logLikADAMValue=NULL,
xregData=xregData, xregNames=xregNames,
xregModelInitials=xregModelInitials, formula=formula,
res=list(objective=0), FI=FIMatrixUse,
iRequired=iRequired,
adamArchitect=adamArchitectUse, adamCreated=adamCreatedUse,
nloptrArgs=nloptrArgsUse);
}
else{
estimatorResult <- omEstimator(etsModel, Etype, Ttype, Stype, lags,
lagsModelSeasonal, lagsModelARIMA,
obsStates, obsInSample,
yInSample, persistence, persistenceEstimate,
persistenceLevel, persistenceLevelEstimate,
persistenceTrend, persistenceTrendEstimate,
persistenceSeasonal, persistenceSeasonalEstimate,
persistenceXreg, persistenceXregEstimate,
persistenceXregProvided,
phi, phiEstimate,
initialType, initialLevel, initialTrend,
initialSeasonal, initialArima, initialEstimate,
initialLevelEstimate, initialTrendEstimate,
initialSeasonalEstimate,
initialArimaEstimate, initialXregEstimate,
initialXregProvided,
arimaModel, arRequired, iRequired, maRequired,
armaParameters,
componentsNumberARIMA, componentsNamesARIMA,
formula, xregModel, xregModelInitials, xregData,
xregNumber, xregNames, regressors,
xregParametersMissing, xregParametersIncluded,
xregParametersEstimated, xregParametersPersistence,
constantRequired, constantEstimate, constantValue,
constantName,
ot, otLogical, occurrenceModel, yFitted,
bounds, loss, lossFunction, "dnorm",
horizon, multisteps, other, otherParameterEstimate,
lambda, B);
list2env(estimatorResult$nloptrArgs, environment());
list2env(estimatorResult, environment());
}
#### Build return object via omFinalFit ####
if(modelDo == "combine"){
modelReturned <- omFinalFit(estimatorResult, hLocal=h, fullObject=TRUE,
yFittedOverride=yFittedCombined,
yForecastOverride=yForecastCombined);
modelReturned$model <- adam_model_name(etsModel, modelOriginal, xregModel, arimaModel,
arOrders, iOrders, maOrders, lags,
regressors, constantRequired, constantName,
occurrenceType, componentsNumberETSSeasonal,
prefix = "o");
modelReturned$models <- individualModels;
modelReturned$ICw <- icWeights;
modelReturned$ICs <- icSelection;
# Weighted estimated parameters across the combined pool (mirrors adam())
nParamMat <- modelReturned$nParam;
nParamMat[1,1] <- sum(sapply(individualModels,
function(x) if(is.null(x)) 0 else nparam(x)) *
icWeights);
nParamMat[1,5] <- sum(nParamMat[1,1:4]);
nParamMat[2,5] <- sum(nParamMat[2,1:4]);
modelReturned$nParam <- nParamMat;
class(modelReturned) <- c("omCombined","om","adam","smooth","occurrence");
} else {
modelReturned <- omFinalFit(estimatorResult, hLocal=h, fullObject=TRUE);
if(modelDo == "select"){
modelReturned$ICs <- icSelection;
}
}
if(!silent){
plot(modelReturned, 7);
}
return(modelReturned);
}
# Transform initial state values for occurrence models.
# Overwrites matVt level, trend, and seasonal rows with values on the
# correct occurrence-model scale. Only touches components whose
# corresponding *Estimate flag is TRUE (i.e. not user-provided).
# Follows the pattern of oesInitialiser in oes.R lines 288-368.
om_initial_transform <- function(matVt, occurrence, Etype, Ttype, Stype,
etsModel,
modelIsTrendy, modelIsSeasonal,
initialLevelEstimate, initialTrendEstimate,
initialSeasonalEstimate,
componentsNumberETS,
componentsNumberETSNonSeasonal,
componentsNumberETSSeasonal,
lagsModel, lagsModelMax, lagsModelSeasonal,
obsInSample, ot,
arimaModel, componentsNumberARIMA,
initialArimaEstimate, initialArimaNumber,
xregModel, xregNumber, initialXregEstimate,
constantRequired, constantEstimate){
occurrenceTransformer <- function(value){
value <- switch(occurrence,
"odds-ratio" = value / (1 - value),
"inverse-odds-ratio" = (1 - value) / value,
"fixed" =,
"direct" = value,
value);
if(Etype == "A" && occurrence %in% c("odds-ratio","inverse-odds-ratio")){
value <- log(value);
}
return(value);
}
# j tracks the number of ETS rows already consumed in matVt.
# When etsModel is FALSE (e.g. pure ARIMA), there is no level row at index 1.
j <- 0;
levelOriginal <- if(etsModel) matVt[1, 1] else NA_real_;
if(etsModel){
#### Level ####
if(initialLevelEstimate){
# Failsafe in case of negative level
if(matVt[1, 1]<0 || matVt[1, 1]>1){
levelOriginal <- mean(ot);
matVt[1, 1:lagsModelMax] <- levelOriginal;
}
matVt[1, 1:lagsModelMax] <- occurrenceTransformer(matVt[1, 1:lagsModelMax]);
}
j <- 1;
#### Trend ####
if(modelIsTrendy){
if(initialTrendEstimate){
# levels <- switch(Ttype,
# "A"=levelOriginal + c(0, matVt[j+1, 1]),
# "M"=levelOriginal * c(1, matVt[j+1, 1]));
# levels[] <- occurrenceTransformer(levels);
# if(Ttype=="A"){
# matVt[j+1, 1:lagsModelMax] <- diff(levels);
# }
# else{
# matVt[j+1, 1:lagsModelMax] <- levels[2]/levels[1];
# }
if(Ttype=="A"){
matVt[j+1, 1:lagsModelMax] <- 0;
}
else{
matVt[j+1, 1:lagsModelMax] <- 1;
}
}
j[] <- j + 1;
}
#### Seasonal ####
if(modelIsSeasonal){
if(any(initialSeasonalEstimate)){
for(i in 1:componentsNumberETSSeasonal){
seasonalOcc <- matVt[j+i, 1:lagsModelSeasonal[i]];
if(Stype=="M"){
# Transform this into the "multiplicative" seasonality
seasonalOcc[] <- seasonalOcc / levelOriginal + 1;
}
else{
# If additive, normalise
seasonalOcc[] <- seasonalOcc - mean(seasonalOcc);
}
matVt[j+i, 1:lagsModelSeasonal[i]] <- seasonalOcc;
}
}
j[] <- j + componentsNumberETSSeasonal;
}
}
#### ARIMA ####
# ARIMA initial states live in the same raw state-space as the level
# *after* the level has been mapped onto the model-native scale; they are
# not probabilities. Running occurrenceTransformer() on them turns the
# default seed of 0 into log(0) = -Inf for Etype="A", which corrupts the
# initial parameter vector handed to nloptr.
if(arimaModel){
j[] <- j + componentsNumberARIMA;
}
#### Xreg ####
# xreg coefficients are regression weights, not probabilities. They can be
# negative; running log(value/(1-value)) on a negative coefficient gives
# NaN, which corrupts the initial parameter vector handed to nloptr.
if(xregModel){
j[] <- j + xregNumber;
}
#### Constant ####
# Same reasoning as for xreg/ARIMA: the constant is on the same scale as
# the (already-transformed) level, so transforming it again is a category
# error. Leaving it untouched.
return(matVt);
}
omLinkFunction <- function(x, Etype, occurrence){
switch(occurrence,
"odds-ratio" = switch(Etype,
"M" = x / (1 + x),
"A" = exp(x) / (1 + exp(x)),
x / (1 + x)),
"inverse-odds-ratio" = switch(Etype,
"M" = 1 / (1 + x),
"A" = 1 / (1 + exp(x)),
1 / (1 + x)),
"fixed" =,
"direct" = pmin(pmax(x, 0), 1),
x);
}
# File-scope occurrence-model cost function. Used by omEstimator() during
# optimisation and by the modelDo=="use" branch of om() for FI computation.
# Takes all dependencies as explicit arguments — no closure state.
omCF_local <- function(B,
etsModel, Etype, Ttype, Stype,
modelIsTrendy, modelIsSeasonal,
componentsNumberETS, componentsNumberETSNonSeasonal,
componentsNumberETSSeasonal, componentsNumberARIMA,
lags, lagsModel, lagsModelMax, lagsModelAll,
indexLookupTable, profilesRecentTable,
matVt, matWt, matF, vecG,
persistenceEstimate, persistenceLevelEstimate,
persistenceTrendEstimate, persistenceSeasonalEstimate,
persistenceXregEstimate, phiEstimate,
initialType, initialEstimate,
initialLevelEstimate, initialTrendEstimate,
initialSeasonalEstimate, initialArimaEstimate,
initialXregEstimate, initialArimaNumber,
arimaModel, arEstimate, maEstimate,
arOrders, iOrders, maOrders,
arRequired, maRequired, armaParameters,
nonZeroARI, nonZeroMA, arimaPolynomials,
arPolynomialMatrix, maPolynomialMatrix,
xregModel, xregNumber,
xregParametersMissing, xregParametersIncluded,
xregParametersEstimated, xregParametersPersistence,
constantRequired, constantEstimate,
bounds, regressors, loss,
ot, otLogical, obsInSample,
nIterations, refineHead,
occurrence, occurrenceChar,
adamCpp,
lambda = 0, lossFunction = NULL){
adamElements <- adam_filler(B,
etsModel, Etype, Ttype, Stype,
modelIsTrendy, modelIsSeasonal,
componentsNumberETS, componentsNumberETSNonSeasonal,
componentsNumberETSSeasonal, componentsNumberARIMA,
lags, lagsModel, lagsModelMax,
matVt, matWt, matF, vecG,
persistenceEstimate, persistenceLevelEstimate,
persistenceTrendEstimate, persistenceSeasonalEstimate,
persistenceXregEstimate, phiEstimate,
initialType, initialEstimate,
initialLevelEstimate, initialTrendEstimate,
initialSeasonalEstimate, initialArimaEstimate,
initialXregEstimate,
arimaModel, arEstimate, maEstimate,
arOrders, iOrders, maOrders,
arRequired, maRequired, armaParameters,
nonZeroARI, nonZeroMA, arimaPolynomials,
xregModel, xregNumber,
xregParametersMissing, xregParametersIncluded,
xregParametersEstimated, xregParametersPersistence,
constantEstimate, adamCpp,
constantRequired, initialArimaNumber);
penalty <- adam_bounds_checker(adamElements, adamElements$arimaPolynomials,
bounds,
etsModel, modelIsTrendy, modelIsSeasonal,
componentsNumberETS, componentsNumberETSNonSeasonal,
componentsNumberETSSeasonal,
arimaModel, arEstimate, maEstimate,
xregModel, regressors, xregNumber,
componentsNumberARIMA,
lagsModelAll, obsInSample,
arPolynomialMatrix, maPolynomialMatrix,
phiEstimate);
if(penalty != 0){
return(penalty);
}
profilesRecentTable[] <- adamElements$matVt[, 1:lagsModelMax];
adamFitted <- adamCpp$fit(adamElements$matVt, adamElements$matWt,
adamElements$matF, adamElements$vecG,
indexLookupTable, profilesRecentTable,
as.numeric(ot), as.numeric(ot),
any(initialType == c("complete","backcasting")),
nIterations, refineHead, occurrenceChar);
yFitted <- omLinkFunction(adamFitted$fitted, Etype, occurrence);
if(any(is.nan(yFitted)) || any(yFitted<0) || any(yFitted>1)){
return(1e+300);
}
# Loss dispatch — mirrors R/adam.R:885-940 single-step block. ``errors``
# are on the probability scale (``ot - yFitted``) since the OM target is
# binary. The LASSO/RIDGE branch follows the same penalty structure as
# adam() (R/adam.R:894-937).
errors <- as.numeric(ot) - yFitted;
if(loss == "custom"){
CFValue <- lossFunction(actual=as.numeric(ot), fitted=yFitted, B=B);
}
else if(loss == "likelihood"){
CFValue <- -(sum(log(yFitted[otLogical])) + sum(log(1 - yFitted[!otLogical])));
}
else if(loss == "MSE"){
CFValue <- mean(errors^2);
}
else if(loss == "MAE"){
CFValue <- mean(abs(errors));
}
else if(loss == "HAM"){
CFValue <- mean(sqrt(abs(errors)));
}
else if(any(loss == c("LASSO","RIDGE"))){
# Trim initials out of B for the penalty — same convention as
# adam() (R/adam.R:897-916). For non-ARIMA OM the typical B is just
# the persistence; we keep that intact and drop nothing if there
# are no initials in B.
BPenalty <- B;
errorTerm <- (1 - lambda) * sqrt(mean(errors^2));
if(loss == "LASSO"){
CFValue <- errorTerm + lambda * sum(abs(BPenalty));
} else {
CFValue <- errorTerm + lambda * sqrt(sum(BPenalty^2));
}
}
else {
# Fallback to MSE — preserves the pre-existing behaviour for any
# unknown loss string that match.arg didn't catch upstream.
CFValue <- mean(errors^2);
}
return(CFValue)
}
#' @export
coefbootstrap.om <- function(object, nsim=1000, size=floor(0.75*nobs(object)),
replace=FALSE, prob=NULL, parallel=FALSE,
method=c("cr","dsr"), ...){
startTime <- Sys.time();
cl <- match.call();
yInSample <- actuals(object);
method <- match.arg(method);
if(is.numeric(parallel)){
nCores <- parallel;
parallel <- TRUE;
}
else if(is.logical(parallel) && parallel){
nCores <- min(parallel::detectCores() - 1, nsim);
}
if(parallel){
if(!requireNamespace("foreach", quietly = TRUE)){
stop("In order to run the function in parallel, 'foreach' package must be installed.", call. = FALSE);
}
if(!requireNamespace("parallel", quietly = TRUE)){
stop("In order to run the function in parallel, 'parallel' package must be installed.", call. = FALSE);
}
if(Sys.info()['sysname']=="Windows"){
if(requireNamespace("doParallel", quietly = TRUE)){
cluster <- parallel::makeCluster(nCores);
doParallel::registerDoParallel(cluster);
}
else{
stop("Sorry, but in order to run the function in parallel, you need 'doParallel' package.",
call. = FALSE);
}
}
else{
if(requireNamespace("doMC", quietly = TRUE)){
doMC::registerDoMC(nCores);
cluster <- NULL;
}
else if(requireNamespace("doParallel", quietly = TRUE)){
cluster <- parallel::makeCluster(nCores);
doParallel::registerDoParallel(cluster);
}
else{
stop("Sorry, but in order to run the function in parallel, you need either 'doMC' (prefered) or 'doParallel' packages.",
call. = FALSE);
}
}
}
# Coefficients of the model
coefficientsOriginal <- coef(object);
nVariables <- length(coefficientsOriginal);
variablesNames <- names(coefficientsOriginal);
obsInsample <- nobs(object);
coefBootstrap <- matrix(0, nsim, nVariables, dimnames=list(NULL, variablesNames));
indices <- c(1:obsInsample);
# Form the call for om()
newCall <- object$call;
newCall$silent <- TRUE;
if(newCall[[1]]=="auto.om"){
newCall[[1]] <- as.symbol("om");
}
newCall$holdout <- FALSE;
newCall$model <- modelType(object);
newCall$occurrence <- object$occurrence;
if(!is.null(object$call$orders$select)){
newCall$orders <- orders(object);
newCall$orders$select <- FALSE;
}
newCall$constant <- !is.null(object$constant);
lags <- lags(object);
newCall$lags <- lags;
newCall$B <- object$B;
newCall$lb <- rep(-Inf, length(object$B));
newCall$ub <- rep(Inf, length(object$B));
newCall$data <- object$data;
regressionPure <- substr(object$model,1,10)=="Regression";
obsMinimum <- max(lags, nVariables) + 2;
if(obsMinimum>=obsInsample && method=="cr"){
warning("Not enough observations to do Case Resampling bootstrap. Changing method to 'dsr'.",
call.=FALSE, immediate.=TRUE);
method <- "dsr";
}
# If this is backcasting, do sampling with moving origin
changeOrigin <- any(object$initialType==c("backcasting","complete"));
sampler <- function(indices,size,replace,prob,regressionPure=FALSE,changeOrigin=FALSE){
if(regressionPure){
return(sample(indices,size=size,replace=replace,prob=prob));
}
else{
indices <- c(1:ceiling(runif(1,obsMinimum,obsInsample)));
startingIndex <- 0;
if(changeOrigin){
startingIndex <- floor(runif(1,0,obsInsample-max(indices)));
}
return(startingIndex+indices);
}
}
#### Bootstrap the data
if(method=="dsr"){
#### Data Shape Replication bootstrap (on the 0/1 occurrence response)
type <- "additive";
if(all(yInSample>=0) && any(yInSample>1)){
type[] <- "multiplicative";
}
dataBoot <- dsrboot(yInSample, nsim=nsim, type=type, intermittent=FALSE);
if(!parallel){
for(i in 1:nsim){
newCall$data <- dataBoot$boot[,i];
testModel <- tryCatch(suppressWarnings(eval(newCall)), error=function(e) NULL);
if(!is.null(testModel)){
coefBootstrap[i,variablesNames %in% names(coef(testModel))] <- coef(testModel);
}
}
}
else{
coefBootstrapParallel <- foreach::`%dopar%`(foreach::foreach(i=1:nsim),{
newCall$data <- dataBoot$boot[,i];
testModel <- tryCatch(eval(newCall), error=function(e) NULL);
if(is.null(testModel)){ return(NULL); }
return(coef(testModel));
})
for(i in 1:nsim){
if(!is.null(coefBootstrapParallel[[i]])){
coefBootstrap[i,variablesNames %in% names(coefBootstrapParallel[[i]])] <- coefBootstrapParallel[[i]];
}
}
}
}
else{
#### Case Resampling bootstrap
# A subsample can be too short / too sparse (few non-zero occurrences)
# for the model to be re-estimated. Such draws are resampled (up to
# maxAttempts) so every replicate is a genuine re-estimation with the
# provided B as the starting point.
maxAttempts <- 100L;
refitOM <- function(){
testModel <- NULL; attempt <- 0L;
while(is.null(testModel) && attempt < maxAttempts){
attempt <- attempt + 1L;
subsetValues <- sampler(indices,size,replace,prob,regressionPure,changeOrigin);
newCall$data <- object$data[subsetValues];
testModel <- tryCatch(suppressWarnings(eval(newCall)), error=function(e) NULL);
if(!is.null(testModel) && length(coef(testModel))==0){ testModel <- NULL; }
}
return(testModel);
}
if(!parallel){
for(i in 1:nsim){
testModel <- refitOM();
if(!is.null(testModel)){
coefBootstrap[i,variablesNames %in% names(coef(testModel))] <- coef(testModel);
}
}
}
else{
coefBootstrapParallel <- foreach::`%dopar%`(foreach::foreach(i=1:nsim),{
testModel <- refitOM();
if(is.null(testModel)){ return(NULL); }
return(coef(testModel));
})
for(i in 1:nsim){
if(!is.null(coefBootstrapParallel[[i]])){
coefBootstrap[i,variablesNames %in% names(coefBootstrapParallel[[i]])] <- coefBootstrapParallel[[i]];
}
}
}
}
if(parallel && !is.null(cluster)){
parallel::stopCluster(cluster);
}
# Get rid of NAs. They mean "zero"
coefBootstrap[is.na(coefBootstrap)] <- 0;
colnames(coefBootstrap) <- names(coefficientsOriginal);
# Centre the coefficients for the calculation of the vcov
coefvcov <- coefBootstrap - matrix(coefficientsOriginal, nsim, nVariables, byrow=TRUE);
return(structure(list(vcov=(t(coefvcov) %*% coefvcov)/nsim,
coefficients=coefBootstrap, method=method,
nsim=nsim, size=NA, replace=NA, prob=NA,
parallel=parallel, model=object$call[[1]], timeElapsed=Sys.time()-startTime),
class="bootstrap"));
}
#' @export
vcov.om <- function(object, bootstrap=FALSE, heuristics=NULL, ...){
ellipsis <- list(...);
if(!is.null(heuristics) && is.numeric(heuristics)){
return(diag(abs(coef(object)) * heuristics));
}
if(bootstrap){
return(coefbootstrap(object, ...)$vcov);
}
h <- if(any(!is.na(object$forecast))) length(object$forecast) else 0;
stepSize <- if(is.null(ellipsis$stepSize)) {
.Machine$double.eps^(1/4);
} else {
ellipsis$stepSize;
};
modelReturn <- suppressWarnings(
om(object$data, h=h, model=object,
formula=formula(object), FI=TRUE, stepSize=stepSize));
if(is.null(modelReturn$FI)){
stop("Could not compute Fisher Information for this om model. ",
"Try a different stepSize.", call.=FALSE);
}
# Rows / cols that are all zero (or contain NaN) carry no information.
brokenVariables <- apply(modelReturn$FI==0, 1, all) |
apply(is.nan(modelReturn$FI), 1, any);
if(any(brokenVariables)){
modelReturn <- suppressWarnings(
om(object$data, h=h, model=object,
formula=formula(object), FI=TRUE,
stepSize=.Machine$double.eps^(1/6)));
brokenVariables <- apply(modelReturn$FI==0, 1, all);
}
if(any(is.nan(modelReturn$FI))){
stop("Fisher Information contains NaN; try a different stepSize ",
"(e.g. stepSize=1e-6).", call.=FALSE);
}
if(any(eigen(modelReturn$FI, only.values=TRUE)$values < 0)){
warning("Observed Fisher Information is not positive semi-definite; ",
"covariance matrix may be unreliable.", call.=FALSE);
}
FIMatrix <- modelReturn$FI[!brokenVariables, !brokenVariables, drop=FALSE];
vcovMatrix <- try(chol2inv(chol(FIMatrix)), silent=TRUE);
if(inherits(vcovMatrix, "try-error")){
vcovMatrix <- try(solve(FIMatrix, diag(ncol(FIMatrix)), tol=1e-20),
silent=TRUE);
if(inherits(vcovMatrix, "try-error")){
warning("Hessian is singular; cannot invert.", call.=FALSE);
vcovMatrix <- diag(1e+100, ncol(FIMatrix));
}
}
modelReturn$FI[!brokenVariables, !brokenVariables] <- vcovMatrix;
modelReturn$FI[brokenVariables, ] <- Inf;
modelReturn$FI[, brokenVariables] <- Inf;
diag(modelReturn$FI) <- abs(diag(modelReturn$FI));
return(modelReturn$FI);
}
#' @rdname forecast.smooth
#' @export
forecast.om <- function(object, h=10, ...){
# Intervals on the probability scale are not implemented yet, so the
# underlying forecast.adam() call is forced to interval="none". The
# remaining slots (level, side, cumulative) are set to their defaults so
# the returned structure is shape-compatible with forecast.adam().
fc <- forecast.adam(object, h=h, interval="none",
level=0.95, side="both", cumulative=FALSE, ...);
Etype <- errorType(object);
occurrence <- object$occurrence;
fc$mean[] <- omLinkFunction(fc$mean, Etype, occurrence);
if(occurrence %in% c("odds-ratio","inverse-odds-ratio") && Etype == "A"){
fc$mean[is.nan(fc$mean)] <- 1;
}
return(fc);
}
#' @export
actuals.om <- function(object, ...){
if(is.null(object$data)){
return(NULL);
}
yObs <- if(is.data.frame(object$data) || is.matrix(object$data)) object$data[,1] else object$data;
yObs[] <- (yObs != 0) * 1;
return(yObs);
}
#' @importFrom stats sigma
#' @export
sigma.om <- function(object, ...){
# `sigma.adam` dispatches on `object$distribution`, which is "plogis"
# for occurrence models -- not in its switch table, so it would
# return `numeric(0)`. Use the link-scale RMS of the residuals
# instead: occurrence residuals live on the logit / log-odds scale,
# so their RMS is a meaningful scale parameter for the underlying
# ETS. Without this, `s2 = NA^2 = NA` propagates through `covarAnal`
# and breaks `multicov(om_obj)` and downstream callers.
e <- residuals(object);
if(is.null(e) || length(e)==0) return(NA_real_);
return(sqrt(mean(e^2, na.rm=TRUE)));
}
#' @export
print.om <- function(x, ...){
cat("Occurrence model\n");
print.adam(x, ...);
}
#' @export
summary.om <- function(object, ...){
cat("Occurrence model\n");
summary.adam(object, ...);
}
#' @export
print.omCombined <- function(x, ...){
cat("Occurrence model\n");
print.adamCombined(x, ...);
}
#' @export
summary.omCombined <- function(object, ...){
cat("Occurrence model\n");
summary.adamCombined(object, ...);
}
#' @export
forecast.omCombined <- function(object, h=NULL, ...){
# Interval-related slots are fixed internally so the returned structure
# matches forecast.adamCombined's field set; intervals on the probability
# scale are not implemented yet.
interval <- "none";
level <- 0.95;
side <- "both";
cumulative <- FALSE;
obsInSample <- nobs(object);
yClasses <- class(actuals(object));
if(is.null(h)){
h <- length(object$forecast);
}
nLevels <- length(level);
if(any(yClasses == "ts")){
yForecastStart <- time(actuals(object))[obsInSample] + deltat(actuals(object));
yFrequency <- frequency(actuals(object));
yForecast <- ts(rep(0, h), start=yForecastStart, frequency=yFrequency);
yLower <- yUpper <- ts(matrix(NA_real_, h, nLevels),
start=yForecastStart, frequency=yFrequency);
} else {
yIndex <- time(actuals(object));
yForecastIndex <- yIndex[obsInSample] + diff(tail(yIndex, 2)) * (1:h);
yForecast <- zoo(rep(0, h), order.by=yForecastIndex);
yLower <- yUpper <- zoo(matrix(NA_real_, h, nLevels),
order.by=yForecastIndex);
}
object$ICw[object$ICw < 1e-2] <- 0;
object$ICw[] <- object$ICw / sum(object$ICw);
for(i in seq_along(object$models)){
if(object$ICw[i] == 0){
next;
}
fc_i <- forecast.adam(object$models[[i]], h=h, interval=interval,
level=level, side=side, cumulative=cumulative, ...);
Etype_i <- errorType(object$models[[i]]);
fc_i$mean[] <- omLinkFunction(fc_i$mean, Etype_i, object$occurrence);
yForecast[] <- yForecast + fc_i$mean * object$ICw[i];
}
object$models <- NULL;
return(structure(list(mean=yForecast,
lower=yLower, upper=yUpper,
model=object,
level=level, interval=interval,
side=side, cumulative=cumulative, h=h),
class=c("adam.forecast","smooth.forecast","forecast")));
}
#' @importFrom stats rstandard
#' @export
rstandard.om <- function(model, ...){
obs <- nobs(model);
df <- obs - nparam(model);
p <- as.numeric(model$fitted);
e <- as.numeric(model$residuals);
return(e / sqrt(p * (1 - p)) * sqrt(obs / df));
}
#' @importFrom stats rstudent
#' @export
rstudent.om <- function(model, ...){
obs <- nobs(model);
df <- obs - nparam(model) - 1;
p <- as.numeric(model$fitted);
e <- as.numeric(model$residuals);
return(e / sqrt(p * (1 - p)) * sqrt(obs / df));
}
#' @title Simulate methods for occurrence (om/omg) state-space models
#' @description Re-simulates probabilities and occurrence indicators
#' from a fitted \code{om} or \code{omg} model. The latent ETS is
#' simulated via the shared C++ kernel (the same one
#' \code{simulate.adam} uses), the latent series is mapped to a
#' probability via \code{omLinkFunction} (or \code{omgLinkFunction}
#' for \code{omg}), and a binomial draw with that probability gives
#' the 0/1 occurrence series.
#'
#' @param object An object of class \code{om} (or \code{omg}).
#' @param nsim Number of simulated series to draw.
#' @param seed Optional integer; forwarded to \code{set.seed} at the
#' start of the simulation. Matches \code{stats::simulate}'s
#' generic signature. When \code{NULL} (default) the global RNG
#' state is used unchanged.
#' @param obs Number of observations per simulated series. Defaults
#' to the in-sample length.
#' @param ... Currently unused; kept for forward compatibility.
#'
#' @return An S3 list of class \code{c("om.sim","oes.sim","smooth.sim")}
#' (or \code{c("omg.sim","oes.sim","smooth.sim")} for \code{omg})
#' with fields:
#' \describe{
#' \item{\code{$probability}}{Simulated probability series of
#' shape \code{(obs, nsim)} -- the equivalent of
#' \code{sim.oes()}'s \code{$probability} output.}
#' \item{\code{$data}}{0/1 occurrence indicators of shape
#' \code{(obs, nsim)}, drawn via \code{rbinom} with the
#' simulated probability.}
#' \item{\code{$states}, \code{$residuals}}{Latent state cube
#' and the errors used internally.}
#' \item{\code{$model}, \code{$occurrence}}{Identifiers carried
#' over from the fit.}
#' \item{\code{$latent}}{Pre-link state-space output --
#' internal, used by \code{simulate.omg} to combine sub-models.}
#' }
#'
#' @details
#' \code{print()} on the returned object dispatches to
#' \code{print.oes.sim} via the inherited \code{"oes.sim"} class.
#'
#' @examples
#' \dontrun{
#' set.seed(7)
#' y <- rbinom(120, 1, prob=0.3 + 0.005*(1:120))
#' m <- om(y, model="MNN", occurrence="odds-ratio", silent=TRUE)
#' sim <- simulate(m, nsim=5, seed=42)
#' range(sim$probability)
#' table(sim$data)
#' }
#'
#' @rdname simulate.om
#' @export
simulate.om <- function(object, nsim=1, seed=NULL, obs=nobs(object), ...){
startTime <- Sys.time();
if(!is.null(seed)){
set.seed(seed);
}
# 1. Simulate the latent ETS via the shared helper. ``om`` inherits
# from ``adam`` so all required fields are present on ``object``.
inner <- simulateADAMCore(object, nsim=nsim, obs=obs, ...);
# 2. Apply the inverse link: latent state -> probability.
Etype <- errorType(object);
occurrence <- object$occurrence;
obsInSample <- inner$obsInSample;
latentMatrix <- matrix(inner$data, obsInSample, nsim);
probability <- matrix(omLinkFunction(c(latentMatrix), Etype, occurrence),
obsInSample, nsim);
# Numerical guard: omLinkFunction's "fixed" / "direct" branches
# already clip, but odds-ratio paths can overshoot under extreme
# latent noise. Clamp uniformly here.
probability[] <- pmin(pmax(probability, 0), 1);
# 3. Draw 0/1 occurrence indicators.
occurrenceData <- matrix(rbinom(obsInSample*nsim, 1, c(probability)),
obsInSample, nsim);
# 4. Preserve time series structure from the in-sample series.
yInSample <- actuals(object);
yClasses <- class(yInSample);
colnames(probability) <- paste0("nsim", c(1:nsim));
colnames(occurrenceData) <- paste0("nsim", c(1:nsim));
if(any(yClasses=="zoo")){
yIndex <- time(yInSample);
yIndexDiff <- diff(head(yIndex, 2));
yTime <- yIndex[1] + yIndexDiff * c(1:(obsInSample-1));
probability <- zoo(probability, order.by=yTime);
occurrenceData <- zoo(occurrenceData, order.by=yTime);
}
else{
probability <- ts(probability, start=start(yInSample),
frequency=frequency(yInSample));
occurrenceData <- ts(occurrenceData, start=start(yInSample),
frequency=frequency(yInSample));
}
# Per-series log-likelihood, mirroring ``sim.oes`` (R/simoes.R:138-143).
# Operate on a plain matrix so the ts/zoo attributes don't get
# mangled by ``pmax``.
probMat <- matrix(as.numeric(probability), obsInSample, nsim);
safeProb <- pmax(probMat, .Machine$double.eps);
if(nsim==1){
logLik <- sum(log(safeProb));
}
else{
logLik <- colSums(log(safeProb));
}
return(structure(list(timeElapsed = Sys.time() - startTime,
model = object$model,
occurrence = occurrence,
probability = probability,
data = occurrenceData,
ot = occurrenceData, # alias used by print.oes.sim
states = inner$states,
residuals = inner$matErrors,
latent = latentMatrix,
logLik = logLik,
other = inner$ellipsis),
class=c("om.sim","oes.sim","smooth.sim")));
}
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.