data.table
包提供了一个data.frame
的高级版本,由于它是C语言编写,相比data.frame
,data.table
能够让你更加快速高效的处理数据。非常适合那些需要处理大型数据集(比如 1GB 到100GB)和需要在内存中处理数据的人。虽然data.table
很强大,但是有些语法还是比较独特的,因此在学习之余顺手翻译了data.table
的文档,给自己个备忘,给大家个参考。
在数据分析中使用data.table
像subset
, group
, update
, join
等数据处理操作都有内在的关联,结合这些相关操作有以下优点:
- 可以不考虑你想要实现的一系列操作,通过简洁一致的语法达到最终目的。
- 在执行分析前,没有把每一步操作都映射到从一组可用的函数中的特定函数上的负担,从而流畅的执行分析。
- 通过精准的识别每一步操作需要的数据,自动优化内部操作,非常高效,从而实现快速计算和内存效率化。
简言之,如果你对缩短编程和计算时间非常感兴趣,这个包就适合你,data.table
坚持的哲学就是使这些成为可能。
Data准备
在这个介绍中使用的是NYC-flights14 数据。这份数据来自于运输统计局,它包含了2014年纽约机场发出的所有航班的准点航班数据。这份数据只有2014年1月到10月是公开的。老规矩,输入下列命令安装并载入data.table
包
|
|
data.table
的快速文件读取通过调用fread()
实现。
|
|
fread()
也可以直接读取网络链接。
|
|
fread()
也可以直接调用系统命令,这里不再详细举例,可以通过?fread
查询。
介绍
首先将会从最基本的操作开始。例如什么是data.table
,data.table
的基本形式,以及行列的抽取、选择和计算,第二部分将会介绍数据的聚合计算。
基本操作
创建变量
使用data.table
创建变量。
|
|
也可以使用as.data.table()
函数将现有数据转换成data.table格式。
|
|
说明:
- 与
data.frame
不同,字符型变量不会被自动转换为因子型。 - 行号与第一列之间会用
:
隔开。 - 当行号超过全局选项
datatable.print.nrows
时(默认为100),data.frame
自动输出前五行与最后五行。 data.frame
不会设置或使用行名。
一般形式
data.table
的一般形式如下,使用数据集DT
, 通过i
选取行,进行j
的计算,由by
进行分组。这类似于SQL语言。
|
|
通过i
进行行操作
抽取出发地为JFK
并且是六月的航班。
|
|
说明:
- 在
data.table
的框架内,列可以被看作是变量,因此我们不需要每次引用时都加上前缀flights$
。 - 例子中
j
,by
参数不存在,只对i
进行运算,结果会返回所有满足条件i
的列。 - 与
data.fram
不同,在i
后面不需要用,
进行分割。
也可以通过行号进行选取。
|
|
先对origin
进行升序排序,在对dest
进行降序排序。
|
|
order()
被内部优化
- 我们可以在框架内通过
-
号进行降序排列。 order()
命令调用forder()
,比base::order
快很多。
通过j
进行列操作
直接调用arr_delay
列,以向量的形式返回。
|
|
- 由于在
data.table
中列被看作是变量,我们可以直接调用我们想要的变量。跳过i
参数,返回的是所有行。
通过list(
)调用arr_delay
列,以data.table的形式返回。
|
|
同时选取arr_delay
和dep_delay
列
|
|
也可以用下面形式
|
|
同时选取arr_delay
和dep_delay
列并重命名为delay_arr
和delay_dep
由于.()
等同于list()
,我们可以在选取创建列表的时候命名列名。
|
|
在j
中进行运算
data.table中的j
不仅能处理列选取,也能处理表达式。例如,计算有多少航班延迟小于0
|
|
在i
中选择,在j
中运算
在data.table
框架内有三个组成部分,分别是i
, j
, by
。data.table计算前查看所有部分并整体优化请求,同时提高了速度和内存效率。例如,计算6月从JFK
机场出发的所有航班的平均到达延迟和起飞延迟。
|
|
查询出2014年6月出发机场为JFK
的航班数量。
|
|
特殊符号.N
.N
是一个特殊的内置变量,保存了当前组的样本长度,它在by
结合使用时非常有用,这在下一节会提到。在没有分组的情况下,它会直接返回抽取样本的长度。因此我们用.N
达到上面length()
的目的。
|
|
这里我们用相同的方法抽取了2014年6月出发机场为JFK
的航班,在j
部分当中我们用.N
只对抽取部分返回了样本长度。注意,我们并没有对.N
做list()
或者.()
处理,因此返回的是一个向量。
当然,我们可以通过nrow(flights[origin == "JFK" & month == 6L])
来实现相同的效果, 但是那样的话我们首先要在整个表单中抽取i
中相应的样本,然后用nrow()
函数返回长度,这样是没有必要且非效率的。
在j
中通过列名调用列(类似data.frame
)
通过使用with = FALSE
可以像data.frame一样调用列。
|
|
这里参数被命名为with
是因为它和R中with()
函数有类似的效果。假设我们要从data.frame
格式的DF中抽取x > 1
的样本,可以通过两种方式来实现,首先是通常的方法。
|
|
其次我们还可以通过with()
函数实现。
|
|
使用with()
函数时x
列会被看成是变量,我们通过with = FALSE
参数的设置就是避免了列被看成变量而被存储成data.frame
类型。通过查看类型我们可以看到ans
是data.table
类型,而通过with()
函数实现的类型则是data.frame
。
|
|
我们还可以通过-
或者!
实现列的删除,例如
|
|
从v1.9.5版本开始,可以通过指定起始列和终止列的列名来抽取列。
|
|
with = TRUE
是默认设置,因为这样允许j
处理表达式从而实现更多的操作,尤其是与by
结合使用。
聚合操作
这一小节中主要说明i
,j
,和by
结合的分组操作。
使用by
进行分组
计算每个出发机场航班的样本长度。
|
|
by
也可以使用字符型向量。由于只有一行,在j
和by
中也没有指定表达式,.()
可以省略掉,可以方便的用第二种形式代替。
|
|
by
也可以接受字符型向量
分组计算乘客代码为"AA"
,各个出发机场的航班的样本长度
|
|
by
可以进行多列计算,只需要提供多个列。按照origin, dest
分组计算乘客代码为"AA"
,各个出发机场的航班的样本长度。
|
|
也可以在by
使用字符型向量。
|
|
计算按照origin, dest, month
分组、乘客代码为"AA"
的平均到达延迟和出发延迟。
|
|
这里我们并没有在j
中指定表达式,结果自动生成V1
,V2
。
keyby
data.table
中保持了原始的分组顺序是有意为之。但是有时我们会需要根据我们分组的变量自动排序,这时可以使用keyby
对分组进行排序
|
|
Chaining
data.table
可以在进行链式调用,例如我们要对上述ans
结果进行按照orgin, -dest
进行排序,我们可以在结果后面加上如下命令。
|
|
上述操作需要对生成中间结果并对其进行覆盖操作,我们也可以直接通过链状表达实现相同的效果。
|
|
我们可以在一个表达式后边跟上另一个表达式,例如:DT[...][...][...]
,也可以垂直链接,例如:
|
|
by
中的表达式
by
也可以接受表达式,例如如果我们想要找到起飞延迟但是提前到达、或者准时到达的,出发和到达都延迟的等等,可以用如下代码实现。
|
|
最后一行代码对应的是dep_delay > 0 = TRUE
and arr_delay > 0 = FALSE
, 我们可以看出26593个航班虽然出发延迟但是都提前或者正点到达。同样,我们也没有给·by
表达式指定名字,变量名在结果中自动分配。
j
中的多列运算:.SD
在j
中我们可以使用我们非常熟悉的基本函数lapply()
来循环计算所有的列。这个方法是由data.table
提供的叫做.SD
的特殊符号来实现。首先我们通过之前的DT
数据概览一下.SD
。
|
|
.SD
默认包含除分组列外的所有列,返回结果是原始排序,即我们所看到的首先是ID = "b"
, 其次是ID = "a"
, 最后是ID = "c"
。
接下来我们简单的调用基本的R函数lapply()
实现多列运算。
|
|
说明:
.SD
按组保存相应的a
,b
,c
列,然后我们调用已经熟悉的基本函数lapply()
计算了每列的平均值。- 每个组返回包含平均值的三个元素的列表,而它们就成为结果的data.table。
- 由于
lapply()
返回的是列表,所以我们不需要额外的添加.()
如果我们不需要遍历所有的行,而是只计算其中两行,我们可以用.SDcols
参数。.SDcols
参数接受列名或者列索引,例如.SDcols = c("arr_delay", "dep_delay")
确保了.SD
每组只包含指定的两列。
和with = FALSE
用法相似,我们可以提供-
或!
符号移除列,也可以使用colA : colB
选择连续列,以及!(colA : colB)
或-(colA : colB)
反选连续列。下面让我们试着.SD
连同.SDcols
一起使用,按orgin
, dest
, month
分组,计算乘客代码为AA
,arr_delay
和dep_delay
两列的平均值。
|
|
对每组进行抽取.SD
我们可以通过如下代码返回每月的前两行数据。
|
|
说明:
.SD
是一个保存了分组所有行的data.table, 就如结果显示那样我们可以简单的抽取前两行。- 对于每个分组,
head(.SD, 2)
以同为列表的data.table形式返回了前两行, 因此我们不需要使用.()。
为什么j
如此灵活
这样我们就有了一致的语法并继续使用已有的(和熟悉的)基础函数而不是学习新的函数。举例说明的话,我们依然使用文章开始我们创建的data.table形式的DT,按ID分组对接列a
, b
。
|
|
说明:
- 就是这样。这里不需要特殊的语法。我们需要知道的只是用来连接向量的基本函数
c()
即可。
如果我们想要连接a
和b
所有值, 但是作为列表列返回呢?
|
|
说明:
- 在这里,我们首先将每个组的值与
c(a,b)
连接起来,然后转换成list()
。因此,对于每个组,我们返回一个所有连接值的列表。
一旦您开始在j
中使用内部用法,您就会意识到语法有多么强大。理解它的一个非常有用的方法是在print()
的帮助下多多尝试。
总结
data.table
的一般形式为:
|
|
i
的调用:
- 我们可以用与data.frame类似的方法抽取行 -由于列在data.frame框架内被看成是变量,你不需要重复使用
DT$
。 - 我们可以使用
order()
来对data.table进行排序,它内部调用data.table的快速索引有更好的表现。
我们可以通过调整参数i
做更多的工作,它可以得到更快速的选取和对接。
j
的调用:
- 得到
data.table
形式的行:DT[, .(colA, colB)]
- 得到
data.frame
形式的行:DT[, c("colA", "colB"), with = FALSE]
- 按行计算:
DT[, .(sum(colA), mean(colB))]
- 指定行名:
DT[, .(sA =sum(colA), mB = mean(colB))]
- 与
i
结合使用:DT[colA > value, sum(colB)]
by
的调用:
- 通过
by
, 我们可以指定列(列表形式),包含列名的字符向量,甚至是表达式来进行分组,j
的灵活性结合by
和i
能够实现非常强大的语法。 by
可以处理多个列,也可以处理表达式。- 我们可以调用
keyby
实现分组结果的自动排序。 - 我们可以使用我们比较熟悉的基本函数,调用
.SD
和.SDcols
在多个列上进行操作,这里有一些例子:DT[, lapply(.SD, fun), by = ..., .SDcols = ...]
: 通过by
分组,同时把fun
运用到.SDcols
指定的所有列上。DT[, head(.SD, 2), by = ...]
: 返回每个分组的前两行。DT[col > val, head(.SD, 1), by = ...]
:i
,j
,by
连同使用。
后记
一开始只想做个笔记,后来就变成了翻译的风格,整体上看来90%都遵照了官方文档的格式,但是英语渣,中文渣加上非专业出身,有好多语句可以理解但是始终无法优雅的用中文表达出来,附上官方文档链接,读不懂的请参照原文,有翻译错误的也请指出,多谢!