From ae177dcaee5c8592452455b40654663545957647 Mon Sep 17 00:00:00 2001 From: Yoann Robin Date: Tue, 7 Sep 2021 15:52:31 +0200 Subject: [PATCH] Add TSMBC method --- R/SBCK/DESCRIPTION | 4 +- R/SBCK/NAMESPACE | 3 + R/SBCK/R/bc.TSMBC.R | 151 ++++++++++++++++++++++++++++ R/SBCK/R/bc.dTSMBC.R | 169 +++++++++++++++++++++++++++++++ R/SBCK/R/tools.Shift.R | 219 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 544 insertions(+), 2 deletions(-) create mode 100644 R/SBCK/R/bc.TSMBC.R create mode 100644 R/SBCK/R/bc.dTSMBC.R create mode 100644 R/SBCK/R/tools.Shift.R diff --git a/R/SBCK/DESCRIPTION b/R/SBCK/DESCRIPTION index 02edbec..9251e2f 100755 --- a/R/SBCK/DESCRIPTION +++ b/R/SBCK/DESCRIPTION @@ -1,8 +1,8 @@ Package: SBCK Type: Package Title: Statistical Bias Correction Kit -Version: 0.3.1 -Date: 2021-03-31 +Version: 0.4.0 +Date: 2021-09-07 Authors@R: c( person("Yoann" , "Robin", email = "yoann.robin.k@gmail.com" , role = c("aut", "cre")), person("Mathieu", "Vrac" , email = "mathieu.vrac@lsce.ipsl.fr", role = c("cph")) ) Description: Implementation of several recent multivariate bias correction diff --git a/R/SBCK/NAMESPACE b/R/SBCK/NAMESPACE index 6e461f1..c21f873 100644 --- a/R/SBCK/NAMESPACE +++ b/R/SBCK/NAMESPACE @@ -15,8 +15,10 @@ export(R2D2) export(RBC) export(SchaakeShuffle) export(SchaakeShuffleRef) +export(Shift) export(SlopeStoppingCriteria) export(SparseHist) +export(TSMBC) export(bin_width_estimator) export(chebyshev) export(cpp_pairwise_distances_XCall) @@ -24,6 +26,7 @@ export(cpp_pairwise_distances_XYCall) export(cpp_pairwise_distances_XYstr) export(cpp_pairwise_distances_Xstr) export(dOTC) +export(dTSMBC) export(data_to_hist) export(dataset_bimodal_reverse_2d) export(dataset_gaussian_2d) diff --git a/R/SBCK/R/bc.TSMBC.R b/R/SBCK/R/bc.TSMBC.R new file mode 100644 index 0000000..4e1b601 --- /dev/null +++ b/R/SBCK/R/bc.TSMBC.R @@ -0,0 +1,151 @@ + +## Copyright(c) 2021 Yoann Robin +## +## This file is part of SBCK. +## +## SBCK is free software: you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation, either version 3 of the License, or +## (at your option) any later version. +## +## SBCK is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with SBCK. If not, see . + + +#' TSMBC (Time Shifted Multivariate Bias Correction) +#' +#' @description +#' Perform a bias correction of auto-correlation +#' +#' @details +#' Correct auto-correlation with a shift approach. +#' +#' @importFrom R6 R6Class +#' +#' @references Robin, Y. and Vrac, M.: Is time a variable like the others in +#' multivariate statistical downscaling and bias correction?, Earth +#' Syst. Dynam. Discuss. [preprint], +#' https://doi.org/10.5194/esd-2021-12, in review, 2021. +#' +#' @examples +#' +#' ## arima model parameters +#' modelX0 = list( ar = base::c( 0.6 , 0.2 , -0.1 ) ) +#' modelY0 = list( ar = base::c( -0.3 , 0.4 , -0.2 ) ) +#' +#' ## arima random generator +#' rand.genX0 = function(n){ return(stats::rnorm( n , mean = 0.2 , sd = 1 )) } +#' rand.genY0 = function(n){ return(stats::rnorm( n , mean = 0 , sd = 0.7 )) } +#' +#' ## Generate two AR processes +#' X0 = stats::arima.sim( n = 2000 , model = modelX0 , rand.gen = rand.genX0 ) +#' Y0 = stats::arima.sim( n = 2000 , model = modelY0 , rand.gen = rand.genY0 ) +#' X0 = as.vector( X0 ) +#' Y0 = as.vector( Y0 + 5 ) +#' +#' ## And correct it with 30 lags +#' tsbc = SBCK::TSMBC$new( 30 ) +#' tsbc$fit( Y0 , X0 ) +#' Z0 = tsbc$predict(X0) +#' +#' @export +TSMBC = R6::R6Class( "TSMBC" , + + active = list( + + ## method ##{{{ + method = function(value) + { + if(missing(value)) + return(self$shift$method) + else + self$shift$method = value + }, + ##}}} + + ## ref ##{{{ + ref = function(value) + { + if(missing(value)) + return(self$shift$ref) + else + self$shift$ref = value + } + ##}}} + + ), + + public = list( + + ############### + ## Arguments ## + ############### + + #' @field shift [Shift class] Shift class to shift data. + shift = NULL, + #' @field bc_method [SBCK::BC_method] Underlying bias correction method. + bc_method = NULL, + #' @field method [character] If inverse is by row or column, see class Shift + #' @field ref [integer] reference column/row to inverse shift, see class + + + ################# + ## Constructor ## + ################# + + ## initialize ##{{{ + #' @description + #' Create a new TSMBC object. + #' @param lag [integer] max lag of autocorrelation + #' @param bc_method [SBCK::BC_METHOD] bias correction method to use after + #' shift of data, default is OTC + #' @param method [character] If inverse is by row or column, see class Shift + #' @param ref [integer] reference column/row to inverse shift, see class + #' Shift. Default is 0.5 * (lag+1) + #' @param ... [] All others arguments are passed to bc_method + #' + #' @return A new `TSMBC` object. + initialize = function( lag , bc_method = OTC , method = "row" , ref = "middle" , ... ) + { + bc_method_args = list(...) + self$bc_method = base::do.call( bc_method$new , bc_method_args ) + if( ref == "middle" ) + ref = as.integer(0.5 * (lag+1) ) + self$shift = Shift$new( lag , method , ref ) + }, + ##}}} + + ## fit ##{{{ + #' @description + #' Fit the bias correction method + #' @param Y0 [matrix: n_samples * n_features] Observations in calibration + #' @param X0 [matrix: n_samples * n_features] Model in calibration + #' + #' @return NULL + fit = function( Y0 , X0 ) + { + Xs = self$shift$transform(X0) + Ys = self$shift$transform(Y0) + self$bc_method$fit( Ys , Xs ) + }, + ##}}} + + ## predict ##{{{ + #' @description + #' Predict the correction + #' @param X0 [matrix: n_samples * n_features or NULL] Model in calibration + #' + #' @return [matrix] Return the corrections of X0 + predict = function(X0) + { + return(self$shift$inverse( self$bc_method$predict( self$shift$transform(X0) ) )) + } + ##}}} + + ) +) diff --git a/R/SBCK/R/bc.dTSMBC.R b/R/SBCK/R/bc.dTSMBC.R new file mode 100644 index 0000000..604e89e --- /dev/null +++ b/R/SBCK/R/bc.dTSMBC.R @@ -0,0 +1,169 @@ + +## Copyright(c) 2021 Yoann Robin +## +## This file is part of SBCK. +## +## SBCK is free software: you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation, either version 3 of the License, or +## (at your option) any later version. +## +## SBCK is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with SBCK. If not, see . + +#' dTSMBC (dynamical Time Shifted Multivariate Bias Correction) +#' +#' @description +#' Perform a bias correction of auto-correlation +#' +#' @details +#' Correct auto-correlation with a shift approach, taking into account of non +#' stationarity. +#' +#' @importFrom R6 R6Class +#' +#' @references Robin, Y. and Vrac, M.: Is time a variable like the others in +#' multivariate statistical downscaling and bias correction?, Earth +#' Syst. Dynam. Discuss. [preprint], +#' https://doi.org/10.5194/esd-2021-12, in review, 2021. +#' @examples +#' +#' ## arima model parameters +#' modelX0 = list( ar = base::c( 0.6 , 0.2 , -0.1 ) ) +#' modelX1 = list( ar = base::c( 0.4 , 0.1 , -0.3 ) ) +#' modelY0 = list( ar = base::c( -0.3 , 0.4 , -0.2 ) ) +#' +#' ## arima random generator +#' rand.genX0 = function(n){ return(stats::rnorm( n , mean = 0.2 , sd = 1 )) } +#' rand.genX1 = function(n){ return(stats::rnorm( n , mean = 0.8 , sd = 1 )) } +#' rand.genY0 = function(n){ return(stats::rnorm( n , mean = 0 , sd = 0.7 )) } +#' +#' ## Generate two AR processes +#' X0 = stats::arima.sim( n = 2000 , model = modelX0 , rand.gen = rand.genX0 ) +#' X1 = stats::arima.sim( n = 2000 , model = modelX1 , rand.gen = rand.genX1 ) +#' Y0 = stats::arima.sim( n = 2000 , model = modelY0 , rand.gen = rand.genY0 ) +#' X0 = as.vector( X0 ) +#' X1 = as.vector( X1 ) +#' Y0 = as.vector( Y0 + 5 ) +#' +#' ## And correct it with 30 lags +#' dtsbc = SBCK::dTSMBC$new( 30 ) +#' dtsbc$fit( Y0 , X0 , X1 ) +#' Z = dtsbc$predict(X1,X0) +#' +#' @export +dTSMBC = R6::R6Class( "dTSMBC" , + + ## Active list ##{{{ + + active = list( + + ## method ##{{{ + method = function(value) + { + if(missing(value)) + return(self$shift$method) + else + self$shift$method = value + }, + ##}}} + + ## ref ##{{{ + ref = function(value) + { + if(missing(value)) + return(self$shift$ref) + else + self$shift$ref = value + } + ##}}} + + ), + ##}}} + + public = list( + + ############### + ## Arguments ## + ############### + + #' @field shift [Shift class] Shift class to shift data. + shift = NULL, + #' @field bc_method [SBCK::BC_method] Underlying bias correction method. + bc_method = NULL, + #' @field method [character] If inverse is by row or column, see class Shift + #' @field ref [integer] reference column/row to inverse shift, see class + #' Shift. Default is 0.5 * (lag+1) + + ################# + ## Constructor ## + ################# + + ## initialize ##{{{ + #' @description + #' Create a new dTSMBC object. + #' @param lag [integer] max lag of autocorrelation + #' @param bc_method [SBCK::BC_METHOD] bias correction method to use after + #' shift of data, default is OTC + #' @param method [character] If inverse is by row or column, see class Shift + #' @param ref [integer] reference column/row to inverse shift, see class + #' Shift. Default is 0.5 * (lag+1) + #' @param ... [] All others arguments are passed to bc_method + #' + #' @return A new `dTSMBC` object. + initialize = function( lag , bc_method = dOTC , method = "row" , ref = "middle" , ... ) + { + bc_method_args = list(...) + self$bc_method = base::do.call( bc_method$new , bc_method_args ) + if( ref == "middle" ) + ref = as.integer(0.5 * (lag+1) ) + self$shift = Shift$new( lag , method , ref ) + }, + ##}}} + + ## fit ##{{{ + #' @description + #' Fit the bias correction method + #' @param Y0 [matrix: n_samples * n_features] Observations in calibration + #' @param X0 [matrix: n_samples * n_features] Model in calibration + #' @param X1 [matrix: n_samples * n_features] Model in projection + #' @return NULL + fit = function( Y0 , X0 , X1 ) + { + X0s = self$shift$transform(X0) + X1s = self$shift$transform(X1) + Y0s = self$shift$transform(Y0) + self$bc_method$fit( Y0s , X0s , X1s ) + }, + ##}}} + + ## predict ##{{{ + #' @description + #' Predict the correction + #' @param X0 [matrix: n_samples * n_features or NULL] Model in calibration + #' @param X1 [matrix: n_samples * n_features] Model in projection + #' @return [matrix or list] Return the matrix of correction of X1 if X0 is + #' NULL, else return a list containing Z1 and Z0, + #' the corrections of X1 and X0 + predict = function( X1 , X0 = NULL ) + { + shX0 = NULL + if( !is.null(X0) ) + shX0 = self$shift$transform(X0) + Z = self$bc_method$predict( self$shift$transform(X1) , shX0 ) + + if( is.null(X0) ) + { + return(self$shift$inverse(Z)) + } + return( list( Z1 = self$shift$inverse(Z$Z1) , Z0 = self$shift$inverse(Z$Z0) ) ) + } + ##}}} + + ) +) diff --git a/R/SBCK/R/tools.Shift.R b/R/SBCK/R/tools.Shift.R new file mode 100644 index 0000000..5577ad3 --- /dev/null +++ b/R/SBCK/R/tools.Shift.R @@ -0,0 +1,219 @@ + +## Copyright(c) 2021 Yoann Robin +## +## This file is part of SBCK. +## +## SBCK is free software: you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation, either version 3 of the License, or +## (at your option) any later version. +## +## SBCK is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with SBCK. If not, see . + +#' Shift +#' +#' @description +#' Class to shift a dataset. +#' +#' @details +#' Transform autocorrelations to intervariables correlations +#' +#' @docType class +#' @importFrom R6 R6Class +#' +#' +#' @return Object of \code{\link{R6Class}} +#' @format \code{\link{R6Class}} object. +#' +#' @section Methods: +#' \describe{ +#' \item{\code{new(lag,method,ref,)}}{This method is used to create object of this class with \code{Shift}} +#' \item{\code{transform(X)}}{Method to shift a dataset} +#' \item{\code{inverse(Xs)}}{Method to inverse the shift of a dataset} +#' } +#' @examples +#' X = base::t(matrix( 1:20 , nrow = 2 , ncol = 10 )) +#' +#' sh = Shift$new(1) +#' Xs = sh$transform(X) +#' Xi = sh$inverse(Xs) +#' +#' @export +Shift = R6::R6Class( "Shift" , + + private = list( ##{{{ + + .method = NULL, + .ref = NULL, + + inverse_by_row = function(Xs)##{{{ + { + n_features = as.integer( dim(Xs)[2] / ( self$lag + 1 )) + n_samples = dim(Xs)[1] + self$lag + Xi = matrix( NA , ncol = n_features , nrow = n_samples ) + for( r in base::c(1:self$lag,self$ref) ) + { + idx = base::seq( r , n_samples - self$lag , self$lag ) + Xs0 = as.vector(base::t(Xs[idx[-length(idx)],1:(base::ncol(Xs) - n_features)])) + Xs1 = as.vector(base::t(Xs[idx[length(idx)],])) + Xs01 = base::t(matrix( base::c(Xs0,Xs1) , nrow = n_features )) + Xi[r:(r+base::nrow(Xs01)-1),] = Xs01 + } + + return(Xi) + }, + ##}}} + + inverse_by_col = function(Xs)##{{{ + { + n_features = as.integer( dim(Xs)[2] / (self$lag + 1) ) + n_samples = dim(Xs)[1] + self$lag + Xi = matrix( NA , nrow = n_samples , ncol = n_features ) + + ref = self$ref + if( ref < 1 ) + ref = 1 + if( ref > n_features ) + ref = n_features + + for( i in base::c( 0:self$lag , ref ) ) + { + db = i * n_features + 1 + de = i * n_features + n_features + tb = i + 1 + te = n_samples - ( self$lag + 1 ) + i + 1 + Xi[tb:te,] = Xs[,db:de] + } + + return(Xi) + } + ##}}} + + ), + ##}}} + + active = list( ##{{{ + + method = function(value)##{{{ + { + if( missing(value) ) + { + return(private$.method) + } + else + { + if( value %in% base::c("col","row") ) + private$.method = value + else + private$.method = "row" + } + }, + ##}}} + + ref = function(value)##{{{ + { + if( missing(value) ) + { + return(private$.ref) + } + else + { + private$.ref = ( (value - 1) %% ( self$lag + 1 ) ) + 1 + } + } + + ), + ##}}} + + ##}}} + + public = list( ##{{{ + + ############### + ## Arguments ## + ############### + + #' @field lag [integer] max lag for autocorrelations + lag = NULL, + #' @field method [character] If inverse is by row or column. + #' @field ref [integer] reference column/row to inverse shift. + + ################# + ## Constructor ## + ################# + + ## initialize ##{{{ + #' @description + #' Create a new Shift object. + #' + #' @param lag [integer] max lag for autocorrelations + #' @param method [character] If "row" inverse by row, else by column + #' @param ref [integer] starting point for inverse transform + #' + #' @return A new `Shift` object. + initialize = function( lag , method = "row" , ref = 1 ) + { + self$lag = lag + self$ref = ref + self$method = method + }, + ##}}} + + + ############# + ## Methods ## + ############# + + ## transform ##{{{ + #' @description + #' Shift the data + #' @param X [matrix: n_samples * n_features] Data to shift + #' + #' @return [matrix] Matrix shifted + transform = function( X ) + { + if( is.vector(X) ) + X = matrix( X , nrow = length(X) , ncol = 1 ) + + n_samples = base::dim(X)[1] + n_features = base::dim(X)[2] + Xs = matrix( NA , nrow = n_samples - self$lag , ncol = ( self$lag + 1 ) * n_features ) + + for( i in 0:self$lag ) + { + db = i * n_features + 1 + de = i * n_features + n_features + tb = i + 1 + te = n_samples - ( self$lag + 1 ) + i + 1 + Xs[,db:de] = X[tb:te,] + } + + return(Xs) + }, + ##}}} + + ## inverse ##{{{ + #' @description + #' Inverse the shift of the data + #' @param Xs [matrix] Data Shifted + #' + #' @return [matrix] Matrix un shifted + inverse = function( Xs ) + { + if( self$method == "row" ) + return(private$inverse_by_row(Xs)) + else + return(private$inverse_by_col(Xs)) + } + ##}}} + + ) + ##}}} +) +