美文网首页
12.4 使用dplyr管道操作处理数据框

12.4 使用dplyr管道操作处理数据框

作者: 11的雾 | 来源:发表于2019-12-26 12:44 被阅读0次

    关于数据操作的另一个流行的包是dplyr,它发明了一种数据操作语法。dplyr扩展包并没有使用构建子集函数([ ]),而是定义了一系列基础的变形函数作为数据操作模块,并且引入了一个管道操作符,利用管道操作符将这些变形函数串联起来,进而完成复杂的多步任务。

    如果还没有安装dplyr,请运行以下代码以从CRAN中安装:

        install.packages("dplyr")
    

    首先,我们重新加载产品表格,将它们重置为原始形式:

    library(readr)
    product_info <- read_csv("data/product-info.csv")      
    product_stats <- read_csv("data/product-stats.csv")      
    product_tests <- read_csv("data/product-tests.csv")      
    toy_tests <- read_csv("data/product-toy-tests.csv")
    

    然后,载入dplyr包:

    library(dplyr)
    ##
    ## Attaching package: 'dplyr'
    ## The following objects are masked from 'package:data.table':
    #
    ##    between, last
    ## The following objects are masked from 'package:stats':
    ##
     ##    filter, lag
     ## The following objects are masked from 'package:base':
            ##
            ##    intersect, setdiff, setequal, union
    

    以上输出信息说明dplyr泛化了很多内置函数。加载这个包之后,这些内置函数便被屏蔽了。

    现在,我们可以使用dplyr包提供的变形函数了。先使用select( ) 函数从数据框中提取列,并将这些列存储在新创建的表中:

     select(product_info, id, name, type, class)
     ## Source:local data frame[6x4]
     ##
     ##      id      name  type  class
     ##  <chr>    <chr> <chr>  <chr>
     ## 1  T01    SupCar  toy vehicle
     ## 2  T02  SupPlane  toy vehicle
     ## 3  M01    JeepX model vehicle
     ## 4  M02 AircraftX model vehicle
     ## 5  M03    Runner model  people
     ## 6  M04    Dancer model  people
    

    打印出来的表格与data.frame和data.table都不太一样。它不仅显示了表格本身,也包括一个表头,用于说明数据框的大小和每一列的数据类型。

    显然,select( ) 使用了非标准计算,所以我们可以直接将数据框的列名作为参数。它和subset( )、transform( ) 以及with( ) 的工作方式类似。

    其次,我们可以使用filter( ) 函数,通过逻辑条件筛选数据框。同样地,这个函数也是在数据框的语义中被计算:

    filter(product_info, released == "yes")
    ## Source:local data frame[4x5]
    ##
    ##      id      name  type  class released
    ##  <chr>    <chr> <chr>  <chr>    <chr>
    ## 1  T01    SupCar  toy vehicle      yes
    ## 2  M01    JeepX model vehicle      yes
    ## 3  M02 AircraftX model vehicle      yes
    ## 4  M03    Runner model  people      yes
    

    如果想要根据多个条件筛选记录,只需要把每个条件都作为filter() 的参数:

    filter(product_info,
    released == "yes", type == "model")
    ## Source:local data frame[3x5]
    ##
    ##      id      name  type  class released
    ##  <chr>    <chr> <chr>  <chr>    <chr>
    ## 1  M01    JeepX model vehicle      yes
    ## 2  M02 AircraftX model vehicle      yes
    ## 3  M03    Runner model  people      yes
    

    mutate( )函数可以创建一个新的数据框,这个数据框包含新列,或者替换原数据框的列。它与transform( )类似,不同的是,如果数据是data.table,它也能支持原地赋值:=:

    mutate(product_stats, density = size / weight)
    ## Source: local data frame [6 x 5]
    ##
    ##      id material  size weight  density
    ##  <chr>    <chr> <int>  <dbl>    <dbl>
    ## 1  T01    Metal  120  10.0 12.000000
    ## 2  T02    Metal  350  45.0  7.777778
    ## 3  M01 Plastics    50    NA        NA
    ## 4  M02 Plastics    85    3.0 28.333333
    ## 5  M03    Wood    15    NA        NA
    ## 6  M04    Wood    16    0.6 26.666667
    

    arrange( ) 函数也是用于创建一个新的数据框,这个数据框是按一个或多个列排序后的。desc( ) 函数表示降序排列:

    arrange(product_stats, material, desc(size), desc(weight))
    ## Source: local data frame [6 x 4]
    ##
    ##      id material  size weight
    ##  <chr>    <chr> <int>  <dbl>
    ## 1  T02    Metal  350  45.0
    ## 2  T01    Metal  120  10.0
    ## 3  M02 Plastics    85    3.0
    ## 4  M01 Plastics    50    NA
    ## 5  M04    Wood    16    0.6
    ## 6  M03    Wood    15    NA
    

    dplyr包提供了丰富的连接函数,包括inner_join( )、left_join( )、right_join( )、full_join( )、semi_join( ) 和anti_join( )。如果要连接的两个表存在无法匹配的记录,这些连接操作的行为会有很大差别。对于product_info和product_tests,它们的记录可以完全匹配,所以left_join( ) 的返回结果和merge( ) 相同:

    product_info_tests <- left_join(product_info, product_tests, by = "id")
    product_info_tests
    ## Source: local data frame [6 x 8]
    ##
    ##    id    name  type  class released quality durability
    ##  <chr>  <chr>  <chr>  <chr>    <chr>  <int>      <int>
    ## 1 T01    SupCar  toy vehicle      yes      NA        10
    ## 2 T02  SupPlane  toy vehicle      no      10          9
    ## 3 M01    JeepX model vehicle      yes      6          4
    ## 4 M02 AircraftX model vehicle      yes      6          5
    ## 5 M03    Runner model  people      yes      5        NA
    ## 6 M04    Dancer model  people      no      6          6
    ## Variables not shown: waterproof (chr)
    

    运行?dplyr::join了解这些连接操作的更多差异。

    为了对数据进行分组汇总,我们需要先利用group_by( ) 创建一个分组后的表格。然后使用summarize( ) 汇总数据。例如,我们想把product_info_tests按照type和class分割开,然后对每一组计算quality和durability的平均值:

    summarize(group_by(product_info_tests, type, class),
              mean_quality = mean(quality, na.rm = TRUE),
              mean_durability = mean(durability, na.rm = TRUE))
    
    ## Source: local data frame [3 x 4]
    ## Groups: type [? ]
    ##
    ##    type  class mean_quality mean_durability
    ##  <chr>  <chr>        <dbl>          <dbl>
    ## 1 model  people          5.5            6.0
    ## 2 model vehicle          6.0            4.5
    ## 3  toy vehicle        10.0            9.5
    

    通过前面的代码示例,我们掌握了这些变形函数: select( )、filter( )、mutate( )、arrange( )、group_by( ) 和summarize( )。这些函数的设计初衷都是对数据进行一个小操作,但是将它们合理地组合到一起,就可以完成复杂的数据处理操作。除了这些函数,dplyr包还从magrittr包中引入了管道操作符 %>%,利用 %>% 将函数连接起来,组合使用。

    假设现在有product_info和product_tests。我们需要对已发布的产品进行分析,对于每种类型和类对应的组,计算该组产品的质量和耐久性的平均值,并将结果数据按照质量均值降序排列。通过使用管道操作符将dplyr变形函数连接起来,可以漂亮地完成这个任务:

    product_info %>% filter(released == "yes") %>%
              inner_join(product_tests, by = "id") %>%
              group_by(type, class) %>%
              summarize(
                mean_quality = mean(quality, na.rm = TRUE),
                mean_durability = mean(durability, na.rm = TRUE)) %>%
              arrange(desc(mean_quality))
    
    ## Source: local data frame [3 x 4]
    ## Groups: type [2]
    ##
    ##    type  class mean_quality mean_durability
    ##  <chr>  <chr>        <dbl>          <dbl>
    ## 1 model vehicle            6            4.5
    ## 2 model  people            5            NaN
    ## 3  toy vehicle          NaN            10.0
    

    但是 %>% 是如何工作的呢?其实,管道操作符基本上只负责一件事情:把符号左侧返回的结果,作为符号右侧调用函数的第1个参数。也就是说,x %>% f(...) 等价于f(x, ...)。因为 %>% 是一个由包定义的二元操作符,所以允许我们将函数调用连接起来,一方面避免存储多余的中间值,另一方面将嵌套调用分解,使每一步操作流程清晰地展现出来。

    假设将d0转化为d3需要3个步骤。在每一步的函数调用中,需要将前面一步的结果作为参数。如果像这样操作数据,可能会有很多中间结果,当数据量很大的时候,会消耗很多内存:

    d1 <- f1(d0, arg1)
    d2 <- f2(d1, arg2)
    d3 <- f3(d2, arg3)
    

    想要避免中间结果,就不得不写嵌套调用。这个任务看起来一点都不友好,特别是在每个函数调用都有多个参数的时候:

    f3(f2(f1(d0, arg1), arg2), arg3)
    

    使用管道操作符,工作流便可以像下面这样重新组织:

    d0 %>% f1(arg1) %>% f2(arg2) %>% f3(arg3)
    

    这样的代码看起来更加简洁和直观。整个表达式不止看起来像一个管道,其工作方式也像一个管道。d0 %>% f1(arg1) 等价于f1(d0, arg1),并会被送往f2(., arg2),紧接着又会被送往f3(., arg3)。每一步的输出结果都会成为下一步的输入。

    而且,管道操作符不止在dplyr的函数中生效,对其他所有的函数也都是适用的。假设我们想要对钻石价格画一个密度图,如图所示。

    data(diamonds, package = "ggplot2")
    plot(density(diamonds$price, from = 0),
         main = "Density plot of diamond prices")
    
    image

    使用管道操作符,我们可以像这样重写代码:

    diamonds$price %>%
              density(from = 0) %>%
              plot(main = "Density plot of diamonds prices")
    

    与data.table类似,dplyr也提供了do( ) 函数来对每组数据进行任意操作。例如,将diamonds按cut分组,每组都按log(price) ~ carat拟合一个线性模型。和data.table不同的是,我们需要为操作指定一个名称,以便将结果储存到列中。而且,do( ) 中的表达式不能直接在分组数据的语义下计算,我们需要使用.来表示数据:

    models <- diamonds %>%
              group_by(cut) %>%
              do(lmod = lm(log(price) ~ carat, data = .))
    models
    ## Source: local data frame [5 x 2]
    ## Groups: <by row>
    ##
    ##        cut    lmod
    ##      <fctr>  <chr>
    ## 1      Fair <S3: lm>
    ## 2      Good <S3: lm>
    ## 3 Very Good <S3: lm>
    ## 4  Premium <S3: lm>
    ## 5    Ideal <S3: lm>
    

    注意到一个新列lmod被创建了。这不是一个典型的原子向量列,而是一个包含了线性回归对象的列表,也就是说,每一个cut的值对应的模型会以列表的形式储存在lmod列的对应位置中。我们可以使用索引来获得每个模型:

    models$lmod[[1]]
    ##
    ## Call:
    ## lm(formula = log(price) ~ carat, data = .)
    ##
    ## Coefficients:
    ## (Intercept)        carat
    ##      6.785        1.251
    

    在需要完成高度定制的操作时,do( ) 函数的优势就更加明显了。举个例子,假如我们需要分析toy_tests数据,要对每种产品的质量和耐久性进行汇总。如果只需要样本数最多的3个测试记录,并且每个产品的质量和耐久性是经样本数加权的平均数,考虑下我们应该做什么。

    使用dplyr包的函数和管道操作符,上述任务可以通过以下代码轻松完成:

    toy_tests %>%
          group_by(id) %>%
          arrange(desc(sample)) %>%
          do(head(., 3)) %>%
          summarize(
             quality = sum(quality *  sample) / sum(sample),
             durability = sum(durability *  sample) / sum(sample))
    ## Source:local data frame[2x3]
    ##
    ##      id  quality durability
    ##  <chr>    <dbl>      <dbl>
    ## 1  T01 9.319149  9.382979
    ## 2  T02 9.040000  8.340000
    

    注意到,当数据分组后,所有的后续操作都是按组进行的。为了查看中间结果,我们可以运行do(head(., 3)) 之前的代码,如下所示:

    toy_tests %>%
        group_by(id) %>%
        arrange(desc(sample))
    
    ## Source: local data frame [8 x 5]
    ## Groups: id [2]
    ##
    ##      id    date sample quality durability
    ##  <chr>    <int>  <int>  <int>      <int>
    ## 1  T0120160405    180      9        10
    ## 2  T0120160302    150      10          9
    ## 3  T0120160502    140      9          9
    ## 4  T0120160201    100      9          9
    ## 5  T0220160403    90      9          8
    ## 6  T0220160502    85      10          9
    ## 7  T0220160303    75      8          8
    ## 8  T0220160201    70      7          9
    

    这样我们就得到了按样本数降序排列的所有记录。然后,do(head(., 3)) 将会对每一个组计算head(. 3),其中,.表示每组数据:

    toy_tests %>%
        group_by(id) %>%
        arrange(desc(sample)) %>%
        do(head(., 3))
    ## Source: local data frame [6 x 5]
    ## Groups: id [2]
    ##
    ##      id    date sample quality durability
    ##  <chr>    <int>  <int>  <int>      <int>
    ## 1  T0120160405    180      9        10
    ## 2  T0120160302    150      10          9
    ## 3  T0120160502    140      9          9
    ## 4  T0220160403    90      9          8
    ## 5  T0220160502    85      10          9
    ## 6  T0220160303    75      8          8
    

    现在,我们得到了每一组的样本数最多的3条记录,如此汇总数据是很方便的。

    dplyr函数定义了一种非常直观的数据操作语法,并且提供了便于使用管道操作符的高性能变形函数。更多内容,请阅读包的指南(https://cran.rstudio.com/web/packages/dplyr/vignettes/introduction.html),并且访问DataCamp上的交互式教程(https://www.datacamp.com/courses/dplyr-data-manipulation-r-tutorial)。

    相关文章

      网友评论

          本文标题:12.4 使用dplyr管道操作处理数据框

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