R语言-数据整形之plyr包

R语言中plyr包

前言

  apply族函数是R语言中很有特色的一类函数,包括了apply、sapply、lapply、tapply、aggregate等等。这一类函数本质上是将数据进行分割、计算和整合。它们在数据分析的各个阶段都有很好的用处。例如在数据准备阶段,我们可以按某个标准将数据分组,然后获得各组的统计描述。或是在建模阶段,为不同组的数据建立模型并比较建模结果。apply族函数与Google提出的mapreduce策略有着一致的思路。因为mapreduce的思路也是将数据进行分割、计算和整合。只不过它是将分割后的数据分发给多个处理核心进行运算。如果你熟悉了apply族函数,那么将数据转为并行运算是轻而易举的事情。plyr包则可看作是apply族函数的扩展,使之更容易运用,功能更为强大。

  plyr包的主函数是**ply形式的,其中首字母可以是(d、l、a),第二个字母可以是(d、l、a、_),不同的字母表示不同的数据格式,d表示数据框格式,l表示列表,a表示数组,_则表示没有输出。第一个字母表示输入的待处理的数据格式,第二个字母表示输出的数据格式。例如ddply函数,即表示输入一个数据框,输出也是一个数据框。

  plyr包是Hadley Wickham大神为解决split – apply – combine问题而写的一个包,其动机在与提供超越for循环和内置的apply函数族的一个一揽子解决方案。使用plyr包可以针对不同的数据类型,在一个函数内同时完成split – apply – combine三个步骤。

  plyr 的功能已经远远超出数据整容的范围,Hadley在plyr中应用了split-apply-combine的数据处理哲学,即:先将数据分离,然后应用某些处理函数,最后将结果重新组合成所需的形式返回。某些人士喜欢用“揉”来表述这样的数据处理;“揉”,把数据当面团捣来捣去,很哲,砖家们的砖头落下来,拍死人绝不偿命。

  先别哲了,来点实际的:plyr的函数命名方式比较规律,很容易记忆和使用。比如 a开头的函数aaply, adply 和 alply 将数组(array)分别转成数组、数据框和列表;daply, ddply 和 dlply 将数据框分别转成数组、数据框和列表;而laply, ldaply, llply将列表(list)分别转成数组、数据框和列表。

目录

 1. 数据转换: split – apply – combine 模式

 2. 知识点_1

 3. 知识点_2

 4. 知识点_3

 5. 知识点_4

 6. 参考资料

