R语言的plyr包简介

来源:互联网 发布:淘宝网上代购可信吗 编辑:程序博客网 时间:2024/05/19 23:58

(转载地址)http://www.jianshu.com/p/bfddfe29aa39

R语言的plyr包简介

字数3040 阅读2875 评论3

R语言中的类SQL操作

plyr包可以进行类似于数据透视表的操作,将数据分割成更小的数据,对分割后的数据进行些操作,最后把操作的结果汇总。

本文主要介绍以下内容:

  • Split-Aapply-Combine 原理介绍
  • baby_names的名字排名
  • 求分段拟合的系数
  • 部分其他函数介绍

在正式开始之前,请确保电脑上已经安装plyr,如果没有,通过install.packages()函数安装。

install.packages(plyr)  # 安装plyr包require(plyr)  #载入plyr包

假设有美国新生婴儿的取名汇总,每一年,会统计男孩和女孩的取名情况,形成如下的一张表。

yearnamepercentsex1880John0.081541boy1880William0.080511boy1880James0.050057boy1880Charles0.045167boy1880George0.043292boy1880Frank0.02738boy1880Joseph0.022229boy1880Thomas0.021401boy

baby_names数据集包含1880 ~ 2008年间的数据, 包含统计的年份(year),新生婴儿的性别、名字、以及改名字的比例。

以提问并解决问题的形式对plyr做介绍。

  1. 想知道数据集中,每年都有多少记录?
  2. 数据集中,男孩和女孩名的各自排名?
  3. 男孩名和女孩名各自排名前100在当年中的比例?

数据集中,每年都有多少记录
先假设我们有某一年的数据,我们会如何统计其中的记录数呢?由于数据集中,每条记录一行,只需要统计对应的行数就可以得到对应的记录数。
写个函数试试

record_count <- function(df) {  return(data.frame(count = nrow(df)))}

返回值类型是data.frame类型,是为即将介绍的ddply()函数做铺垫。先来看看2008年,数据集中有多少记录。

baby_names_2008 <- subset(baby_names, year == 2008)record_count(baby_names_2008)# 2000

结果显示2000条,貌似我们已经得到答案。下面想想,该如何得到1880 ~ 2008这129年间,每年的记录数呢?

