0

My task is to write a function, which aims to calculate logarithms of given variables (vars) in a given data set (dset) by levels of a declared variable (byvar). If the minimum of a given variable for a given level of byvar is greater than 0, a simple natural logarithm is calculated. Otherwise, new value of a given variable for a given segment is calculated as:

new.value =  log(old.value + 1 + abs(min.value.of.given.var.for.given.level)

In order to achieve this, I wrote such a code (for a reproducible example):

set.seed(1234567)  

data(iris)
iris$random <- rnorm(nrow(iris), 0, 1)

log.vars <- function(dset, vars, byvar, verbose = F){

  # a loop by levels of "byvar"

  for(i in 1:length(unique(dset[[byvar]]))){

    if(verbose == T){
      print(paste0("------ level=", unique(dset[[byvar]])[i], "----"))
    }

    # a loop by variables in "vars"

    for(j in 1:length(vars)){

      min.var <- min(dset[[vars[j]]][dset[[byvar]] == unique(dset[[byvar]])[i]])

      # if minimum of a given variable for a given level is greater than 0 then
      # calculate its logarithm;
      # otherwise, add to its value 1 and the mode of its minimum and calculate
      # its logarithm

      dset[[paste0("ln_", vars[j])]][dset[[byvar]] == unique(dset[[byvar]])[i]] <- 
        if(min.var > 0){
          log(dset[[vars[j]]][dset[[byvar]] == unique(dset[[byvar]])[i]])
        } else{
          log(dset[[vars[j]]][dset[[byvar]] == unique(dset[[byvar]])[i]] + 1 +
              abs(min.var))
        }
    }
  }
  return(dset)
}

iris2 <- log.vars(dset = iris,
         vars = c("Sepal.Length", "random", "Sepal.Width"),
         byvar = "Species",
         verbose = T)

head(iris2)

It works, however, there is a clear problem with its readability. Additionally, I wonder if its performance could be enhanced. Last but not least, the aim is to preserve the order of the observations in a data set. Any kind of help/suggestions would be appreciated

kaksat
  • 709
  • 1
  • 7
  • 18
  • Given that it works and you're just looking for improvements to readability and possibly performance, this is actually a more appropriate question for our sister site, CodeReview SE. – Hack-R Aug 23 '16 at 21:30

1 Answers1

2

Turning my comments to an answer:

Don't reinvent the wheel. There are good way to "do function by group" in base (tapply and ave), data.table, plyr, and dplyr. You don't just have to provide the function:

my_log = function(x) {
    m = min(x)
    if (m > 0) return(log(x))
    return(log1p(x - m))
}

The above implements the log you describe. Since you want to run this on the same grouping for multiple columns, dplyr::mutate_each can make our lives easy:

library(dplyr)
iris %>% group_by(Species) %>%
    mutate_each(funs = funs(logged = my_log))
# Source: local data frame [150 x 11]
# Groups: Species [3]
# 
#    Sepal.Length Sepal.Width Petal.Length Petal.Width Species       random Sepal.Length_logged
#           <dbl>       <dbl>        <dbl>       <dbl>  <fctr>        <dbl>               <dbl>
# 1           5.1         3.5          1.4         0.2  setosa  0.156703769            1.629241
# 2           4.9         3.0          1.4         0.2  setosa  1.373811191            1.589235
# 3           4.7         3.2          1.3         0.2  setosa  0.730670244            1.547563
# 4           4.6         3.1          1.5         0.2  setosa -1.350800927            1.526056
# 5           5.0         3.6          1.4         0.2  setosa -0.008514961            1.609438
# 6           5.4         3.9          1.7         0.4  setosa  0.320981863            1.686399
# 7           4.6         3.4          1.4         0.3  setosa -1.778148409            1.526056
# 8           5.0         3.4          1.5         0.2  setosa  0.909503835            1.609438
# 9           4.4         2.9          1.4         0.2  setosa -0.919404336            1.481605
# 10          4.9         3.1          1.5         0.1  setosa -0.157714831            1.589235
# # ... with 140 more rows, and 4 more variables: Sepal.Width_logged <dbl>, Petal.Length_logged <dbl>,
# #   Petal.Width_logged <dbl>, random_logged <dbl>

And that's all there is to it! That seems nice, concise, and readable. If you'd like to "functionalize" it even more, you can wrap that up into a function, something like below, for the same result:

log_vars = function(data, vars, byvar) {
    data %>% group_by_(byvar) %>%
        mutate_each_(funs = funs(logged = my_log), vars = vars) %>%
        return
}

log_vars(iris, vars = c("Sepal.Width", "random"), byvar = "Species")

Regarding your three asks:

  1. Readable - this seems much more readable. Can be rewritten without the %>% pipes if you prefer.
  2. Performance - this will be faster where it counts: largeish data with lots of groups.
  3. Order - the order of rows will not be changed.
Gregor Thomas
  • 136,190
  • 20
  • 167
  • 294