数据转换: split – apply – combine 模式

  按:这一篇是读Hadley Wickham的文章The Split-Apply-Combine Strategy for Data Analysis的笔记。

  在数据分析中,有许多问题可以由类似的类型和方法步骤解决,可称之为模式,设计模式或者分析模式。下面要讨论的是数据转换的一个常用模式:split – apply – combine。其解决之道,在R语言中,有3种方式:

  (1) for 显式循环,但是这种方式的缺点也很明显,代码长,易出错,也难以并行化;

  (2) 拜R语言的向量计算特点所赐,在R当中,大多数问题不需要用显示循环方式,而代之以base包中的apply函数族及其它的一些函数,直接对向量,数组,列表和数据框实现循环的操作。

  (3) Hadley Wickham大神觉得apply族还是不够简洁,所以开发了pylr包,以更少的代码来解决split – apply – combine问题。

    1. split – apply – combine 模式

  大型数据集通常是高度结构化的,结构使得我们可以按不同的方式分组,有时候我们需要关注单个组的数据片断,有时需要聚合不同组内的信息,并相互比较。

  因此对数据的转换,可以采用split – apply – combine模式来进行处理:

    split:把要处理的数据分割成小片断;

    apply:对每个小片断独立进行操作;

    combine:把片断重新组合。

  利用这种模式解决问题在很多数据分析或编程问题中都会出现:

    Python当中,是map( ),filter( ),reduce( );

    Google 有MapReduce;

    R 当中是split( ),*apply( ),aggregate( )…,以及plyr包。

    1. 分划:split函数

  在R当中,split这个步骤是由split( ),subset( )等等函数完成的。

  下面主要介绍split这个函数。

  函数split()可以按照分组因子,把向量,矩阵和数据框进行适当的分组。它的返回值是一个列表,代表分组变量每个水平的观测。这个列表可以使用sapply(),lappy()进行处理(apply – combine步骤),得到问题的最终结果。

  split( )的基本用法是:group <- split(X,f)

  其中X 是待分组的向量,矩阵或数据框。f是分组因子。

   例1:对向量分组

	> library(MASS)
	#使用Cars93数据集,利用其中的Origin变量(两个水平),对Price变量分组
	> g<-split(Cars93$Price,Cars93$Origin) 
	
	#分组结果是个列表:
	$USA
	 [1] 15.7 20.8 23.7 26.3 34.7 40.1 13.4 11.4 15.1 15.9 16.3 16.6 18.8 38.0
	[15] 18.4 15.8 29.5 9.2 11.3 13.3 19.0 15.6 25.8 12.2 19.3 7.4 10.1 11.3
	[29] 15.9 14.0 19.9 20.2 20.9 34.3 36.1 14.1 14.9 13.5 16.3 19.5 20.7 14.4
	[43] 9.0 11.1 17.7 18.5 24.4 11.1
	$`non-USA`
	 [1] 15.9 33.9 29.1 37.7 30.0 8.4 12.5 19.8 12.1 17.5 8.0 10.0 10.0 13.9
	[15] 47.9 28.0 35.2 8.3 11.6 16.5 19.1 32.5 31.9 61.9 10.3 26.1 11.8 15.7
	[29] 19.1 21.5 28.7 8.4 10.9 19.5 8.6 9.8 18.4 18.2 22.7 9.1 19.7 20.0
	[43] 23.3 22.7 26.7
	
	#计算组的长度和组内均值
	> sapply(g,length)
	    USA non-USA 
	     48 45 
	> sapply(g,mean)
	     USA non-USA 
	18.57292 20.50889
	
	#用lapply也可以,返回值是列表
	> lapply(g,mean)
	$USA
	[1] 18.57292
	$`non-USA`
	[1] 20.50889

   例2:对矩阵分组(按列)

	> m<-cbind(x=1:10,y=11:20)
	> split(m,col(m))
	$`1`
	 [1] 1 2 3 4 5 6 7 8 9 10
	$`2`
	 [1] 11 12 13 14 15 16 17 18 19 20

   例3:对数据框进行分组

	#还是利用Cars93,它就是个数据框
	g<-split(Cars93,Cars93$Origin)
	#分组结果
	> summary(g)
	        Length Class Mode
	USA 27 data.frame list
	non-USA 27 data.frame list

  split还有一个逆函数,unsplit,可以让分组完好如初。在base包里和split功能接近的函数有cut(对属性数据分划),strsplit(对字符串分划)以及subset(对向量,矩阵或数据框按给定条件取子集)等。

    1. base包中的apply()函数族

  接下来的apply – combine步骤主要由apply函数族完成。对于R的初学者来说,apply函数族以其强大的向量运算功能,把for循环语句送到了被遗忘的角落,这种震撼效果,无异于天赐神器。但是,这些冠以*apply的函数,也经常让人使用混乱,找不着方向。

  那么apply包括哪些函数呢?在 R console中键入??apply,可以看到在base包中包含如下函数:(apply在别的包里还有起到各自功能的相关函数,我们只看涉及数据转换的这些个)

	apply :Apply Functions Over Array Margins 
	by :Apply a Function to a Data Frame Split by Factors
	eapply :Apply a Function Over Values in an Environment 
	lapply :Apply a Function over a List or Vector 
	mapply :Apply a Function to Multiple List or Vector Arguments 
	rapply :Recursively Apply a Function to a List 
	tapply :Apply a Function Over a Ragged Array

  除此之外,还有可作为lapply变形的sapply,vapply和 replicate,共计10个函数。具体的内容,见apply()函数族的详细讲解!!!

