最近使用data.table这个增强包,来计算数据的聚合信息,比sql语句简单明了不是一点半点,于是想把data.table的资料翻译出来。
目录:
1) data.table 介绍
2) 语义引用
3) 主键、基于二分法搜索的subset
4) [二次索引和自动索引]()
5) Efficient reshaping using data.tables
原文地址:
1) Introduction to data.table
2) Reference semantics
3) Keys and fast binary search based subsets
4) Secondary indices and auto indexing
5) Efficient reshaping using data.tables
6) Frequently asked questions
本教程介绍data.table的语法,大概的形式,如何subset行,如何按列select/compute,如何分组聚合。熟悉data.frame的数据结构是有帮助的,不过不熟悉也没关系。
使用data.table分析数据
支持操作数据的功能,例如subset、group、update、join等。
这些功能可以让我们:
* 通过简洁一致的语法,实行想要的操作,达到目的。
* 从一系列函数到最终的分析,都没有将所有的操作都对应到函数的负担。能够流畅地执行分析。
* 精确地知道每步操作所需要的数据,内部自动优化操作,在运行速度和内存开销两方面都很有效果。
简要的讲,如果你对减小计算复杂度和计算时间有着迫切的需求,那么这个package就是为你量身打造的。data.table就是干这事儿的。我们通过这一系列教程,说明这些功能。
数据
在这个教程中,我们使用NYC-flights14的数据。它包含了2014年纽约机场发出的所有航班信息。这份数据只有2014年1月到10月是公开的。
我们可以使用data.table的fread()函数,用下面的方式,快速直接读取航班数据:
flights <- fread("https://raw.githubusercontent.com/wiki/arunsrinivasan/ flights/NYCflights14/flights14.csv")
flights
# year month day dep_time dep_delay arr_time arr_delay cancelled carrier tailnum flight
# 1: 2014 1 1 914 14 1238 13 0 AA N338AA 1
# 2: 2014 1 1 1157 -3 1523 13 0 AA N335AA 3
# 3: 2014 1 1 1902 2 2224 9 0 AA N327AA 21
# 4: 2014 1 1 722 -8 1014 -26 0 AA N3EHAA 29
# 5: 2014 1 1 1347 2 1706 1 0 AA N319AA 117
# ---
# 253312: 2014 10 31 1459 1 1747 -30 0 UA N23708 1744
# 253313: 2014 10 31 854 -5 1147 -14 0 UA N33132 1758
# 253314: 2014 10 31 1102 -8 1311 16 0 MQ N827MQ 3591
# 253315: 2014 10 31 1106 -4 1325 15 0 MQ N511MQ 3592
# 253316: 2014 10 31 824 -5 1045 1 0 MQ N813MQ 3599
# origin dest air_time distance hour min
# 1: JFK LAX 359 2475 9 14
# 2: JFK LAX 363 2475 11 57
# 3: JFK LAX 351 2475 19 2
# 4: LGA PBI 157 1035 7 22
# 5: JFK LAX 350 2475 13 47
# ---
# 253312: LGA IAH 201 1416 14 59
# 253313: EWR IAH 189 1400 8 54
# 253314: LGA RDU 83 431 11 2
# 253315: LGA DTW 75 502 11 6
# 253316: LGA SDF 110 659 8 24
dim(flights)
# [1] 253316 17
既然整个教程我们都会使用这份数据,那你不妨先下载到你的电脑上,然后每次使用的时候再读取。
介绍
在本章中,我们会学习下面两点:
1. 基础 - 什么是data.table,它的形式,如何subset行,如何select列,如何按列进行运算。
2. 聚合 - 按组聚合的效果。
1.基础
a) 什么是data.table
data.table是R语言的一个包,它是对data.frames的增强。在上文(读取航班)“数据”的部分,我们通过函数fread()创建了一个data.table。我们也可以通过函数data.table()创建一个data.table,比如这样:
DT = data.table(ID = c("b","b","b","a","a","c"), a = 1:6, b = 7:12, c=13:18)
DT
# ID a b c
# 1: b 1 7 13
# 2: b 2 8 14
# 3: b 3 9 15
# 4: a 4 10 16
# 5: a 5 11 17
# 6: c 6 12 18
class(DT$ID)
# [1] "character"
我们也可以通过as.data.table()将已经存在的对象转化成data.table。
注意:
* 不同于data.frames,字符型的列,不会被自动转化成因子。
* 行号后面有个冒号,用于隔开第一列的内容。
* 如果数据的条目超过了全局选项datatable.print.nrows所定义的数值(默认是100条),那么只会输出数据最开头和最末尾的5行。就如同上文(读取航班)“数据”的部分。
getOption("datatable.print.nrows")
# [1] 100
* data.table不能设置行的名称。我们会在第三讲中说明原因。
b) 形式-data.table增强了什么
和data.frame相反,我们能做的可不仅仅局限于subset行或者select列。首先介绍下data.table的语法,如下所示:
DT[i, j, by]
## R: i j by
## SQL: where select | update group by
如果你有SQL语句的基础,那么你应该能马上明白data.table的语法。
语法是:
对于DT这个data.table,使用 i 来subset行,然后计算 j ,最后用 by 分组。
c) subset行
- 获取六月份所有从”JFK”机场起飞的航班
ans <- flights[origin == "JFK" & month == 6L]
head(ans)
# year month day dep_time dep_delay arr_time arr_delay cancelled carrier tailnum flight origin
# 1: 2014 6 1 851 -9 1205 -5 0 AA N787AA 1 JFK
# 2: 2014 6 1 1220 -10 1522 -13 0 AA N795AA 3 JFK
# 3: 2014 6 1 718 18 1014 -1 0 AA N784AA 9 JFK
# 4: 2014 6 1 1024 -6 1314 -16 0 AA N791AA 19 JFK
# 5: 2014 6 1 1841 -4 2125 -45 0 AA N790AA 21 JFK
# 6: 2014 6 1 1454 -6 1757 -23 0 AA N785AA 117 JFK
# dest air_time distance hour min
# 1: LAX 324 2475 8 51
# 2: LAX 329 2475 12 20
# 3: LAX 326 2475 7 18
# 4: LAX 320 2475 10 24
# 5: LAX 326 2475 18 41
# 6: LAX 329 2475 14 54
说明:
* 通过data.frame的frame?,列可以像变量一样被引用。因此,我们不需要加上 flights$ 前缀,比如 flights$dest 和 flights$month,而是直接简单地引用 dest 和 month这两列。
* 满足 origin == "JFK" & month == 6L 这两个条件的行会被抽出出来。既然我们没有指定其他的条件,一个包含原数据里面所有列的data.table会被返回。
* 语法里面[i,j,k]的逗号不是必须的,当然如果指定了逗号,比如 flights[dest == "JFK" & month == 6L, ] 也是没问题的。但在data.frame里面,逗号却是必须的。
- 获取 flights 开头的两行
ans <- flights[1:2]
ans
# year month day dep_time dep_delay arr_time arr_delay cancelled carrier tailnum flight origin
# 1: 2014 1 1 914 14 1238 13 0 AA N338AA 1 JFK
# 2: 2014 1 1 1157 -3 1523 13 0 AA N335AA 3 JFK
# dest air_time distance hour min
# 1: LAX 359 2475 9 14
# 2: LAX 363 2475 11 57
说明:
* 我们没有指定任何条件。行的索引已经自动提供给参数 i 了。因此,我们得到一个包含原数据 flight 里所有列的data.table(for 这些行的索引?)。
- 排序(先按 origin列 的升序,再按 dest 的降序排列) 我们可以通过R语言的基础函数 order() 来完成这个功能。
ans <- flights[order(origin, -dest)]
head(ans)
# year month day dep_time dep_delay arr_time arr_delay cancelled carrier tailnum flight origin
# 1: 2014 1 5 836 6 1151 49 0 EV N12175 4419 EWR
# 2: 2014 1 6 833 7 1111 13 0 EV N24128 4419 EWR
# 3: 2014 1 7 811 -6 1035 -13 0 EV N12142 4419 EWR
# 4: 2014 1 8 810 -7 1036 -12 0 EV N11193 4419 EWR
# 5: 2014 1 9 833 16 1055 7 0 EV N14198 4419 EWR
# 6: 2014 1 13 923 66 1154 66 0 EV N12157 4419 EWR
# dest air_time distance hour min
# 1: XNA 195 1131 8 36
# 2: XNA 190 1131 8 33
# 3: XNA 179 1131 8 11
# 4: XNA 184 1131 8 10
# 5: XNA 181 1131 8 33
# 6: XNA 188 1131 9 23
说明:
内置的 order() 函数
* 我们可以对一个字符型的列,使用减号“-”,来实现降序排列。
* 另外,函数 order() 其实调用了data.table的快速基数排序函数 forder(),它比 base::order 快很多。这是一个说明它们基本区别的例子:
odt = data.table(col=sample(1e7))
(t1 <- system.time(ans1 <- odt[base::order(col)]))
(t2 <- system.time(ans2 <- odt[order(col)]))
(identical(ans1, ans2))
order() 比 base::order 大约快了16倍。我们会在data.table的内部教程中讨论data.table快速排序的更多细节。
* 因此,使用我们熟悉的函数,就可以显著地提高分析效率。
d) select列
- 选取 arr_delay 列,返回值是向量
ans <- flights[, arr_delay]
head(ans)
# [1] 13 13 9 -26 1 0
说明:
* 既然列可以作为变量被引用,我们可以直接引用我们想选取的列。
* 既然我们想选取所有的行,我们毋需指定参数 i。
* 返回了所有行的 arr_delay 列。
- 选取 arr_delay 列,返回值是data.table
ans <- flights[, list(arr_delay)]
head(ans)
# arr_delay
# 1: 13
# 2: 13
# 3: 9
# 4: -26
# 5: 1
# 6: 0
说明:
* 我们用 list() 把列名 arr_delay 包围起来,它可以确保返回值是data.table。正如前面一个例子,如果不这样做,返回值就是一个向量。
* data.table也允许用 .() 来包围列名,它是 list() 的别名,它们的效果是同样的。教程后面会使用 .() 来说明。
注意:
只要参数 j 返回一个list,这个list的每一个元素都会被转换成结果data.table的一列。你马上就会发现,这个功能是多么强大。
- 选取 arr_delay 列和 dep_delay 列
ans <- flights[, .(arr_delay, dep_delay)]
head(ans)
# arr_delay dep_delay
# 1: 13 14
# 2: 13 -3
# 3: 9 2
# 4: -26 -8
# 5: 1 2
# 6: 0 4
## alternatively
# ans <- flights[, list(arr_delay, dep_delay)]
说明:
* 使用 .() 或者 list() 都可以。
- 选取 arr_delay 列和 dep_delay 列,并把列名改为 delay_arr 和 delay_dep 既然 .() 是 list() 的别名,那么我们可以在创建 list 的时候对列命名。
ans <- flights[, .(delay_arr = arr_delay, delay_dep = dep_delay)]
head(ans)
# delay_arr delay_dep
# 1: 13 14
# 2: 13 -3
# 3: 9 2
# 4: -26 -8
# 5: 1 2
# 6: 0 4
就是这样。
e) 在参数j里运算
- 有多少航班完全没有延误
ans <- flights[, sum((arr_delay + dep_delay) < 0)]
ans
# [1] 141814
说明:
刚刚发生了什么?
* 参数 j 能做的,可不只是选取列这么简单,它能处理表达式,比如对列进行计算。这没什么大惊小怪的,因为列可以作为变量被引用嘛。所以,我们可以对这些变量调用函数。我们刚刚就是对两列求和(sum)了。
f) 在参数i里选取,在参数j里运算
- 在六月份,从”JFK”机场起飞的航班中,计算起飞和到达的平均延误时间
ans <- flights[origin == "JFK" & month == 6L,
.(m_arr=mean(arr_delay), m_dep=mean(dep_delay))]
ans
# m_arr m_dep
# 1: 5.839349 9.807884
说明:
* 我们首先在i参数里,找到所有符合 origin (机场)是"JFK",并且 month (月份)是 6 这样条件的行。此时,我们还没有subset整个data.table。
* 现在,我们看看参数j,它只使用了两列。我们需要分别计算这两列的平均值 mean()。这个时候,我们才subset那些符合i参数里条件的列,然后计算它们的平均值。
因为这三个参数(i,j和by)都被指定在同一个方括号中,data.table能同时接受这三个参数,并在计算之前,选取最优的计算方法,而不是分步骤计算。所以,我们可以避免对整个data.table计算,同时,在计算速度和内存使用量这两方面,取得最优的效果。
- 在六月份,从”JFK”机场起飞的航班一共有多少
ans <- flights[origin == "JFK" & month == 6L, length(dest)]
ans
# [1] 8422
函数 length() 需要一个参数。我们只需要知道,结果里有多少行数据。我们可以使用任何一列作为函数 length() 的参数。
这一类的操作特别频繁,特别是在下一节里,当我们需要分组的时候,会讲到这个特别的符号 .N。
特别的符号 .N
.N 是一个内建的变量,它表示当前的分组中,对象的数目。在下一节,当它和 by 一起使用的时候,我们会发现它特别有用。还没有涉及到分组的时候,它只是简单地返回行的数目。
所以,我们可以用 .N 来完成这个任务:
ans <- flights[origin == "JFK" & month == 6L, .N]
ans
# [1] 8422
说明:
* 再说一遍,首先在i参数里,找到所有符合 origin (机场)是"JFK",并且 month (月份)是 6 这样条件的行。
* 在参数j里,我们只指定了 .N,其他什么也没指定。所以实际上我们什么也没做。我们只是返回了符合条件的行的数目(就是行的 length长度)。
* 注意,我们没有用 list() 或者 .() 包围 .N,所以返回值是个向量。
我们也可以这样完成这个任务 nrow(flights[origin == "JFK" & month == 6L])。但是,这会从整个data.table里面subset符合条件的行,然后用 nrow() 返回行的数目,这是没有必要的,而且效率低下。我们会在 data.table的设计 这个教程里面说明这一点和其他的优化方法。
g) 太棒了!但我应该如何用参数j里面的名字引用列(就像在data.frame那样)
你可以使用 with = FALSE 来引用列名。
- 用data.frame的方式,选取 arr_delay 和 dep_delay 两列
ans <- flights[, c("arr_delay", "dep_delay"), with=FALSE]
head(ans)
# arr_delay dep_delay
# 1: 13 14
# 2: 13 -3
# 3: 9 2
# 4: -26 -8
# 5: 1 2
# 6: 0 4
这个参数叫做 with,是根据 R里面的函数 with() 演变而来的。假设你有一个data.frame叫做 DF,想要subset所有符合 x>1 的行。
DF = data.frame(x = c(1,1,1,2,2,3,3,3), y = 1:8)
## (1) normal way
DF[DF$x > 1, ] # data.frame needs that ',' as well
# x y
# 4 2 4
# 5 2 5
# 6 3 6
# 7 3 7
# 8 3 8
## (2) using with
DF[with(DF, x > 1), ]
# x y
# 4 2 4
# 5 2 5
# 6 3 6
# 7 3 7
# 8 3 8
说明:
* 在上面的 (2) using with里面,使用 with(),我们像变量一样使用DF的x列。
因此,在data.table里,我们设置 with=FALSE,使得我们不能再像变量一样引用列了,这被保存在“data.frame mode”中。
* 我们也可以使用 - 或者 ! 来排除列。比如:
ans <- flights[, !c("arr_delay", "dep_delay"), with=FALSE]
ans <- flights[, -c("arr_delay", "dep_delay"), with=FALSE]
* R语言从V1.9.5版开始,可以指定开始和结束的列名,比如通过指定 year:day 来选择前三列。
ans <- flights[, year:day, with=FALSE]
ans <- flights[, day:year, with=FALSE]
ans <- flights[, -(year:day), with=FALSE]
ans <- flights[, !(year:day), with=FALSE]
这在交互式的工作中特别方便。
with=FALSE 是data.table的默认值,因为我们可以通过参数j表达式,来做更多的事,特别是接下来一节我们要讲到的,和 by 的联合。
2.聚合
在前面一节,我们已经了解了参数i和j,知道了data.table的基本语法。在这一节,我们学习如何跟 by 相结合,做一些分组的操作。先来看看几个例子。
a) 用by分组
- 如何获取每个机场起飞的航班数
ans <- flights[, .(.N), by=.(origin)]
ans
说明:
* 我们知道 .N 表示当前的分组中,对象的数目。先按照 origin 列分组,再用 .N 获取每组的数目。
* 通过 head(flights),我们可以看到结果里面,机场是按照“JFK”, “LGA” 然后 “EWR” 的顺序排列的。原始数据里,被分组的那一列变量的顺序,也体现在结果里面。
* 既然我们没有在参数j里面指定列名,那这一列就自然是 N 了。
* by 也接受一个包含列名的字符向量作为参数。这在写代码的时候特别有用,比如设计一个函数,它的参数是要被分组的列。
* 当参数j和by里面只有一列,我们可以省略 .(),这实在很方便。刚刚的任务我们可以这样做:
ans <- flights[, .N, by=origin]
ans
# origin N
# 1: JFK 81483
# 2: LGA 84433
# 3: EWR 87400
只要允许,我们就会使用这种方便的形式。
- 如何获取美航(carrier code代码是“AA”)在每个机场起飞的航班数 航空公司代码“AA”代表美航。每个航空公司的代码都是唯一的。
ans <- flights[carrier == "AA", .N, by=origin]
ans
# origin N
# 1: JFK 11923
# 2: LGA 11730
# 3: EWR 2649
说明:
* 我们首先通过参数i,指定表达式 carrier == "AA",选取符合条件的行。
* 对于这些行,我们再按 origin 分组,获取每组的数目。再次声明,实际上没有列被重新创建,因为参数j表达式不需要获取列,因此在计算速度和内存使用量这两方面,取得最优的效果。
- 如何获取美航在所有机场的起/降的数目
ans <- flights[carrier == "AA", .N, by=.(origin,dest)]
head(ans)
说明:
* 参数by 可以接受多个列。我们可以指定所有我们想分组的列。
- 如何获取美航在所有机场的起/降的平均延误时间
ans <- flights[carrier == "AA",
.(mean(arr_delay), mean(dep_delay)),
by=.(origin, dest, month)]
ans
# origin dest month V1 V2
# 1: JFK LAX 1 6.590361 14.2289157
# 2: LGA PBI 1 -7.758621 0.3103448
# 3: EWR LAX 1 1.366667 7.5000000
# 4: JFK MIA 1 15.720670 18.7430168
# 5: JFK SEA 1 14.357143 30.7500000
# ---
# 196: LGA MIA 10 -6.251799 -1.4208633
# 197: JFK MIA 10 -1.880184 6.6774194
# 198: EWR PHX 10 -3.032258 -4.2903226
# 199: JFK MCO 10 -10.048387 -1.6129032
# 200: JFK DCA 10 16.483871 15.5161290
说明:
* 我们没有在参数j表达式中指定列名,它们会自动命名为(V1, V2)。 * 再次声明,原数据里面的顺序,会反映在结果中。
可是,如果我们想让结果按照 origin, dest 和 month 排序呢?
b) 参数keyby
data.table本身就被设计成能保持原数据的顺序。在一些情况下,必须保持原来的顺序。但是,有时我们希望自动根据分组的变量排序。
- 如何按照分组的变量排序
ans <- flights[carrier == "AA",
.(mean(arr_delay), mean(dep_delay)),
keyby=.(origin, dest, month)]
ans
# origin dest month V1 V2
# 1: EWR DFW 1 6.427673 10.0125786
# 2: EWR DFW 2 10.536765 11.3455882
# 3: EWR DFW 3 12.865031 8.0797546
# 4: EWR DFW 4 17.792683 12.9207317
# 5: EWR DFW 5 18.487805 18.6829268
# ---
# 196: LGA PBI 1 -7.758621 0.3103448
# 197: LGA PBI 2 -7.865385 2.4038462
# 198: LGA PBI 3 -5.754098 3.0327869
# 199: LGA PBI 4 -13.966667 -4.7333333
# 200: LGA PBI 5 -10.357143 -6.8571429
说明:
* 我们做的,只是把 by 改为了 keyby。这会自动的将结果按照升序排列。注意 keyby() 在完成操作后生效,例如,在计算结果后再排序。
keys:实际上 keyby 做的不只是排序。它在排序之后,设置一个叫做sorted的属性。我们会在下一教程学习更多关于 keys的内容。
现在,你需要知道的,就是使用 keyby 自动排序。
c) chaining表达式
让我们再来考虑下“获取美航在所有机场的起/降的数目”的问题。
ans <- flights[carrier == "AA", .N, by = .(origin, dest)]
- 如何让 ans 按origin的升序、按dest的降序排列 我们可以将中间结果保存为一个临时变量,再对这个变量使用 order(origin, -dest) 排序。这看上去还挺简洁明了的。
ans <- ans[order(origin, -dest)]
head(ans)
# origin dest N
# 1: EWR PHX 121
# 2: EWR MIA 848
# 3: EWR LAX 62
# 4: EWR DFW 1618
# 5: JFK STT 229
# 6: JFK SJU 690
说明:
* 回忆一下,我们在函数 order()中,对一个字符型的列使用 "-" 来降序排列。由于data.table的内部查询优化(internal query optimisation),这样做是可行的。
* 再回忆一下 order(...)已经通过data.table内部的快速基数排序函数 forder()优化过了。那么,我们可以继续使用熟悉的R的基础函数,而不是考虑使用data.table提供的速度快内存消耗少的排序方法。我们会在data.table internals的教程中说明更多细节。
但是这么做会生成一个临时变量,然后再修改这个临时变量。其实我们可以通过添加chaining表达式,避免生成临时变量。
ans <- flights[carrier == "AA", .N, by=.(origin, dest)][order(origin, -dest)]
head(ans, 10)
# origin dest N
# 1: EWR PHX 121
# 2: EWR MIA 848
# 3: EWR LAX 62
# 4: EWR DFW 1618
# 5: JFK STT 229
# 6: JFK SJU 690
# 7: JFK SFO 1312
# 8: JFK SEA 298
# 9: JFK SAN 299
# 10: JFK ORD 432
说明:
* 我们可以一个接一个地添加表达式,做一系列操作,就像这样:DT[...][...][...]。
* 或者你可以换行写:
DT[...
][...
][...
]
d) by表达式
- 参数by也可以接受表达式吗?还是只能指定列
当然可以接受表达式。举个例子,如果我们想要知道,有多少航班起飞延误但却提前/准时到达的,有多少航班起飞和到达都延误了……
ans <- flights[, .N, .(dep_delay>0, arr_delay>0)]
ans
# dep_delay arr_delay N
# 1: TRUE TRUE 72836
# 2: FALSE TRUE 34583
# 3: FALSE FALSE 119304
# 4: TRUE FALSE 26593
说明:
* 最后一行,满足 dep_delay > 0 = TRUE 且 arr_delay > 0 = FALSE 的条件。我们知道有26593次航班起飞延误但却提前/准时到达了。
* 注意,我们没有在by表达式里面指定任何列名。然而结果里面,列名还是自动的生成了。
* 我们可以在表达式里面指定其他的列,比如:DT[, .N, by=.(a, b>0)]。
e) 在参数j里面指定多个列
- 必须分别对每列指定 mean() 函数吗 当然不必分别对每列输入 mean(myCol) 了。要是我们有100列要计算平均值,不就惨了吗。
如何高效地计算呢。记不记得这个小贴士-“只要参数j 返回一个list,这个list的每一个元素都会被转换成结果data.table的一列。”假设我们分组的时候,可以像变量一样,引用每个分组的数据,那么就可以循环对所有的列应用函数 lapply() ,而不需要学习新的函数。
特殊的语法 .SD:
data.table提供一个特殊的语法,形式是 .SD。它是 Subset of Data 的缩写。它自身就是一个data.table,包含通过by 分组后的每一组。
回忆一下,一个data.table本质上是一个list,它们的列包含的元素个数都相同(其实就是行数)。
让我们用之前的一个data.table DT来看看 .SD 是如何使用的。
DT
# ID a b c
# 1: b 1 7 13
# 2: b 2 8 14
# 3: b 3 9 15
# 4: a 4 10 16
# 5: a 5 11 17
# 6: c 6 12 18
DT[, print(.SD), by=ID]
# a b c
# 1: 1 7 13
# 2: 2 8 14
# 3: 3 9 15
# a b c
# 1: 4 10 16
# 2: 5 11 17
# a b c
# 1: 6 12 18
# Empty data.table (0 rows) of 1 col: ID
说明:
* .SD 包含除了分组依据的那一列以外的所有列。
* 返回值依旧保持了原数据的顺序。首先打印出来的是 ID=“b” 的数据,然后是 ID=“a” 的,最后是 ID=“c” 的。
为了对复数的列进行计算,我们可以简单地使用函数 lapply()。
DT[, lapply(.SD, mean), by=ID]
# ID a b c
# 1: b 2.0 8.0 14.0
# 2: a 4.5 10.5 16.5
# 3: c 6.0 12.0 18.0
说明:
* .SD 分别包含了ID是 a、b、c的所有行,它们分别对应了各自的组。我们应用函数 lapply() 对每列计算平均值。
* 每一组返回包含三个平均数的list,这些构成了最终返回的data.table。
* 既然函数 lapply() 返回 list,我们就不需要在外面多加 .() 了。
差不多可以了,再补充一点。在 flights 这个 data.table里面,我们执行计算 arr_delay 和 dep_delay 这两列的平均值。但是,.SD 默认包含用于分组的所有列的平均值。
-如何指定希望计算平均值的列
.SDcols
使用参数 .SDcols。它接受列名或者列索引。比如,.SDcols = c("arr_delay", "dep_delay")能确保.SD之包含 arr_delay 和 dep_delay 这两列。
和 with = FALSE 一样,我们也可以使用 - 或者 ! 来移除列。比如,我们指定 !(colA:colB) 或者 -(colA:colB)表示移除从 colA 到 colB 的所有列。
现在让我们试着用 .SD 和 .SDcols 来获取 arr_delay 和 dep_delay 这两列的平均值,并且按照 origin, dest 和 month 来分组。
flights[carrier == "AA",
lapply(.SD, mean),
by=.(origin, dest, month),
.SDcols=c("arr_delay", "dep_delay")]
f) 对每组subset .SD
- 如何返回每个月的前两行
ans <- flights[, head(.SD, 2), by=month]
head(ans)
# month year day dep_time dep_delay arr_time arr_delay cancelled carrier tailnum flight origin
# 1: 1 2014 1 914 14 1238 13 0 AA N338AA 1 JFK
# 2: 1 2014 1 1157 -3 1523 13 0 AA N335AA 3 JFK
# 3: 2 2014 1 859 -1 1226 1 0 AA N783AA 1 JFK
# 4: 2 2014 1 1155 -5 1528 3 0 AA N784AA 3 JFK
# 5: 3 2014 1 849 -11 1306 36 0 AA N784AA 1 JFK
# 6: 3 2014 1 1157 -3 1529 14 0 AA N787AA 3 JFK
# dest air_time distance hour min
# 1: LAX 359 2475 9 14
# 2: LAX 363 2475 11 57
# 3: LAX 358 2475 8 59
# 4: LAX 358 2475 11 55
# 5: LAX 375 2475 8 49
# 6: LAX 368 2475 11 57
说明:
* .SD 包含了每组的所有行。我们可以简单的subset各组数据的前两行。
* 对每组数据,head(.SD, 2)返回的data.table同时也是个list。所以不需要用 .() 包围起来。
g) 为什么参数j这么灵活
这样,我们有了符合R语言风格的语法,我们也使用R语言里面既存的函数定义,而不是定义新的函数。我们用教程一开始创建的DT来说明。
-如何保存按照ID分组后数据中的 a列和 b列 的信息
DT
# ID a b c
# 1: b 1 7 13
# 2: b 2 8 14
# 3: b 3 9 15
# 4: a 4 10 16
# 5: a 5 11 17
# 6: c 6 12 18
DT[, .(val = c(a,b)), by=ID]
# ID val
# 1: b 1
# 2: b 2
# 3: b 3
# 4: b 7
# 5: b 8
# 6: b 9
# 7: a 4
# 8: a 5
# 9: a 10
# 10: a 11
# 11: c 6
# 12: c 12
说明:
* 就这样,不需要特殊的语法。我们需要知道的,就是用函数 c() 指定需要连结的向量。
- 如何将刚刚的数据,作为一列返回
DT[, .(val = list(c(a,b))), by=ID]
# ID val
# 1: b 1,2,3,7,8,9
# 2: a 4, 5,10,11
# 3: c 6,12
说明:
* 我们首先用 c(a,b) 连结了每组的值,然后用 list() 包围起来。那么对于每组数据,我们返回一个所有连结后的值的 list。
* 注意,那些逗号都是用来辅助显示的。一个list中的元素可以包含任何对象。在这个例子里,每个元素是一个向量,它们的长度都不相同。
一旦你对参数j的用法产生了兴趣,你会发现语法是多么强大。理解这些的一个有效的方法就是,在 print() 的帮助下,多多使用。
例如:
## (1) look at the difference between
DT[, print(c(a,b)), by=ID]
# [1] 1 2 3 7 8 9
# [1] 4 5 10 11
# [1] 6 12
# Empty data.table (0 rows) of 1 col: ID
## (2) and
DT[, print(list(c(a,b))), by=ID]
# [[1]]
# [1] 1 2 3 7 8 9
#
# [[1]]
# [1] 4 5 10 11
#
# [[1]]
# [1] 6 12
# Empty data.table (0 rows) of 1 col: ID
在(1)里面,每组返回一个向量,它们的长度分别是6,4,2.但是(2)里面,每组返回一个长度都为1的list,它们的第一个元素包含了长度为6,4,2的向量。因此,(1)的结果的长度是6+4+2=12,(2)的结果的长度是1+1+1=3。
总结
data.table的语法形式是:
DT[i, j, by]
指定参数i:
* 类似于data.frame,我们可以subset行,除非不需要重复地使用 DT$,既然我们能将列当做变量来引用。
* 我们可以使用order()排序。为了得到更快速的效果,order()函数内部使用了data.table的快速排序。
我们可以通过参数i做更多的事,得到更快速的选取和连结。我们可以在教程“Keys and fast binary search based subsets”和“Joins and rolling joins”中学到这些。
指定参数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可以很灵活地配置参数i和by实现强大的功能。
* by可以指定多个列,也可以指定表达式。
* 我们可以用 keyby,对分组的结果自动排序。
* 我们可以在参数j中指定 .SD 和 .SDcols,对复数的列进行操作。例如:
1.把函数fun 应用到所有 .SDcols指定的列上,同时对参数by指定的列进行分组:DT[, lapply(.SD, fun), by=., .SDcols=...]。
2.返回每组册前两行:DT[, head(.SD, 2), by=.]。
3.三个参数联合使用:DT[col > val, head(.SD, 1), by=.]。
小提示:
只要j返回一个list,这个list的每个元素都会是结果data.table的一列。
下一讲,我们学习如何用reference来add/update/delete某一列,如何通过i和by合并它们。