Skip to content
Snippets Groups Projects
forecastmodel.R 17 KiB
Newer Older
  • Learn to ignore specific revisions
  • pbac's avatar
    pbac committed
    #' @export
    forecastmodel <- R6::R6Class("forecastmodel", public = list(
    
    pbac's avatar
    pbac committed
        #----------------------------------------------------------------
        # Fields used for setting up the model
        # 
        # The expression (as character) used for generating the regprm
    
    pbac's avatar
    pbac committed
        regprmexpr = NA,
    
    pbac's avatar
    pbac committed
        # Regression parameters for the function used for fitting (rls, ls, etc.)
    
    pbac's avatar
    pbac committed
        regprm = list(), 
    
    pbac's avatar
    pbac committed
        # The off-line parameters
    
    pbac's avatar
    pbac committed
        prmbounds = as.matrix(data.frame(lower=NA, init=NA, upper=NA)),
    
    pbac's avatar
    pbac committed
        # List of inputs (which are R6 objects) (note the "cloning of list of reference objects" issue below in deep_clone function)
    
    pbac's avatar
    pbac committed
        inputs = list(),
    
    pbac's avatar
    pbac committed
        # Name of the output
    
    pbac's avatar
    pbac committed
        output = "y",
    
    pbac's avatar
    pbac committed
        # The range of the output to be used for cropping the output
    
    pbac's avatar
    pbac committed
        outputrange = NA,
    
    pbac's avatar
    pbac committed
        #----------------------------------------------------------------
    
    pbac's avatar
    pbac committed
    
        
    
    pbac's avatar
    pbac committed
        #----------------------------------------------------------------
        # Fields to be used when the model is fitted
        #
        # The horizons to fit for
    
    pbac's avatar
    pbac committed
        kseq = NA,
    
    pbac's avatar
    pbac committed
        # The (transformation stage) parameters (only the ones set in last call of insert_prm())
    
    pbac's avatar
    pbac committed
        prm = NA,
    
    pbac's avatar
    pbac committed
        # Stores the maximum lag for AR terms
    
    pbac's avatar
    pbac committed
        maxlagAR = NA,
    
    pbac's avatar
    pbac committed
        # Stores the maxlagAR past values of y for the update when new obs becomes available
    
    pbac's avatar
    pbac committed
        yAR = NA,
    
    pbac's avatar
    pbac committed
        # The fits, one for each k in kseq (simply a list with the latest fit)
    
    pbac's avatar
    pbac committed
        Lfits = list(),
    
    pbac's avatar
    pbac committed
        # Transformed input data (data.list with all inputs for regression)
    
    pbac's avatar
    pbac committed
        datatr = NA,
    
    pbac's avatar
    pbac committed
        #----------------------------------------------------------------
    
    pbac's avatar
    pbac committed
    
        
    
    pbac's avatar
    pbac committed
        #----------------------------------------------------------------
        # Contructor function
    
    pbac's avatar
    pbac committed
        initialize = function(){},
    
    pbac's avatar
    pbac committed
        #----------------------------------------------------------------
    
    pbac's avatar
    pbac committed
    
        
    
    pbac's avatar
    pbac committed
        #----------------------------------------------------------------    
        # Add inputs to the model
    
    pbac's avatar
    pbac committed
        add_inputs = function(...){
            dots <- list(...)
            for (i in 1:length(dots)){
                self$inputs[[ nams(dots)[i] ]] <- input_class$new(dots[[i]], model=self)
            }
        },
    
    pbac's avatar
    pbac committed
        #----------------------------------------------------------------
    
    pbac's avatar
    pbac committed
    
    
    pbac's avatar
    pbac committed
        #----------------------------------------------------------------
        # Add the expression (as character) which generates the regression parameters
    
    pbac's avatar
    pbac committed
        add_regprm = function(regprmexpr){
            self$regprmexpr <- regprmexpr
            self$regprm <- eval(parse(text = self$regprmexpr))
        },
    
    pbac's avatar
    pbac committed
        #----------------------------------------------------------------
    
    pbac's avatar
    pbac committed
    
        
    
    pbac's avatar
    pbac committed
        #----------------------------------------------------------------
        # Add the transformation parameters and bounds for optimization
    
    pbac's avatar
    pbac committed
        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
                }
            }
        },
    
    pbac's avatar
    pbac committed
        #----------------------------------------------------------------
    
    pbac's avatar
    pbac committed
    
    
    
    pbac's avatar
    pbac committed
        #----------------------------------------------------------------
    
    pbac's avatar
    pbac committed
        # Get the transformation parameters (set for optimization)
    
    pbac's avatar
    pbac committed
        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)
        },
    
    pbac's avatar
    pbac committed
        #----------------------------------------------------------------
    
    pbac's avatar
    pbac committed
    
    
    
    pbac's avatar
    pbac committed
        #----------------------------------------------------------------
        # Insert the transformation parameters prm in the input expressions and regression expressions, and keep them (simply string manipulation)
    
    pbac's avatar
    pbac committed
        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)
            }
    
    pbac's avatar
    pbac committed
            # MUST INCLUDE SOME checks here and print useful messages if something is not right
    
    pbac's avatar
    pbac committed
            if(any(is.na(prm))){ stop(pst("None of the parameters (in prm) must be NA: prm=",prm)) }
    
    pbac's avatar
    pbac committed
            # Keep the prm given
    
    pbac's avatar
    pbac committed
            self$prm <- prm
    
    pbac's avatar
    pbac committed
            # Find if any opt parameters, first the one with "__" hence for the inputs
    
    pbac's avatar
    pbac committed
            pinputs <- prm[grep("__",nams(prm))]
    
    pbac's avatar
    pbac committed
            # If none found for inputs, then the rest must be for regression
    
    pbac's avatar
    pbac committed
            if (length(pinputs) == 0 & length(prm) > 0) {
                preg <- prm
            } else {
                preg <- prm[-grep("__",nams(prm))]
            }
    
    pbac's avatar
    pbac committed
            # ################
            # For the inputs, insert from prm if any found
    
    pbac's avatar
    pbac committed
            if (length(pinputs)) {
                pnms <- unlist(getse(strsplit(nams(pinputs),"__"), 1))
                pprm <- unlist(getse(strsplit(nams(pinputs),"__"), 2))
    
    pbac's avatar
    pbac committed
                #
    
    pbac's avatar
    pbac committed
                for(i in 1:length(self$inputs)){
                    for(ii in 1:length(pnms)){
    
    pbac's avatar
    pbac committed
                        # Find if the input i have prefix match with the opt. parameter ii
    
    pbac's avatar
    pbac committed
                        if(pnms[ii]==nams(self$inputs)[i]){
    
    pbac's avatar
    pbac committed
                            # if the opt. parameter is in the expr, then replace
    
    pbac's avatar
    pbac committed
                            self$inputs[[i]]$expr <- private$replace_prmvalue(name = pprm[ii],
    
    pbac's avatar
    pbac committed
                                                                           value = pinputs[ii],
                                                                           expr = self$inputs[[i]]$expr)
                        }
                    }
                }
            }
    
    pbac's avatar
    pbac committed
            # ################
    
    pbac's avatar
    pbac committed
            # For the regression parameters, insert from prm if any found
    
    pbac's avatar
    pbac committed
            if (length(preg) & any(!is.na(self$regprmexpr))) {
                nams(preg)
                for(i in 1:length(preg)){
    
    pbac's avatar
    pbac committed
                    # if the opt. parameter is in the expr, then replace
    
    pbac's avatar
    pbac committed
                    self$regprmexpr <- private$replace_prmvalue(name = nams(preg)[i],
    
    pbac's avatar
    pbac committed
                                                             value = preg[i],
                                                             expr = self$regprmexpr)
                }
            }
            self$regprm <- eval(parse(text = self$regprmexpr))
        },
    
    pbac's avatar
    pbac committed
        #----------------------------------------------------------------
    
    pbac's avatar
    pbac committed
    
    
    
    pbac's avatar
    pbac committed
        #----------------------------------------------------------------
        # 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)
        },
        #----------------------------------------------------------------
    
    
    pbac's avatar
    pbac committed
        #----------------------------------------------------------------
        # Function for transforming the input data to the regression data
    
    pbac's avatar
    pbac committed
        transform_data = function(data){
    
    pbac's avatar
    pbac committed
            # Evaluate for each input the expresssion to generate the model input data
    
    pbac's avatar
    pbac committed
            L <- lapply(self$inputs, function(input){
    
    pbac's avatar
    pbac committed
                # Evaluate the expression (input$expr)
    
    pbac's avatar
    pbac committed
                L <- input$evaluate(data)
    
    pbac's avatar
    pbac committed
                # Must return a list
    
                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)) })) }
    
    pbac's avatar
    pbac committed
                return(flattenlist(L))
    
    pbac's avatar
    pbac committed
            })
    
    pbac's avatar
    pbac committed
            # 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"
    
    pbac's avatar
    pbac committed
            return(L)
        },
    
    pbac's avatar
    pbac committed
        #----------------------------------------------------------------
    
    pbac's avatar
    pbac committed
    
    
    
    pbac's avatar
    pbac committed
        #----------------------------------------------------------------
        # Resets the input states
    
    pbac's avatar
    pbac committed
        reset_state = function(){
    
    pbac's avatar
    pbac committed
            # Reset the inputs state
    
    pbac's avatar
    pbac committed
            lapply(self$inputs, function(input){
                input$state_reset()
            })
    
    pbac's avatar
    pbac committed
            # Reset stored data
    
    pbac's avatar
    pbac committed
            self$datatr <- NA
            self$yAR <- NA
        },
    
    pbac's avatar
    pbac committed
        #----------------------------------------------------------------
    
    pbac's avatar
    pbac committed
    
    
    
    pbac's avatar
    pbac committed
        #----------------------------------------------------------------
        # Check if the model and data is setup correctly
    
    pbac's avatar
    pbac committed
        check = function(data = NA){
    
    pbac's avatar
    pbac committed
            # 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
    
    pbac's avatar
    pbac committed
            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))
            }
    
    pbac's avatar
    pbac committed
            # ################################
            # Check that the kseq is set in the model
    
    pbac's avatar
    pbac committed
            if( !is.numeric(self$kseq) ){
                stop("'model$kseq' is not set. Must be an integer (or numeric) vector.")
            }
    
    pbac's avatar
    pbac committed
            # ################################
            # Check all input variables are correctly set data
    
    pbac's avatar
    pbac committed
            for(i in 1:length(self$inputs)){
    
    pbac's avatar
    pbac committed
                # Find all the variables in the expression
    
    pbac's avatar
    pbac committed
                nms <- all.vars(parse(text=self$inputs[[i]]$expr[[1]]))
                for(nm in nms){
                    if(class(data[[nm]]) %in% c("data.frame","matrix")){
    
    pbac's avatar
    pbac committed
                        # It's a forecast input, hence must have the k columns in kseq
    
    pbac's avatar
    pbac committed
                        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=","))
                        }
    
    pbac's avatar
    pbac committed
                        # Check if the number of observations match
    
    pbac's avatar
    pbac committed
                        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"){
    
    pbac's avatar
    pbac committed
                        # Observation input, check the length
    
    pbac's avatar
    pbac committed
                        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]],"'")
                    }
                }
            }
        },
    
    
    pbac's avatar
    pbac committed
        #----------------------------------------------------------------
    
    pbac's avatar
    pbac committed
        clone_deep = function(){
    
    pbac's avatar
    pbac committed
            # First clone with deep=TRUE. Now also the inputes get cloned.
    
    pbac's avatar
    pbac committed
            newmodel <- self$clone(deep=TRUE)
    
    pbac's avatar
    pbac committed
            # The inputs are cloned now, however the model fields in the inputs have not been updated, so do that
    
    pbac's avatar
    pbac committed
            if(length(newmodel$inputs) > 0){
                for(i in 1:length(newmodel$inputs)){
                    newmodel$inputs[[i]]$model <- newmodel
                }
            }
            return(newmodel)
        }
    
    pbac's avatar
    pbac committed
        #----------------------------------------------------------------
    
    pbac's avatar
    pbac committed
        
        ),
    
    pbac's avatar
    pbac committed
        #----------------------------------------------------------------
    
    pbac's avatar
    pbac committed
    
    
    
    pbac's avatar
    pbac committed
        #----------------------------------------------------------------
        # Private functions
    
    pbac's avatar
    pbac committed
        private = list(
    
    pbac's avatar
    pbac committed
        #----------------------------------------------------------------
    
    pbac's avatar
    pbac committed
    
    
    
    pbac's avatar
    pbac committed
        #----------------------------------------------------------------
        # Replace the value in "name=value" in expr
    
    pbac's avatar
    pbac committed
        replace_prmvalue = function(name, value, expr){
    
    pbac's avatar
    pbac committed
            # First make regex
    
    pbac's avatar
    pbac committed
            pattern <- gsub("\\.", ".*", name)
    
    pbac's avatar
    pbac committed
            # Try to find it in the input
    
    pbac's avatar
    pbac committed
            pos <- regexpr(pattern, expr)
    
    pbac's avatar
    pbac committed
            # Only replace if prm was found
    
    pbac's avatar
    pbac committed
            if(pos>0){
                pos <- c(pos+attr(pos,"match.length"))
    
    pbac's avatar
    pbac committed
                # Find the substr to replace with the prm value
    
    pbac's avatar
    pbac committed
                tmp <- substr(expr, pos, nchar(expr))
    
    pbac's avatar
    pbac committed
                pos2 <- regexpr(",|)", tmp)
    
    pbac's avatar
    pbac committed
                # Insert the prm value and return
    
    pbac's avatar
    pbac committed
                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="")}
    
    pbac's avatar
    pbac committed
            }
            return(expr)
        },
    
    pbac's avatar
    pbac committed
        #----------------------------------------------------------------
    
    pbac's avatar
    pbac committed
    
    
    pbac's avatar
    pbac committed
        #----------------------------------------------------------------
        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)
            }
        },
        #----------------------------------------------------------------
        
    
    pbac's avatar
    pbac committed
        #----------------------------------------------------------------
        # For deep cloning, in order to get the inputs list of R6 objects copied
    
    pbac's avatar
    pbac committed
        deep_clone = function(name, value) {
    
    pbac's avatar
    pbac committed
            # With x$clone(deep=TRUE) is called, the deep_clone gets invoked once for
            # each field, with the name and value.
    
    pbac's avatar
    pbac committed
            if (name == "inputs") {
    
    pbac's avatar
    pbac committed
                # 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
    
    pbac's avatar
    pbac committed
                return(lapply(value, function(x){ x$clone(deep=FALSE) }))
    
    pbac's avatar
    pbac committed
                # # `a` is an environment, so use this quick way of copying
                # list2env(as.list.environment(value, all.names = TRUE),
                #          parent = emptyenv())
    
    pbac's avatar
    pbac committed
            }
    
    pbac's avatar
    pbac committed
            # For all other fields, just return the value
    
    pbac's avatar
    pbac committed
            return(value)
        }
    
    pbac's avatar
    pbac committed
        #----------------------------------------------------------------
    
    pbac's avatar
    pbac committed
        )
    )
    
    
    
    #' 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.
    #' 
    
    pbac's avatar
    pbac committed
    #' @export
    
    print.forecastmodel <- function(x, ...){
        model <- x
    
    pbac's avatar
    pbac committed
        #    cat("\nObject of class forecastmodel (R6::class)\n\n")
    
        cat("\nOutput:",model$output)
    
    pbac's avatar
    pbac committed
        cat("\nInputs: ")
    
    pbac's avatar
    pbac committed
            cat("\nNo inputs\n\n")
    
        }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")
                }
    
    pbac's avatar
    pbac committed
        }
    }