data.table是由Matt Dowle创件的一个数据处理的包。其设计的主要目的是高效迅速的对大批量数据分组处理操作。 它通过高效内存操作,设置keys、快速分组,以及滚动时序的快速合并的技巧,使其达到令人惊讶的效率。data.table主要通过二元检索法大大提高数据操作的效率,当然同时它也兼容适用于data.frame的向量检索法。
本文编译自Andrew Brooks总结的一些data.table的实用技巧。本文目的不在于介绍data.table的基本用法,而是在假设读者对data.table有一定了解后,通过具体的实例展示data.table的更加巧妙及强大的数据操作能力。如果没有接触过data.table,并想快速上手,建议参考datacamp提供的一个cheat sheet(https://s3.amazonaws.com/assets.datacamp.com/img/blog/data+table+cheat+sheet.pdf)。
文中所用数据实例来自R base中的Motor Trend Car Road Tests数据集,包含了32款车的11种特征:油耗, 马力, 车重等等。
1.数据结构与赋值
Columns of lists
如下的语句利用data.table语法summary一个数据表(long or narrow),和普通常用方法相比,没有特别之处。
dt <- data.table(mtcars)[, .(cyl, gear)]
dt[,unique(gear), by=cyl]
cyl V1
1: 6 4
2: 6 3
…如果我们想得到对于每个cyl值,所有可能的gear列表,并把结果作为单独的一列添加到原始的表中(后续的例子中可以了解这样做的原因),data.table可以非常容易的做到。 如下给出了两种语法。
dt <- data.table(mtcars)[,.(gear, cyl)]
dt[,gearsL:=list(list(unique(gear))), by=cyl]
dt[,gearsL:=.(list(unique(gear))), by=cyl]
gear cyl gearsL
1: 4 6 4,3,5
2: 4 6 4,3,5
…使用第二种方法更简洁,可是不直观。 但是,如果了解并习惯data.table中.(的操作,在这里其和list是等效的操作。
Accessing elements from a column of lists
完成上面的操作后,新得到的列gerasL实际是一个由list的元素组成的一个list。如果我们想得到每一行对应的这个list中特定位置的值,应该怎么用data.table完成?
提示:对list的批量操作,我们可以使用lapply()和sapply()这两个函数。
dt[,gearL1:=lapply(gearsL, function(x) x[2])]
dt[,gearS1:=sapply(gearsL, function(x) x[2])]
head(dt)
gear cyl gearsL gearL1 gearS1
1: 4 6 4,3,5 3 3
2: 4 6 4,3,5 3 3
…注意,新得到的两列gearL1和gearS1本质上是不同的数据结构, 如下所示:
str(head(dt[,gearL1]))
List of 6
:num3 : num 3
…
str(head(dt[,gearS1]))
num [1:6] 3 3 3 3 5 3有没有更简洁的操作呢? 类似我们先前的例子,可以考虑使用`[操作符(即R的取值操作):
dt[,gearL1:=lapply(gearsL, [, 2)]
dt[,gearS1:=sapply(gearsL, [, 2)]上面的例子中,得到了对于每一行数据的,相同cyl的所有的gear的可能值列表,进一步,想得到所有出去该行数据的gear值的所有相同cyl组中的其他可能值,如何操作? 可以考虑使用mapply()函数。
dt[,other_gear:=mapply(function(x, y)
setdiff(x, y), x=gearsL, y=gear)]
head(dt)
gear cyl gearsL gearL1 gearS1 other_gear
1: 4 6 4,3,5 3 3 3,5
2: 4 6 4,3,5 3 3 3,5
…进一步简化,可以写成:
dt[,other_gear:=mapply(setdiff, gearsL, gear)]
Suppressing intermediate output with {}
这里是借用了base的{功能,将一系列的语句括成一段,可以按顺序完成一系列的操作,而不需要保留中间过程的临时变量。 比如如下的例子:
dt <- data.table(mtcars)
默认情况下,只返回{}括起的程序段中最后一个指令的结果,并且是未命名的。
dt[,{tmp1=mean(mpg); tmp2=mean(abs(mpg-tmp1)); tmp3=round(tmp2, 2)}, by=cyl]
cyl V11: 6 1.19
2: 4 3.83
3: 8 1.79也可以通过list操作,将选定的多个结果传递出来
dt[,{tmp1=mean(mpg); tmp2=mean(abs(mpg-tmp1)); tmp3=round(tmp2, 2);list(tmp2=tmp2, tmp3=tmp3)}, by=cyl]cyl tmp2 tmp3
1: 6 1.191837 1.19
2: 4 3.833058 3.83
3: 8 1.785714 1.79如果写成多行操作,可以略去”;”。
dt[,{tmp1=mean(mpg)
tmp2=mean(abs(mpg-tmp1))
tmp3=round(tmp2, 2)
list(tmp2=tmp2, tmp3=tmp3)},
by=cyl]cyl tmp2 tmp31: 6 1.191837 1.19
2: 4 3.833058 3.83
3: 8 1.785714 1.79这种在data.table使用语句块的方法,没有内置的:= 赋值方法直接,但:= 不适合与{合用。使用:= 给多列同时赋值的时候,不能像先前我们展示的那样,利用先前构建的列来计算新构建的列。如果使用:=可以利用data.table的Chaining功能,然后在最后舍弃中间变量:
dt <- data.table(mtcars)[,.(cyl, mpg)]
dt[,tmp1:=mean(mpg), by=cyl][,tmp2:=mean(abs(mpg-tmp1)), by=cyl][,tmp1:=NULL]
head(dt)
cyl mpg tmp2
1: 6 21.0 1.191837
2: 6 21.0 1.191837
…Fast looping with set
大多数情况下, loop和set操作的结合可以用:= 赋值操作在data.table中简单有效的实现。多数情况下,:=非常灵活和有效。 但当某些情况下,必须进行loop操作时,set的效率远大于R base中的赋值操作。
(注:最新版的R base DF赋值操作的效率已经有了显著提高,和set的效率差别有缩小,但仍然有近60倍的区别)M = matrix(1,nrow=100000,ncol=100)
DF = as.data.frame(M)
DT = as.data.table(M)
system.time(for (i in 1:1000) DF[i,1L] <- i) # 0.238s
system.time(for (i in 1:1000) DT[i,V1:=i]) # 0.356s
system.time(for (i in 1:1000) M[i,1L] <- i) # 0.020s
system.time(for (i in 1:1000) set(DT,i,1L,i)) # 0.004s某些情况下,结合lapply函数与.SD的操作,data.table的作者偏好用loop+set完成。当我们需要对特定的列的子集操作的时候,使用set是非常好的选择。(相同的结果也可以使用.SDcols 来得到)
dt <- data.table(mtcars)[,1:5, with=F]
for (j in c(1L,2L,4L)) set(dt, j=j, value=-dt[[j]])
integers using ‘L’ passed for efficiency
for (j in c(3L,5L)) set(dt, j=j, value=paste0(dt[[j]],’!!’))
head(dt)mpg cyl disp hp drat1: -21.0 -6 160!! -110 3.9!!
2: -21.0 -6 160!! -110 3.9!!
…Using shift to lead/lag vectors and lists
这个功能之在1.9.5后续版本才有。Base R没有一个很好的工具,来处理向量中的leads/lags。
dt <- data.table(mtcars)[,.(mpg, cyl)]
dt[,mpg_lag1:=shift(mpg, 1)]
dt[,mpg_forward1:=shift(mpg, 1, type=’lead’)]
head(dt)mpg cyl mpg_lag1 mpg_forward11: 21.0 6 NA 21.0
2: 21.0 6 21.0 22.8
…shift with by
creating some data
n <- 30
dt <- data.table(
date=rep(seq(as.Date(‘2010-01-01’), as.Date(‘2015-01-01’), by=’year’), n/6),
ind=rpois(n, 5),
entity=sort(rep(letters[1:5], n/5))
)
setkey(dt, entity, date) # important for ordering
dt[,indpct_fast:=(ind/shift(ind, 1))-1, by=entity]
lagpad <- function(x, k) c(rep(NA, k), x)[1:length(x)]
dt[,indpct_slow:=(ind/lagpad(ind, 1))-1, by=entity]head(dt, 10)
date ind entity indpct_fast indpct_slow1: 2010-01-01 4 a NA NA
2: 2011-01-01 5 a 0.2500000 0.2500000
3: 2012-01-01 5 a 0.0000000 0.0000000
…Create multiple columns with := in one statement
利用一个操作时创建多个新的列,可以使用:=来实现,需要注意的是,新建的列只能由表中已有的列计算得到。
dt <- data.table(mtcars)[,.(mpg, cyl)]
dt[,:=(avg=mean(mpg), med=median(mpg), min=min(mpg)), by=cyl]
head(dt)mpg cyl avg med min1: 21.0 6 19.74286 19.7 17.8
2: 21.0 6 19.74286 19.7 17.8
…Assign a column with := named with a character object
新创建列时,列名已经保存在你程序中的某变量中,使用:=赋值操作时,只需要简单的用括号括上该字符串变量。
dt <- data.table(mtcars)[, .(cyl, mpg)]
thing2 <- ‘mpgx2’
dt[,(thing2):=mpg*2]head(dt)
cyl mpg mpgx21: 6 21.0 42.0
2: 6 21.0 42.0
…也可以使用with=FALSE这个option,但不推荐使用该方法。
thing3 <- ‘mpgx3’
dt[,thing3:=mpg*3, with=F]
head(dt)cyl mpg mpgx2 mpgx31: 6 21.0 42.0 63.0
2: 6 21.0 42.0 63.0
…1. by的使用
Calculate a function over a group (using by) excluding each entity in a second category.
我们可以从一个具体的实例场景来更好的理解data.table中by的实现机制和具体作用。利用我们先前的数据例子:mtcars,假设我们想比较每一行数据(每一款车)中的mpg和与其相同类别(相同的cylinders)中的其他车的平均mpg,但是在计算平均值进行比较时,需要的是无偏的估计,也就是需要计算去除该车以外其他所有车的mpg的均值。 如何在data.table中高效的实现?
METHOD 1: in-line
首先来计算有偏均值计算
dt <- data.table(mtcars)[,.(cyl, gear, mpg)]
dt[, mpg_biased_mean:=mean(mpg), by=cyl]
head(dt)cyl gear mpg mpg_biased_mean1: 6 4 21.0 19.74286
2: 6 4 21.0 19.74286
…然后利用data.table中.GRP操作, 求无偏均值。
dt[, dt[!gear %in% unique(dt$gear)[.GRP], mean(mpg), by=cyl], by=gear] gear cyl V11: 4 6 19.73333
2: 4 8 15.10000
3: 4 4 25.96667
…我们可以简单的验证如上的结果,比如说,想得到所有cyl为6的但gear不为4的mpg均值:
dt[gear!=4 & cyl==6, mean(mpg)]
[1] 19.73333对应先前结果的第一行。
类似于使用.GRP方法的等效操作,可以写成:
dt[, dt[!gear %in% .BY[[1]], mean(mpg), by=cyl], by=gear] #unbiased mean
gear cyl V11: 4 6 19.73333
2: 4 8 15.10000
3: 4 4 25.96667
…下面我们来分析下,.GRP在这里的具体作用原理
首先:.GRP是按by操作的结果给每个group一个访问的ID.这里是按cyl分组。
dt[, .GRP, by=cyl]
cyl GRP
1: 6 1
2: 4 2
3: 8 3然后, 可以得到每一个cyl组中,有多少不同的gear
dt[, .(.GRP, unique(dt$gear)[.GRP]), by=cyl]
cyl GRP V2
1: 6 1 4
2: 4 2 3
3: 8 3 5最外层,再次嵌套按gear分组,把得到的信息扩展开
dt[,dt[, .(.GRP, unique(dt$gear)[.GRP]), by=cyl], by=gear]
gear cyl GRP V21: 4 6 1 4
2: 4 4 2 3
3: 4 8 3 5
…如何提高多次分组的效率? 使用data.table中setting key功能。
setkey(dt, gear)
uid <- unique(dt$gear)
dt[, dt[!.(uid[.GRP]), mean(mpg), by=cyl] , by=gear] #unbiased meangear cyl V11: 3 6 19.74000
2: 3 4 27.18000
3: 3 8 15.40000
…检查结果
mean(dt[cyl==4 & gear!=3,mpg])
[1] 27.18METHOD 2: using {} and .SD
我们在前面的例子中已经了解过,利用{}操作可以隐藏数据操作中的中间变量,这里我们可以利用这个特性来解决求无偏均值的问题。
一步的实现
dt[, .SD[, mean(mpg)], by=gear] # same as dt[, mean(mpg), by=gear]
gear V11: 3 16.10667
2: 4 24.53333
3: 5 21.38000dt[, .SD[, mean(mpg), by=cyl], by=gear]
same as dt[, mean(mpg), by=.(cyl, by=gear)]
or dt[, mean(mpg), by=.(cyl, gear)]嵌套data.table和by操作
下面的程序嵌套了两个by操作和两层不同的data.table。其中的n是某cyl类别中车的数量,而N是某cyl组中特定gear的车的数量。多个计算组成程序块,由{}括起。
dt[,{
vbar = sum(mpg)
n = .N
.SD[,.(n, .N, sum_in_gear_cyl=sum(mpg), sum_in_cyl=vbar), by=gear]
} , by=cyl]cyl gear n N sum_in_gear_cyl sum_in_cyl1: 6 3 7 2 39.5 138.2
2: 6 4 7 4 79.0 138.2
3: 6 5 7 1 19.7 138.2
…作为测试,检查结果:
dt[,sum(mpg), by=cyl]
cyl V11: 6 138.2
2: 8 211.4
3: 4 293.3有了这个结果后, 计算无偏均值就很直接简单了。
dt[,{
vbar = mean(mpg)
n = .N
.SD[,(n*vbar-sum(mpg))/(n-.N),by=gear]} , by=cyl]
cyl gear V1
1: 6 3 19.74000
2: 6 4 19.73333
3: 6 5 19.75000
…METHOD 3: Super Fast Mean calculation
有没有方法更快的完成这个任务? 可以考虑利用向量操作的快速的特点对每一个gear和cyl的组合来计算无偏的mpg。实现上,利用类似的想法,先计算有偏的均值,然后减去需要排除的实例。
dt <- data.table(mtcars)[,.(mpg,cyl,gear)]
dt[,:=(avg_mpg_cyl=mean(mpg), Ncyl=.N), by=cyl]
dt[,:=(Ncylgear=.N, avg_mpg_cyl_gear=mean(mpg)), by=.(cyl, gear)]
dt[,unbmean:=(avg_mpg_cyl*Ncyl-(Ncylgear*avg_mpg_cyl_gear))/(Ncyl-Ncylgear)]
setkey(dt, cyl, gear)
head(dt)mpg cyl gear avg_mpg_cyl Ncyl Ncylgear avg_mpg_cyl_gear unbmean1: 21.5 4 3 26.66364 11 1 21.500 27.18000
2: 22.8 4 4 26.66364 11 8 26.925 25.96667
3: 24.4 4 4 26.66364 11 8 26.925 25.96667
…为了代码的易读和维护,可以把一系列操作抽提成自定义函数。
leaveOneOutMean <- function(dt, ind, bybig, bysmall) {
dtmp <- copy(dt)
# copy so as not to alter original dt object w intermediate assignments
dtmp <- dtmp[is.na(get(ind))==F,]
dtmp[,:=(avg_ind_big=mean(get(ind)), Nbig=.N), by=.(get(bybig))]
dtmp[,:=(Nbigsmall=.N, avg_ind_big_small=mean(get(ind))), by=.(get(bybig), get(bysmall))]
dtmp[,unbmean:=(avg_ind_big*Nbig-(Nbigsmall*avg_ind_big_small))/(Nbig-Nbigsmall)]
return(dtmp[,unbmean])
}dt <- data.table(mtcars)[,.(mpg,cyl,gear)]
dt[,unbiased_mean:=leaveOneOutMean(.SD, ind=’mpg’, bybig=’cyl’, bysmall=’gear’)]
dt[,biased_mean:=mean(mpg), by=cyl]
head(dt)mpg cyl gear unbiased_mean biased_mean1: 21.0 6 4 19.73333 19.74286
2: 21.0 6 4 19.73333 19.74286
…下面比较上述三种方法的速度。
dt <- data.table(mtcars)
dt <- dt[sample(1:.N, 100000, replace=T), ]
increase # of rows in mtcars
dt$gear <- sample(1:300, nrow(dt), replace=T)
adding in more cateogries方法3:
system.time(dt[,unbiased_mean_vectorized:=leaveOneOutMean(.SD, ind=’mpg’, bybig=’cyl’, bysmall=’gear’)])
user system elapsed0.053 0.004 0.056
方法2:
system.time(dt[,{
+ vbar = mean(mpg)
+ n = .N
+ .SD[,(n*vbar-sum(mpg))/(n-.N),by=gear]
+ } , by=cyl])user system elapsed
0.016 0.000 0.016方法1:
uid <- unique(dt$gear)system.time(dt[, dt[!gear %in% (uid[.GRP]), mean(mpg), by=cyl] , by=gear][order(cyl, gear)])
user system elapsed
3.58 0.00 3.47可以看出,方法2最快,方法3比方法1要快100倍。需要注意的是,方法2得到的是summary table,需要有额外的操作才能把结果join到原始表中。
Using [1], [.N], setkey and by for within group subsetting
问题:列出表中column X的最大值,其对应的column Y是按column Z分组中的最大值。
首先,例如,列出每一cyl组中最大的qsec
dt <- data.table(mtcars)[, .(cyl, mpg, qsec)]
dt[, max(qsec), by=cyl]
cyl V11: 6 20.22
2: 4 22.90
3: 8 18.00每一cyl组中最大mpg对应的qsec?
setkey(dt, mpg)
dt[,qsec[.N], by=cyl]cyl V11: 8 17.05
2: 6 19.44
3: 4 19.90每一cyl组中最小mpg对应的qsec?
dt[,qsec[1], by=cyl]
cyl V11: 8 17.98
2: 6 18.90
3: 4 18.60找到qsec值相对应的mpg为每一cyl组的中值?
dt[,qsec[round(.N/2)], by=cyl]
cyl V11: 8 18.0
2: 6 15.5
3: 4 16.7这里,大概可以了解了[1]或[.N]的机制了,下面我们看一个稍微复杂的例子。创建两列,一个是每一cyl组中mpg的标准偏差,另一列是每一cyl组中top 50%的标准偏差。
dt <- data.table(mtcars)
setkey(dt,mpg)dt[, .(sd(mpg), sd(mpg[1:round(.N/2)])), by=cyl]
cyl V1 V21: 8 2.560048 2.0926174
2: 6 1.453567 0.8981462
3: 4 4.509828 1.77285081. FUNCTIONS
如何在函数中传递data.table的列名做为参数?
方法1: 不使用引号,使用deparse和substitute的结合。
dt <- data.table(mtcars)[,.(cyl, mpg)]
myfunc <- function(dt, v) {
v2=deparse(substitute(v))
dt[,v2, with=F][[1]] # [[1]] returns a vector
}
myfunc(dt, mpg)方法2: 使用引号,利用get函数操作
dt <- data.table(mtcars)
myfunc <- function(dt, v) dt[,get(v)]myfunc(dt, ‘mpg’)