Using a list of symbols in data.table's 'by'

Viewed 172

I want to write a function outer_fun(), which does some things and also calls another function inner_fun(). All arguments from outer_fun() are passed to inner_fun().

inner_fun() does some calculations on a data.table (which is an argument to both functions). Another argument to be passed through the function is by.

Here is a sketch of what I have:

library(data.table)

data("CO2")
setDT(CO2)

outer_fun <- function(DT, by) {
    # some other stuff
    by <- substitute(by)
    inner_fun(DT, by)
}

inner_fun <- function(DT, by) {
    DT[, .(mean = mean(uptake)),
    by = list(Plant, by)]
}

outer_fun(CO2, by = Type)

This throws an error:

Error in `[.data.table`(DT, , .(mean = mean(uptake)), by = list(Plant,  : 
  column or expression 2 of 'by' or 'keyby' is type language. Do not quote column names. Usage: DT[,sum(colC),by=list(colA,month(colB))] 

As far as I understand the issue, I do have to properly combine the two lists in by within inner_fun(). Another attempt to do this was this:

outer_fun <- function(DT, by) {
    # some other stuff
    by <- substitute(by)
    inner_fun(DT, by)
}
inner_fun <- function(DT, by) {
    .by <- append(by, substitute(Plant), after = 0L)
    DT[, .(mean = mean(uptake)),
       by = eval(as.expression(list(.by)))]
}

outer_fun(CO2, by = Type)

This throws a similar error:

 Error in `[.data.table`(DT, , .(mean = mean(uptake)), by = eval(as.expression(list(.by)))) : 
  column or expression 1 of 'by' or 'keyby' is type symbol. Do not quote column names. Usage: DT[,sum(colC),by=list(colA,month(colB))] 

I'm struggeling here for two days now and I would really appreciate help on this!

EDIT: It seems, I have not been clear on what the desired solution should be capable of. The result should work for all kinds of by which are allowed in data.table, such as:

outer_fun(CO2, by = Type)
outer_fun(CO2, by = "Type")
outer_fun(CO2, by = .(Type, Treatment))
outer_fun(CO2, by = c("Type", "Treatment"))
outer_fun(CO2, by = "Type,Treatment")
outer_fun(CO2, by = Treatment == "chilled")
outer_fun(CO2, by = cut(conc, breaks = quantile(conc)))
...
3 Answers

I think it is better to let [.data.table function handle the by argument by itself. This will make both the inner_fun and outer_fun much easier. The drawback is that other fixed grouping variables like Plant should be provided in by argument of outer_fun.

outer_fun <- function(DT, ...) {
    inner_fun(DT, ...)
}
inner_fun <- function(DT, ...) {
    DT[, .(mean = mean(uptake)), ...]
}

Then, all the following examples are possible:

outer_fun(CO2, by = .(Plant, Type, Treatment))
outer_fun(CO2, by = c("Plant", "Type", "Treatment"))
outer_fun(CO2, by = "Plant,Type,Treatment")
outer_fun(CO2, by = .(Plant, Treatment == "chilled"))
outer_fun(CO2, by = .(Plant, cut(conc, breaks = quantile(conc), include.lowest = T)))

I would adress the problem by providing the by information as characters eventually

library(data.table)

data("CO2")
setDT(CO2)

outer_fun <- function(DT, by) {
  # some other stuff
  by <- substitute(by)
  inner_fun(DT, by)
}

inner_fun <- function(DT, by) {

  # add Plant to the by Information
  byFun <- c("Plant", as.character(by))

  # remove list oder c()- function names
  byFun <- byFun[!byFun %in% c(".", "list", "c")] 

  DT[, .(mean = mean(uptake)),
     by = byFun]
 }

# single column name unquoted
outer_fun(CO2, by = Type)[1:3]
#>    Plant   Type     mean
#> 1:   Qn1 Quebec 33.22857
#> 2:   Qn2 Quebec 35.15714
#> 3:   Qn3 Quebec 37.61429


#list of column names unquoted
outer_fun(CO2, by = .(Type, Treatment))[1:3]
#>    Plant   Type  Treatment     mean
#> 1:   Qn1 Quebec nonchilled 33.22857
#> 2:   Qn2 Quebec nonchilled 35.15714
#> 3:   Qn3 Quebec nonchilled 37.61429

outer_fun(CO2, by = list(Type, Treatment))[1:3]
#>    Plant   Type  Treatment     mean
#> 1:   Qn1 Quebec nonchilled 33.22857
#> 2:   Qn2 Quebec nonchilled 35.15714
#> 3:   Qn3 Quebec nonchilled 37.61429


# single column name as string
outer_fun(CO2, by = "Type")[1:3]
#>    Plant   Type     mean
#> 1:   Qn1 Quebec 33.22857
#> 2:   Qn2 Quebec 35.15714
#> 3:   Qn3 Quebec 37.61429


# multiple column names as string
outer_fun(CO2, by = c("Type", "Treatment"))[1:3]
#>    Plant   Type  Treatment     mean
#> 1:   Qn1 Quebec nonchilled 33.22857
#> 2:   Qn2 Quebec nonchilled 35.15714
#> 3:   Qn3 Quebec nonchilled 37.61429

outer_fun(CO2, by = list("Type", "Treatment"))[1:3]
#>    Plant   Type  Treatment     mean
#> 1:   Qn1 Quebec nonchilled 33.22857
#> 2:   Qn2 Quebec nonchilled 35.15714
#> 3:   Qn3 Quebec nonchilled 37.61429

Here is an option using Chapter 6: Computing on the language:

outer_fun <- function(DT, outby) {
    inner_fun(DT, substitute(outby))
}

inner_fun <- function(DT, inby) {
    l <- as.list(inby)

    if (l[[1L]] == as.symbol("list") || l[[1L]] == as.symbol(".")) 
        l <- l[-1L]

    if (is.character(l[[length(l)]])) {
        if (length(l) == 1L)
            l <- strsplit(l[[1L]],",")[[1L]]
        l <- lapply(l, as.name)

        if (l[[1L]] == as.symbol("c"))
            l <- l[-1L]
    }

    BY <- as.call(c(as.symbol("list"), as.name("Plant"), l))
    eval(bquote(DT[, mean(uptake), .(BY)]))
}


outer_fun(CO2, outby = list(Type, Treatment))
outer_fun(CO2, outby = list(Type))
outer_fun(CO2, outby = .(Type, Treatment))
outer_fun(CO2, outby = list(Treatment == "chilled"))
outer_fun(CO2, outby = list(cut(conc, breaks = quantile(conc))))
outer_fun(CO2, outby = Type)

outer_fun(CO2, outby = "Type")
outer_fun(CO2, outby = c("Type", "Treatment"))
outer_fun(CO2, outby = "Type,Treatment")
Related