知识点_1

  (1)下面我们看看如何使用ldply函数将ath1121501.db包中的KEGG列表数据转成数据框:

	> library(ath1121501.db) 
	> keggs <- as.list(ath1121501PATH[mappedkeys(ath1121501PATH)]) 
	> head(ldply(keggs, paste, collapse='; ')) 
	        .id                                              V1 
	1 261579_at                                           00190 
	2 261569_at                                           04712 
	3 261583_at 00010; 00020; 00290; 00620; 00650; 01100; 01110 
	4 261574_at                      00903; 00945; 01100; 01110 
	5 261043_at                             00051; 00520; 01100 
	6 261044_at                                           04122 

  (2)对于简单的问题,plyr和apply函数的效果差不多

	> m <- matrix(c(1:4,1,4,1:6),ncol=3)
	> apply(m,1,mean)
	[1] 1.666667 3.333333 3.000000 4.000000
	> aaply(m,1,mean)
	       1 2 3 4 
	1.666667 3.333333 3.000000 4.000000

  plyr包的函数较多,不再一一介绍,更多用法请参考它的在线帮助,Hadley 也写了很详细的tutorial:http://plyr.had.co.nz/09-user/

知识点_2

  下面首先来用一个简单的例子说明一下用法。还是用iris数据集,其中包括了一个分类变量和四个数值变量。我们希望数据按不同类别,分别计算数值变量的均值。下面我们分别用三种方法来得到同样的结果。

	library(plyr)
	library(reshape2)
	# 用aggregate函数进行数据汇总
	aggregate(iris[1:4],list(iris$Species),mean)
	# 用reshape2包进行数据汇总
	data.melt <- melt(iris,id=c('Species'))
	dcast(data.melt,Species~variable,mean)
	# 用ddply函数进行数据汇总
	# 原代码是 ddply(iris,.(Species),function(df) mean(df[1:4]))
	# 错误在于数据框的统计应该用colMeans
	ddply(iris,.(Species),function(df) colMeans(df[1:4]))
	# 这个例子并没有显示出什么优势,但我们可以学习到plyr的基础使用方式,即对于**ply这种格式的函数,其第一个数据参数是数据源,其他参数可以用?来查询.

  初看起来plyr包所具备的功能并不很出彩,下面我们看一个略为复杂例子。还是用iris数据,我们希望对每一种花做一个简单回归。

	# 首先定义回归函数
	model <- function(x) {
	    lm(Petal.Length~Petal.Width,data=x)
	}
	# 如果用普通的函数则需要如下的分割、计算、整合三个步骤共四条命令
	# Split函数的作用是将数据框按照指定字段分组,但不做后续计算。lapply函数可以对每组数据都执行同样的算法。Split和lapply两者结合可以实现本案例。
	pieces <- split(iris,list(iris$Species))
	models <- lapply(pieces,model)
	result <- lapply(models,coef)
	do.call('rbind',result)
	# 用plyr包只用下面两个函数,每个函数都内置了分割、计算、整合的功能。
	result1 <- dlply(iris,.(Species),model)
	result2 <- ldply(result1,function(x) coef(x))

  plyr包中还有两个比较特别的函数,分别是rply和mply,它们分别对应的是replicate和mapply函数。

	replicate(20,mean(runif(100)))
	rdply(20, mean(runif(100)))
	mapply(rnorm,mean=1:5,sd=1:5, n=2)
	mdply(data.frame(mean = 1:5, sd = 1:5), rnorm, n = 2)

  最后我们来看一个mdply函数的应用,我们希望用神经网络包来为不同的花进行分类,使用BP神经网络需要的一个参数就是隐藏层神经元的个数。我们来尝试用1到10这十个参数运行模型十次,并观察十个建模结果的预测准确率。但我们并不需要手动运行十次。而是使用mdply函数来完成这个任务。

	library(nnet)
	# 确定建模函数
	nnet.m <- function(...) {
	  nnet(Species~.,data=iris,trace=F,...)
	}
	# 确定输入参数
	opts <- data.frame(size=1:10,maxiter=50)
	# 建立预测准确率的函数
	accuracy <- function(mod,true) {
	  pred <- factor(predict(mod,type='class'),levels=levels(true))
	  tb <- table(pred,true)
	  sum(diag(tb))/sum(tb)
	}
	# 用mlply函数建立包括10个元素的列表,每个元素包括了一个建模结果
	models <- mlply(opts,nnet.m)
	# 再用ldply函数读取列表,计算后得到最终结果
	ldply(models,'accuracy',true=iris$Species)

  下面是其他操作函数

  • each():each(min, max)等价于function(x) c(min = min(x), max = max(x))。

  • colwise():colwise(median)将计算列的中位数。

  • arrange():超级顺手的函数,可以方便的给dataframe排序。

  • rename():又是一个handy的函数,按变量名而不是变量位置重命名。

  • count():返回unique值,等价于length(unique(**))。

  • match_df():方便的配合count()等,选出符合条件的行,有点像merge(…,all=F)的感觉。

  • join():对于习惯SQL的童鞋,可能比merge()用起来更顺手吧(当然也更快一点),不过灵活性还是比不上merge()。