ddply(baby_names,     # 数据集      .(year),        # 分类的标准      record_count    # 函数)

结果比较长,只摘取其中一部分

yearcount18802000188120001882200018832000188420001885200018862000188720001888200018892000189020001891200018922000

不错,每年都是2000条记录。再来看看,刚在我们做了什么。

  • 定义了一个负责计数的函数record_count()
  • 调用ddply(),这里出现刚刚定义的函数

ddply()函数是plyr包中用于对data.frame结构的数据做处理的函数,其结果也是data.frameddply的参数列表如下:

ddply(.data, .variables, .fun = NULL, ..., .progress = "none",  .inform = FALSE, .drop = TRUE, .parallel = FALSE, .paropts = NULL)

各部分解释如下
  1. 第一个参数是要操作的原始数据集,比如baby_name
  2. 第二个参数是按照某个(也可以几个)变量,对数据集分割,比如按照year对数据集分割,可以写成.(year)的形式
  3. 第三个参数是具体执行操作的函数,对分割后的每一个子数据集,调用该函数
  4. 第四个参数可选,表示第三个参数对应函数所需的额外参数

其他参数,可以暂时不用考虑。ddply()函数会自动的将分割后的每一小部分的计算结果汇总,以data.frame的格式保存。<span style="color:red">分割后的数据,是fun的第一个参数。</span>

在上面的描述中,提到的分割、__操作汇总__,在plyr包中是一种处理方式("frame"),即"Split - Apply - Combine"。在plyr包中有很多这种处理方式的函数,在介绍这些函数之前,我们再来看看ddply()的一些更深入的用法。

各年,男孩名与女孩名的各自排名
以2008年的数据为例,男孩名"Jacob"的比例最高,排名应当是第一,"Michael"紧跟其后,排名应当第二,依此类推。对于女孩名,"Emma"排名第一,"Isabella"排名第二,"Emily"排名第三等等。我们希望得到这样的结果。

对于2008年的数据,可以通过简单的rank即可得到,不过要对男孩和女孩分别排序。

baby_names_2008_boy <- subset(baby_names_2008, sex == "boy") # 获取男孩名baby_names_2008_boy$rank <- rank(- baby_names_2008_boy$percent) # 排序head(baby_names_2008_boy) # 查看

对女孩名也执行相同的操作,这里就不写出来了,只需要在subset中,将"boy"替换成"girl"就行。下面来看看2008年,男孩名的排名情况

yearnamepercentsexrank2008Jacob0.010355boy12008Michael0.009437boy22008Ethan0.009301boy32008Joshua0.008799boy42008Daniel0.008702boy52008Alexander0.008566boy6

再来看看女孩名的排名结果:

yearnamepercentsexrank2008Emma0.009043girl12008Isabella0.008941girl22008Emily0.008377girl32008Madison0.008199girl42008Ava0.008198girl52008Olivia0.008196girl6

如何利用ddply()对原始数据集做相应的操作呢?这里需要介绍R语言中的一个函数transform(),该函数对原始数据集做一些操作,并把结果存储在原始数据中,更详细的用法,参见帮助文档?transform

第一个版本的处理方式是这样的

ddply(baby_names,       .(year, sex),       transform,       rank = rank(-percent, ties.method = "first"))

第二个参数有点变化,除了year,还有sex,这表示对baby_name数据集,对yearsex分类(类似于SQL中的group by year, sex)。
第四个参数是transform的额外参数,如果查看transform的帮助文档,其函数调用方式如下:

transform(_data, ...)

第一参数为操作的数据,在ddply()中为按年份和性别分割后的子数据集;后面的...参数是tag = value的形式,这种tag:value将追加在数据中。

由于rank默认对数据进行升序排序,若要实现逆序排序,常规的做法是将数据的符号取反,这也就是上面的rank函数中出现-percent的原因。在plyr中,有一个类似的函数,实现取反的操作,是desc

x <- 1:10desc(x)# -1  -2  -3  -4  -5  -6  -7  -8  -9 -10

所以,上面对percent取反的操作,可以写得更优雅些,就有了第二个版本的函数

baby_names <- ddply(baby_names,                     .(year, sex),                     transform,                     rank = rank(desc(percent), ties.method = "first"))

注意这里把结果赋给了baby_name,因为后面还会用到排名的信息,就把结果保存下来。

排名前100的男孩名与女孩名在当年中的比例
跟前一问类似,处理方法是:

  • 把每年排名前100的数据筛选出来
  • 把男孩和女孩对应的percent相加
baby_names_top100 <- subset(baby_names, rank <= 100)  # 将前100排名的数据筛选出来baby_names_top100_trend <- ddply(baby_names_top100,                                  .(year, sex), # 按年和性别分割                                 summarize, # 汇总数据                                 trend = sum(percent)) # 汇总方式(求和)

这里出现一个新的操作函数summarize(),该函数是对数据做汇总,与transform不一样的是,该函数并不追加结果到原始数据,而是产生新的数据集。比如想知道,2008年的男孩名中,排名最高和最低的名字的百分比之差,可以通过如下方式求得:

summarize(baby_names_2008_boy, trend = max(percent) - min(percent))# 0.010266

回到刚才的问题,从1880 ~ 2008年间,男孩名与女孩名的前100所占比例(可以衡量名字大众化的程度)到底是什么样的呢?画个图就知道了。


naming_trend.jpeg

还有什么类似函数
上面介绍的ddply()plyr包中处理data.frame的函数,还有处理listarray的函数,汇总起来如下

 arrarydata.framelistdiscardedarraryaaplyadplyalplya_plydata.framedaplyddplydlplyd_plylistlaplyldplyllplyl_ply

所有的函数具有xyply的形式,其中x表示数据数据类型,y表示输出数据类型,而_表示丢弃。

应用举例

R语言基础数据集中,有mtcars数据,其中记录了车重"weight"、"miles per galon"、"cylinder"等参数。由图可知,不同气缸下,车重与行驶里程有着不同的关系,如果以线性函数来刻画,是三条有着明显区别的函数。


mtcars.jpeg

该如何求着三条直线的参数呢(截距与斜率)?
将问题简化下,对于数据集df,有自变量x,因变量y,如何求y = a x + b的参数ab?写个函数试试

linear_fit <- function(df) {  model <- lm(mpg ~ wt, df)  linear_coef <- coef(model)  linear_coef <- data.frame(intercept = linear_coef[1],                            slope = linear_coef[-1])  row.names(linear_coef) <- NULL  linear_coef}

下面再应用split - apply - combine的思想求出每一种cyl对应数据的截距和斜率

mtcars_coef <- ddply(mtcars, .(cyl), linear_fit)names(mtcars_coef)[2:3] <- c("intercept", "slope")

所得拟合直线的截距和斜率为

 cylinterceptslope1439.57120-5.6470252628.40884-2.7801063823.86803-2.192438

再结合这原图,把这些直线画出来,与原图做个比较。


mtcars_with_linear_model.jpeg

黑色的线为拟合的曲线,而彩色短线为系统所绘制的拟合曲线,说明我们的方法正确。

再来看看上面的拟合过程,将对每个子数据集的拟合封装成一个函数linear_fit,这样做没有问题,但是使得代码的可读性比较差,一种比价优雅的方式是在dlply的第三个参数处,直接放上lm函数,将额外的参数赋给第四个参数。

mtcars_model <- dlply(mtcars, .(cyl), lm,                       formula = mpg ~ wt)mtcars_coef <- ldply(mtcars_model, coef)names(mtcars_coef)[2:3] <- c("intercept", "slope")

注意,这里通过dlply()函数调用拟合函数lm,而把具体的拟合形式formula = mpg ~ wt赋值给第四个参数。dlply()函数返回的是listlist的每个元素是一个lm的返回结果,通过ldply()调用coef获得每个模型对应的系数,记得到上述结果。

读入多个文件中的数据,并合并
下面来看看一个实际生活中的问题:

假设文件夹下有若干.csv文件, 每个文件的数据格式相同,且含有表头,如何将多个文件合并成一个文件呢?

如果没有表头的话,操作起来比较容易,可以直接用命令行工具实现,比如在linux下可以cat *.csv > total.csv实现文件合并。 此处给出一种使用plyr包中提供的ldaply的函数,实现上出操作,效率不一定是最高的,但可以进一步掌握plyr包的特性。

可以继续使用上述使用的baby_names数据集,使用如下命令, 将baby_names按年份写到不同的csv文件中。

d_ply(baby_names, .(year),       function(baby) write.csv(baby, paste0(baby$year[1], ".csv"), row.names = FALSE))

上述命令将在当前文件夹下,产生129个csv文件,从1880 ~ 2008, 每年一个文件,以年份命名。

使用如下的命令将

files <- list.files(pattern = "^\\d+\\.csv")baby_names_recovered <- ldply(files, read.csv, stringsAsFactors = FALSE)

上述命令将129个文件名存储在files变量中,通过ldply,读取每个文件,并最后通过ldply合并成一个data.frame。需要说明的是ldply的第一个参数要求list,但是files变量却是vector,这个没有影响,函数内部会将第一个参数通过as.list()转换成list

现在需要验证读入的baby_names_recovered与原始的baby_names一致,使用如下参数可以做相应的比较。

identical(arrange(baby_names, year, name, sex), arrange(baby_names_recovered, year, name, sex))# TRUE

返回的结果是TRUE,即二者其实是一致的。至于为什么要用arrange函数对数据做一下排列,是因重新生成的baby_names_recovered,其读入数据的顺序并没有严格按照年份进行。

这里抛出一个问题,如果不使用plyr包,如何实现上述操作。
提示:查阅lapplydo.call函数,剩下的函数,已经在上面的示例中讲解。
部分其他函数
这一部分将简略介绍plyr 包中未提及的函数,以及其用法。

未完待续

0 0