5 min read

data.table FAQ中文版

data.table中文版功能强大,但是某些用法却很隐晦。一直想熟悉一遍data.table的FAQs,进一步提高自己的数据清洗能力。

1 初学者FAQs

1.1 为什么DT[ , 5]和DT[2, 5]返回包括一列的data.table,而不是像data.frame一样返回向量

请注意,下边的写法,作者并不推荐。

library(data.table)
DT = data.table(x=rep(c("b","a","c"),each=3), y=c(1,3,6), v=1:9)
DT[ , 3] #返回data.table
##    v
## 1: 1
## 2: 2
## 3: 3
## 4: 4
## 5: 5
## 6: 6
## 7: 7
## 8: 8
## 9: 9
class(DT[ , 3])
## [1] "data.table" "data.frame"
DT[2, 3] #返回data.table
##    v
## 1: 2
class(DT[2, 3])
## [1] "data.table" "data.frame"

当在函数中使用 data.table 时,可能会接受不同的输入,为了保持一致性,使用DT[…]可以确保返回一个data.table。您不必像在data.frame中那样,需要记住使用drop=FALSE参数。data.table 于 2006 年首次发布,与 data.frame 的这种差异从一开始就就存在。

DF = setDF(copy(DT))
DF[ , 3] #返回向量
## [1] 1 2 3 4 5 6 7 8 9
class(DF[ , 3])
## [1] "integer"
DF[ , 3, drop=FALSE] #返回data.frame
##   v
## 1 1
## 2 2
## 3 3
## 4 4
## 5 5
## 6 6
## 7 7
## 8 8
## 9 9
class(DF[ , 3, drop=FALSE])
## [1] "data.frame"

您可能听说过,按数字而不是名称引用列通常是不好的做法。如果你的同事后来读你的代码,他们可能不得不四处寻找哪一列是第5列。如果某个人在R程序中更改了列的位置大于5,但是忘记更改代码中引用列编号 5 的所有位置,则可能会产生错误的结果,并且不会发出警告或错误。这是人为错误错,不是R的错,也不是data.table的错。这真的很糟糕。请不要这样做。这与专业SQL开发人员的口头禅相同:永远不要使用select *,总是按列名显式选择,这会对将来的更改保持鲁棒性。

ps:尽量不用直接用数字提取列信息

假设第 5 列被命名为"Region",您真的需要以向量的形式而不是 data.table提取该列内容。使用列名,写为DT$region或者DT[["region"]],具有更强的鲁棒性。如果使用R基础语法,鼓励使用$;如果使用data.table,推荐DT[["region"]]。当与<-组合赋值时(使用:=代替),但仅按名称选择单个列时,鼓励使用它们(意思不明确)。

DT[['x']] #返回向量
## [1] "b" "b" "b" "a" "a" "a" "c" "c" "c"
DT$x #返回向量
## [1] "b" "b" "b" "a" "a" "a" "c" "c" "c"
DT[ , x] #返回向量
## [1] "b" "b" "b" "a" "a" "a" "c" "c" "c"
typeof(DT) #DT本质上是以列表存储的
## [1] "list"

ps:如果想以向量的形式提取列内容,可以写作DT$Region或者DT[["Region"]],DT本质上可以理解为一个列表。

在某些情况下,通过数字引用列似乎是唯一的方法,例如有序列。在这些情况下,就像data.frame一样,你可以这样写为DT[ , 5:20],或者DT[ , c(1, 4, 10)]。 但是,考虑到将来列数和列顺序的变化,建议使用一个列命名范围如DT[,columnRed:columnViolet] 或者命名每一列DT[,c("columnRed","columnOrange","columnYellow")]。前期工作量可能会比较大,但将来你,以及你的同事可能会感谢自己,你的同事将来可能会感谢你。至少你可以说,如果出现问题,至少你已经尽力编写健壮的代码。

