加入收藏 | 设为首页 | 会员中心 | 我要投稿 李大同 (https://www.lidatong.com.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 大数据 > 正文

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

发布时间:2020-12-14 03:10:05 所属栏目:大数据 来源:网络整理
导读:本文为data.table包介绍最后一篇,前两篇链接如下 R数据处理|data.table篇(一) - 知乎专栏 R数据处理|data.table篇(二) - 知乎专栏 本文主要讲解data.table包中一些比较不常用的函数,还有data.table包高效的深层原理。下面是本文目录 其他函数 改进了的

本文为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","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","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,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,30,20),y=c("a","b","b"),z=1:6)
DT
rowid(DT$x) # 1,2
rowidv(DT,cols="x") # 同上
rowid(DT$x,prefix="group") # 数字前面加"group"
# 返回 ?"group1" "group1" "group2" "group1" "group2" "group2"
rowid(DT$x,DT$y) # 多列组合看重复
# 返回1,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,4,4))
y <- data.table(a=c(2,5))
x
y
fintersect(x,y) ? ? ? ? ? ?# 返回相交部分并去重
fintersect(x,y,all=TRUE) ?# 相交,保留重复值
fsetdiff(x,y) ? ? ? ? ? ? ?# x中有y中没有的,去重
fsetdiff(x,all=TRUE) ? ?# 保留重复值
funion(x,y) ? ? ? ? ? ? ? ?# 并集,去重
funion(x,all=TRUE) ? ? ?# 保留重复值
fsetequal(x,y) ? ? ? ? ? ? # 返回一个F,二者不完全相等

rank

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

x = c(2,5,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,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,”lead”)][] # 添加两列shift(xy,type=”lag”,give.names=T)
xy[,shift(.SD,”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,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,y = runif(100L))
system.time(ans <- df[df$x == 1L,]) # 时间消耗比较小,但是每次运行时间相同

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

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

dt = data.table(x = sample(1e5L,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,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,也无法提高提取速度


性能之fast binary search

 
 

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

  • 在没有排序的时候,匹配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",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",c = 13:18)
DD <- DT[,c:=18:13][]
DT;DD # 二者相同
DT["b",on="ID"]
DT;DD # 二者仍相同,说明改变DT的同时也改变了DD
rm(DT,DD) # 删除变量重新试验

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

DT = data.table(ID = c("b",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函数检查地址,而不用试验(通过地址来检查各个对象是否改变)

 
 

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

 
 

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

by reference

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

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

setorder

 
 

setDT和setDF

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

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

daf <- data.frame(name1,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机器学习入门?

(编辑:李大同)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

    推荐文章
      热点阅读