我正在尝试编写一些包装函数来减少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
复杂的是,由于参数i
和j
是在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")
答案 0 :(得分:12)
Gavin和Josh是对的。这个答案只是为了增加更多背景。我们的想法是,您不仅可以使用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
,j
和by
变量表达式中的评论和反馈。您可以做的另一件事是有一个表,其中列包含表达式,然后查找要放在该表的i
,j
或by
中的表达式。
答案 1 :(得分: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)
将查找查找变量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
....
答案 2 :(得分:10)
关于如何在 data.table 中控制作用域的一般问题,Gavin的回答让你得到了很好的解决。
要真正充分利用 data.table 软件包的优势,您应该为data.table
对象设置密钥。密钥会导致数据被预先排序,以便来自分组因子的相同级别(或级别组合)的行存储在连续的内存块中。与“示例中使用的ad hoc类型”相比,这可以反过来大大加快分组操作。 (在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会权衡这两者中的哪一个更好/更快。