R语言dplyr包
前言
2014年刚到, 就在 Feedly 订阅里看到 RStudio Blog 介绍 dplyr 包已发布 (Introducing dplyr), 此包将原本 plyr 包中的 ddply() 等函数进一步分离强化, 专注接受dataframe对象, 大幅提高了速度, 并且提供了更稳健的与其它数据库对象间的接口。 既然是 Hadley Wickham 的新作, 并自称 a grammar of data manipulation,当然要先学为快了, 正好新申了域名, 就把原本记在 Rmd 里的笔记组织一下,放在这里,算是个简短的教程吧,仅供入门。
正文: 学习笔记
以下内容主要参照 Introducing dplyr 和 dplyr 包自带的简介 ( Introduction to dplyr ), 复制了原文对应代码, 并夹杂了个人理解和观点 (多附于括号内)。
目录
0. 初始化
1. 基本操作
2. 分组动作 group_by()
3. 连接符 %.%
4. 实用代码汇总
5. 感想
6. 深入学习
参考资料
0. 初始化
0.1 安装
install.packages("dplyr")
0.2 示范数据
library(Lahman): Lahman 包里的棒球比赛数据集 Batting
library(hflights): hflights 包里的飞机航班数据
0.3 数据集类型
将过长过大的数据集转换为显示更友好的 tbl_df 类型:
hflights _df <- tbl _df(hflights)
可以 hflights_df 感受一下不再被刷屏的感觉。
### code
install.packages("dplyr")
install.packages("Lahman")
install.packages("hflights")
library(Lahman)
library(hflights)
library(dplyr)
hflights_df <- tbl_df(hflights)
head(hflights)
class(hflights)
head(hflights_df)
class(hflights_df)
1. 基本操作
把常用的数据操作行为归纳为以下五种(Five basic verbs): filter, select, arrange, mutate, summarise (plus group_by)
窗口函数(Window functions): min _ rank, top_n, lag
实用函数(Convenience functions): sample _ n, sample_frac, glimpse, Count
连接数据库操作(Connecting to databases)
1.1 筛选: filter()
按给定的逻辑判断筛选出符合要求的子数据集, 类似于 base::subset() 函数,例如:
filter(hflights_df, Month == 1, DayofMonth == 1)
用R自带函数实现:
hflights[hflights$Month == 1 & hflights$DayofMonth == 1, ]
除了代码简洁外, 还支持对同一对象的任意个条件组合, 如:
filter(hflights_df, Month == 1 | Month == 2) # 或 filter(hflights_df, Month %in% c(1, 2))
注意:这里需要提醒的是,对于多条件的选择,需要完整条件的,然后使用集合运算符将条件拼接起来。集合运算符有 !、 |、 &、 xor(交补)。(表示 AND 时要使用 & 而避免 &&)条件的判断符有>(=)、 <(=)、 ==、 !=、 %in% (判断元素是否在集合或者列表内,返回逻辑值)。
# 错误写法 filter(hflights_df, Month == 1 | 2)
1.2 排列:arrange()
按给定的列名依次对行进行排序,例如:
arrange(hflights_df, DayofMonth, Month, Year)
对列名加 desc() 进行倒序:
arrange(hflights_df, desc(ArrDelay))
这个函数和 plyr::arrange() 是一样的,类似于order()。 用R自带函数实现:
hflights[order(hflights$DayofMonth, hflights$Month, hflights$Year), ] hflights[order(desc(hflights$ArrDelay)), ]
1.3 选择:select() || plus contains, starts_with, ends_with, matches
用列名作参数来选择子数据集:
select(hflights_df, Year, Month, DayOfWeek)
还可以用 : 来连接列名, 没错,就是把列名当作数字一样使用:
select(hflights_df, Year:DayOfWeek)
用 - 来排除列名:
select(hflights_df, -(Year:DayOfWeek))
同样类似于R自带的 subset() 函数 (但不用再写一长串的 c("colname1", "colname2") 或者 which(colname(data) == "colname3"),甚至还要去查找列号)
1.4 变形:mutate()
对已有列进行数据运算并添加为新列:
mutate(hflights_df, gain = ArrDelay - DepDelay, speed = Distance / AirTime * 60 )
作用与 plyr::mutate() 相同,与 base::transform() 相似,优势在于可以在同一语句中对刚增加的列进行操作:
mutate(hflights_df, gain = ArrDelay - DepDelay, gain_per_hour = gain / (AirTime / 60) )
而同样操作用R自带函数 transform() 的话就会报错:
# 错误写法 transform(hflights, gain = ArrDelay - DepDelay, gain_per_hour = gain / (AirTime / 60) ) # 正确写法(原因:gain是中间变量) transform(hflights, gain = ArrDelay - DepDelay, gain_per_hour = (ArrDelay - DepDelay) / (AirTime / 60) )
1.5 汇总:summarise() || plus group_by, summarise_each, n, n_distinct, tally
对数据框调用其它函数进行汇总操作, 返回一维的结果:
summarise(hflights_df, delay = mean(DepDelay, na.rm = TRUE))
等同于 plyr::summarise(),原文说该函数功能尚不是非常有用, 大概以后的更新会加强吧。
1.6 抽样:sample _ n(), sample_frac()
抽样函数表示从表中随机抽取任意行数据。第一行代码:按照个数随机从数据集中选择十行,第二行代码:按照比例随机从数据集中选择10%行(15 = 153 * 10%)。
# library(datasets)
# library(dplyr)
# head(airquality)
# dim(airquality)
# 按个数抽个
sample_n(airquality, size = 10)
# 按比例抽取
sample_frac(airquality, size = 0.1)
- 1.7 计数: Count()
count
The count function tallies observations based on a group. It is slightly similar to the table function in the base package. For example:
计数函数应用场景适用于分组观测值。它稍微类似于基本函数包(base包)中的table函数。例如:
count(airquality, Month)
Month n
1 5 31
2 6 30
3 7 31
4 8 31
5 9 30
2. 分组动作 group_by()
以上5个动词函数已经很方便了, 但是当它们跟分组操作这个概念结合起来时,那才叫真正的强大! 当对数据集通过 group_by() 添加了分组信息后,mutate()、 arrange() 和 summarise() 函数会自动对这些 tbl 类数据 执行分组操作 (R语言泛型函数的优势)。
例如: 对飞机航班数据按飞机编号 (TailNum) 进行分组, 计算该飞机航班的次数 (count = n()), 平均飞行距离 (dist = mean(Distance, na.rm = TRUE)) 和 延时 (delay = mean(ArrDelay, na.rm = TRUE)) 。
planes <- group_by(hflights_df, TailNum)
delay <- summarise(planes,
count = n(),
dist = mean(Distance, na.rm = TRUE),
delay = mean(ArrDelay, na.rm = TRUE))
delay <- filter(delay, count > 20, dist < 2000)
用 ggplot2 包作个图观察一下, 发现飞机延时不延时跟飞行距离没太大相关性:
ggplot(delay, aes(dist, delay)) +
geom_point(aes(size = count), alpha = 1/2) +
geom_smooth() +
scale_size_area()
更多例子见 vignette("introduction", package = "dplyr")
另: 一些汇总时的小函数
n(): 计算个数。
n_distinct(): 计算 x 中唯一值的个数。 (原文为 count_distinct(x), 测试无用)
first(x), last(x) 和 nth(x, n): 返回对应秩的值, 类似于自带函数 x[1], x[length(x)], 和 x[n]
注意: 分组计算得到的统计量要清楚样本已经发生了变化, 此时的中位数是不可靠的
3. 连接符 %.% (Using chaining syntax for more readable code)
包里还新引进了一个操作符, 使用时把数据名作为开头, 然后依次对此数据进行多步操作。比如:
Batting %.%
group_by(playerID) %.%
summarise(total = sum(G)) %.%
arrange(desc(total)) %.%
head(5)
# 结果
playerID total
1 henderi01 2295
2 cobbty01 2246
3 bondsba01 2227
4 ruthba01 2174
5 aaronha01 2174
这样可以按进行数据处理时的思路写代码, 一步步深入, 既易写又易读,接近于从左到右的自然语言顺序,对比一下用R自带函数实现的:
head(arrange(summarise(group_by(Batting, playerID), total = sum(G)) , desc(total)), 5)
或者像这篇文章所用的方法:
totals <- aggregate(. ~ playerID, data=Batting[,c("playerID","R")], sum)
ranks <- sort.list(-totals$R)
totals[ranks[1:5],]
文章里还表示: 用他的 MacBook Air 跑 %.% 那段代码用了 0.036 秒, 跑上面这段代码则用了 0.266 秒,运算速度提升了近7倍。 (当然这只是一例,还有其它更大的数字。)
更多请 ?"%.%", 至于这个新鲜的概念会不会和 ggplot2 里的 + 连接号一样,发挥出种种奇妙的功能呢? 还是在实际使用中多体验感受吧。
Pipe(类似于管道操作)
The pipe operator in R, represented by %>% can be used to chain code together. It is very useful when you are performing several operations on data, and don’t want to save the output at each intermediate step.
For example, let’s say we want to remove all the data corresponding to Month = 5, group the data by month, and then find the mean of the temperature each month. The conventional way to write the code for this would be:
# library(datasets)
# library(dplyr)
# head(airquality)
# dim(airquality)
filteredData <- filter(airquality, Month != 5)
groupedData <- group_by(filteredData, Month)
summarise(groupedData, mean(Temp, na.rm = TRUE))
With piping, the above code can be rewritten as:
airquality %>%
filter(Month != 5) %>%
group_by(Month) %>%
summarise(mean(Temp, na.rm = TRUE))
4. 实用代码汇总
library(dplyr)
#将数据整理成的tbl_df数据(处理速度快)
iris <- tbl_df(iris)
##变量筛选select 对应select 删除-
select(iris,Sepal.Length,Sepal.Width)
select(iris,-Species)
##对数据运算并添加为新列mutate() 对应 count(a) as t1
mutate(iris,t1=Sepal.Length*2)
##计算
n(): 计算个数
n_distinct() #: 计算 x 中唯一值的个数
first(x), last(x) 和 nth(x, n)#: 返回对应秩的值, 类似于自带函数 x[1], x[length(x)], 和 x[n]
##过滤filter 对应 where
filter(iris,Sepal.Length>5,Sepal.Width<4)
filter(iris,Sepal.Length>5 & Sepal.Width<4 & (Species == "setosa" | Species == "versicolor"))
##数据排序arrange 对应 order by
arrange(iris,Sepal.Length)
arrange(iris,desc(Sepal.Length))
##汇总group_by() 分组-汇总
group_by(iris, Species)
group_by(iris,Species,Petal.Width) %>% summarise(c1=n(),c2=n_distinct(Species))
##计算summarise()
summarise(iris,c1=n(),c2=mean(Sepal.Length))
##多步操作连接符%>%
filter(iris,Sepal.Length>5,Sepal.Width<4) %>% summarise(c1=n(),c2=mean(Sepal.Length))
##抽样sample_n sample_frac
sample_n(iris,20)
##左连接 ab交集 差集
left_join(a, b, by="x1")
right_join(a, b, by="x1")
inner_join(a, b, by="x1")##保留匹配的数据
outer_join(a, b, by="x1")##保留所有数据
semi_join(a, b, by="x1") # 数据集a中能与数据集b匹配的记录
anti_join(a, b, by="x1") # 数据集a中雨数据集b不匹配的记录
intersect(x, y): x 和 y 的交集(按行)
union(x, y): x 和 y 的并集(按行)
setdiff(x, y): x 和 y 的补集 (在x中不在y中)
##列合并
bind_cols(y, z)
##行合并
bind_rows(y, z)
5. 感想
可以看到,用 dplyr 所含函数实现的代码都要简洁易读得多,说到底, R语言只是一个工具, 作为工具, 就是要拿来用的, 越称手越便利越简洁越好, 可是, 正如 Hadley Wickham 在2013年的访谈中提到的那样:
如果你用了8小时进行数据清理和数据整理,而只用了2小时进行建模,那么很明显,你希望了解如何将数据清理和整理的时间尽可能缩短。
反思之下,本人也是将大把的时间花在了对数据的反复调整上,或许是手生,当然R语言在这方面也确实有一定不足,大神又说了:
数据分析有两个瓶颈,一是我们的目标是什么,二是我们如何用计算机去实现。我现有的很多作品,如 ggplot2,plyr 和 reshape2,更关注的是如何更简单地表达你的目标,而不是如何让计算机算得更快。
这种内在的理念正是要将工具工具化,把无谓的时间减少,让精力用在真正需要考虑的地方。 正如 Vim 一样,在投入一定的学习成本后,继续用继续学,不知不觉地就能心手如一,想做什么,就已经按下去了,从而更多地思考要编辑什么,而不必纠结于光标移动选择等细节。 这其中的巧妙之处在于:实现过程要以人脑的思维运作方式为标准,让工具来适应人,以实现目的为导向,ggplot2 的图形图层语法也是如此。不管是软件也好,编程语言也好,高效的方法都是相通的,这也正是许多人努力的方向。
6. 深入学习
如欲进一步学习,可参阅:
- dplyr 包自带的60页详细文档
- 其余几个 vignettes (网页) 或 vignette(package = "dplyr") , 包含了数据库相关, 混合编程,运算性能比较,以及新的 window-functions 等内容。
- 简单看了下 vignette("window-functions", package = "dplyr") ,提供了一系列函数,扩展了原来只能返回一个数值的聚焦类函数(如sum(), mean())至返回等长度的值,变成 cumsum()和 cummean(),以及 n(),lead() 和 lag()等便捷功能。
- plyr 包的相关文档: 主页
- 还有 data.table 包也是很强大的哦,空下来可以学一学。
If this small example has whet your interest, you can learn more from the built-in vignettes. First install dplyr with install.packages("dplyr"), then run:
vignette("introduction",package = "dplyr") to learn how the main verbs of dplyr work with data frames.
vignette("databases", package = "dplyr") to learn how to work with databases from dplyr.
You can track development progress at http://github.com/hadley/dplyr, report bugs at http://github.com/hadley/dplyr/issues and get help with data manipulation challenges at https://groups.google.com/group/manipulatr. If you ask a question specifically about dplyr on StackOverflow, please tag it with dplyr and I’ll make sure to read it.