查看原文
其他

R数据处理|data.table篇(三)

2017-05-30 Dwzb R语言中文社区

本文为data.table包介绍最后一篇,前两篇链接如下

R数据处理|data.table篇(一) - 知乎专栏

R数据处理|data.table篇(二) - 知乎专栏

本文主要讲解data.table包中一些比较不常用的函数,还有data.table包高效的深层原理。下面是本文目录

  • 其他函数

  • 改进了的函数

  • options设置

  • 性能之Secondary indices and auto indexing

  • 性能之fast binary search 

  • 浅复制和深复制(shallow vs deep copy)

  • by reference

其他函数

具体举例子讲述以下函数

copy setnames setDT  setDF rleid rowid tables tstrsplit

copy 复制一个数据框

name1 <- c("Bob","Mary","Jane","Kim") name2 <- c("Bob","Mary","Kim","Jane") weight <- c(60,65,45,55) height <- c(170,165,140,135) birth <- c("1990-1","1980-2","1995-5","1996-4") accept <- c("no","ok","ok","no") library(data.table) dft <- data.table(name1,weight,height,accept) dtt <- copy(dft)

这种复制不同于直接用 <- 赋值,在本专题的后面我会专门讲一下R语言的深复制

setnames 修改列名

setnames(dtt,letters[1:4]) colnames(dtt)<-letters[2:5] # 也可以实现列名的修改 setnames(dtt,"c","C") # 修改特定列名 setnames(dtt,1:2,c("m","n"))

setDF 将data.table转化为data.frame

setDF(dtt) class(dtt) # "data.frame" # setDT 将data.frame转化为data.table setDT(dtt) class(dtt) # "data.table" "data.frame"

rleid

# 可以接在by后面,每次连续作为一组 dft = data.table(x=rep(c("b","a","c"),each=3), v=c(1,1,1,2,2,1,1,2,2), y=c(1,3,6), a=1:9, b=9:1) rleid(dft$v) # 返回一个和原向量等长的向量,值与其一一对应。值从1开始,原向量从头往后看,值不变则仍为1,变一次加1 dft[, .N, by=rleid(v)] # 根据上面形成的向量分组(每次连续相同的值为一组)

rowid

一个组合出现第几次就显示为几

DT = data.table(x=c(20,10,10,30,30,20), y=c("a", "a", "a", "b", "b", "b"), z=1:6) DT rowid(DT$x) # 1,1,2,1,2,2 rowidv(DT, cols="x") # 同上 rowid(DT$x, prefix="group") # 数字前面加"group" # 返回  "group1" "group1" "group2" "group1" "group2" "group2" rowid(DT$x, DT$y) # 多列组合看重复 # 返回1,1,2,1,2,1 rowidv(DT, cols=c("x","y")) # 同上 DT[, .(N=seq_len(.N)), by=.(x,y)]$N # 上面相当于做了这样的事 # 应用 dcast(DT, x ~ rowid(x, prefix="group"), value.var="z") # 将x为10的两个z值放在同一行,x为20的放在同一行....

tables

tables() # 返回当前所有的datatable,并展示数据集行列数、大小、列名、key等信息

tstrsplit
看过本专题前面讲dplyr和tidyr包的读者可能还记得tidyr包中的那个将日期拆分成年月日的函数,在data.table包中,我们可以使用一个有趣的字符串处理函数来实现相同的功能

name <- 1:3 dates <- c("2016-3-4","2016-3-14","2016-3-24") nd <- data.table(name,dates) strsplit(dates,"-") tstrsplit(dates,"-") # 好像把strsplit得到的结果转置了一样 nd[,c("year","month","day"):=tstrsplit(dates,"-")] # 实现拆分

改进了的函数

%chin%替代了%in% fsort替代了sort chmatch替代了match,两个参数返回和前者等长的向量,是前者每一个元素在后者中的索引 chorder或者chgroup代替order,返回一个向量,排列顺序为:最小值在向量中的索引,第二小的... duplicated替代duplicated unique替代unique,另有uniqueN直接计算去重之后的个数

上面改进是功能相同,只是运行速度有所提高。下面列举的函数是不仅在运行速度上,而且在功能上也根据data.table包的特性做了一些增强

集合操作函数

增加了all参数,控制重复值。基础函数只能返回去重之后的结果

函数变化:union intersect setdiff setequal 前面都加了一个f

基础函数作用于两个向量,data.table中函数作用于两个data.table数据框,而且列名需要相同

