R语言数据处理利器——dplyr简介
dplyr是由Hadley Wickham主持开发和维护的一个主要针对数据框快速计算、整合的函数包,同时提供一些常用函数的高速写法以及几个开源数据库的连接。此包是plyr包的深化功能包,其名字中的字母“d”即来源于data frame,以示其专注于数据框数据的整理和操作。我们将在本章中着重介绍一些数据处理方面的常用功能函数。
1.1管道函数
在前面的简介中,我们计算了cran上的可用的函数包的数量:
> contrib.url("http://mirrors.xmu.edu.cn/CRAN","source")%>%available.packages%>%nrow
[1] 6450
在以上的代码中,我们使用了“%>%”这个符号(或者说函数),这可以被称为管道函数。管道函数在其他的语言(比如shell)中也经常被使用,而其他的函数包中也有类似的功能函数(pipeR包和magrittr包),我们稍后也会对magrittr包中管道函数做介绍。
“%>%”这个管道函数把左件的值发送给右件的表达式,并作为右件表达式函数的第一个参数。
左件%>%右件
通常来说,可以把“%>%”读作then,即然后。
如果你需要同时操作多个数据集或者多个函数时,使用管道函数将会更加方便、快速和有逻辑性。比如以上的计算函数包数量的代码若改为一般写法:
nrow(available.packages(contrib.url("http://mirrors.xmu.edu.cn/CRAN","source")))
或者是这样:
x=contrib.url("http://mirrors.xmu.edu.cn/CRAN","source")
x= available.packages (x)
nrow(x)
对比这三段代码,第二段代码包含了3对括号,可读性比较差;第三段代码将之分为三句,代码量增加,并且留下了x这个中间产物;而第一段代码乍一看很长,但实际上对于代码的运行、传递机制表现得很清晰,各阶段的函数分工明确,逻辑清楚,如果习惯使用后,可用性相当好。
值得一提的是,管道函数还可以用在自定义函数(function)中,比如我们定义一个对向量中的数求整后取绝对值求和的函数。
一般代码:
> f1=function(x)sum(abs(round(x)))
管道函数代码
> f2=.%>%round%>abs%>%sum
我们来看看源码的显示:
> f1
function(x)sum(abs(round(x)))
> f2
Functional sequence with the following components:
1. round(.)
2. abs(.)
3. sum(.)
Use \'functions\' to extract the individual functions.
毫无疑问的是,两者并没有什么不同,来看下面的结果。
> set.seed(1000)
> a=rnorm(20,10,10)
> a%>f1
[1] 161
> a%>f2
[1] 161
当然,R中并不仅仅只有这一个管道函数,同样由Hadley Wickham开发的magrittr包中介绍了其他功能的管道函数。
函数名 功能
%<>% 在%>%的基础上,会把右件的最终返回值返回给左件(注意是最终)。
%T>% 把左件的值传入后,不产生任何返回值(你可以对计算的中间过程画个图,再接着计算)。
%$% 选取左件中的任意个变量名来操作,但左件中原来的数据将不会被保存,仅剩下计算后的值。
> test=data.frame(x=a,y=sample(0:1,20,replace=T))%T>%
+ plot%$%
+ f2(x)
> test%<>%"*"(10)
以上代码简单地运用了上述3个管道函数,第一行先构建一个数据框对数据框画散点图并对x变量运算赋值给test,第二行对test乘以10倍。
管道函数是一个非常方便快捷的工具,在本书后续的其他程序里,我们将大量地使用管道函数,以增加代码的可读性,减少代码量。
1.2基础函数
在数据分析中,我们往往受限于分析目标和具体实现两个瓶颈。在日常交流中,数据分析师们也经常表示自己大概会分配70%-80%的项目时间用于数据处理,毫无疑问,Hadley Wickham所开发的一系列函数包也都围绕着如何减少数据处理时间这个目标来进行。能够快捷方便地表达并实现自己心中的目标,才有更多的时间用于思考和分析。
dplyr包的函数能处理很大一部分结构化数据处理场景中所需的功能,筛选、排序、变量选择、变形、汇总等。
1.2.1 filter筛选
filter按照筛选条件或逻辑筛选出符合目标的子集,与base中的subset十分相似,不过跟其他dplyr包中的基础函数一样,filter中可以直接调用数据框中的变量名,而无需attach或者使用”$”。
举个栗子:iris数据集中Species不等于setosa和virginica,且Sepal.Width大于等于3.2.
> iris%>%filter(!Species%in%c("setosa","virginica"),Sepal.Width>=3.2)
Sepal.Length Sepal.Width Petal.Length Petal.Width Species
1 7.0 3.2 4.7 1.4 versicolor
2 6.4 3.2 4.5 1.5 versicolor
3 6.3 3.3 4.7 1.6 versicolor
4 5.9 3.2 4.8 1.8 versicolor
5 6.0 3.4 4.5 1.6 versicolor
1.2.2 arrange排序
arrange可以根据变量名依次对数据框进行排序,靠前的变量优先级越高,对变量名使用desc函数即为倒序。plyr(我们以后会介绍的一个包,同样出品自Hadley Wickham)中也有一个相同的此函数。
在R的base中,可以使用order来实现相同功能。
> arrange(mtcars, cyl, disp)%>%head(3)
mpg cyl disp hp drat wt qsec vs am gear carb
1 33.9 4 71.1 65 4.22 1.835 19.90 1 1 4 1
2 30.4 4 75.7 52 4.93 1.615 18.52 1 1 4 2
3 32.4 4 78.7 66 4.08 2.200 19.47 1 1 4 1
> arrange(mtcars, desc(disp))%>%head(3)
mpg cyl disp hp drat wt qsec vs am gear carb
1 10.4 8 472 205 2.93 5.250 17.98 0 0 3 4
2 10.4 8 460 215 3.00 5.424 17.82 0 0 3 4
3 14.7 8 440 230 3.23 5.345 17.42 0 0 3 4
Base写法:
mtcars[with(mtcars,order(cyl,disp)),]%>%head(3)
mtcars[order(mtcars$cyl,mtcars$disp),]%>%head(3)
说起排序,就不能忘记了排名,dplyr中有一系列排名的函数,就是ranking系列。
函数名 功能
row_number 通用排名,并列的名次结果按先后顺序不一样,靠前出现的元素排名在前
min_rank 通用排名,并列的名次结果一样,占用下一名次。
dense_rank 中国式排名,并列排名不占用名次,如:无论有几个并列第2名,之后的排名仍应该是第3名
percent_rank 按百分比的排名
cume_dist 累计分布区间的排名
ntile 粗略地把向量按堆排名,n即是堆的数量
> set.seed(1000)
> x=sample(1:5,7,replace=T)
> print(x)
[1] 2 4 1 4 3 1 4
> row_number(x)
[1] 3 5 1 6 4 2 7
> min_rank(x)
[1] 3 5 1 5 4 1 5
> dense_rank(x)
[1] 2 4 1 4 3 1 4
> percent_rank(x)
[1] 0.3333333 0.6666667 0.0000000 0.6666667 0.5000000 0.0000000 0.6666667
> cume_dist(x)
[1] 0.4285714 1.0000000 0.2857143 1.0000000 0.5714286 0.2857143 1.0000000
> ntile(x,3)
[1] 1 2 1 3 2 1 3
另一个函数top_n还实现了min_rank和filter的组合,用以筛选按排序显示的数据框。
1.2.3 select变量选择
对于一个稍微了解SQL或者更多的数据分析师来说,select自然是再熟悉不过的一个单词了,这里的select在某种程度上也类似于SQL中的select,其功能是按变量名选择数据框中的变量。
> select(mtcars,mpg,cyl,carb)%>%head(3)
mpg cyl carb
Mazda RX4 21.0 6 4
Mazda RX4 Wag 21.0 6 4
Datsun 710 22.8 4 1
同时,“-”即减号也是可以运用在这里的,选择除此以外的变量:
> select(mtcars,-mpg,-cyl,-carb)%>%head(3)
disp hp drat wt qsec vs am gear
Mazda RX4 160 110 3.90 2.620 16.46 0 1 4
Mazda RX4 Wag 160 110 3.90 2.875 17.02 0 1 4
Datsun 710 108 93 3.85 2.320 18.61 1 1 4
也可以把用“:”,把变量名连接起来,这货把变量当成数字了:
> select(mtcars,mpg:vs)%>%head(3)
mpg cyl disp hp drat wt qsec vs
Mazda RX4 21.0 6 160 110 3.90 2.620 16.46 0
Mazda RX4 Wag 21.0 6 160 110 3.90 2.875 17.02 0
Datsun 710 22.8 4 108 93 3.85 2.320 18.61 1
当然,直接使用数字也是可以的:
> select(mtcars,2:8)%>%head(3)
cyl disp hp drat wt qsec vs
Mazda RX4 6 160 110 3.90 2.620 16.46 0
Mazda RX4 Wag 6 160 110 3.90 2.875 17.02 0
Datsun 710 4 108 93 3.85 2.320 18.61 1
PS:这里要乱入一个鸡肋函数,slice,按行号筛选(如果这算筛选的话)数据框。
slice(mtcars,1:2);mtcars[1:2,]
slice(mtcars,n())
slice(mtcars,30:n())
诚如所言,有点鸡肋,完全可以用filter和row_number来实现,或者直接用“[]”好了,不过,天知道。
第三行代码的n()是计数的一个函数,不能单独使用。
1.2.4 mutate变量变形
mutate可以对数据框中已有的变量进行操作或者增加变量,值得称赞的是,一段mutate的代码中,靠后的变量操作可以操作前期新添加或改变的变量,这是transform所不具备的特性。
> mutate(mtcars,V1=mpg/cyl,V2=disp/hp,V3=V1+V2)%>%
+ select(V1:V3)%>%
+ head(3)
V1 V2 V3
1 3.5 1.454545 4.954545
2 3.5 1.454545 4.954545
3 5.7 1.161290 6.861290
同时若你嫌麻烦一个个地对变量进行操作,还可以使用mutate_each函数对数据框中的变量批量操作,通过调整funs(即functions)和vars(variables)参数控制functions的数量,以及参与变形的variables,这里控制variables的技巧与select函数相似。
iris%>%mutate_each(funs(dense_rank)) iris%>%mutate_each(funs(dense_rank,min_rank),-Petal.Width)
其家族内的另一个函数,transmute,返回值中不包含原数据集变量,只保留计算转换后的变量。
1.2.5 summarise数据汇总
summarise是对数据框中的变量调用函数进行数据汇总,单一地说来,其与plyr包中的summarise是一样的,不过,我们即将介绍dplyr包中的另一大功能,分组计算,使用分组计算的summarise能做的事情就多了非常多,其可以实现几乎所有的类似于Excel中数据透视表的汇总功能。
> summarise(mtcars,meanDisp=mean(disp),sumMpg=sum(mpg))
meanDisp sumMpg
1 230.7219 642.9
summarise也同样有个each版本,
> iris%>%summarise_each(funs(mean,sum))
Sepal.Length_mean Sepal.Width_mean Petal.Length_mean Petal.Width_mean
1 5.843333 3.057333 3.758 1.199333
Species_mean Sepal.Length_sum Sepal.Width_sum Petal.Length_sum
1 2 876.5 458.6 563.7
Petal.Width_sum Species_sum
1 179.9 300
1.2.6 join——拒绝merge
在base中,我们使用merge函数来合并两个数据框的行或列。虽然merge如此强大,但其也有个致命的弱点,那就是——慢。我们构建一个一百万行的随机数据框来测试一下。
> set.seed(1000)
> a=sample(1:1000000,1000000)
> df1=data.frame(a,x=rnorm(1000000))
> df2=data.frame(a=sample(a,1000000,replace=T),y=rnorm(1000000,10,10))
> system.time(merge(df1,df2,by="a",all=T))
用户 系统 流逝
8.11 0.02 8.13
> system.time(full_join(df1,df2,by="a"))
用户 系统 流逝
1.50 0.09 1.59
以上full_join的性能完爆了merge,速度提升了80%左右,如果是把数量级提升到千万的话,join的优势更加明显:
> system.time(merge(df1,df2,by="a",all=T))
用户 系统 流逝
122.26 1.60 123.91
> system.time(full_join(df1,df2,by="a"))
用户 系统 流逝
21.12 0.50 21.63
以上的测试使用的是如下所示的电脑,有兴趣的童鞋也可以自行做做测试:
dplyr包中所带的6个join函数的功能如下所示,其差异仅在于返回值的不同,我们假设其形式均为join(x,y):
函数名 功能
inner_join 返回所有在y中能查找到的x的行,且包含x和y的所有列;
left_join 返回所有x的行,且包含x和y的所有列,在y中没有查找到的x的行新增的列的值会以NA填充;
right_join 同上,只是x和y调换了一下;
full_join 返回所有x和y的行和列,未查找的部分同样会被NA填充;
anti_join 返回所有未能在y中能查找到的x的行,也只返回x的列
semi_join 返回所有在y中能查找到的x的行,也只返回x的列
1.3分组操作
提起group_by,想必或多或少接触过SQL的数据分析师们并不陌生。对,你没有听错……
此group_by的语法意义几乎与SQL中的group by完全一样,其也是针对被group by的变量进行分组的操作与计算,前提是有这样的操作与计算。在1.2.5中,我们提到了summarise配合使用分组计算能做到很大部分的数据透视表可以做的事情:
> group_by(iris,Species)%>%
+ summarise(mean=mean(Sepal.Length),max=max(Sepal.Width),
+ min=min(Sepal.Width),sd=sd(Petal.Width))%>%
+ ungroup%>%
+ mutate(distTest=max-min)
Source: local data frame [3 x 6]
Species mean max min sd distTest
1 setosa 5.006 4.4 2.3 0.1053856 2.1
2 versicolor 5.936 3.4 2.0 0.1977527 1.4
3 virginica 6.588 3.8 2.2 0.2746501 1.6
在上面这段代码中,我们根据鸢尾花的种类(Species)进行了分组,统计(summarise)了Sepal.Length的均值,Sepal.Width的最大值和最小值,Petal.Width的标准差。在结束统计后(ungroup的目的是解除group_by的操作),又插入了一个新的字段(mutate),计算不同种类的Sepal.Width极差。
通常来说,group_by与summarise这两个函数会放在一起使用,不过特殊的应用场景中,group_by也可以单独出现,比如针对每个组进行一些操作:
> group_by(iris,Species)%>%
+ filter(Petal.Width<=max(Petal.Width)-0.5)%>%
+ ungroup%>%
+ head(3)
Source: local data frame [3 x 5]
Sepal.Length Sepal.Width Petal.Length Petal.Width Species
1 5.5 2.3 4.0 1.3 versicolor
2 5.7 2.8 4.5 1.3 versicolor
3 4.9 2.4 3.3 1.0 versicolor
这里我们的筛选条件是选出小于每个种类的鸢尾花的Petal.Width的最大值减去0.5,看起来很绕是吧,反正你说不定哪天就需要做这样的操作了呢。
1.4 rowwise
rowwise这个函数的名字就十分萌萌哒,其还有个兄弟(or姐妹)函数叫做colwise(源自plyr包),两者之间用法虽然不尽相同,却也有着相似的思想(一个按行分组一个按列分组好吗,要知道行和列在data.frame里面差距还是蛮大的哎)。
对,你理解的没错,其实就是apply(x,1,FUN)啦,但是apply的效率,你懂得……我们运行一个较大的数据集来测试一下。
> m=matrix(1:16000000,ncol=2)%>�ta.frame
> system.time(m%>%rowwise%>%summarise(sum(X1,X2)))
用户 系统 流逝
10.52 0.00 10.52
> system.time(m%>%apply(1,sum))
用户 系统 流逝
55.87 0.10 55.97
1.5其他工具函数
Hadley Wickham在dplyr中开发了大量用到想哭的函数——是感动哭,如果当年这个包早点出现,说不定我看起来没有现在这么老……
1.5.1 tally系列
tally是一个很方便的计数函数,其根据最初的调用而决定下一次调用n或者sum(n)。它还有其他的小伙伴比如count和n,都是计数家族的。
> iris%>%group_by(Species)%>%tally
Source: local data frame [3 x 2]
Species n
1 setosa 50
2 versicolor 50
3 virginica 50
以上代码与
iris%>%group_by(Species)%>%summarise(n=n())
iris%>%count(Species)
等价,而如果用base写法,即table(iris$Species),但输出结果明显不够友善。
> iris%>%group_by(Species)%>%tally%>%tally
Using n as weighting variable
Source: local data frame [1 x 1]
n
1 150
1.5.2 sample系列
此sample系列是对数据框进行随机抽样,只作用于数据框和dplyr自带的tbl等格式的数据。sample_n为按行数随机抽样,而sample_frac为按比例抽样;其weight参数可以设置抽样的权重而replace参数为有放回抽样。
sample_n(by_cyl,2,replace=TRUE)
sample_n(by_cyl,2,weight=mpg/mean(mpg))
sample_frac(mtcars,0.1)
sample_frac(mtcars,0.1,weight=1/mpg)
1.5.3 cumall系列
dplyr在base的cum系列函数的基础上增加了几个新的累积计算函数,cumall,cummean,cumany。对于cumall和cumany,会将输入的向量转化为逻辑值,其可用于多条件的“和运算”和“或运算”;而cummean更多是补充cumsum之类的函数,得出累积的平均数。
> x=c(5,2,3,0,1,NA,2)
> cumall(x)
[1] TRUE TRUE TRUE FALSE FALSE FALSE FALSE
> cumany(x)
[1] TRUE TRUE TRUE TRUE TRUE TRUE TRUE
> cummean(x)
[1] 5.000000 3.500000 3.333333 2.500000 2.200000 NA NA
1.5.4 distinct系列
distinct就和它的名字一样孤独,dplyr中有两个函数,distinct为计算数据集中的唯一值,是unique的高效版本;
> x=sample_n(mtcars,1000000,replace=T)
> system.time(distinct(x))
用户 系统 流逝
0.11 0.00 0.11
> system.time(unique(x))
用户 系统 流逝
11.55 0.01 11.56
而n_distinct相当于length+unique,计算向量中唯一值的个数,不过这个函数的速度反而不及length+unique,这明显不符合Hadley Wickham的风格啊。
> set.seed(1000)
> x=rnorm(10000000)
> system.time(unique(x)%>%length)
用户 系统 流逝
0.78 0.05 0.82
> system.time(n_distinct(x))
用户 系统 流逝
7.08 0.31 7.39
1.5.5 glimpse
顾名思义,对数据的惊鸿一瞥,本函数把数据集倒转,有点像str,但结果更为友善,其会显示出所有的变量名,同时尽量显示出更多的原始数据,有兴趣的童鞋可以glimpse(mtcars)感受一下。
1.5.6 failwith
这个函数更多用于构建function时产生的错误值的处理,一般情况我们需要使用if等语句来判断错误值产生时function的返回值,而使用failwith的话,可以很方便地给予一个返回值给function。
> f=function(x)ifelse(x==1,stop("error"),1)
> f(1)
Error in ifelse(x == 1, stop("error"), 1) : error
> f(2)
[1] 1
> f1=failwith(NA,f)
> f1(1)
Error in ifelse(x == 1, stop("error"), 1) : error
[1] NA
可以看到虽然报出error,但是函数仍然有了返回值。
1.5.7 rbind_all系列
在最新版本的dplyr中,rbind_all已然被作者建议弃用,代替以bind_cols和bind_rows,bind_rows支持各种按行的合并,比如两个变量一致的数据框,或者由多个相同格式数据框组成的一个列表的合并。而使用bind_cols的时候,还需要匹配相同的行,由于强大的join系列的存在,bind_cols相对不太常用。
1.5.8 其他函数
between提供了x>= left & x<=right的简略版。
> between(1:5,2,3)
[1] FALSE TRUE TRUE FALSE FALSE
nth系列(first,last,nth)函数,其返回序列(vector)中的第一个(最后一个或者第N个)值。
> x=c(5,2,3,0,1,NA,2)
> first(x)
[1] 5
> last(x)
[1] 2
> nth(x,3)
[1] 3
dplyr包配合其他几个程序包(plyr,tidyr等等)几乎解决了日常数据整理中遇到的大部分问题,其还包括了连接几个开源数据库的函数,可用于远程数据库计算取数;也推出了data_frame和tbl_df等快速方便的数据存储对象;do语句使得在同一数据集中根据不同分组情况创建不同的模型更加方便。在后面的章节中,我们将大量的应用到本包的函数,并且在代码风格上也将大量植入管道函数,这也是我们在第一章即介绍本包的初衷。