分享

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

 张可卿 2022-08-16 发布于安徽

关于数据操作的另一个流行的包是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./web/packages/dplyr/vignettes/introduction.html),并且访问DataCamp上的交互式教程(https://www./courses/dplyr-data-manipulation-r-tutorial)。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多