关于数据操作的另一个流行的包是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)。
网友评论