x <- data.table(a=c(1,2,2,2,3,4,4)) y <- data.table(a=c(2,3,4,4,4,5)) x y fintersect(x, y)            # 返回相交部分并去重 fintersect(x, y, all=TRUE)  # 相交,保留重复值 fsetdiff(x, y)              # x中有y中没有的,去重 fsetdiff(x, y, all=TRUE)    # 保留重复值 funion(x, y)                # 并集,去重 funion(x, y, all=TRUE)      # 保留重复值 fsetequal(x, y)             # 返回一个F,二者不完全相等

rank

frank比rank函数速度更快,而且增加参数ties.method参数的一种取值”dense”,即当有两个值相等并列第二时,让二者都为2,之后的数排名不是第4,而是3,这样结果数值不会发生跳跃

x = c(2, 1, 4, 5, 3, NA, 4) frank(x) # 自动将NA当成最大的了 frank(x, na.last=F) # 自动将NA当成最小的 frank(x, na.last="keep") # NA仍然是NA frank(x, ties.method = "min") frank(x, ties.method = "dense") DT = data.table(x, y=c(1, 1, 1, 0, NA, 0, 2)) frank(DT, cols="x")

滞后

shift函数,参数如下

  • n控制变换阶数

  • fill控制填充内容

  • type取”lag”或者”lead”,看去除后面的值向后靠(前面添NA),还是去除前面的值向前靠(后面添NA)

y <- x <- 1:5 xy <- data.table(x,y) shift(x, n=1, fill=NA, type=”lag”) shift(x, n=1:2, fill=0, type=”lag”) xy[,(c(“a”,”b”)):=shift(.SD,1,0,”lead”)][] # 添加两列shift(xy, n=1, fill=0, type=”lag”, give.names=T) xy[,shift(.SD,1,0,”lead”,give.names = T)][] # 自动生成名字


上下合并数据框

使用rbindlist函数,先将数据框转化为list再进行合并

DT1 = data.table(A=1:3,B=letters[1:3]) DT2 = data.table(A=4:5,B=letters[4:5]) DT3 = data.table(B=letters[4:5],A=4:5) DT4 = data.table(B=letters[4:5],C=factor(1:2)) l1 = list(DT1,DT2) l2 = list(DT1,DT3) l3 = list(DT1,DT4) rbindlist(l1) rbindlist(l1,idcol=T) # 多出一列,对数据框分组(来自不同数据框) rbindlist(l2) # 不同列名直接合并 rbindlist(l2,use.names=T) # 将相同列名的合并在一起 rbindlist(l3) # 不同列名直接合并 rbindlist(l3,fill=T) # 选择相同列名合并,不匹配的填入NA


options设置

在控制台中输入options()会打印出一个list,这是当前的options设置值,比如显示保留几位小数等。加载data.table包之后,这里新增了一些data.table专用的参数,可以用下面的命令查看

ops <- options() # ops就是一个list,参数和值的一一对应 # ops$  这样输入在rstudio中就会自动提示后面的参数 # 由于data.table专用参数都是以datatable为前缀,使用我们输入时可以这样 # ops$datatable.  这样输入提示的会都是以datatable为前缀的参数,当然当你打出da的时候就已经差不多全是data.table的参数了 ops$datatable.print.nrows # 查看这个参数,返回100 getOption("datatable.print.topn") # 也可以这样查看,返回5

我们拿打印行数来举例子,看这样两个参数datatable.print.topn和datatable.print.nrows

  • datatable.print.topn 当省略输出时输出几行,默认为5

  • datatable.print.nrows 行数达到多少时开始省略输出

d <- data.table(a=1:200, b=2:201) d # 200行数据自动只输出前5行和后5行 op <- options(datatable.print.topn=10) # 设置打出前10行和后10行 d # 打出前10行和后10行 options(op) # 恢复默认值5 f <- data.table(a=1:50, b=2:51) f # 50行全打了出来 op <- options(datatable.print.nrows = 30) # 设置行数超过30行时就省略打出 f # 只打出前5行和后5行 options(op) # 恢复默认值100

下面我们再深入一点讲解options设置的内部运行机制

上面打印的参数设置其实调用了print函数,options里面设置的参数被print函数自动调用

