查看原文
其他

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

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

上文我们讨论到使用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机器学习入门 

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

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