R语言的plyr包简介

作者: leengsmile | 来源:发表于2015-08-09 23:22 被阅读19620次

    R语言中的类SQL操作

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

    本文主要介绍以下内容:

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

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

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

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

    | year| name| percent| sex|
    |---------:||---------:||---------:||---------:|
    | 1880| John| 0.081541| boy|
    | 1880| William| 0.080511| boy|
    | 1880| James| 0.050057| boy|
    | 1880| Charles| 0.045167| boy|
    | 1880| George| 0.043292| boy|
    | 1880| Frank| 0.02738| boy|
    | 1880| Joseph| 0.022229| boy|
    | 1880| Thomas| 0.021401| boy|

    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    # 函数
    )
    

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

    year count
    1880 2000
    1881 2000
    1882 2000
    1883 2000
    1884 2000
    1885 2000
    1886 2000
    1887 2000
    1888 2000
    1889 2000
    1890 2000
    1891 2000
    1892 2000

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

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

    ddply()函数是plyr包中用于对data.frame结构的数据做处理的函数,其结果也是data.frameddply的参数列表如下:
    <pre>
    ddply(.data, .variables, .fun = NULL, ..., .progress = "none",
    .inform = FALSE, .drop = TRUE, .parallel = FALSE, .paropts = NULL)
    </pre>
    各部分解释如下

    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年,男孩名的排名情况

    year name percent sex rank
    2008 Jacob 0.010355 boy 1
    2008 Michael 0.009437 boy 2
    2008 Ethan 0.009301 boy 3
    2008 Joshua 0.008799 boy 4
    2008 Daniel 0.008702 boy 5
    2008 Alexander 0.008566 boy 6

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

    year name percent sex rank
    2008 Emma 0.009043 girl 1
    2008 Isabella 0.008941 girl 2
    2008 Emily 0.008377 girl 3
    2008 Madison 0.008199 girl 4
    2008 Ava 0.008198 girl 5
    2008 Olivia 0.008196 girl 6

    如何利用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的帮助文档,其函数调用方式如下:
    <pre>
    transform(_data, ...)
    </pre>
    第一参数为操作的数据,在ddply()中为按年份和性别分割后的子数据集;后面的...参数是tag = value的形式,这种tag:value将追加在数据中。

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

    x <- 1:10
    desc(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的函数,汇总起来如下

    arrary data.frame list discarded
    arrary aaply adply alply a_ply
    data.frame daply ddply dlply d_ply
    list laply ldply llply l_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") 
    
    

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

    cyl intercept slope
    1 4 39.57120 -5.647025
    2 6 28.40884 -2.780106
    3 8 23.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 包中未提及的函数,以及其用法。

    未完待续

    参考文献

    1. http://courses.had.co.nz/09-user/
    2. baby names. http://courses.had.co.nz/09-user/code-data/bnames.csv

    相关文章

      网友评论

      • f111d134efcb:请问“因重新生成的baby_names_recovered,其读入数据的顺序并没有严格按照年份进行”是什么意思?最后的identical得出的结果是false
        leengsmile:我重新运行了下,identical 语句的输出依旧是TRUE。

        另“因重新生成的baby_names_recovered,其读入数据的顺序并没有严格按照年份进行” 这句话是指,
        1. ldply 函数读入文件时,按照list.files的返回结果的顺序读取,若list.files不是按数字递增的顺序返回的结果,ldply返回的结果自然也会有所不同。在文中,list.files返回的文件按数字大小排序的,与baby_names中的顺序一致,所以看不出什么差异。
        2. 若ldply指定.parallel参数,则可以并行地读取list.files返回的内容,此时ldply的返回结果一定会与baby_names的顺序不一致。可以用如下语句测试你的数据

        require(doParallel)

        cl <- registerDoParallel(2)
        baby_names_recovered2 <- ldply(files, read.csv, stringsAsFactors = FALSE,
        .parallel = TRUE)
        identical(arrange(baby_names, year, name, sex),
        arrange(baby_names_recovered2, year, name, sex))

        identical(baby_names,
        baby_names_recovered2)

        stopImplicitCluster()


        为了进行数据比较,将数据按照一定的顺序做出排序(使用arrange)。
      • d991f7378605:求baby_names数据集……直接上传一个吧
        leengsmile:baby names的下载链接已经附在文末,也可以点击文章开始处 baby_names的超链接
        leengsmile:@网速卡卡 好吧,我找找
      • 付德刚:第三个参数,transform 和summarize一直没搞明白☹
        端盘的服务生:跟传递自定义函数一样,文中写了这两个都是base自带的函数
      • 程sir:baby_names数据集在哪里啊?
        leengsmile:@程sir 在http://courses.had.co.nz/09-user/ 下的bnames.csv
        文档也是根据这个网站改写的。
      • aristotll:不错的文章,plyr很不错

      本文标题:R语言的plyr包简介

      本文链接:https://www.haomeiwen.com/subject/ntkiqttx.html