【问题标题】:Using data.table i and j arguments in functions在函数中使用 data.table i 和 j 参数
【发布时间】:2012-03-31 02:53:17
【问题描述】:

我正在尝试编写一些包装函数来减少 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 的框架。

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

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

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

tableOrder(mtcars, "car")

【问题讨论】:

    标签: r data.table


    【解决方案1】:

    加文和乔希是对的。这个答案只是为了添加更多背景。这个想法是,您不仅可以将变量列名传递给这样的函数,还可以使用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]
    }
    

    欢迎在ijby 变量表达式区域提出意见和反馈。您可以做的另一件事是创建一个表,其中列包含表达式,然后从该表中查找要放入 ijby 的表达式。

    【讨论】:

    • +1 有用信息马修,我想知道文档中“表达式”的真正含义是什么,并试图摆弄expression(),所以指向quote() 的指针最有帮助。
    • @Gavin NP。您看到常见问题解答 1.6 和 1.7 了吗?我只记得他们完全涵盖了这一点。
    • @Gavin 任何改进文档的技巧都非常感谢。我离树太近了,看不到树林。
    • @GavinSimpson -- 关于 R 上下文中“表达式”一词的歧义性非常好。 quote(car) 实际上是一个 调用,它计算出一个 name 类的对象。 group 是一个对象,其 value 是一个 name 对象。 expression 类的对象根本不进入。不过,我不确定什么是最好的解释。说“by 也可以取值为名称的对象的名称”是准确的,但对于大多数人来说可能会令人困惑。像 Matthew 所做的那样,让示例保持突出可能是最好的方法。
    • +1 谢谢。 FWIW,我详细研究了常见问题解答 1.6 和 1.7,但在发布此问题之前仍然找不到解决方案。这些常见问题解答非常有助于指出我使用 eval(...) 作为 j 参数,但我不记得在相同的情况下看到对 substitute() 的引用。无论如何,非常感谢。你的答案正是我所需要的。
    【解决方案2】:

    使用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) 将寻找一个查找变量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
    ....
    

    【讨论】:

      【解决方案3】:

      对于如何在 data.table 中控制范围的一般问题,Gavin 的回答已经为您提供了很好的解决方案。

      不过,要真正充分利用 data.table 包的优势,您应该为 data.table 对象设置密钥。一个键会导致您的数据被预先排序,以便来自分组因子的同一级别(或级别组合)的行存储在连续的内存块中。与您的示例中使用的那种“ad hoc by”相比,这反过来可以大大加快分组操作。 (在 datatable-faq (warning, 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"),那么"Dodge" 排在"Datsun" 之前。在这种情况下,我可能仍然更喜欢事先重新排序密钥,而不是事后重新排序结果。也许 Matthew Dowle 会权衡这两者中哪一个更受欢迎/更快。

      【讨论】:

      • 按要求称重......你就在现场。如果组很大,那么让它们在 RAM 中连续可将页面提取保存到 L2。但是,如果有数百万个 2 行组,那么差别不大。即使内置了辅助键,由于这个原因,主键上的 by 仍然会更快。非常类似于 SQL 中的聚集索引;即,这就是行实际存储在磁盘上的方式,以保存数据块读取。这很令人困惑,不是吗,因为人们可能会自然地认为,之后对(数量少得多的)聚合行进行排序会更快。
      • keyed by 不需要先排序,但 ad hoc 需要。所以(我认为) setkey + keyedby 与 ad hoc by 相当。需要小心比较苹果和苹果。无论如何,如果j 使用DT 列的一小部分,那么setkey 上的DT[,columnsneeded] (仅)可能是值得的,以节省重新排序不需要的列。
      • @MatthewDowle -- 谢谢。这很有启发性。使用setkey 的一个好处是您只需要对数据进行一次排序。但是,使用它对 data.table 进行排序是否也比使用 method="radix" 对同一个 data.table 排序一次要快得多?我怀疑您在 FAQ 3.2 中写道“这也是 setkey() 快速的原因之一”时暗示了一些这样的速度优势,但我不确定我是否知道在哪里可以获得更多详细信息。
      • 看起来我们的最后一个 cmets 交叉路径,但我们正在考虑同样的问题。不过,你说得更简洁;)
      • setkey 所做的排序与由 ad hoc 调用的函数相同:data.table:::fastorder。 FAQ 3.2 只是意味着排序很快。在 v1.8.0 中(在 R-Forge 上稳定,很快在 CRAN 上稳定),character 的排序也很快,character 允许在键中。最后!在内部,它对character 使用计数排序,并且在某些测试中甚至可以比“基数”更快(在其他测试中几乎一样快),但对于character。因素不再是强制性的,但仍受支持。
      猜你喜欢
      • 2012-10-20
      • 1970-01-01
      • 2019-04-19
      • 2012-08-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-12-11
      • 1970-01-01
      相关资源
      最近更新 更多