知识点_3

  pylr包的使用

  • (1)命名规则
plyr的基本函数集
array data frame list nothing
array aaply adply alply a_ply
data frame daply ddply dlply d_ply
list laply ldply llply l_ply
n replicates raply rdply rlply r_ply
function
arguments
maply mdply mlply m_ply
plyr 与 R-Base基本函数对应表
array data frame list nothing
array apply adply alply a_ply
data frame daply aggregate by d_ply
list sapply ldply lapply l_ply
n replicates replicate rdply replicate r_ply
function
arguments
mapply mdply mapply m_ply

  命名规则:前三行是基本类型。

  根据输入类型和输出类型:a=array,d=data frame,l=list,_ 表示输出放弃。第一个字母表示输入,第2个字母表示输出。

  后两行是对应apply族的replicates和mapply这两个函数,分别表示n次重复和多元函数参数的情况,第2个字母还是表示输出类型。

  从命名特点来看,我们不需要列出每个函数的情况了,只要从输入和输出两方面分别讨论即可。

  • (2)参数说明

      a*ply(.data, .margins, .fun, ..., .progress = "none")
      d*ply(.data, .variables, .fun, ..., .progress = "none")
      l*ply(.data, .fun, ..., .progress = "none")
    

  这些函数有两到三个主要的参数,依赖于输入的类型:

  参数.data是我们要用来分片-计算-合并的;参数.margins或者.variables描述了分片的方式;参数.fun表示用来处理的函数,其它更多的参数(...)是传递给处理函数的;参数.progress用来控制显示一个进度条。

  • (3) 输入

  输入类型有三种,每一种类型给出了如何进行分片的不同方法。

  简单来说:

   a*ply( ):数组(包括矩阵和向量)按维数分为低维的片。

   d*ply( ):数据框被变量组合分成子集。

   l*ply( ):列表的每个元素就是一个分片。

  因此,对输入数据集的分片,不是取决于数据的结构,而是取决于所采用的方法。

   1. 一个对象采用 a*ply( )分片必须对应dim( )且接受多维的索引;

   2. 采用 d*ply( )分片,要利用split( )并强制转换为列表;

   3. 采用 l*ply( ),需要用 length() and [[。

  所以数据框可以被传递给 a * ply( ),可以象2维的矩阵那样处理,也可以传递给l*ply( ),被视为一个向量的列表。

  三种类型各自的特点:

  (a): 输入数组( a*ply() )

  a*ply()的分片特点在于.margins参数,它和apply很相似。对于2维数组, .margins 可以取1,2,或者c(1:2),对应2维数组的3种分片方式。

	• .margins = 1: Slice up into rows.
	• .margins = 2: Slice up into columns.
	• .margins = c(1,2): Slice up into individual cells.

  对于2维数组,则有3种分片方式:

R-plyr包-a*ply()

  对于3维数组,则有7种分片方式:

R-plyr包-a*ply()

  .margins对应更高维的情况,可能会面临一种爆发式的组合。

  (b)输入数据框( d*ply() )

  使用d*ply时,需要特别指定分组所用的变量或变量函数,它们会被首先计算,然后才是整个数据框。
有下面几种指定方式:

	• .(var1)。按照变量var1的值来对数据框分组
	• 多重变量 .(a,b,c)。将按照三个变量的交互值来分组。

R-plyr包-a*ply()

  这种形式输出的时候,有点复杂。如果输出为数组,则数组会有三个维度,分别以 a,b,c 的值作为维数名。如果输出为数据框,将会包含 a,b,c 取值的三个额外的列。如果输出为列表,则列表元素名为按周期分割的 a,b,c 的值。

	• 作为列名的字符向量:c("var1", "var2")。
	• 公式~ var1 + var2。

  (c) 输入列表( l*ply() )

   l * ply() 不需要描述如何分片的函数,因为列表本身就是按照元素的分划。使用l * ply()相当于a*ply()作用于一维数组的效果。

知识点_4

  plyr包案例集

  本文给出几个关于使用plyr包来进行数据处理的例子。重点在于plyr包的应用,而不是深入地探讨数据的分析。

  • 1. 点球成金

  此例来源于 Wickham 的论文[ Hadley Wickham :The Split-Apply-Combine Strategy for Data Analysis Journal of Statistical Software,April 2011, Volume 40, Issue 1. ]。案例里的图形是我作的。

  布拉德.皮特在《点球成金》里用数据方法发掘棒球运动员的价值,我们也可以来试试。plyr 包的baseball数据集包括了1887-2007年间1228位美国职业棒球运动员15年以上的击球记录。

	> library(plyr)
	> data(baseball)
	> dim(baseball)
	[1] 21699    22

  下面的分析,是要研究击球手的表现。

  关注四个变量:

	id:运动员的身份;
	year:纪录的年份;
	rbi:跑垒得分,运动员在赛季内的跑动数目;
	ad:轮到击球或者面对投手的次数。

  先来计算“职业年份”,就是运动员开始职业生涯以来的年数。

  对某一位运动员:

	> baberuth <- subset(baseball, id == "ruthba01")
	> # 在baberuth里加入cyear 
	> baberuth <- transform(baberuth, cyear = year - min(year) + 1) 

  对所有的运动员:

	> baseball <- ddply(baseball, .(id), transform, cyear = year - min(year) + 1)

  先看看Babe Ruth这位运动员的模式,画rbi/ab(每次击球的跑动)的时间序列图:

	> library(ggplot2)
	> cyear <- baberuth$cyear
	> ra <- (baberuth$rbi)/(baberuth$ab)
	> a <- data.frame(cyear,ra)
	> p <- ggplot(a,aes(cyear,ra))
	> p + geom_line()
	> # 或者(画图代码)
	> # qplot(cyear, rbi / ab, data = baberuth, geom = "line")

R-plyr包-a*ply()

  下面使用d_ply函数按下面的方式对各位运动员们画图并保存为pdf格式:

  (1)按照运动员的平均rbi / ab值对运动员排序,这样确保如果某个图画不出的时候可以画出其它的。

  (2)只取ab大于25的运动员

	> baseball <- subset(baseball, ab >= 25)
	> xlim <- range(baseball$cyear, na.rm=TRUE)
	> ylim <- range(baseball$rbi / baseball$ab, na.rm=TRUE)
	> plotpattern <- function(df) {
	+   qplot(cyear, rbi / ab, data = df, geom = "line", xlim = xlim, ylim = ylim)
	+ }
	> pdf("paths.pdf", width = 8, height = 4)
	> d_ply(baseball, .(reorder(id, rbi / ab)), failwith(NA, plotpattern), .print = TRUE)
	> dev.off()

  这些图里能看出大概的线性趋势。来对每个运动员拟合一下线性模型。先看Babe Ruth的:

	> model <- function(df) {
	+ lm(rbi / ab ~ cyear, data = df)
	+ }
	> model(baberuth)
	
	Call:
	lm(formula = rbi/ab ~ cyear, data = df)
	
	Coefficients:
	(Intercept) cyear 
	   0.203200 0.003413 

  然后用dlply对所有的运动员:

	> bmodels <- dlply(baseball, .(id), model)

  这样共有1145个模型的列表,下面提取这些模型的系数(slope and intercept)和R^2,

	> rsq <- function(x) summary(x)$r.squared
	> bcoefs <- ldply(bmodels, function(x) c(coef(x), rsquare = rsq(x)))
	> names(bcoefs)[2:3] <- c("intercept", "slope")
	> head(bcoefs)
	> 
	         id  intercept         slope     rsquare
	1 aaronha01 0.18329371  0.0001478121 0.000862425
	2 abernte02 0.00000000            NA 0.000000000
	3 adairje01 0.08670449 -0.0007118756 0.010230121
	4 adamsba01 0.05905337  0.0012002168 0.030184694
	5 adamsbo03 0.08867684 -0.0019238835 0.108372596
	6 adcocjo01 0.14564821  0.0027382939 0.229040266

  把所有模型的R^2绘直方图:(代码有问题???没画出来???待修改)

	> qplot(rsquare, data=bcoefs, geom="histogram", binwidth=0.01)

R-plyr包-a*ply()

  可以看出这些模型整体来说拟合的效果是很差的。

  下面把那些拟合效果好的模型找出来:先利用系数进行“合并”,再挑出R^2为1的模型:

	> baseballcoef <- merge(baseball, bcoefs, by = "id")
	> subset(baseballcoef, rsquare > 0.999)$id
	> 
	 [1] "bannifl01" "bannifl01" "bedrost01" "bedrost01"
	 [5] "burbada01" "burbada01" "carrocl02" "carrocl02"
	 [9] "cookde01"  "cookde01"  "davisma01" "davisma01"
	[13] "jacksgr01" "jacksgr01" "lindbpa01" "lindbpa01"
	[17] "oliveda02" "oliveda02" "penaal01"  "penaal01" 
	[21] "powerte01" "powerte01" "splitpa01" "splitpa01"
	[25] "violafr01" "violafr01" "wakefti01" "wakefti01"
	[29] "weathda01" "weathda01" "woodwi01"  "woodwi01"

  所有这些拟合好的模型都只有两个数据点。

  换个角度看一下这些模型。

  绘制斜率和截距的散点图:(代码有问题???没画出来???待修改)

	> p <- ggplot(bcoefs, aes(slope, intercept))
	> p <- p + geom_point(aes( size = rsquare), alpha = 0.6)

R-plyr包-a*ply()

  放大局部:(代码有问题???没画出来???待修改)

	> s <- subset(baseballcoef,slope>-0.01&slope<0.01)
	> p <- ggplot(s, aes(slope, intercept))
	> p <- p + geom_point(aes( size = rsquare), alpha = 0.6)

R-plyr包-a*ply()

  可以看出,斜率和截距之间是负相关的,且拟合坏的模型其估计值都接近于0。

  • 2. 起个好名字

  plyr包还有很多对于数据集操作的函数。本文涉及summarise,arrange,mutate

  这个案例来自Wickham的课程主页(http://stat405.had.co.nz/)。

  数据集Baby name包括了美国从1880年到2008年间排名前1000位的婴儿姓名。数据集在课程主页下载。

	library(plyr)
	library(ggplot2)
	bnames <- read.csv("bnames2.csv.bz2")
	births <- read.csv("births.csv")
	# 先取一个名字看一看,
	steve<-subset(bnames,name == "Steve")
	qplot(year, prop, data = steve, geom = "line")

R-plyr包-a*ply()

  苹果教主生于1955年,看来是个当时的流行名字。

	# 在steve里求概括统计量
	summarise(steve,least = year[prop == min(prop)],most = year[prop == max(prop)]) 
	  least most
	1 2008 1959
	# 按比例降序重排steve
	arrange(steve, desc(prop))
	# 在steve中增加一列per1000,取1000 * prop的整数
	mutate(steve, per1000 = round(1000 * prop))

  下面来探索一下名字的趋势,

  这涉及到使用外部数据集,下面合并bnames和 births

	bnames2 <- join(bnames, births, by = c("year", "sex"))
	bnames2 <- mutate(bnames2, n = round(prop * births))

  看看出生趋势

	qplot(year, births, data = births, geom = "line",color = sex)

R-plyr包-a*ply()

	# 那些年,有多少个steve出生呢
	steve1<- subset(bnames2, name == "Steve")
	summarise(steve1, sum(n))
	sum(n)
	1 237613
	# 对每个名字都算一下:(这就是split-apply-combine步骤了)
	counts <- ddply(bnames2, "name", summarise, n=sum(n))

  把名字按数量重排一下:

	best<-arrange(counts, desc(n))

  大众款的名字是:

	head(best)
	     name n
	1 James 5043259
	2 John 5036828
	3 Robert 4771447
	4 Michael 4226596
	5 Mary 4111514
	6 William 3966170

  稀有版:

	 tail(best)
	       name n
	6777 Delina 4
	6778 Delle 4
	6779 Dema 4
	6780 Dollye 4
	6781 Eithel 4
	6782 Elzada 4

参考资料

posted @ 2016-05-30 00:32  银河统计  阅读(11510)  评论(0编辑  收藏  举报