From a12c984d5e0ccc1ee5a6e75eaa2539c617980d6f Mon Sep 17 00:00:00 2001 From: nperez Date: Thu, 18 Jul 2019 14:47:24 +0200 Subject: [PATCH 1/3] Season uses Apply --- .Rbuildignore | 1 + DESCRIPTION | 13 ++- NAMESPACE | 3 + R/Season.R | 221 ++++++++++++++++++++++++++--------- man/Season.Rd | 66 ++++------- tests/testthat/test-Season.R | 91 +++++++++++++++ 6 files changed, 297 insertions(+), 98 deletions(-) create mode 100644 tests/testthat/test-Season.R diff --git a/.Rbuildignore b/.Rbuildignore index 5493f6df..aa8227b1 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -8,3 +8,4 @@ README\.Rmd$ README\.md$ \..*\.RData$ vignettes +.gitlab-ci.yml diff --git a/DESCRIPTION b/DESCRIPTION index 99c08ff6..c47f128f 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -4,7 +4,8 @@ Version: 2.8.5 Authors@R: c( person("BSC-CNS", role = c("aut", "cph")), person("Virginie", "Guemas", , "virginie.guemas@bsc.es", role = "aut"), - person("Nicolau", "Manubens", , "nicolau.manubens@bsc.es", role = c("aut", "cre")), + person("Nicolau", "Manubens", , "nicolau.manubens@bsc.es", role = c("aut")), + person("Nuria", "Perez-Zanon", , "nuria.perez@bsc.es", role = c("ctb", "cre")), person("Javier", "Garcia-Serrano", , "javier.garcia@bsc.es", role = "aut"), person("Neven", "Fuckar", , "neven.fuckar@bsc.es", role = "aut"), person("Louis-Philippe", "Caron", , "louis-philippe.caron@bsc.es", role = "aut"), @@ -28,7 +29,13 @@ Authors@R: c( person("Isabel", "Andreu-Burillo", , "isabel.andreu.burillo@ic3.cat", role = "ctb"), person("Ramiro", "Saurral", , "ramiro.saurral@ic3.cat", role = "ctb"), person("An-Chi", "Ho", , "an.ho@bsc.es", role = "ctb")) -Description: Set of tools to verify forecasts through the computation of typical prediction scores against one or more observational datasets or reanalyses (a reanalysis being a physical extrapolation of observations that relies on the equations from a model, not a pure observational dataset). Intended for seasonal to decadal climate forecasts although can be useful to verify other kinds of forecasts. The package can be helpful in climate sciences for other purposes than forecasting. +Description: Set of tools to verify forecasts through the computation of typical + prediction scores against one or more observational datasets or reanalyses (a + reanalysis being a physical extrapolation of observations that relies on the + equations from a model, not a pure observational dataset). Intended for seasonal + to decadal climate forecasts although can be useful to verify other kinds of + forecasts. The package can be helpful in climate sciences for other purposes + than forecasting. Depends: maps, methods, @@ -43,6 +50,7 @@ Imports: ncdf4, parallel, plyr, + multiApply, SpecsVerification (>= 0.5.0) Suggests: easyVerification, @@ -53,3 +61,4 @@ BugReports: https://earth.bsc.es/gitlab/es/s2dverification/issues LazyData: true SystemRequirements: cdo Encoding: UTF-8 +RoxygenNote: 5.0.0 diff --git a/NAMESPACE b/NAMESPACE index 7c49d6b9..a8364a3e 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,2 +1,5 @@ +# Generated by roxygen2: do not edit by hand + exportPattern("^[^\\.]") import(abind, bigmemory, GEOmap, geomapdata, graphics, grDevices, mapproj, maps, methods, NbClust, ncdf4, parallel, plyr, stats, SpecsVerification) +import(multiApply) diff --git a/R/Season.R b/R/Season.R index bf549a7e..089982e6 100644 --- a/R/Season.R +++ b/R/Season.R @@ -1,58 +1,169 @@ -Season <- function(var, posdim = 4, monini, moninf, monsup) { - while (monsup < moninf) { - monsup <- monsup + 12 - } - # - # Enlarge the size of var to 10 - # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - # - dimsvar <- dim(var) - if (is.null(dimsvar)) { - dimsvar <- length(var) - } - ntime <- dimsvar[posdim] - enlvar <- Enlarge(var, 10) - outdim <- c(dimsvar, array(1, dim = (10 - length(dimsvar)))) - u <- IniListDims(outdim, 10) - v <- IniListDims(outdim, 10) - # - # Compute the seasonal means - # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - # - ind <- 1:ntime - months <- ((ind - 1) + monini - 1) %% 12 + 1 - years <- ((ind - 1) + monini - 1) %/% 12 +#'Computes Seasonal Means +#' +#'Computes seasonal means (or other operations) on monthly timeseries from n-dimensional arrays with named dimensions +#' +#'@param var a numeric n-dimensional array with named dimensions on monthy frequency. +#'@param posdim a character indicating the name of the dimension or a integer numeric indicating the position along which to compute seasonal means (or other operations). By default, 'time' dimension is expected. +#'@param monini an integer indicating the first month of the time series: 1 to 12. +#'@param moninf an integer indicating the month when to start the seasonal means: 1 to 12. +#'@param monsup an integer indicating the month when to stop the seasonal means: 1 to 12. +#'@param operation a character or function indicating the name of a function to be applied in seasonal basins. By default, means are computed. Other operations can be 'sum' for total precipitation. +#' +#'@return Array with the same dimensions as var except along the posdim dimension whose length corresponds to the number of seasons. Partial seasons are not accounted for. +#' +#'@import multiApply +#'@examples +#'dat <- 1 : (12 * 5 * 2 * 3 * 2) +#'dim(dat) <- c(dat = 1, memb = 3, time = 12 * 5, lon = 2, lat = 2) +#'res <- Season(var = dat, monini = 1, moninf = 1, monsup = 2) +#'dat <- 1 : (12 * 2 * 3) +#'dim(dat) <- c(dat = 2, time = 12, memb = 3) +#'res <- Season(var = dat, monini = 1, moninf =1, monsup = 2) +#'@export +Season <- function(var, posdim = 'time', monini, moninf, monsup, + operation = mean) { + # Check var + if (is.null(var)) { + stop("Parameter 'var' cannot be NULL.") + } + if (!is.numeric(var)) { + stop("Parameter 'var' must be a numeric array.") + } + if (is.null(dim(var))) { + dim(var) <- c(length(var)) + if (is.character(posdim)) { + names(dim(var)) <- posdim + } else { + names(dim(var)) <- 'time' + posdim <- 'time' + } + time_dim <- 1 + } else { + if (is.null(names(dim(var)))) { + if (is.numeric(posdim)) { + names(dim(var)) <- paste0("D", 1 : length(dim(var))) + names(dim(var))[posdim] <- 'time' + time_dim <- posdim + posdim <- 'time' + } else { + stop("Parameter 'var' must contain dimesnion names.") + } + } else { + if (is.numeric(posdim)) { + time_dim <- posdim + posdim <- names(dim(var))[posdim] + } else if (is.character(posdim)) { + time_dim <- which(names(dim(var)) == posdim) + } else { + stop("Parameter 'posdim' must be a integer or character", + "indicating the 'time' dimension.") + } + } + } - for (jmon in moninf:monsup) { - u[[posdim]] <- ind[which(months == ((jmon - 1) %% 12 + 1))] - ind0 <- u[[posdim]][1] - indf <- u[[posdim]][length(u[[posdim]])] - if (indf > (ntime - (monsup - jmon))) { - u[[posdim]] <- u[[posdim]][-which(u[[posdim]] == indf)] - } - if (ind0 < (jmon - moninf + 1)) { - u[[posdim]] <- u[[posdim]][-which(u[[posdim]] == ind0)] + dim_names <- names(dim(var)) +# series <- apply(var, margins, .Season, +# monini = monini, moninf = moninf, monsup = monsup, +# operation = operation) + series <- Apply(list(var), + target_dims = posdim, + fun = .Season, + monini = monini, moninf = moninf, monsup = monsup, + operation = operation)$output1 + if (is.null(dim(series))) { + dim(series) <- c(time = length(series)) + } else if (names(dim(series))[1] != "") { #& length(dim(series)) > 1) { + dim(series) <- c(1, dim(series)) + names(dim(series))[1] <- posdim + } else { + names(dim(series))[1] <- posdim + } + if (any(dim_names != names(dim(series)))) { + pos <- match(dim_names, names(dim(series))) + series <- aperm(series, pos) + names(dim(series)) <- dim_names + } + return(series) +} + +.Season <- function(x, monini, moninf, monsup, operation = mean) { + # Checks: + if (!is.numeric(x)) { + stop("Parameter 'x' must be a numeric vector.") + } + if (!is.numeric(monini)) { + stop("Parameter 'monini' must be numeric.") } - if (jmon == moninf) { - nseas <- length(u[[posdim]]) - dimsvar[posdim] <- nseas - outdim[posdim] <- nseas - enlvarout <- array(0, dim = outdim) - } - v[[posdim]] <- 1:nseas - enlvarout[v[[1]], v[[2]], v[[3]], v[[4]], v[[5]], v[[6]], v[[7]], v[[8]], - v[[9]], v[[10]]] <- enlvarout[v[[1]], v[[2]], v[[3]], v[[4]], - v[[5]], v[[6]], v[[7]], v[[8]], - v[[9]], v[[10]]] + enlvar[u[[1]], - u[[2]], u[[3]], u[[4]], u[[5]], u[[6]], - u[[7]], u[[8]], u[[9]], u[[10]]] - } - varout <- array(dim = dimsvar) - varout[] <- enlvarout - varout <- varout / (monsup - moninf + 1) - # - # Outputs - # ~~~~~~~~~ - # - varout + if (length(monini) > 1) { + monini <- monini[1] + warning("Parameter 'monini' has length > 1 and only the first ", + "element will be used.") + } + if (monini %% 1 != 0) { + stop("Parameter 'monini' must be an integer.") + } + if (!is.numeric(moninf)) { + stop("Parameter 'moninf' must be numeric.") + } + if (length(moninf) > 1) { + moninf <- moninf[1] + warning("Parameter 'moninf' has length > 1 and only the first ", + "element will be used.") + } + if (moninf %% 1 != 0) { + stop("Parameter 'moninf' must be an integer.") + } + if (!is.numeric(monsup)) { + stop("Parameter 'monsup' must be numeric.") + } + if (length(monsup) > 1) { + monsup <- monsup[1] + warning("Parameter 'monsup' has length > 1 and only the first ", + "element will be used.") + } + if (monsup %% 1 != 0) { + stop("Parameter 'monsup' must be an integer.") + } + # Check fun operation + if (is.character(operation)) { + fun_name <- operation + err <- try({operation <- get(operation)}, silent = TRUE) + if (!is.function(operation)) { + stop("Could not find the function '", fun_name, "'.") + } + } + if (!is.function(operation)) { + stop("Parameter 'operation' must be a function or a character string ", + "with the name of a function.") + } + + # Correction e.g. 'winter': + while (monsup < moninf) { + monsup <- monsup + 12 + } + # Correction need if monini is not January: + moninf <- moninf - monini + 1 + monsup <- monsup - monini + 1 + moninf <- ifelse(moninf <= 0, moninf + 12, moninf) + monsup <- ifelse(monsup <= 0, monsup + 12, monsup) + + #### Create position index: + # Basic index: + pos <- moninf : monsup + # Extended index for all period: + if (length(x) > pos[length(pos)]) { + pos2 <- lapply(pos, function(y) {seq(y, length(x), 12)}) + } else { + pos2 <- pos + } + # Correct if the final season is not complete: + maxyear <- min(unlist(lapply(pos2, length))) + pos2 <- lapply(pos2, function(y) {y[1 : maxyear]}) + # Convert to array: + pos2 <- unlist(pos2) + dim(pos2) <- c(year = maxyear, month = length(pos2)/maxyear) + + timeseries <- apply(pos2, 1, function(y) {operation(x[y])}) + return(timeseries) } + diff --git a/man/Season.Rd b/man/Season.Rd index aa143e27..36e0901e 100644 --- a/man/Season.Rd +++ b/man/Season.Rd @@ -1,52 +1,36 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/Season.R \name{Season} \alias{Season} -\title{ -Computes Seasonal Means -} -\description{ -Computes seasonal means on timeseries organized in a array of any number of dimensions up to 10 dimensions where the time dimension is one of those 10 dimensions. -} +\title{Computes Seasonal Means} \usage{ -Season(var, posdim = 4, monini, moninf, monsup) +Season(var, posdim = "time", monini, moninf, monsup, operation = mean) } \arguments{ - \item{var}{ -Array containing the timeseries along one of its dimensions. - } - \item{posdim}{ -Dimension along which to compute seasonal means = Time dimension - } - \item{monini}{ -First month of the time series: 1 to 12. - } - \item{moninf}{ -Month when to start the seasonal means: 1 to 12. - } - \item{monsup}{ -Month when to stop the seasonal means: 1 to 12. - } +\item{var}{a numeric n-dimensional array with named dimensions on monthy frequency.} + +\item{posdim}{a character indicating the name of the dimension or a integer numeric indicating the position along which to compute seasonal means (or other operations). By default, 'time' dimension is expected.} + +\item{monini}{an integer indicating the first month of the time series: 1 to 12.} + +\item{moninf}{an integer indicating the month when to start the seasonal means: 1 to 12.} + +\item{monsup}{an integer indicating the month when to stop the seasonal means: 1 to 12.} + +\item{operation}{a character or function indicating the name of a function to be applied in seasonal basins. By default, means are computed. Other operations can be 'sum' for total precipitation.} } \value{ Array with the same dimensions as var except along the posdim dimension whose length corresponds to the number of seasons. Partial seasons are not accounted for. } -\examples{ -# Load sample data as in Load() example: -example(Load) -leadtimes_dimension <- 4 -initial_month <- 11 -mean_start_month <- 12 -mean_stop_month <- 2 -season_means_mod <- Season(sampleData$mod, leadtimes_dimension, initial_month, - mean_start_month, mean_stop_month) -season_means_obs <- Season(sampleData$obs, leadtimes_dimension, initial_month, - mean_start_month, mean_stop_month) -PlotAno(season_means_mod, season_means_obs, startDates, - toptitle = paste('winter (DJF) temperatures'), ytitle = c('K'), - legends = 'ERSST', biglab = FALSE, fileout = 'tos_season_means.eps') +\description{ +Computes seasonal means (or other operations) on monthly timeseries from n-dimensional arrays with named dimensions } -\author{ -History:\cr -0.1 - 2011-03 (V. Guemas, \email{virginie.guemas at ic3.cat}) - Original code\cr -1.0 - 2013-09 (N. Manubens, \email{nicolau.manubens at ic3.cat}) - Formatting to CRAN +\examples{ +dat <- 1 : (12 * 5 * 2 * 3 * 2) +dim(dat) <- c(dat = 1, memb = 3, time = 12 * 5, lon = 2, lat = 2) +res <- Season(var = dat, monini = 1, moninf = 1, monsup = 2) +dat <- 1 : (12 * 2 * 3) +dim(dat) <- c(dat = 2, time = 12, memb = 3) +res <- Season(var = dat, monini = 1, moninf =1, monsup = 2) } -\keyword{datagen} + diff --git a/tests/testthat/test-Season.R b/tests/testthat/test-Season.R new file mode 100644 index 00000000..0fa18ea8 --- /dev/null +++ b/tests/testthat/test-Season.R @@ -0,0 +1,91 @@ +context("Generic tests") +test_that("Sanity checks", { + expect_error( + Season(var = "A"), "Parameter 'var' must be a numeric array.") + expect_error( + Season(var = 1, monini = "A", moninf = "B", monsup = "C"), + "Parameter 'monini' must be numeric.") + expect_error( + Season(var = 1, monini = 1, moninf = "B", monsup = "C"), + "Parameter 'moninf' must be numeric.") + expect_error( + Season(var = 1, monini = 1, moninf = 1, monsup = "C"), + "Parameter 'monsup' must be numeric.") + expect_error( + Season(var = 1, monini = 1.5, moninf = 1, monsup = "C"), + "Parameter 'monini' must be an integer.") + expect_error( + Season(var = 1, monini = 1, moninf = 1.5, monsup = "C"), + "Parameter 'moninf' must be an integer.") + expect_error( + Season(var = 1, monini = 1, moninf = 1, monsup = 1.5), + "Parameter 'monsup' must be an integer.") + expect_warning( + Season(var = 1, monini = c(1, 2), moninf = 1, monsup = 1), + "Parameter 'monini' has length > 1 and only the first element will be used.") + + expect_warning( + Season(var = 1, monini = 1, moninf = c(1, 2), monsup = 1), + "Parameter 'moninf' has length > 1 and only the first element will be used.") + + expect_warning( + Season(var = 1, monini = 1, moninf = 1, monsup = c(1, 2)), + "Parameter 'monsup' has length > 1 and only the first element will be used.") + + expect_error( + Season(var = 1, monini = 1, moninf = 1, monsup = 1, operation = "RRR"), + "Could not find the function 'RRR'.") + + expect_error(Season(var = NULL), "Parameter 'var' cannot be NULL.") + + var <- array(1 : 20, dim = c(2, 4, 5)) + expect_error(Season(var), + "Parameter 'var' must contain dimesnion names.") + expect_equal(Season(var, posdim = 3, monini = 1, moninf = 1, monsup = 2), + array(5 : 12, dim = c(D1 = 2, D2 = 4, time = 1))) + + var <- array(1 : 20, dim = c(2, 4, 5)) + names(dim(var)) <- c('x', 'y', 'time') + output <- array(1 : 8, dim = c(x = 2, y = 4, time = 1)) + expect_equal( + Season(var, monini = 1, moninf = 1, monsup = 1), output) + output <- array(5 : 12, dim = c(x = 2, y = 4, time = 1)) + expect_equal( + Season(var, monini = 1, moninf = 1, monsup = 2), output) + output <- array(rep(seq(1.5, 19.5, 2),2), dim = c(x = 1, y = 4, time = 5)) + expect_equal( + Season(var, posdim = 1, monini = 1, moninf = 1, monsup = 2), output) + + var <- 1 : (12 * 2 * 3) + dim(var) <- c(dat = 2, time = 12, sdate = 3) + output <- array(c(2 : 3, 26 : 27, 50 : 51), + dim = c(dat = 2, time = 1, sdate = 3)) + expect_equal( + Season(var, posdim = 'time', monini = 1, moninf = 1, monsup = 2), + output) + expect_equal( + Season(var, posdim = 2, monini = 1, moninf = 1, monsup = 2), + output) + output <- array(13 : 36, dim = c(dat = 2, time = 12, sdate = 1)) + expect_equal( + Season(var, posdim = 3, monini = 1, moninf = 1, monsup = 2), + output) + + var <- 1 : 10 # caso 1 + output <- array(1.5, dim = c(time = 1)) + expect_equal( + Season(var, posdim = 'time', monini = 1, moninf = 1, monsup = 2), + output) + + var <- 1 : 10 ; dim(var) <- c(2, 5) #caso 2 + expect_error( + Season(var, posdim = 'time', monini = 1, moninf = 1, monsup = 2), + "Parameter 'var' must contain dimesnion names.") + output <- array(2 : 3, dim = c(D1 = 2, time = 1)) + expect_equal( + Season(var, posdim = 2, monini = 1, moninf = 1, monsup = 2), + output) + + +}) + -- GitLab From 42341c4131a042f4c59c7ec8f72544c25e0f56b5 Mon Sep 17 00:00:00 2001 From: aho Date: Mon, 5 Aug 2019 11:37:45 +0200 Subject: [PATCH 2/3] Add two unit tests. --- tests/testthat/test-Season.R | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/testthat/test-Season.R b/tests/testthat/test-Season.R index 0fa18ea8..c42774c0 100644 --- a/tests/testthat/test-Season.R +++ b/tests/testthat/test-Season.R @@ -71,6 +71,11 @@ test_that("Sanity checks", { Season(var, posdim = 3, monini = 1, moninf = 1, monsup = 2), output) + output <- array(c(11:12, 35:36, 59:60), dim = c(dat = 2, time = 1, sdate = 3)) + expect_equal( + Season(var, posdim = 2, monini = 10, moninf = 2, monsup = 4), + output) + var <- 1 : 10 # caso 1 output <- array(1.5, dim = c(time = 1)) expect_equal( @@ -85,6 +90,10 @@ test_that("Sanity checks", { expect_equal( Season(var, posdim = 2, monini = 1, moninf = 1, monsup = 2), output) + output <- array(c(4, 6), dim = c(D1 = 2, time = 1)) + expect_equal( + Season(var, posdim = 2, monini = 1, moninf = 1, monsup = 2, operation = sum), + output) }) -- GitLab From b2077618ac49f036f9248019bcf463af2b236b22 Mon Sep 17 00:00:00 2001 From: nperez Date: Mon, 5 Aug 2019 13:34:01 +0200 Subject: [PATCH 3/3] Parameter na.rm --- R/Season.R | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/R/Season.R b/R/Season.R index 089982e6..98c56074 100644 --- a/R/Season.R +++ b/R/Season.R @@ -8,6 +8,7 @@ #'@param moninf an integer indicating the month when to start the seasonal means: 1 to 12. #'@param monsup an integer indicating the month when to stop the seasonal means: 1 to 12. #'@param operation a character or function indicating the name of a function to be applied in seasonal basins. By default, means are computed. Other operations can be 'sum' for total precipitation. +#'@param na.rm a logical value indicating whether NA values should be stripped before the computation proceeds. #' #'@return Array with the same dimensions as var except along the posdim dimension whose length corresponds to the number of seasons. Partial seasons are not accounted for. #' @@ -18,10 +19,16 @@ #'res <- Season(var = dat, monini = 1, moninf = 1, monsup = 2) #'dat <- 1 : (12 * 2 * 3) #'dim(dat) <- c(dat = 2, time = 12, memb = 3) -#'res <- Season(var = dat, monini = 1, moninf =1, monsup = 2) +#'res <- Season(var = dat, monini = 1, moninf = 1, monsup = 2) +#'dat <- 1 : (24 * 2 * 3) +#'dim(dat) <- c(dat = 2, time = 24, memb = 3) +#'res <- Season(var = dat, monini = 1, moninf = 1, monsup = 2) +#'dat[c(1, 20, 50)] <- NA +#'res1 <- Season(var = dat, monini = 1, moninf = 1, monsup = 2, na.rm = FALSE) +#'res2 <- Season(var = dat, monini = 1, moninf = 1, monsup = 2, na.rm = TRUE) #'@export Season <- function(var, posdim = 'time', monini, moninf, monsup, - operation = mean) { + operation = mean, na.rm = FALSE) { # Check var if (is.null(var)) { stop("Parameter 'var' cannot be NULL.") @@ -69,7 +76,7 @@ Season <- function(var, posdim = 'time', monini, moninf, monsup, target_dims = posdim, fun = .Season, monini = monini, moninf = moninf, monsup = monsup, - operation = operation)$output1 + operation = operation, na.rm = na.rm)$output1 if (is.null(dim(series))) { dim(series) <- c(time = length(series)) } else if (names(dim(series))[1] != "") { #& length(dim(series)) > 1) { @@ -86,7 +93,7 @@ Season <- function(var, posdim = 'time', monini, moninf, monsup, return(series) } -.Season <- function(x, monini, moninf, monsup, operation = mean) { +.Season <- function(x, monini, moninf, monsup, operation = mean, na.rm = FALSE) { # Checks: if (!is.numeric(x)) { stop("Parameter 'x' must be a numeric vector.") @@ -162,8 +169,16 @@ Season <- function(var, posdim = 'time', monini, moninf, monsup, # Convert to array: pos2 <- unlist(pos2) dim(pos2) <- c(year = maxyear, month = length(pos2)/maxyear) - - timeseries <- apply(pos2, 1, function(y) {operation(x[y])}) + if (na.rm == TRUE) { + if (length(which(is.na(x[pos2]))) > 0) { + pos2 <- apply(pos2, 1, function(y){y[!is.na(x[y])]}) + timeseries <- unlist(lapply(pos2, function(y) {operation(x[y])})) + } else { + timeseries <- apply(pos2, 1, function(y) {operation(x[y])}) + } + } else { + timeseries <- apply(pos2, 1, function(y) {operation(x[y])}) + } return(timeseries) } -- GitLab