?print.data.table # 可以查看打印data.table的函数的帮助文档,发现函数参数设置如下 print(x,    topn=getOption("datatable.print.topn"),          # default: 5    nrows=getOption("datatable.print.nrows"),        # default: 100    class=getOption("datatable.print.class"),  # default: FALSE    row.names=getOption("datatable.print.rownames"), # default: TRUE    quote=FALSE,...) # 所以我们之前在options里面设置的参数都在这里被调用 # 所以我们也可以直接使用print函数来实现和options设置相同的功能 print(d) print(d,topn=10) print(f) print(f,nrows=30)


性能之Secondary indices and auto indexing

上面我们提到setkey设置键值方便以后提取,但是它会自动按照键将整个数据框排序,这是是非常耗费时间的。我们可以选择用setindex函数省去这部分时间,同时不损失提取效率。

下面我们首先来介绍一下index的创建和查询,以及index和判断提取的关系。

dft <- data.table(name1,weight,height,accept) setindex(dft, name1) # 设置按照name1列来索引,但不进行排序 names(attributes(dft)) # 多出了属性index indices(dft) # 查看现有的index,"name1" setindex(dft,accept) # 增加一个index indices(dft) # "name1"  "accept" setindex(dft,NULL) # 去掉index dft[name1=="Bob"] # 用==判断提取 indices(dft) # 自动生成index为name1 dft[weight==45] # 这样之后就有两个index了 setindex(dft,NULL) # 去掉index dft[.(60),on="weight"] # 使用on判断提取 indices(dft) # 不会创建index

我们会发现使用==进行提取时就已经自动创建了index,所以一般没有必要提前用setindex去设置

那么创建index有什么好处呢?主要是运行速度上的问题,我们来看一下实例

set.seed(1L) dt = data.table(x = sample(1e5L, 1e7L, TRUE), y = runif(100L)) print(object.size(dt), units = "Mb") # 114.4 Mb system.time(ans <- dt[.(988L),on="x"]) # 有一定的时间消耗,多次运行这条命令,实现消耗几乎没有区别 system.time(ans <- dt[x == 989L]) # 时间消耗与使用on基本相同 system.time(ans <- dt[x == 1L]) # 几乎没有时间消耗 system.time(ans <- dt[.(988L),on="x"]) # 这时使用on也不耗费了 system.time(ans <- dt[y == 989L]) # 有较大时间消耗 system.time(ans <- dt[y == 9]) # 几乎没有时间消耗 setindex(dt,NULL) system.time(ans <- dt[x == 1L]) # 仍有一定的时间消耗 # 看普通数据框 df = data.frame(x = sample(1e5L, 1e7L, TRUE), y = runif(100L)) system.time(ans <- df[df$x == 1L,]) # 时间消耗比较小,但是每次运行时间相同

我们可以看到,使用==提取创建了index耗费了一些时间后,第二次提取就几乎不耗费时间了,而用on提取每次都要创建index。

下面我们来看一下设置index的耗时,和index与key的对比

dt = data.table(x = sample(1e5L, 1e7L, TRUE), y = runif(100L)) head(dt) system.time(setindex(dt,x)) # 0.28 setindex(dt,NULL) # 这样删除之后再重新加,时间不变 system.time(setindex(dt,x)) # 0.28 # setkey system.time(setkey(dt,x)) # setkey多了排序,时间要长一些,0.72 setkey(dt,NULL) head(dt) # 即使删除后,依然按照x排序 system.time(setkey(dt,x)) # 因为排序仍然保留,所以再重新加时间缩短了非常多,0.03 system.time(setkey(dt,y)) # 时间还是很多 system.time(setkey(dt,x)) # 因为按y排序,x被打乱了,所以这一次时间也延长了 dt = data.table(x = sample(1e5L, 1e7L, TRUE), y = runif(100L)) system.time(dt[x==2]) # 有一定的时间消耗 setkey(dt,x) system.time(dt[x==2]) # 几乎不耗费时间 system.time(dt[.(1),on="x"]) # 几乎不耗费时间

总结一下

  • 设置index之后提取速度明显加快的原理是,它将设置的这一列进行了排序,并把结果存储到了index属性之中,日后根据这个新的索引来寻找会快很多。

  • 而设置key则不止将这一列排序,而且把整个数据框都排了个序,因此耗时较长。

  • 无论是设置了index还是key,都可以一次设定,日后提取无忧

我们也可以通过设置options参数来禁止index的使用,主要有两个参数

  • datatable.auto.index 为F时,使用==不会自动创建index

  • datatable.use.index 为F时,即使创建了index,也无法提高提取速度

