Newer
Older
#' @export
forecastmodel <- R6::R6Class("forecastmodel", public = list(
#----------------------------------------------------------------
# Fields used for setting up the model
#
# The expression (as character) used for generating the regprm
# Regression parameters for the function used for fitting (rls, ls, etc.)
prmbounds = as.matrix(data.frame(lower=NA, init=NA, upper=NA)),
# List of inputs (which are R6 objects) (note the "cloning of list of reference objects" issue below in deep_clone function)
#----------------------------------------------------------------
# Fields to be used when the model is fitted
#
# The horizons to fit for
# The (transformation stage) parameters (only the ones set in last call of insert_prm())
# Stores the maxlagAR past values of y for the update when new obs becomes available
#----------------------------------------------------------------
# Contructor function
#----------------------------------------------------------------
# Add inputs to the model
add_inputs = function(...){
dots <- list(...)
for (i in 1:length(dots)){
self$inputs[[ nams(dots)[i] ]] <- input_class$new(dots[[i]], model=self)
}
},
#----------------------------------------------------------------
# Add the expression (as character) which generates the regression parameters
add_regprm = function(regprmexpr){
self$regprmexpr <- regprmexpr
self$regprm <- eval(parse(text = self$regprmexpr))
},
#----------------------------------------------------------------
# Add the transformation parameters and bounds for optimization
add_prmbounds = function(...) {
dots <- list(...)
for (i in 1:length(dots)) {
nm <- names(dots)[i]
if (nm %in% rownames(self$prmbounds)) {
self$prmbounds[nm, ] <- dots[[i]]
} else {
if(nrow(self$prmbounds) == 1 & is.na(self$prmbounds[1,2])){
self$prmbounds[1, ] <- dots[[i]]
}else{
self$prmbounds <- rbind(self$prmbounds, dots[[i]])
}
rownames(self$prmbounds)[nrow(self$prmbounds)] <- nm
}
}
},
# Get the transformation parameters (set for optimization)
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
get_prmbounds = function(nm){
if(nm == "init"){
if(is.null(dim(self$prmbounds))){
val <- self$prmbounds[nm]
}else{
val <- self$prmbounds[ ,nm]
if(is.null(nams(val))){
nams(val) <- rownames(self$prmbounds)
}
}
}
if(nm == "lower"){
if("lower" %in% nams(self$prmbounds)){
val <- self$prmbounds[,"lower"]
if(is.null(nams(val))){
nams(val) <- rownames(self$prmbounds)
}
}else{
val <- -Inf
}
}
if(nm == "upper"){
if("upper" %in% nams(self$prmbounds)){
val <- self$prmbounds[,"upper"]
if(is.null(nams(val))){
nams(val) <- rownames(self$prmbounds)
}
}else{
val <- Inf
}
}
names(val) <- row.names(self$prmbounds)
return(val)
},
#----------------------------------------------------------------
# Insert the transformation parameters prm in the input expressions and regression expressions, and keep them (simply string manipulation)
insert_prm = function(prm){
# If just NA or NULL given, then don't do anything
if(is.null(prm) | (is.na(prm)[1] & length(prm) == 1)){
return(NULL)
}
# MUST INCLUDE SOME checks here and print useful messages if something is not right
if(any(is.na(prm))){ stop(pst("None of the parameters (in prm) must be NA: prm=",prm)) }
# Find if any opt parameters, first the one with "__" hence for the inputs
if (length(pinputs) == 0 & length(prm) > 0) {
preg <- prm
} else {
preg <- prm[-grep("__",nams(prm))]
}
if (length(pinputs)) {
pnms <- unlist(getse(strsplit(nams(pinputs),"__"), 1))
pprm <- unlist(getse(strsplit(nams(pinputs),"__"), 2))
for(i in 1:length(self$inputs)){
for(ii in 1:length(pnms)){
self$inputs[[i]]$expr <- private$replace_prmvalue(name = pprm[ii],
value = pinputs[ii],
expr = self$inputs[[i]]$expr)
}
}
}
}
# For the regression parameters, insert from prm if any found
if (length(preg) & any(!is.na(self$regprmexpr))) {
nams(preg)
for(i in 1:length(preg)){
self$regprmexpr <- private$replace_prmvalue(name = nams(preg)[i],
value = preg[i],
expr = self$regprmexpr)
}
}
self$regprm <- eval(parse(text = self$regprmexpr))
},
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
#----------------------------------------------------------------
# Return the values of the parameter names given
get_prmvalues = function(prmnames){
#
regprm <- eval(parse(text = self$regprmexpr))
# From the input parameters
val <- sapply(prmnames, function(nm){
if(length(grep("__",nm))){
tmp <- strsplit(nm, "__")[[1]]
if(tmp[1] %in% names(self$inputs)){
return(as.numeric(private$get_exprprmvalue(tmp[2], self$inputs[[tmp[1]]]$expr)))
}else{
return(NA)
}
}else{
if(nm %in% names(regprm)){
return(as.numeric(regprm[nm]))
}else{
return(NA)
}
}
})
return(val)
},
#----------------------------------------------------------------
#----------------------------------------------------------------
# Function for transforming the input data to the regression data
# Evaluate for each input the expresssion to generate the model input data
if(class(L)[1]=="matrix"){ return(list(as.data.frame(L))) }
if(class(L)[1]=="data.frame"){ return(list(L)) }
if(class(L)[1]!="list"){ stop(pst("The value returned from evaluating: ",input$expr,", was not a matrix, data.frame or a list of them."))}
if(class(L[[1]])[1]=="matrix"){ return(lapply(L, function(mat){ return(as.data.frame(mat)) })) }
# Make it a data.list with no subsubelements (it's maybe not a data.list, since it miss "t", however to take subsets etc., it must be a data.list)
L <- flattenlist(L)
class(L) <- "data.list"
#----------------------------------------------------------------
# Resets the input states
lapply(self$inputs, function(input){
input$state_reset()
})
#----------------------------------------------------------------
# Check if the model and data is setup correctly
# some checks are done here, maybe more should be added (??also when transforming inputs, if something goes wrong its caught and message is printed)
#
# ################################
# First check if the output is set correctly
if( is.na(self$output) ){
stop("Model output is NA, it must be set to the name of a variable in the data.list used.")
}
if( !(self$output %in% names(data)) ){
stop("Model output '",self$output,"' is not in the data provided: It must be set to the name of a variable in the data.list used.")
}
if( !(is.numeric(data[[self$output]])) ){
stop("The model output '",self$output,"' is not a numeric. It has to be a vector of numbers.")
}
if( length(data[[self$output]]) != length(data$t) ){
stop("The length of the model output '",self$output,"' is ",length(data[[self$output]]),", which is not equal to the length of the time vector (t), which is ",length(data$t))
}
# ################################
# Check that the kseq is set in the model
if( !is.numeric(self$kseq) ){
stop("'model$kseq' is not set. Must be an integer (or numeric) vector.")
}
# ################################
# Check all input variables are correctly set data
nms <- all.vars(parse(text=self$inputs[[i]]$expr[[1]]))
for(nm in nms){
if(class(data[[nm]]) %in% c("data.frame","matrix")){
if(!all(self$kseq %in% as.integer(gsub("k","",names(data[[nm]]))))){
missingk <- which(!self$kseq %in% as.integer(gsub("k","",names(data[[nm]]))))
stop("The input variable '",nm,"' doesn't have all needed horizons.\nIt has ",pst(names(data[[nm]]),collapse=","),"\nIt is missing ",pst("k",self$kseq[missingk],collapse=","))
}
if( nrow(data[[nm]]) != length(data$t) ){
stop(pst("The input variable '",nm,"' doesn't have the same number of observations as time vector 't' in the data. It has ",nrow(data[[nm]]),", but 't' has ",length(data$t)))
}
}else if(class(data[[nm]]) == "numeric"){
if( length(data[[nm]]) != length(data$t) ){
stop("The input variable '",nm,"' doesn't have the same number of observations as time vector 't' in the data. It has ",length(data[[nm]]),", but 't' has ",length(data$t))
}
}else{
stop("The variable '",nm,"' is missing in data, or it has the wrong class.\nIt must be class: data.frame, matrix or vector.\nIt is needed for the input expression '",self$inputs[[i]]$expr[[1]],"'")
}
}
}
},
# The inputs are cloned now, however the model fields in the inputs have not been updated, so do that
if(length(newmodel$inputs) > 0){
for(i in 1:length(newmodel$inputs)){
newmodel$inputs[[i]]$model <- newmodel
}
}
return(newmodel)
}
#----------------------------------------------------------------
# Private functions
#----------------------------------------------------------------
# Replace the value in "name=value" in expr
expr <- pst(substr(expr,1,pos-1), "=", value, substr(expr,pos+pos2-1,nchar(expr)))
# Print? Not used now
#if(printout){ message(names(value),"=",value,", ",sep="")}
#----------------------------------------------------------------
get_exprprmvalue = function(name, expr){
#name <- "degree"
#expr <- "bspline(tday, Boundary.knots = c(start=6,18), degree = 5, intercept=TRUE) %**% ones() + 2 + ones()"
#expr <- "one()"
expr <- gsub(" ", "", expr)
# First make regex
pattern <- gsub("\\.", ".*", name)
# Try to find it in the input
pos <- regexpr(pattern, expr)
# Only replace if prm was found
if(pos>0){
pos <- c(pos+attr(pos,"match.length"))
# Find the substr to replace with the prm value
(tmp <- substr(expr, pos, nchar(expr)))
pos2 <- regexpr(",|)", tmp)
return(substr(tmp, 2, pos2-1))
}else{
return(NA)
}
},
#----------------------------------------------------------------
#----------------------------------------------------------------
# For deep cloning, in order to get the inputs list of R6 objects copied
# With x$clone(deep=TRUE) is called, the deep_clone gets invoked once for
# each field, with the name and value.
# Don't clone the inputs deep, since they have the model as a field and then it gets in an infinitie loop!
# But have to update the model references, so therefore the function above "clone_deep" must be used
# # `a` is an environment, so use this quick way of copying
# list2env(as.list.environment(value, all.names = TRUE),
# parent = emptyenv())
)
)
#' Prints a forecast model
#'
#' A simple print out of the model output and inputs
#'
#' @title Print forecast model
#' @param x A forecastmodel object
#' @param ... Not used.
#'
print.forecastmodel <- function(x, ...){
model <- x
if(length(model$inputs) == 0 ){
}else{
cat(names(model$inputs)[1],"=",model$inputs[[1]]$expr,"\n")
if(length(model$inputs) > 1){
for(i in 2:length(model$inputs)){
cat(" ",names(model$inputs)[i],"=",model$inputs[[i]]$expr,"\n")
}
}
cat("\n")