![9bf800bd066d96e4af1ccf1498602651.png](https://img-blog.csdnimg.cn/img_convert/9bf800bd066d96e4af1ccf1498602651.png)
在上一篇文章里面,我们已经讲了data.table的基础,在这篇文章里,主要讲一些高级技巧。
在by关键字后面使用表达式
我们先创造一个data.table对象,基于mtcars数据框
> mtcars_dt=data.table(mtcars)
> head(mtcars_dt)
mpg cyl disp hp drat wt qsec vs am gear carb
1: 21.0 6 160 110 3.90 2.620 16.46 0 1 4 4
2: 21.0 6 160 110 3.90 2.875 17.02 0 1 4 4
3: 22.8 4 108 93 3.85 2.320 18.61 1 1 4 1
4: 21.4 6 258 110 3.08 3.215 19.44 1 0 3 1
5: 18.7 8 360 175 3.15 3.440 17.02 0 0 3 2
6: 18.1 6 225 105 2.76 3.460 20.22 1 0 3 1
按照初级篇介绍的语法,我们可以把不同的车型按照vs(引擎类型)和am(自动还是手动挡)分组,并计算每一组的平均mpg:
> mtcars_dt[,.(mean_mpg=mean(mpg)),by=.(vs,am)]
vs am mean_mpg
1: 0 1 19.75000
2: 1 1 28.37143
3: 1 0 20.74286
4: 0 0 15.05000
我们不仅可以对不同的列直接分组,也可以在by后面直接使用表达式(expression),比如下面的代码:
> mtcars_dt[,.(mean_mpg=mean(mpg)),by=.(vs==1,am==1)]
vs am mean_mpg
1: FALSE TRUE 19.75000
2: TRUE TRUE 28.37143
3: TRUE FALSE 20.74286
4: FALSE FALSE 15.05000
setindex和setindexv
初级篇里面我们介绍了setkey和setkeyv的使用。每一次setkey,data.table都会被排序,排序的复杂度是O(nlogn),如果可以避免还是应该避免的。setindex和setindexv则不会对data.table对象进行重新排序。另外,一个data.table只能有一组key,但是可以有不同的indices,这也是key与index的一个主要区别。
我们可以给予index快速地对一个data.table进行筛选,比如我们可以选择gear=4的所有行:
> mtcars_dt[.(4), on='gear']
mpg cyl disp hp drat wt qsec vs am gear carb
1: 21.0 6 160.0 110 3.90 2.620 16.46 0 1 4 4
2: 21.0 6 160.0 110 3.90 2.875 17.02 0 1 4 4
3: 22.8 4 108.0 93 3.85 2.320 18.61 1 1 4 1
4: 24.4 4 146.7 62 3.69 3.190 20.00 1 0 4 2
5: 22.8 4 140.8 95 3.92 3.150 22.90 1 0 4 2
6: 19.2 6 167.6 123 3.92 3.440 18.30 1 0 4 4
7: 17.8 6 167.6 123 3.92 3.440 18.90 1 0 4 4
8: 32.4 4 78.7 66 4.08 2.200 19.47 1 1 4 1
9: 30.4 4 75.7 52 4.93 1.615 18.52 1 1 4 2
10: 33.9 4 71.1 65 4.22 1.835 19.90 1 1 4 1
11: 27.3 4 79.0 66 4.08 1.935 18.90 1 1 4 1
12: 21.4 4 121.0 109 4.11 2.780 18.60 1 1 4 2
请注意以上语法,on='gear'表明我们要基于gear这一列进行过滤,过滤的条件就是gear=4,4需要放在.()里面。如果gear不是numeric类型,那我们不需要把4放在.()中,比如下面的代码:
> setindex(iris_dt,Species)
> head(iris_dt['virginica',on='Species'])
Sepal.Length Sepal.Width Petal.Length Petal.Width Species
1: 4.9 2.5 4.5 1.7 virginica
2: 5.6 2.8 4.9 2.0 virginica
3: 5.7 2.5 5.0 2.0 virginica
4: 5.8 2.7 5.1 1.9 virginica
5: 5.8 2.7 5.1 1.9 virginica
6: 5.8 2.8 5.1 2.4 virginica
也许读到这你会有一个疑问,为什么需要通过setindex来过滤,为什么不直接用gear==4来过滤呢,因为setindex之后data.table会使用二分查找(复杂度是O(logn)),而不是线性的筛选(复杂度O(n))。如果一个index只被用到一次,那在生成index的时候,会有一个排序的操作,复杂度是O(nlogn),所以并不是很合算,但是index在data.table中是可以重复使用的,也就是一次排序之后可以进行多次二分查找。
我们也可以基于index进行更复杂的操作,比如我们想从mtcars数据中选择gear==4的车型,根据发动机的缸数分组,计算每一组的平均mpg并且对这一结果命名为mean_mpg,并且我们想要结果按照发动机的缸数进行排序。对于这样的一个复杂操作,在data.table里面只要简介的一行代码:
> mtcars_dt[.(4L),.(gear,mean_mpg=mean(mpg)),keyby='cyl',on='gear']
cyl gear mean_mpg
1: 4 4 26.925
2: 4 4 26.925
3: 4 4 26.925
4: 4 4 26.925
5: 4 4 26.925
6: 4 4 26.925
7: 4 4 26.925
8: 4 4 26.925
9: 6 4 19.750
10: 6 4 19.750
11: 6 4 19.750
12: 6 4 19.750
上面的keyby和by的区别在与keyby不仅会分组,还会根据分组的列进行排序,所以我们看到结果是按照cyl进行排序的。看到这里你有没有觉得data.table优雅又强大?
值得注意的一点是,data.table对象可能会自动创造index;比如当我们对某一列使用==或者 %in%,那么给予这一列的index会被自动创造。
set函数
我们在初级篇里面介绍了:=操作符可以在原始的data.table对象中创造新的列。如果这一列已经存在,按:=可以修改这一列的值。在data.table中还有一个set()函数,也可以用来修改data.table中的元素的值。:=的速度已经很快,但是如果你需要在循环中改变元素的值,也许set()函数的速度更快。下面的例子里,我们先创造一个1000✖️1000的数组,然后创造一个同样形状的data.table对象,然后我们分别用:=和set()函数修改data.table对象的元素。
> a=array(0,c(1000,1000))
> a_dt=data.table(a)
> system.time(for (i in 1:1000) a[i,1L] = i)
user system elapsed
0.003 0.000 0.003
> system.time(for (i in 1:1000) a_dt[i,V1:=i])
user system elapsed
1.448 0.020 0.377
> system.time(for (i in 1:1000) set(a_dt,i,1L,i))
user system elapsed
0.003 0.000 0.003
上面的结果显示,和for loop联合使用时,set()的性能远超:=,几乎和对数组直接操作的速度一致。当然,我的建议是如果可以用array类型,就不要去用data.frame或者data.table,避免任何的额外性能开销。
shift
>stock_dt=data.table(company=c('apple','apple','apple','orange','orange'),price=c(200,220,250,200,240),date=c(as.Date('2011-01-01'),as.Date('2011-01-02'),as.Date('2011-01-03'),as.Date('2011-01-01'),as.Date('2011-01-02')))
> stock_dt
company price date
1: apple 200 2011-01-01
2: apple 220 2011-01-02
3: apple 250 2011-01-03
4: orange 200 2011-01-01
5: orange 240 2011-01-02
我们想计算每一只股票每日较前一个交易日的价格变化
> stock_dt[,change:=price-shift(price,1),by=company]
> stock_dt
company price date change
1: apple 200 2011-01-01 NA
2: apple 220 2011-01-02 20
3: apple 250 2011-01-03 30
4: orange 200 2011-01-01 NA
5: orange 240 2011-01-02 40
使用:=同时创造多个新的列
我们可以使用:=操作符生成新的一列,那么如果想同时生成多列呢?直接的办法就是使用:=多次,每一次产生一列。其实我们可以用:=同时完成多列的生成,下面的代码就是一个例子:
> mtcars_dt[, `:=`(avg=mean(mpg), med=median(mpg), min=min(mpg)), by=cyl]
> mtcars_dt[1:5,]
mpg cyl disp hp drat wt qsec vs am gear carb avg med min
1: 21.0 6 160 110 3.90 2.620 16.46 0 1 4 4 19.74286 19.7 17.8
2: 21.0 6 160 110 3.90 2.875 17.02 0 1 4 4 19.74286 19.7 17.8
3: 22.8 4 108 93 3.85 2.320 18.61 1 1 4 1 26.66364 26.0 21.4
4: 21.4 6 258 110 3.08 3.215 19.44 1 0 3 1 19.74286 19.7 17.8
5: 18.7 8 360 175 3.15 3.440 17.02 0 0 3 2 15.10000 15.2 10.4
注意上面的:=放在 `` 里面,是把 :=当作函数名来调用。
{}
括号{}在data.table里面很神奇,举个例子,适当的使用{}可以帮我们隐藏一些过渡的中间变量。比如我想在一个data.table里面加入新的两列——x和y,x=z^2+1, y=z^2-1,我可以用下面的办法实现:
> z_dt=data.table(z=1:5)
> z_dt
z
1: 1
2: 2
3: 3
4: 4
5: 5
> z_dt[, .(x=z^1+1,y=z^2-1)]
x y
1: 2 0
2: 3 3
3: 4 8
4: 5 15
5: 6 24
这段代码里,我们计算了z^2两次,如果是更复杂的操作,更大的数据,这样做的效率比较低。更有效的办法是创造一个中间过渡的变量,temp=z^2,然后 x=temp+1, y=temp-1。最后我们又不想temp出现在结果中,怎么实现呢?
> z_dt[, {temp=z^2; .(x=temp+1,y=temp-1)}]
x y
1: 2 0
2: 5 3
3: 10 8
4: 17 15
5: 26 24
data.table还有一些其他的高级技巧,限于篇幅就不在这里介绍了,如果大家喜欢我们的文章并且希望看到更多可以留言,我们可以再写一篇关于data.table的使用文章。
在此也希望大家点赞或者转发支持,你们的支持就是我们创作的最大动力!
也欢迎关注我们的微信公众号 - 机器会学习