DT[ , x:y] #列范围方式也是可以的
##    x y
## 1: b 1
## 2: b 3
## 3: b 6
## 4: a 1
## 5: a 3
## 6: a 6
## 7: c 1
## 8: c 3
## 9: c 6
class(DT[ , x:y])
## [1] "data.table" "data.frame"
DT[ , c("x", "y", "v")] #直接指定字符类型的列名作为向量也可以提取
##    x y v
## 1: b 1 1
## 2: b 3 2
## 3: b 6 3
## 4: a 1 4
## 5: a 3 5
## 6: a 6 6
## 7: c 1 7
## 8: c 3 8
## 9: c 6 9
class(DT[ , c("x", "y", "v")])
## [1] "data.table" "data.frame"
DT[ , "x"] #只提取x列
##    x
## 1: b
## 2: b
## 3: b
## 4: a
## 5: a
## 6: a
## 7: c
## 8: c
## 9: c
DT[ , c("x")] #同上
##    x
## 1: b
## 2: b
## 3: b
## 4: a
## 5: a
## 6: a
## 7: c
## 8: c
## 9: c

ps:考虑使用列命名范围如DT[,columnRed:columnViolet]代替使用DT[ , 5:20]

但是,我们真正希望您做的是使用DT[ , .(columnRed,columnOrange,columnYellow)]这种形式来写代码;即,使用列名,就好像它们真的是DT[...]内部的变量一样。您不必像在 data.frame 中那样为每一列添加前缀DT$.()可以理解为list()的一个别名,如果您愿意,可以使用list()代替.()。你可以在list()中放置带有表达式的列名,并且返回不同长度的不同类型。我们过去曾强烈鼓励你这样做,以至于我们故意根本不让DT[,5]这种写法生效。在 2016年 11 月v1.9.8 发布之前 ,DT[,5]仅能返回数字5。当时的想法是,我们可以更简单地告诉大家,即DT[...]内部分总是在DT的框架内进行评估(它们将列名视为变量)。5评估为5,这种行为与上述单一规则一致。如果您真的想按列名称或数字选择列,我们要求您写为DT[,5,with=FALSE]。从 2016 年 11 月开始,您不需要使用with=FALSE,我们将看到为了与 data.frame 保持更大一致性,将如何帮助或阻碍新用户和长期用户。没有阅读此FAQ的新用户,甚至没有阅读第一个条目,如果他们期望data.table像data.frame一样工作,希望不会像以前那样被data.table绊倒。希望他们不会错过理解我们在DT[i, j, by]内放置列表达式的意图和建议。如果他们像data.frame一样使用data.table,他们将不会获得任何好处。如果您认识这样的人,请友好地推荐他们像您一样阅读本文档。

ps:当要进行列计算时,使用DT[ , .(columnRed,columnOrange,columnYellow)]更方便;.()等同于list()。

DT[ , .(x,y)] #data.table作者鼓励这种写法
##    x y
## 1: b 1
## 2: b 3
## 3: b 6
## 4: a 1
## 5: a 3
## 6: a 6
## 7: c 1
## 8: c 3
## 9: c 6
DT[ , list(x,y)] #效果同上
##    x y
## 1: b 1
## 2: b 3
## 3: b 6
## 4: a 1
## 5: a 3
## 6: a 6
## 7: c 1
## 8: c 3
## 9: c 6
DT[ , .(toupper(x), y^2)] #可以直接对列进行计算和变换
##    V1 V2
## 1:  B  1
## 2:  B  9
## 3:  B 36
## 4:  A  1
## 5:  A  9
## 6:  A 36
## 7:  C  1
## 8:  C  9
## 9:  C 36

ps: DT[ , 5]这种写法是不鼓励的,只是为了保持跟data.frame的一致性。

提醒:您可以像使用变量一样将列名包裹在任何R表达式中放入DT[...];例如,尝试DT[, colA*colB/2]因为您使用列名就好像它们是变量一样,所以这里确实会返回一个向量。使用.()包装后,将会返回一个data.table,DT[,.(colA*colB/2)];写为DT[,.(myResult = colA*colB/2)],命名返回的列。我们将让您猜测如何从此查询中返回两件事。在匿名主体中做一堆事情也很常见:DT[, { x<-colA+10; x*x/2 }];或者调用另一个包的函数:DT[ , fitdistr(columnA, "normal")]

