23

我正在尝试编写一些包装函数来减少代码重复data.table

这是一个使用mtcars. 首先,设置一些数据:

library(data.table)
data(mtcars)
mtcars$car <- factor(gsub("(.*?) .*", "\\1", rownames(mtcars)), ordered=TRUE)
mtcars <- data.table(mtcars)

现在,这是我通常会写的按组汇总计数的内容。在这种情况下,我按以下方式分组car

mtcars[, list(Total=length(mpg)), by="car"][order(car)]

      car Total
      AMC     1
 Cadillac     1
   Camaro     1
...
   Toyota     2
  Valiant     1
    Volvo     1

复杂之处在于,由于参数ij是在 的框架中评估的,因此如果要传入变量data.table,则必须使用:eval(...)

这有效:

group <- "car"
mtcars[, list(Total=length(mpg)), by=eval(group)]

但现在我想按相同的分组变量对结果进行排序。我无法得到以下任何变体来给我正确的结果。注意我总是得到单行结果,而不是有序集。

mtcars[, list(Total=length(mpg)), by=eval(group)][order(group)]
   car Total
 Mazda     2

我知道为什么:这是因为group在 中评估parent.frame,而不是在data.table.

我如何group在 的上下文中进行评估data.table

更一般地说,我如何在函数中使用它?我需要以下函数来给我所有的结果,而不仅仅是第一行数据:

tableOrder <- function(x, group){
  x[, list(Total=length(mpg)), by=eval(group)][order(group)]
}

tableOrder(mtcars, "car")
4

3 回答 3

14

加文和乔希是对的。这个答案只是为了添加更多背景。这个想法是,您不仅可以将变量列名传递给这样的函数,还可以使用quote().

group = quote(car)
mtcars[, list(Total=length(mpg)), by=group][order(group)]
      group Total
        AMC     1
   Cadillac     1
     ...
     Toyota     2
    Valiant     1
      Volvo     1

虽然,诚然更难开始,但它可以更灵活。反正就是这个想法。您需要的内部功能substitute(),如下所示:

tableOrder = function(x,.expr) {
    .expr = substitute(.expr)
    ans = x[,list(Total=length(mpg)),by=.expr]
    setkeyv(ans, head(names(ans),-1))    # see below re feature request #1780
    ans
}

tableOrder(mtcars, car)
      .expr Total
        AMC     1
   Cadillac     1
     Camaro     1
      ...
     Toyota     2
    Valiant     1
      Volvo     1

tableOrder(mtcars, substring(car,1,1))  # an expression, not just a column name
      .expr Total
 [1,]     A     1
 [2,]     C     3
 [3,]     D     3
 ...
 [8,]     P     2
 [9,]     T     2
[10,]     V     2

tableOrder(mtcars, list(cyl,gear%%2))   # by two expressions, so head(,-1) above
     cyl gear Total
[1,]   4    0     8
[2,]   4    1     3
[3,]   6    0     4
[4,]   6    1     3
[5,]   8    1    14

v1.8.0(2012 年 7 月)中添加了一个新参数keyby,使其更简单:

tableOrder = function(x,.expr) {
    .expr = substitute(.expr)
    x[,list(Total=length(mpg)),keyby=.expr]
}

和变量表达式领域i的评论和反馈是最受欢迎的。您可以做的另一件事是有一个表,其中一列包含表达式,然后查找要放入的表达式,或者从该表中查找。jbyijby

于 2012-03-14T17:32:57.403 回答
11

用于get(group)引用 中命名的对象group

> mtcars[, list(Total=length(mpg)), by=eval(group)][order(get(group))]
        car Total
        AMC     1
   Cadillac     1
     Camaro     1
   Chrysler     1
     Datsun     1
      Dodge     1
     Duster     1
    Ferrari     1
       Fiat     2
       Ford     1
      Honda     1
     Hornet     2
    Lincoln     1
      Lotus     1
   Maserati     1
      Mazda     2
       Merc     7
    Pontiac     1
    Porsche     1
     Toyota     2
    Valiant     1
      Volvo     1
cn      car Total
> # vs
> mtcars[, list(Total=length(mpg)), by=eval(group)][order(group)]
       car Total
[1,] Mazda     2

起作用的原因order(get(group))是表达式data.table. 在那里,get(group)将寻找一个 find 变量car。如果您在全球环境中评估它不存在

> get(group)
Error in get(group) : object 'car' not found

但它确实在进行评估的框架中。group那里不存在,但按照通常的规则,它会搜索父帧,直到找到匹配的东西,group在这种情况下是全局环境。因此,您需要注意您group在实际函数中使用的对象的名称 - 例如,您不想使用可能在data.table对象中匹配的东西。我猜使用类似.group函数 arg 的东西会很安全。

这是您的功能,已修改:

tableOrder <- function(x, .group){
  x[, list(Total=length(mpg)), by=eval(.group)][order(get(.group))]
}

> tableOrder(mtcars, "car")
        car Total
        AMC     1
   Cadillac     1
     Camaro     1
   Chrysler     1
     Datsun     1
....
于 2012-03-14T16:17:35.477 回答
10

对于如何在data.table中控制范围的一般问题,Gavin 的回答已经很好地涵盖了您。

但是,要真正充分利用data.table包的优势,您应该为data.table对象设置键。一个键会导致您的数据被预先排序,以便来自分组因子的同一级别(或级别组合)的行存储在连续的内存块中。与您的示例中使用的那种“ad hoc by”相比,这反过来可以大大加快分组操作。(在数据表常见问题解答(警告,pdf )中搜索“ad hoc”以获取更多详细信息)。

在许多情况下(包括您的示例),使用键还具有简化操作 data.table 所需的代码的愉快副作用。另外,它会自动按照键指定的顺序输出结果,这通常也是您想要的。

首先,如果您只需要按'car'列进行子集化,您可以简单地执行以下操作:

## Create data.table with a key
group <- "car"
mtcars <- data.table(mtcars, key = group)

## Outputs results in correct order
mtcars[, list(Total=length(mpg)), by = key(mtcars)]
        car Total
        AMC     1
   Cadillac     1
     Camaro     1
   Chrysler     1
     Datsun     1

即使您的密钥包含多列,使用密钥仍然可以简化代码(并且您获得了加速,这可能是您首先使用 data.table 的真正原因!):

group <- "car"
mtcars <- data.table(mtcars, key = c("car", "gear"))
mtcars[, list(Total=length(mpg)), by = eval(group)]

编辑:一个挑剔的注意事项

如果by参数用于基于作为键的一部分但不是键的第一个元素的列执行分组,则结果的顺序可能仍需要后处理。因此,在上面的第二个示例中,如果key = c("gear", "car"), then"Dodge"排序在 之前"Datsun"。在这种情况下,我可能仍然更喜欢事先重新排序密钥,而不是事后重新排序结果。也许 Matthew Dowle 会权衡这两者中的哪一个更受欢迎/更快。

于 2012-03-14T17:05:31.897 回答