R数据处理|data.table篇(二)
上文我们讨论到使用data.table包完成数据分析框架,遗留如下几个问题
增添列,另外一种删除列的方法,修改数据框
计算或分组计算时,可不可以一次对所有列进行计算,而不需要再每一列都指定(.SD)
DT中可不可以按照行名来提取(key)
普通合并数据框(改进了的函数,会在下一篇中介绍)
融合重铸
本文介绍data.table包更深入的使用方法,顺带解决上述问题,下面是本文目录
key的使用
融合重铸的深入使用和改进
特殊符号如.N .SD := 等
高效读写文件函数及参数解释
Key
数据集创建
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)
DT数据框的行名及key的介绍
之前提到过提取时没有使用行名提取这个方法,这是因为data.table没有行名,如果硬要说有,那就是1234,而且不能修改,也不能根据行名来做提取等操作。
rownames(dft) # "1" "2" "3" "4"
rownames(dft) <- letters[1:4]
rownames(dft) # "a" "b" "c" "d"
dft # 虽然上面rownames改过来了,但是这样输出发现还没有变
# dft["a","weight"] # 报错,无法根据行名提取
dft[1,"weight"] # 使用行数才可以
这不是data.table的漏洞,而是因为它有更强大的操作,根本就不需要使用行名。具体的使用方法是,把data.frame的行名当成一列读进去,通过设置key来指定该列为行名。这样做的好处是,不止可以指定这一列,任意一列都可以,还可以指定多列,我们下面来看一看。
ndt <- copy(dft) # 为了和原数据框对比,创建一个新的
setkey(ndt,name1)
ndt # 发现数据框自动按照name1这一列进行排序了
# 如果想去掉key,则setkey(ndt,NULL)
# 实现通过行名提取
ndt["Bob",weight]
ndt["Bob","weight"]
ndt["Bob",2]
dft["Bob",2,on="name1"] # key 相当于使用了on
ndt["Bob"]
# 注意
setkey(ndt, weight)
ndt[60] # 认为是提取第60行
ndt[.(60)] # 下面两个才是提取weight为60的行
ndt[J(60)]
当我们把data.frame数据框转化成data.table时,默认抛弃行名,不过我们也可以用一个参数保留行名成为新的一列
df1 <- data.frame(weight,height,row.names = name1)
dt1 <- as.data.table(df1)
dt2 <- as.data.table(df1,keep.rownames=T) # 将原来数据框中的行名当成一列,列名为rn
dt1;dt2
as.data.table(df1,keep.rownames = "rownames") # 自己指定新增列的列名
设置key时还有另外一个函数setkeyv
name1 <- "weight"
setkey(ndt,name1) # 设置name1这一列为key
ndt # 观察这里的排序和下面一个的区别
setkeyv(ndt,name1) # 设置name1这个变量指向的weight这一列为key
ndt
setkey(ndt,name1,weight) # 设置两个key
setkeyv(ndt,c("name1","weight"))
这里注意到数据框会自动按照设置key的列进行排序
检查key的函数
haskey(ndt) # 返回TF值,检查是否有Key
key(ndt) # 检查它的key是什么
使用key来辅助计算
setkey(ndt,accept)
ndt["ok",sum(weight)] # 指定计算accept为ok的weight之和
ndt[c("ok","no"),sum(weight)] # 全部合在一起算
ndt[c("ok","no"),sum(weight),by=.EACHI] # 分类算
ndt[accept,sum(weight),by=.EACHI] # 每一类计算完,保留所有行输出
ndt[unique(accept),sum(weight),by=.EACHI] # 只显示和类数相同的行
ndt[,sum(weight),by=accept] # 上面等价于分组计算
我们可以发现,key的作用相当于设定on参数。计算时使用key,再指定计算哪些行,by=.EACHI,可以实现和分组计算一样的功能。
设置多个key
# 设置多个key,可以对多列进行筛选
setkey(ndt,name1,weight)
ndt[.("Kim",50:60)] # 不匹配的全部显示NA
ndt[.("Kim",50:60),nomatch=0] # 不匹配的删除掉
ndt[.("Kim",50:60),roll=T] # 没有信息的用上面的添
ndt[.("Kim",50:60),roll=Inf] # 和上一条一样
ndt[.("Kim",50:60),roll=-Inf] # 没有信息的用下面的添
ndt[!"Kim"]
ndt[!.("Kim",56)]
融合重铸
data.table包改写了reshape2包中的融合重铸功能。加载了data.table包之后,不需要加载reshape2包,但是它的所有用法都可以照常使用。而data.table不仅运行速度更快,而且增加了一些reshape2包没有的功能,melt和dcast函数都有改进。
melt
先来看一下两个包中的函数的帮助文档,在控制台输入 ?melt 会发现这两个包中都有这个函数,下面是函数参数
# reshape2包中处理data.frame的melt函数
#(在页面中点击melt.data.frame,这涉及到R语言中的泛函,我们以后会专门讲解)
# 现在理解就是当接的data是数据框时,使用 melt.data.frame这个函数
melt(data, id.vars, measure.vars,
variable.name = "variable", ..., na.rm = FALSE, value.name = "value",
factorsAsStrings = TRUE)
# data.table包中的melt函数
melt(data, id.vars, measure.vars,
variable.name = "variable", value.name = "value",
..., na.rm = FALSE, variable.factor = TRUE,
value.factor = FALSE,
verbose = getOption("datatable.verbose"))
读者如果看过我的上一篇讲融合重铸的文章(https://zhuanlan.zhihu.com/p/26123110)会发现,这里的很多参数我都没有使用到,在这里我会用它们实现更多的功能。下面我们列出三个主要功能,并总结每一个功能中两个包的差异
1.得到的数据是否转化为factor的问题
reshape2中的factorsAsStrings参数可以控制value列是否为因子型,而variable列则自动为因子型
data.table中通过variable.factor和value.factor分别控制两列是否为因子型
明显可以看出data.table的使用更灵活,更方便
2.多种数据类型融合问题
之前我们遇到的问题都是被融合的数据是同样的数据类型,比如都是整数或都是字符串,现在我们要面对的是很多不同数据类型的数据框。这里我们使用data.table包中例子里创建的数据框
set.seed(45)
DT <- data.table(
i_1 = c(1:5, NA),
i_2 = c(NA,6,7,8,9,10),
f_1 = factor(sample(c(letters[1:3], NA), 6, TRUE)),
f_2 = factor(c("z", "a", "x", "c", "x", "x"), ordered=TRUE),
c_1 = sample(c(letters[1:3], NA), 6, TRUE),
d_1 = as.Date(c(1:3,NA,4:5), origin="2013-09-01"),
d_2 = as.Date(6:1, origin="2012-01-01"))
DT[, l_1 := DT[, list(c=list(rep(i_1, sample(5,1)))), by = i_1]$c]
DT[, l_2 := DT[, list(c=list(rep(c_1, sample(5,1)))), by = i_1]$c]
DT
# i_1 i_2 f_1 f_2 c_1 d_1 d_2 l_1 l_2
# 1: 1 NA b z b 2013-09-02 2012-01-07 1,1 b,b,b,b
# 2: 2 6 b a NA 2013-09-03 2012-01-06 2,2,2,2,2 NA,NA
# 3: 3 7 c x a 2013-09-04 2012-01-05 3 a
# 4: 4 8 a c c <NA> 2012-01-04 4,4,4 c
# 5: 5 9 NA x b 2013-09-05 2012-01-03 5,5,5,5,5 b,b,b
# 6: NA 10 NA x c 2013-09-06 2012-01-02 NA c,c
我们可以看到,这个数据框中各列的数据类型是不相同的,总结一下是这样
i前缀的列是数值型
f前缀的列是因子型
c前缀的列是字符型
d前缀的列是时间型
l前缀的列是列表
我们如果直接用一条命令保留前两列
melt(DT, id=c("i_1","i_2"))
# 或者
melt(DT, id=1:2)
其他列融合在一起,就会出现warning,强制转换数据类型。这时就需要我们能够选择相同类型的数据融合在一起,也就是相同的前缀融合在一起。两个包都是使用了measure.vars参数来控制要被融合的列
melt(DT, id=1:2, measure="f_1") # 只选择f_1列来融合
melt(DT, id=c("i_1", "i_2"), measure=3) # 和上面一样
melt(DT, id=3:4, measure=c("d_1", "d_2"))
# 接list时,产生两列value,同时variable列不再显示原来的各列名,而是用123代替,因为有两列value,无法对应同一个列名
melt(DT, id=1:2, measure=list(3:4, c("d_1", "d_2")))
(细心的读者可能会注意到参数明明是id.vars, measure.vars,为什么我使用时就用了id和measure,这是参数的模糊匹配功能,只要提取前面的字符,不会发生歧义,就能代表这个参数)
上面四行代码两个包中都可以使用,不过data.table包中提供了选择列更好的方法
melt(DT, id=1:2, measure=patterns("^f_", "^d_"), value.factor=TRUE)
melt(DT, id=1:2, measure=patterns("l_", "c_"), na.rm=TRUE)
使用patterns指定,将所有以f开头的列融合在一列,d开头的列融合在一列,产生两列value。
特殊情况
如果id和measure两个参数只指定一个,则默认将剩余所有列全部赋予未指定的那个参数上
如果两个参数都不指定,则默认measure指定numeric/integer/logical类型数据,其余为id指定
3.新列命名
融合之后得到一列自动命名为variable和value,如果我想自己指定名字,就使用variable.name和value.name两个参数,在这个功能上,两个包没有区别
melt(DT, id=3:4, measure=c("d_1", "d_2")) # 默认
melt(DT, id=3:4, measure=c("d_1", "d_2"),variable.name="newvar",value.name="newval") # 指定
# 两列value时,只指定一个,自动添加123区分
melt(DT, id=1:2, measure=patterns("l_", "c_"), na.rm=TRUE, variable.name="newvar", value.name="newval")
melt(DT, id=1:2, measure=patterns("l_", "c_"), na.rm=TRUE, variable.name="newvar", value.name=c("nv1","nv2")) # 使用向量自己指定
cast
我们还是用?dcast查看帮助文档,发现两个包中都有这个函数,其中reshape2包中还有acast函数,而data.table包中没有
# reshape2中的函数
dcast(data, formula, fun.aggregate = NULL, ..., margins = NULL,
subset = NULL, fill = NULL, drop = TRUE,
value.var = guess_value(data))
# data.table中的函数
dcast(data, formula, fun.aggregate = NULL, sep = "_",
..., margins = NULL, subset = NULL, fill = NULL,
drop = TRUE, value.var = guess(data),
verbose = getOption("datatable.verbose"))
和melt一样,上一篇文章中我们没有深入了解函数的各种参数,这里我们通过这些参数来比较两个包中的重铸功能
1.两个包相同的参数margins/fill/drop/subset,相同的使用方法
margins=T或者字符串向量,可以对得到的矩阵的每行每列在使用该函数计算
fill当遇到缺失值时如何填充问题
drop是否要去除掉不匹配的行,即全是NA的行
subset提取结果的一部分进行展示
DT <- data.table(v1 = rep(1:2, each = 6),
v2 = rep(rep(1:3, 2), each = 2),
v3 = factor(rep(c(1,3),6),levels=1:3),
v4 = rnorm(6))
# margins参数
dcast(DT,v1~v2,mean) # 生成2*3的矩阵,默认选择v4作为value计算
dcast(DT,v1~v2,mean,margins=T) # 3*4的矩阵,多出了对每行每列求的均值
dcast(DT,v1~v2,mean,margins="v1") # 只每列求均值
dcast(DT,v1~v2,mean,margins="v2") # 只每行求均值
# drop 参数
# v3因子型,2没有出现过,在融合时还会与其他列进行匹配,就会出现一行全是NA的情况
dcast(DT,v1+v3~v2,drop=F)
dcast(DT,v1+v3~v2,drop=T)
# fill参数
dcast(DT,v1+v3~v2,drop=F,fill=0) # 是NA的地方填充0
# subset参数
dcast(DT,v1~v2,mean)
dcast(DT,v1~v2,mean,subset=.(v3==1)) # 挑选v3是1的出来计算
dcast(DT,v1~v2,mean,subset=.(v1==1)) # 计算完挑选v1是1的出来
dcast(DT,v1~v2,mean,subset=.(v2==1)) # 计算完挑选v2是1的出来
dcast(DT,v1~v2,mean,subset=.(v1==1&v3==1)) # 挑选v3是1的出来计算,之后挑选v1是1的来展示
2.value.var参数
之前那篇讲reshape2包的文章中,value正好只有一列,但当有多列可以作为value时,如果不加以指定,计算的结果可能不是我们想要的
运行了上面代码的读者也会发现,每次输出结果时都会打印出如今选用的作为value的列是什么,我们如果想要自己指定哪一列作为value,就要用value.var参数
在这个参数的使用上,data.table包比reshape2包功能更强大
# 两个包都可以这样使用
DT <- data.table(v1 = rep(1:2, each = 6),
v2 = rep(rep(1:3, 2), each = 2),
v3 = factor(rep(c(1,3),6),levels=1:3),
v4 = rnorm(6))
dcast(DT,v1~v3,mean) # 默认使用v4来计算
dcast(DT,v1~v3,mean,value.var="v2") # 指定v2作为value来计算
dcast(DT,v1~v3,mean,value.var="v4") # 使用v4和默认的结果相同
下面是data.table包独有的功能
dcast(DT,v1~v3,mean,value.var=c("v4","v2")) # v3中的元素分别和v2和v4组合,生成四列
dcast(DT,v1~v3,fun=list(sum, mean),value.var="v2") # 同时使用两种计算函数生成四列
dcast(DT,v1~v3,fun=list(sum, mean),value.var=c("v4","v2")) # 二者结合,生成8列
dcast(DT,v1~v3,fun=list(sum, mean),value.var=list("v4","v2")) # v4的使用sum,v2的使用mean,生成4列
特殊符号
添加、更新和删除 := 符号
这个符号可以实现在本身直接更改,而无需产生一个新的数据框,再赋值给原本相同的变量名
dft <- data.table(name1,weight,height,accept)
dft[,u:=1] # 添加一个全是1的列
dft[,height:=1:4] # 更改height列
dft[,c("accept","height"):=.(1:4,2:5)] # 作用于多个列
dft[,`:=`(m=1:4,n=3:6)] # 使用:=函数的真正调用方式
dft[,weight:=NULL] # 删除weight列
dft[,c("m","n"):=NULL] # 删除多列
dft[2,height:=22][] # 只修改一个值,加一个[]返回得到的数据框
dft <- data.table(name1,weight,height,accept)
dft["Bob",accept:="yes",on="name1"] # 通过逻辑判断修改
dft[,m:=mean(height),by=accept] # 增加一个列,这个列根据分组计算得出
# 注意一点
dft[name1=="Bob"][,height:=13][] # :=作用在提取之后的数据框,所以对原数据框没有改变
dft
# 使用一个指向字符串的变量作为新名称
a <- "aa"
dft[,a:=1][] # 使用a作为列名
dft[,(a):=2][] # 使用aa作为列名
.N
.N 代表行的数量,用by参数分组时则是每一组的行数量
dft[.N-1] # 返回倒数第二行
dft[,.N] # 返回数据框一共有几行(放在第二个参数位置表示计算并输出结果)
dft[,.N,by=accept] # 分组计算行数
.SD
.SD 代表整个数据框,用by参数分组时则是每一组的数据框
dft <- data.table(name1,weight,height,accept)
dft[,print(.SD),by=accept]
dft[,head(.SD,1),by=accept]
dft[,.SD[2],by=accept]
dft[,lapply(.SD[,c(-1,-4)],sum),by=accept] # 分组多列计算
之前我们提到过,在DT中计算时输出的总是DT,但是如果我想返回多个矩阵怎么办,那就是使用嵌套list,像把矩阵压缩成一个元素一样,放在DT中。这里我们要用分组计算,返回矩阵。
下面这个例子是要对分组之后的每个数据框求covariance,计算得到的是列与列两两对应协方差矩阵。
dft[,cov(.SD[,c(-1,-4)]),by=accept] # 矩阵被变成向量
l <- dft[,.(.(cov(.SD[,c(-1,-4)]))),by=accept]
l[[1,2]]
这里解释一下连续嵌套两层list的理由
第一个list是指对每一组计算出来的结果矩阵,用list压缩成为可以放在DT中的元素
第二个list是将两个压缩了的元素整合在一起,使之成为一列。这一层中还可以加第二个元素,输出结果放在下一列展示,功能类似我们平时使用的计算多个结果
l <- dft[,.(.(cov(.SD[,c(-1,-4)])),.(cor(.SD[,c(-1,-4)]))),by=accept] l <- dft[,.(.(cov(.SD[,c(-1,-4)])),.(cor(.SD[,c(-1,-4)])),sum(weight)),by=accept] dft[,.(m=mean(weight),s=sum(weight)),by=accept] # 外层list功能类似这里
.SDcols
.SDcols 指定.SD 代表的数据框包括哪些列
dft[,lapply(.SD[,c(-1,-4)],sum),by=accept]
# 下面4条命令和上面那条有相同的效果
dft[,lapply(.SD,sum),by=accept,.SDcols=c("weight","height")] #.SD中只包含这两列
dft[,lapply(.SD,sum),by=accept,.SDcols=weight:height] #用:指定这列到这列之间的所有列
dft[,lapply(.SD,sum),by=accept,.SDcols=2:3]
dft[,lapply(.SD,sum),by=accept,.SDcols=-c(1,4)]
.I
.I 表示(分组后)每一行在原数据框中是第几行
dft[,.I[2],by=accept]
.GRP
如果不使用by参数,则为1。使用by,则是组的计数(第一组的值是1,第二组是2)
dft[,grp:=.GRP][]
dft[,grp:=.GRP,by=accept][]
串联操作,避免多余中间变量
dft[weight>50][height>100][order(height)]
%between% 范围
# 以下6个等价
dft[weight>=50&weight<=60]
dft[weight %between%c(50,60)]
dft[weight %inrange%c(50,60)]
dft[weight %between% list(rep(50,4),rep(60,4))]
dft[between(weight,50,60)]
dft[inrange(weight,50,60)]
%like% 字符串中含有某个字符
dft[name1%like%"a"]
读写文件
data.table包中的fread和fwrite读写文件的速度非常快,可以处理.txt.csv.dat等文件,下面是最基本的使用方法
dataw <- data.table(a=1:10,b=2:11)
fwrite(dataw,"dataw.csv")
fwrite(dataw,"dataw.txt")
fwrite(dataw,"dataw.dat")
fread("dataw.csv") # 读取文件直接用字符串,赋予参数input
fread(file="dataw.csv") # 也可以赋予参数file
下面我们来详细讨论fread函数的各个参数
data = "A,B,C,D\n1,3,a,7\n2,4,6,NA\n2,,6,8\n"
cat(data) # 看一下这个数据长什么样
fread(data) # 第一行作为列名,生成一个data.table
str(fread(data))
str(fread(data, stringsAsFactors=T)) # stringsAsFactors默认F,自动将字符型转化为因子型
fread(data,col.names = letters[1:4]) # 改变列名
fread(data,verbose = T) # 显示一些信息
fread(data,data.table=F) # 生成data.frame
fread(data,showProgress = T) # 显示进度条
# data.table和showProgress都可以再options里面设置
fread(data) # header默认为"auto",自动第一行作为列名
fread(data, header=F) # header第一行不作为行名
data1 = "1,1,a,7\n2,4,6,NA\n2,,6,8\n"
fread(data1) # 而当数据是这样时又不会自动将第一行作为列名
fread(data1,header=T) # 将第一行作为列名
fread(data1,header=T,check.names = T) # 检查,避免数字作为列名,避免两列名重复
# sep控制列与列之间用什么分隔。默认为"auto",会自动寻找 空格 , ; : | \t 这些符号自动分列
# 当既有:又有空格时,当:很规整,若空格非常整齐,一个不落,:会被当成字符处理,否则就会把:当做分隔符
dataa1 = "a: b: c: d\n1: 2: 3: 4\n3: 4: 5:6" # 只有最后一个少空格
dataa2 = "a: b: c: d\n1: 2: 3: 4\n3: 4: 5: 6"
dataa3 = "a : b: c: d\n1: 2: 3: 4\n3: 4: 5: 6" # 前面加一个空格
fread(dataa1) # 按照:分,得到规整的数据
fread(dataa2) # 带有:
fread(dataa3) # 规整的数据
# 当改变sep的取值时
fread(data, sep="\n") # 此时读成一列
# nrow控制读取第几行,默认-1全部读取,0取列名,1只取第一行
fread(data, nrow=0)
fread(data, nrow=1) # 取第一行,一般用这个来检测读进来的形式是否正确
fread(data, nrow=3) # 取前三行
# na.strings默认将 ,, 这样没有值的、"NA" "N/A" "null" 当成缺失值
fread(data, na.strings="a") # 将读进去的字符"a"当成缺失值
str(fread(data, na.strings=NULL)) # 不将那些转化为缺失值
# autostart自动选择合适的行开始
data1 = "i,can,do,it\n A,B,C,D\n1,3,a,7\n2,4,6,NA\n2,,6,8\n"
data2 = "i,can,do,it\n friends are kind to\n A,B,C,D\n1,3,a,7\n2,4,6,NA\n2,,6,8\n"
fread(data1) # i,can,do,it 作为列名,(同时注意到,,没有变成NA,因为这列是字符串)
fread(data2) # 自动从A,B,C,D开始读取,将其作为列名
# skip跳过前几列不读,默认是0,自动从autostart点开始读取
data2 = "i,can,do,it\n friends are kind \n A,B,C,D\n1,3,a,7\n2,4,6,NA\n2,,6,8\n"
fread(data2) # 从ABCD开始读
fread(data2,skip=2) # skip=0,1,2结果相同,ABCD作为列名
fread(data2,skip=3) # 自动不将第一列作为列名
data3 = "i,can,do,it\n friends are kind \n A,B,C,D\nbob,mary,and,jane\n1,3,a,7\n2,4,6,NA\n2,,6,8\n"
fread(data3) # 从ABCD开始读
fread(data3,skip="bob") # 从bob这一行开始读,bob等作为列名
# select and drop
data = "A,B,C,D\n1,3,a,7\n2,4,6,NA\n2,,6,8\n"
fread(data, select="A") # 只取A这列
fread(data, select=c("A","B")) # 取AB列
fread(data, drop="A") # 取除了A这列
# colclasses
data = "A,B,C,D\n1,3,a,7\n2,4,6,NA\n2,,6,8\n"
fread(data)
str(fread(data,colClasses = c(A="character")))
str(fread(data,colClasses = c(A="character",B="double")))
str(fread(data,colClasses = list(character=c("A","B"))))
str(fread(data,colClasses = list(character=1:3)))
fread(data,colClasses = list(NULL=1:3)) # 只保留第四列
专栏信息
专栏主页:Data Analysis(https://zhuanlan.zhihu.com/Data-AnalysisR)
专栏目录:目录(https://zhuanlan.zhihu.com/p/25780082)
文末彩蛋
这里介绍R语言几个行名列名函数的联系和区别
colnames和names
rownames和row.names
这两对区别如下
names和colnames的区别在于,作用于数据框时二者相同,作用于矩阵时names无效
rownames和row.names基本上没差别
a <- data.frame(a=1:4,b=letters[1:4]) ma <- as.matrix(a) names(a) # "a" "b" colnames(a) # "a" "b" names(ma) # NULL colnames(ma) # "a" "b" rownames(a) # "1" "2" "3" "4" row.names(a) # "1" "2" "3" "4" rownames(ma) # NULL row.names(ma) # NULL
Dwzb , R语言中文社区专栏作者,厦门大学统计专业学生。
知乎专栏:Data Analysis
https://zhuanlan.zhihu.com/Data-AnalysisR
微信回复关键字即可学习
回复 R R语言快速入门免费视频
回复 统计 统计方法及其在R中的实现
回复 用户画像 民生银行客户画像搭建与应用
回复 大数据 大数据系列免费视频教程
回复 可视化 利用R语言做数据可视化
回复 数据挖掘 数据挖掘算法原理解释与应用
回复 机器学习 R&Python机器学习入门