dt = data.table(x = sample(1e5L, 1e7L, TRUE), y = runif(100L)) op <- options(datatable.auto.index = F) # 使用==时不会自动创建index system.time(ans <- dt[x == 989L]) # 多次运行,每次消耗时间相同 indices(dt) # NULL setindex(dt,x) system.time(ans <- dt[x == 989L]) # 特意设置index还是可以不消耗时间 options(op)op <- options(datatable.use.index = F) # 使用==时不会自动创建index setindex(dt,x) system.time(ans <- dt[x == 989L]) # 特意设置index也要消耗时间 indices(dt) # 虽然有index:”x” options(op)


性能之fast binary search

dt = data.table(x = sample(1e5L, 1e7L, TRUE), y = runif(100L)) system.time(dt[x==1&y==0.5]) indices(dt) # NULL,说明使用&连接两个选择无法创建index setindex(dt,x,y) indices(dt) "x__y" system.time(dt[x==1&y==0.5]) # 速度没有改善,所以说Index应该是只能处理单种选择 # 多种同时选择还是要用key setkey(dt,x,y) system.time(dt[.(1,0.5)]) # 几乎不耗时

这里解释一下排序之后提取速度变快的原因

  • 在没有排序的时候,匹配x==1,需要生成nrow个逻辑值,从中挑选出为T的打印出来

  • 排序之后,就可以使用二分法来减少匹配次数,大大提高运算速度

  • 计算复杂度从O(n)变成了O(log n)

浅复制和深复制(shallow vs deep copy)

使用R语言基础函数进行数据处理时,常常默认使用的是深复制的方法,当处理数据集较大时,运行速度就会很慢,data.table在一些地方使用了浅复制,极大提高了运行效率。不过浅复制也会有一些副作用,本节后面会进行介绍。

浅复制和深复制的区别

比如我们要修改一个数据框中某一列的值,用R基础函数的[]处理,其实处理之后得到的数据框已经完全不是最初的数据框本身,它是把原有数据框复制出一个完整的备份,再在这个备份上进行修改,修改的过程中,还可能多次复制,这样的复制不仅极大增加了运行时间,同时也非常消耗内存。这就是所谓的深复制。

而data.table在处理的时候,会使用改变后的新值,而其他没改变的内容还是用原来那些,没有重新复制出来使用。虽然也是一个新的数据框,但是只是新创建了一个指针,指向原有的内容。这样不需要把大量数据全部复制一遍,会大大缩短运行时间,这就是浅复制。

而浅复制有一个弊端,就是新数据框合旧数据框都指向同一个内容,只要在一个数据框中把这个内容改变,另外的数据框也会受到影响。这就是copy函数存在的意义,这样深复制一下可以让两个数据框之间互不影响。下面我们用具体的例子来解释

使用函数来判断数据框的复制

R语言中可以用tracemem函数来跟踪一个变量名指向的地址。地址是变量名指向的内容的存放位置,如果改变数据框时地址发生变化,说明在其他位置复制出了一个一模一样的数据框,新的数据框则使用新产生的那个。因为每次复制数据框,都要分配给它一个新的地址来储存,所以我们可以通过地址变化的次数来反映数据框被复制的次数。

tracemem函数作用在一个变量名上,如果这个变量名指向的地址发生改变,就会print出一条信息。

DF <- data.frame(ID = c("b","b","b","a","a","c"), a = 1:6, b = 7:12, c = 13:18) # 先测试基础函数的复制情况 tracemem(DF) # 打印出此时地址 "<0000000002F25938>" DF$c <- 18:13 # 修改数据框,打印出三条更改信息,说明这个过程中,数据框被复制了三次 DF$c[DF$ID == "b"] <- 15:13 # 这样改变则复制了四次 untracemem(DF) # 结束检测

接下来我们测试一下data.table

DT <- as.data.table(DF) tracemem(DT) DT[,c:=18:13] DT["b",c:=15:13,on="ID"] untracemem(DT)

修改的过程中一次信息都没有print出来,说明没有进行过一次深复制,这是data.table处理高效的原因之一。

浅复制的副作用

上面我们已经说明了data.table的处理方式是浅复制,下面我们用例子说明浅复制中相互影响带来的负面影响。

DT <- data.table(ID = c("b","b","b","a","a","c"), a = 1:6, b = 7:12, c = 13:18) DD <- DT[,c:=18:13][] DT;DD # 二者相同 DT["b",c:=15:13,on="ID"] DT;DD # 二者仍相同,说明改变DT的同时也改变了DD rm(DT,DD) # 删除变量重新试验