DT[ , y*v] #返回一个变量
## [1]  1  6 18  4 15 36  7 24 54
DT[ , .(y*v)] #返回一个data.table
##    V1
## 1:  1
## 2:  6
## 3: 18
## 4:  4
## 5: 15
## 6: 36
## 7:  7
## 8: 24
## 9: 54
DT[ , list(y*v)] #跟上边等同
##    V1
## 1:  1
## 2:  6
## 3: 18
## 4:  4
## 5: 15
## 6: 36
## 7:  7
## 8: 24
## 9: 54
DT[ , .(yv=y*v)] #将返回的新列命名为yv,推荐这种写法!!!
##    yv
## 1:  1
## 2:  6
## 3: 18
## 4:  4
## 5: 15
## 6: 36
## 7:  7
## 8: 24
## 9: 54
DT[ , {z <- y*10; z*z/2}] #在j(列计算)位置,可以放置多个表达式,返回最后一个表达式的值,同样是向量
## [1]   50  450 1800   50  450 1800   50  450 1800
DT[ , .({z <- y*10; z*z/2})] #返回列表
##      V1
## 1:   50
## 2:  450
## 3: 1800
## 4:   50
## 5:  450
## 6: 1800
## 7:   50
## 8:  450
## 9: 1800
DT[ , MASS::fitdistr(y*v,"normal")] #调用MASS包中的fitdistr函数求均值和标准差
##      mean         sd    
##   18.333333   16.377491 
##  ( 5.459164) ( 3.860212)

简单小结一下:

  • DT[ , 5]返回data.table

  • DT[ , c("Region")] 返回data.table

  • DT[ , .(Region)] 返回data.table;推荐这种写法,可以方便做列转换和计算

  • DT$Region 返回一个向量

  • DT[["Region"]] 返回一个向量,推荐这种写法

  • DT[ , Region] 返回一个向量

1.2 为什么DT[,“region”]返回一个单列的data.table而不是向量?

答案参见 1.1。可以用DT$Region或者DT[["Region"]]返回向量。

1.3 为什么DT[ , region]返回一个向量?我想要返回一个单列的data.table。

DT[ , region]返回一个向量的解释,参见1.1,是这样说的:因为您使用列名就好像它们是变量一样,所以这里确实会返回一个向量。如果想返回data.table,可以使用DT[ , .(region)]这种写法。

1.4 为什么DT[ , x, y, z]不起作用?我想要返回包括三列x、y和z的一个data.table。

data.table的书写形式为DT[i, j, by],列计算和提取需要放在第二个参数j中。正确的写法:DT[ , .(x, y, z)]或者DT[ , c("x", "y", "z")]

1.5 我对变量mycol进行赋值mycol = "x",但是DT[ , mycol]返回一个字符”x”,而不是DT中x列的内容。如何让DT中查找mycol变量中包含的列名x?

在2016年11月发布的v1.9.8中,有一种启用新行为的功能:options(datatable.WhenJisSymbolThenCallingScope=TRUE)。然后它就会像您预期的那样工作,就像data.Frame一样。如果您是data.table的新用户,您可能应该这样做。您可以将此命令放在.Rprofile文件中,这样您就不必再次记住。

在没有启用这种新行为的情况下,j表达式看到的是调用作用域中的对象。变量mycol并不存在于DT的列名中,因此data.table随后在调用作用域中查找,在那里找到mycol,并返回它的值”x”。当前,这是正确的行为。如果mycol是列名,那么该列的数据就会被返回。如果写作DT[ , mycol, with=False],将根据需求,返回DT中x列的数据。这种写法,在未来也会奏效。因为data.table也是一个列表,如果要返回向量,可以写作DT[[mycol]]

ps:这种需求在数据处理中很常见。譬如,需要灵活的指定提取某些列的内容。我们通常会提前在一个变量中定义要提取的列名。如果是要返回一个data.table,可以这样写:DT[ , ..mycol]或者DT[ , mycol, with = FALSE]或者DT[ , .SD, .SDcols = mycol]

稍微扩展一下:前缀..是告诉data.table显式引用变量mycol的父范围,而不是来自DT数据集。

如果想返回一个向量,可以这样写DT[[mycol]]

mycol <- c("x")
DT[ , ..mycol] #返回一个data.table
##    x
## 1: b
## 2: b
## 3: b
## 4: a
## 5: a
## 6: a
## 7: c
## 8: c
## 9: c
DT[ , mycol, with = FALSE] #返回一个data.table
##    x
## 1: b
## 2: b
## 3: b
## 4: a
## 5: a
## 6: a
## 7: c
## 8: c
## 9: c
DT[ , .SD, .SDcols=mycol] #利用.SD返回一个data.table
##    x
## 1: b
## 2: b
## 3: b
## 4: a
## 5: a
## 6: a
## 7: c
## 8: c
## 9: c
DT[[mycol]] #返回一个向量
## [1] "b" "b" "b" "a" "a" "a" "c" "c" "c"