diff --git a/DESCRIPTION b/DESCRIPTION index 55ed6b449..1aaf0d501 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Type: Package Package: datawizard Title: Easy Data Wrangling and Statistical Transformations -Version: 0.9.0.1 +Version: 0.9.0.2 Authors@R: c( person("Indrajeet", "Patil", , "patilindrajeet.science@gmail.com", role = "aut", comment = c(ORCID = "0000-0003-1995-6531", Twitter = "@patilindrajeets")), @@ -33,7 +33,7 @@ BugReports: https://github.com/easystats/datawizard/issues Depends: R (>= 3.6) Imports: - insight (>= 0.19.4), + insight (>= 0.19.6), stats, utils Suggests: diff --git a/NEWS.md b/NEWS.md index 4e88f6690..404961f86 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,10 @@ # datawizard 0.9.0.9000 (development version) +CHANGES + +* `rescale()` gains `multiply` and `add` arguments, to expand ranges by a given + factor or value. + # datawizard 0.9.0 NEW FUNCTIONS diff --git a/R/data_rescale.R b/R/data_rescale.R index ce5059160..530cde1db 100644 --- a/R/data_rescale.R +++ b/R/data_rescale.R @@ -7,9 +7,20 @@ #' @inheritParams find_columns #' @inheritParams standardize.data.frame #' -#' @param to Numeric vector of length 2 giving the new range that the variable will have after rescaling. -#' To reverse-score a variable, the range should be given with the maximum value first. -#' See examples. +#' @param to Numeric vector of length 2 giving the new range that the variable +#' will have after rescaling. To reverse-score a variable, the range should +#' be given with the maximum value first. See examples. +#' @param multiply If not `NULL`, `to` is ignored and `multiply` will be used, +#' giving the factor by which the actual range of `x` should be expanded. +#' For example, if a vector range from 5 to 15 and `multiply = 1.1`, the current +#' range of 10 will be expanced by the factor of 1.1, giving a new range of +#' 11. Thus, the rescaled vector would range from 4.5 to 15.5. +#' @param add If not `NULL`, `to` is ignored and `add` will be used, giving the +#' amount by which the actual range of `x` should be expanded. For example, +#' if a vector range from 5 to 15 and `add = 1`, the current range of 10 will +#' be expanced by 1, giving a new range of 11. Thus, the rescaled vector would +#' range from 4.5 to 15.5, because the lower and upper bounds are each expanded +#' by half of the amount specified in `add`. #' @param range Initial (old) range of values. If `NULL`, will take the range of #' the input vector (`range(x)`). #' @param ... Arguments passed to or from other methods. @@ -37,6 +48,22 @@ #' "Sepal.Length" = c(0, 1), #' "Petal.Length" = c(-1, 0) #' ))) +#' +#' # "expand" ranges by a factor or a given value +#' x <- 5:15 +#' x +#' # both will expand the range by 10% +#' rescale(x, multiply = 1.1) +#' rescale(x, add = 1) +#' +#' # "expand" range by 50% +#' rescale(x, multiply = 1.5) +#' rescale(x, add = 5) +#' +#' # Specify list of multipliers +#' d <- data.frame(x = 5:15, y = 5:15) +#' rescale(d, multiply = list(x = 1.1, y = 0.5)) +#' #' @inherit data_rename #' #' @return A rescaled object. @@ -75,6 +102,8 @@ rescale.default <- function(x, verbose = TRUE, ...) { #' @export rescale.numeric <- function(x, to = c(0, 100), + multiply = NULL, + add = NULL, range = NULL, verbose = TRUE, ...) { @@ -91,6 +120,9 @@ rescale.numeric <- function(x, range <- c(min(x, na.rm = TRUE), max(x, na.rm = TRUE)) } + # check if user specified "multiply" or "add", and then update "to" + to <- .update_to(x, to, multiply, add) + # called from "makepredictcal()"? Then we have additional arguments dot_args <- list(...) required_dot_args <- c("min_value", "max_value", "new_min", "new_max") @@ -144,6 +176,8 @@ rescale.grouped_df <- function(x, select = NULL, exclude = NULL, to = c(0, 100), + multiply = NULL, + add = NULL, range = NULL, append = FALSE, ignore_case = FALSE, @@ -188,6 +222,8 @@ rescale.grouped_df <- function(x, select = select, exclude = exclude, to = to, + multiply = multiply, + add = add, range = range, append = FALSE, # need to set to FALSE here, else variable will be doubled add_transform_class = FALSE, @@ -207,6 +243,8 @@ rescale.data.frame <- function(x, select = NULL, exclude = NULL, to = c(0, 100), + multiply = NULL, + add = NULL, range = NULL, append = FALSE, ignore_case = FALSE, @@ -245,9 +283,48 @@ rescale.data.frame <- function(x, if (!is.list(to)) { to <- stats::setNames(rep(list(to), length(select)), select) } + # Transform the 'multiply' so that it is a list now + if (!is.null(multiply) && !is.list(multiply)) { + multiply <- stats::setNames(rep(list(multiply), length(select)), select) + } + # Transform the 'add' so that it is a list now + if (!is.null(add) && !is.list(add)) { + add <- stats::setNames(rep(list(add), length(select)), select) + } + # update "to" if user specified "multiply" or "add" + to[] <- lapply(names(to), function(i) { + .update_to(x[[i]], to[[i]], multiply[[i]], add[[i]]) + }) x[select] <- as.data.frame(sapply(select, function(n) { rescale(x[[n]], to = to[[n]], range = range[[n]], add_transform_class = FALSE) }, simplify = FALSE)) x } + + +# helper ---------------------------------------------------------------------- + +#' expand the new target range by multiplying or adding +#' @keywords internal +.update_to <- function(x, to, multiply, add) { + # check if user specified "multiply" or "add", and if not, return "to" + if (is.null(multiply) && is.null(add)) { + return(to) + } + # only one of "multiply" or "add" can be specified + if (!is.null(multiply) && !is.null(add)) { + insight::format_error("Only one of `multiply` or `add` can be specified.") + } + # multiply? If yes, calculate the "add" value + if (!is.null(multiply)) { + x_range <- range(x, na.rm = TRUE) + x_diff <- diff(x_range) + add <- x_diff * (multiply - 1) + } + # add? + if (!is.null(add)) { + to <- c(min(x, na.rm = TRUE) - (add / 2), max(x, na.rm = TRUE) + (add / 2)) + } + to +} diff --git a/man/dot-update_to.Rd b/man/dot-update_to.Rd new file mode 100644 index 000000000..28a494203 --- /dev/null +++ b/man/dot-update_to.Rd @@ -0,0 +1,12 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/data_rescale.R +\name{.update_to} +\alias{.update_to} +\title{expand the new target range by multiplying or adding} +\usage{ +.update_to(x, to, multiply, add) +} +\description{ +expand the new target range by multiplying or adding +} +\keyword{internal} diff --git a/man/rescale.Rd b/man/rescale.Rd index fc8b0f2bf..23bdf7caf 100644 --- a/man/rescale.Rd +++ b/man/rescale.Rd @@ -11,13 +11,23 @@ rescale(x, ...) change_scale(x, ...) -\method{rescale}{numeric}(x, to = c(0, 100), range = NULL, verbose = TRUE, ...) +\method{rescale}{numeric}( + x, + to = c(0, 100), + multiply = NULL, + add = NULL, + range = NULL, + verbose = TRUE, + ... +) \method{rescale}{data.frame}( x, select = NULL, exclude = NULL, to = c(0, 100), + multiply = NULL, + add = NULL, range = NULL, append = FALSE, ignore_case = FALSE, @@ -31,9 +41,22 @@ change_scale(x, ...) \item{...}{Arguments passed to or from other methods.} -\item{to}{Numeric vector of length 2 giving the new range that the variable will have after rescaling. -To reverse-score a variable, the range should be given with the maximum value first. -See examples.} +\item{to}{Numeric vector of length 2 giving the new range that the variable +will have after rescaling. To reverse-score a variable, the range should +be given with the maximum value first. See examples.} + +\item{multiply}{If not \code{NULL}, \code{to} is ignored and \code{multiply} will be used, +giving the factor by which the actual range of \code{x} should be expanded. +For example, if a vector range from 5 to 15 and \code{multiply = 1.1}, the current +range of 10 will be expanced by the factor of 1.1, giving a new range of +11. Thus, the rescaled vector would range from 4.5 to 15.5.} + +\item{add}{If not \code{NULL}, \code{to} is ignored and \code{add} will be used, giving the +amount by which the actual range of \code{x} should be expanded. For example, +if a vector range from 5 to 15 and \code{add = 1}, the current range of 10 will +be expanced by 1, giving a new range of 11. Thus, the rescaled vector would +range from 4.5 to 15.5, because the lower and upper bounds are each expanded +by half of the amount specified in \code{add}.} \item{range}{Initial (old) range of values. If \code{NULL}, will take the range of the input vector (\code{range(x)}).} @@ -138,6 +161,22 @@ head(rescale(iris, to = list( "Sepal.Length" = c(0, 1), "Petal.Length" = c(-1, 0) ))) + +# "expand" ranges by a factor or a given value +x <- 5:15 +x +# both will expand the range by 10\% +rescale(x, multiply = 1.1) +rescale(x, add = 1) + +# "expand" range by 50\% +rescale(x, multiply = 1.5) +rescale(x, add = 5) + +# Specify list of multipliers +d <- data.frame(x = 5:15, y = 5:15) +rescale(d, multiply = list(x = 1.1, y = 0.5)) + } \seealso{ See \code{\link[=makepredictcall.dw_transformer]{makepredictcall.dw_transformer()}} for use in model formulas. diff --git a/tests/testthat/test-data_rescale.R b/tests/testthat/test-data_rescale.R index 3539e2fc0..e0db32a01 100644 --- a/tests/testthat/test-data_rescale.R +++ b/tests/testthat/test-data_rescale.R @@ -109,3 +109,32 @@ test_that("data_rescale regex", { ignore_attr = TRUE ) }) + + +# expanding range ------------------------------ +test_that("data_rescale can expand range", { + # for vectors + x <- 5:15 + expect_equal( + rescale(x, multiply = 1.1), + c(4.5, 5.6, 6.7, 7.8, 8.9, 10, 11.1, 12.2, 13.3, 14.4, 15.5), + ignore_attr = TRUE + ) + expect_equal(rescale(x, multiply = 1.1), rescale(x, add = 1), ignore_attr = TRUE) + expect_error(rescale(x, multiply = 0.9, add = 1), regex = "Only one of") + + # for data frames + d <- data.frame(x = 5:15, y = 5:15) + expect_equal( + rescale(d, multiply = 1.1), + rescale(d, add = 1), + ignore_attr = TRUE + ) + expect_equal( + rescale(d, multiply = list(x = 1.1, y = 0.5)), + rescale(d, add = list(x = 1, y = -5)), + ignore_attr = TRUE + ) + expect_error(rescale(d, multiply = 0.9, add = 1), regex = "Only one of") + expect_error(rescale(d, multiply = list(x = 0.9, y = 2), add = list(y = 1)), regex = "Only one of") +})