使用copy函数实现复制,不影响原来数据框

DT = data.table(ID = c("b","b","b","a","a","c"), a = 1:6, b = 7:12, c = 13:18) assign_DT <- DT copy_DT  <- copy(DT) DT;assign_DT;copy_DT # 此时三者一样 DT[,c:=18:13] # 改变其中一个 DT;assign_DT # 通过普通赋值符号产生的数据框也跟着改变了 copy_DT # 通过copy深复制才没有被影响 rm(DT,assign_DT,copy_DT)

也可以用address函数检查地址,而不用试验(通过地址来检查各个对象是否改变)

DT = data.table(ID = c("b","b","b","a","a","c"), a = 1:6, b = 7:12, c = 13:18) assign_DT <- DT copy_DT  <- copy(DT) sapply(c("DT","assign_DT","copy_DT"),       function(x) address(get(x))) # 我们可以直接看出,前两者的地址是相同的,copy复制后是不同的

我们可以用同样的方法来检查一下data.frame

DF <- data.frame(ID = c("b","b","b","a","a","c"), a = 1:6, b = 7:12, c = 13:18) address(DF) DF1 <- DF sapply(c("DF","DF1"),function(x) address(get(x))) # 相同 DF[1,2] <- 3 DF;DF1 # DF1没有因此而改变 sapply(c("DF","DF1"),function(x) address(get(x))) # DF改变,DF1未变

我们可以看到,data.frame中使用 <- 时,也没有进行深复制,而是共用的同一个内容。不过当其中一个发生变化时,另一个却不受影响,因为那个改变的会进行一次深复制,将它的内容存在了另一个地方。

by reference

我们上文提到的 := 来改变数据框称为 add/update/delete columns by reference。by reference 的含义在于,除了工作记忆以外,没有任何副本,处理时只占一列这么大的空间而不是整个数据框,这会让处理数据更加高效。

data.table包中所有set*函数都是by reference的,除此之外就是:=函数了。下面举几个例子

setorder

dft <- data.table(name1,weight,height,accept) setorder(dft,weight,-height) # 按照weight从小到大排列,如果weight相同,则按照height从大到小 dft# 我们会发现使用这个函数是在原有数据框中进行的更改 setorderv(dft,c("weight","height"),c(1,-1)) # 和上面等价

setDT和setDF

dat <- data.frame(name1,weight,height,accept) tracemem(dat) setDF(dat) # dat 本身变成了data.frame,没有复制 untracemem(dat)

setDF同理,与此做对比的as.data.table函数,这个函数是通过转化的(as.data.frame同理)

daf <- data.frame(name1,weight,height,accept) tracemem(daf) dat <- as.data.table(daf) # 经过了两次复制 untracemem(daf) class(daf) # 其本身没有改变

除此之外,还有

setkey 设置键值 setcolorder 对列排序 setattr(x,name,value) 设置属性 setnames(x,old,new) 改变列名

参考资料

本文主要参考data.table包帮助文档中

  • 对每一个函数的介绍

  • User guides, package vignettes and other documentation.部分系列文章

专栏信息

专栏主页:Data Analysishttps://zhuanlan.zhihu.com/Data-AnalysisR
专栏目录:目录(https://zhuanlan.zhihu.com/p/25780082)

文末彩蛋

这里分享几个Rstudio文本编辑快捷键

  • alt + up/down 将光标所在行向上/下移动

  • shift + alt + up/down 将光标所在行复制粘贴在上面/下面一行

本文上面有很多相似的代码都是这样快速复制得到的,这样快速复制一个备份来调整参数,有时再上下调整一下位置,非常方便

  • alt + left/right 光标瞬间移动到行首/行尾

  • alt + shift + left/right 选择本行光标左/右侧的所有内容


Dwzb , R语言中文社区专栏作者,厦门大学统计专业学生。
知乎专栏:Data Analysis
https://zhuanlan.zhihu.com/Data-AnalysisR 




微信回复关键字即可学习

回复 R              R语言快速入门免费视频 
回复 统计          统计方法及其在R中的实现
回复 用户画像   民生银行客户画像搭建与应用 
回复 大数据      大数据系列免费视频教程
回复 可视化      利用R语言做数据可视化
回复 数据挖掘   数据挖掘算法原理解释与应用
回复 机器学习   R&Python机器学习入门 

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存