R数据处理|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","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 Analysis(https://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机器学习入门