# 数据处理之-dplyr {#Data:Manipulation-dplyr}
本章节主要目的是通过阐述dplyr动词用法,实现与`Excel透视表`或`sql`相同功能,从而达到不同的数据整理、聚合需求。
本章主要从以下方面阐述:
1. 行条件筛选
2. 列筛选
3. 字段重命名
4. 列位置排序
5. 行排序
6. 新增计算字段
7. 分组聚合
8. 表关联
9. 行列操作
10. 使用dplyr编写自定义函数
其中9,10行列操作和自定义函数有一定难度,大家可以先熟悉dplyr基本用法后再了解其用法。
与`sql`相比,用dplyr的优势:
- 代码量极大减少
- 逻辑复杂时,dplyr动词可以按照顺序一步步实现,无需嵌套,实现过程简单
- 代码可读性好
- 配合`dbplyr`包使用,大部分情况下可以扔掉`sql`语法,从而实现不同数据库间语法并不完全一致时,代码可重复使用
> 本章节中部分案例照搬dplyr包的官方案例,
dplyr动词从数据库相关操作中抽象而来,从sql迁移成本低
## 前言 {#dplyr:description}
`dplyr`包是`tidyverse`系列的核心包之一。dplyr是**A Grammar of Data Manipulation **,即dplyr是数据处理的语法。数据操作在数据库中往往被增、改、删、查四字描述,加上表连接查询基本涵盖了大部分的数据操作。
`dplyr`包通过提供一组动词来解决最常见的数据处理问题:
- `mutate()` 添加新变量,现有变量的函数
- `select()` 筛选列,根据现有变量名称选择变量
- `filter()` 筛选行,根据条件筛选
- `summarise()` 按照一定条件汇总聚合
- `arrange()` 行排序
以上动词都可以和`group_by()`结合,使我们可以按组执行以上任何操作。除了以上单个表操作的动词,dplyr中还有操作两表(表关联)的动词,可以通过`vignette("two-table")`查看学习。
### 安装 {#dplyr:install-package}
dplyr包可以直接安装。
```{r eval=FALSE}
## 最简单是的方式就是安装tidyverse
install.packages('tidyverse')
## 或者仅仅安装 tidyr:
install.packages('dplyr')
## 或者从github 安装开发版本
## install.packages("devtools")
devtools::install_github("tidyverse/dplyr")
```
> 在开始使用前,请确保自己dplyr版本是较新版本,因为1.0.0版本有较大更新。
```{r}
packageVersion('dplyr')
```
### Excel and Sql 类比{#dplyr:difference-of-sql}
与Excel相比,dplyr使用`filter`实现筛选,`mutate`实现列新增计算,`summarise`配合`group_by`实现数据透视表,`arrange`实现排序功能。
`dplyr::left_join()`等表连接功能,实现Excel中的`vlookup`,`xlookup`等函数效果。
请看案例:
>案例中使用的数据集是R包`nycflights13`带的flights数据集。
Excel实现
![透视表截图](./picture/data-table/01picture.png)
R实现:
```{r message=FALSE,warning=FALSE}
library(tidyverse,warn.conflicts = FALSE)
data %
filter(year==2014,month==6) %>%
group_by(origin,dest) %>%
summarise(distance求和项 = sum(distance))
```
Sql实现:
```{r include=FALSE}
library(DBI)
data
### 常见问题
1. 筛选订单表中的1-5月订单数据,按照城市汇总,求每个城市的销售额和门店数(去重)?
```{r eval=FALSE}
data %>%
filter(between(月,1,5)) %>%
group_by(城市) %>%
summarise(金额 = sum(金额),门店数 = n_distinct(门店编码))
```
2. 近30天商品销量排名?
```{r eval=FALSE}
data %>%
filter(订单日期 >= Sys.Date()-30) %>%
group_by(分析大类,商品编码) %>%
summarise(商品销量 = sum(数量)) %>%
group_by(分析大类) %>%
mutate(商品排名 = dense_rank(desc(商品销量)))
# 注意用desc倒序,销量高排第一
```
3. 销售和库存形成笛卡尔积表,计算商品有货率、动销率?
**Cheat Sheet**
手册搬运于dplyr[官方介绍](https://dplyr.tidyverse.org/),方便下载查阅。
![dplyr-sheet](./picture/dplyr/data-transformation.pdf){width=100% height=400}
Rstudio提供的其它手册:
## 基础用法 {#dplyr:usage}
基础用法部分,我们将从行筛选,重命名、列位置调整、新增计算列、排序、分组聚合几个方面阐述`dplyr`动词功能。
首先加载包,加载包时可能会有一些重名函数的提示,可以通过warn.conflict参数禁掉提示。如下所示:
```{r}
# 禁掉提示
library(dplyr,warn.conflicts = FALSE)
```
### filter{#dplyr-filter}
`filter`动词顾名思义即筛选功能,按照一定条件筛选data.frame;与Excel中的筛选功能和`SQL`中`where`条件一致。
filter条件筛选中可以分为单条件筛选和多条件筛选;多条件中间用`,`分隔。
- 单条件
条件为` species == "Droid" `时,如下所示:
```{r}
starwars %>%
filter(species == "Droid")
```
```{sql eval = FALSE}
select * from starwars where species = "Droid" -- 注意=与==的区别
```
- 多条件
多条件筛选时,用英文逗号隔开多个条件。sql中用“and”连接多个条件,与 R 中用逗号隔开效果相同,“and” 在 R 中用 & 表示。
```{r}
starwars %>%
filter(species == "Droid",skin_color == "gold")
# 同上
# starwars %>%
# filter(species == "Droid" & skin_color == "gold")
```
```{sql eval =FALSE}
select * from starwars where species = "Droid" and skin_color = "gold"
```
- 多情况筛选
`%in%`类似`SQL`中 `in` 的用法,与Excel中筛选条件时"或"条件相当。
```{r}
starwars %>%
filter(species %in% c("Droid",'Clawdite'))
```
```{sql eval = FALSE}
select * from starwars where species in ("Droid","Clawdite") --sql查询
```
- 逻辑关系筛选
条件运算分为逻辑运算、关系运算。
关系运算符 >、=、<=分别代表大于、小于、等于、不等于、大于等于、小于等于。
逻辑运算符 &、|、!。 `|`为 或, `&` 为并、且条件,`!`为非。
```{r}
library(nycflights13)
filter(flights, !(arr_delay > 120 | dep_delay > 120))
filter(flights, arr_delay <= 120, dep_delay <= 120)
# same above
filter(flights, arr_delay <= 120 & dep_delay <= 120)
# %in% 的反面
starwars %>%
filter(!species %in% c("Droid",'Clawdite'))
```
> !的运算级别相比 %in% 更高
### select {#dplyr-select}
当完整数据集列较多时,我们某次分析可能并不需要那么多列,通过动词`select()`筛选列,剔除不需要的列。
- 基础用法
通过指定列名称筛选,并指定列之间顺序
```{r}
starwars %>%
select(name,height,mass,hair_color,skin_color,eye_color)
```
- 列索引
通过列名或数字向量索引,但是不建议用数字索引,避免原始数据列顺序变化后导致报错。
```{r}
starwars %>%
select(name : eye_color)
# 同上
starwars %>%
select(1:6)
# starwars %>% select(c(1,2,4,5,7))
```
- 新增列筛选方式
```{r}
# starwars %>% select(!(name:mass))
# iris %>% select(!ends_with("Width"))
# iris %>% select(starts_with("Petal") & ends_with("Width"))
# iris %>% select(starts_with("Petal") | ends_with("Width"))
```
### rename {#dplyr-rename}
列重命名使用`rename()`函数,新名称写前面,如下所示:
```{r rename}
starwars %>% rename(home_world = homeworld)
# 多列同换
starwars %>% rename(home_world = homeworld,skincolor = skin_color)
```
```{sql eval = FALSE}
select * ,homeworld as home_word from starwars
select * ,homeworld home_word from starwars
```
> as 可以省略,但中间有一个以上空格。与R的差异是新增home_word列,原始列继续存在,R中是替换列名。
有时我们需要批量修改列名,改如何实现?
```{r eval=FALSE}
names(starwars) % relocate(sex:homeworld, .before = height)
```
### mutate {#dplyr-mutate}
动词`mutate`可以新增计算列,删除列,更新已有列,列之间的计算都可以通过mutate实现。
- 新增计算列
```{r}
starwars %>%
mutate(bmi = mass / ((height / 100) ^ 2)) %>%
select(name:mass,bmi)
```
- 新增计算列基础上新增列
```{r}
starwars %>%
mutate(bmi = mass / ((height / 100) ^ 2),newbmi = bmi *2) %>%
select(name:mass,bmi,newbmi)
```
- 删除列
```{r}
starwars %>% mutate(height = NULL)
```
### arrange {#dplyr-arrange}
- 单列排序,默认升序,通过`desc()`降序排列
```{r}
starwars %>%
arrange(desc(mass))
```
- 多列排序
```{r}
starwars %>%
arrange(height,desc(mass))
```
```{sql eval=FALSE}
select * from starwars order by height,mass desc
```
### summarise {#dplyr-summarise}
`summarise`常与`group_by`结合使用。
```{r}
mtcars %>%
summarise(mean = mean(disp), n = n())
```
> n()是dplyr包中的计算当前组大小的函数,用在summarise()和mutate()中。通常用来组计算。
### group_by {#dplyr-groupby}
聚合前一般都需要分组,`group_by()`动词实现该功能,与`SQL`中`group by ···`类似。
```{r}
starwars %>%
group_by(species) %>%
summarise(
n = n(),
mass = mean(mass, na.rm = TRUE)
)
```
```{sql eval=FALSE}
SELECT species,
count(species) n,
AVG(mass) mass
FROM [spb].[dbo].[starwars]
GROUP BY species
```
## 常用函数 {#dplyr-functions}
本小节介绍dplyr处理数据时常用的函数,类似sql中的case_when、count、distinct、row_number、between等函数。
### 条件判断
dplyr::`if_else()`相比于`base::ifelse`,`if_else`输出类型更为严格,无论`TRUE`或`FALSE`输出类型一致,这样计算速度更快。
dplyr::`if_else()`参数:
```{r dplyr-ifelse ,eval=FALSE}
if_else(condition, true, false, missing = NULL)
```
```{r}
x 5,"大于五","小于等于五",missing = "空值")
```
与`ifelse`不同的是,`if_else`保留类型
```{r dplyr-ifelse-1}
x 与`data.table::fifelse()`功能相似
### case_when
当条件嵌套较多时,使用`case_when`,使代码可读并且不易出错。与sql 中的case when 等价。
```{r}
x %
mutate(性别 = case_when(
sex == "male" ~ "雄性",
sex == "female" ~ "雌性",
sex == "hermaphroditic" ~ "雌雄同体",
TRUE ~ "无"
)) %>%
pull(性别) %>%
table()
```
> pull()功能与 data$.类似,为了在管道中使用设计
### 计数函数
- 计数
`count()`函数用来计数。下面两种表达方式等价。
```{r dplyr-count,eval =FALSE}
df %>% count(a, b)
# same above
df %>% group_by(a, b) %>% summarise(n = n())
```
```{r dplyr-count-1}
starwars %>% count(species)
# same above 等价
starwars %>% group_by(species) %>% summarise(n = n())
```
- 非重复计数
`n_distinct()`与` length(unique(x))`等价,但是更快更简洁。当我们需要给门店或订单之类数据去重计算时采用该函数。
```{r}
x %
group_by(商品大类) %>%
slice_head(n = 5)
```
#### 函数用法
```{r eval=FALSE}
slice(.data, ..., .preserve = FALSE)
slice_head(.data, ..., n, prop)
slice_tail(.data, ..., n, prop)
slice_min(.data, order_by, ..., n, prop, with_ties = TRUE)
slice_max(.data, order_by, ..., n, prop, with_ties = TRUE)
slice_sample(.data, ..., n, prop, weight_by = NULL, replace = FALSE)
```
#### 参数解释
.data: 一个 data.frame , data frame extension (tibble)
...: 必须是整数,如果是正数将保留,负值将删除。提供的值超过行数的将会被忽略
.preserve :默认为FALSE,根据结果数据重新计算分组结构,否则保持原样分组
n,p: 提供要选择的行数 n,或行的比列比如 prop = 0.4,如果都未提供则默认n = 1
order_by: 要排序的变量或者是被函数作用的变量
with_ties: 针对 _min 或 _max 函数,相等时候是否强制输出指定行数。
weight_by: 抽样的权重
replace: 是否允许重复抽样,默认为FALSE
#### slice 案例
- slice()
```{r}
# 筛选第一行
mtcars %>% slice(1L)
# 筛选最后一行
mtcars %>% slice(n())
# 筛选5至最后一行
mtcars %>% slice(5:n())
# 删除前面四行
slice(mtcars, -(1:4))
```
- slice_head slice_tail
```{r}
# 基于现有顺序筛选前面行或最后行
mtcars %>% slice_head(n = 5)
mtcars %>% slice_tail(n = 5)
```
- slice_min slice_max
```{r}
# 基于变量筛选
mtcars %>% slice_min(mpg, n = 5) #最小的五行
mtcars %>% slice_max(mpg, n = 5) #最大的五行
# slice_min()可能返回更多行,通过with_ties参数控制
mtcars %>% slice_min(cyl, n = 1)
mtcars %>% slice_min(cyl, n = 1, with_ties = FALSE)
```
- slice_sample
在数据集中抽样,通过 replace 参数控制是否可以重复。
```{r}
mtcars %>% slice_sample(n = 5)
mtcars %>% slice_sample(n = 5, replace = TRUE)
```
weight_by 参数调整抽样权重
```{r}
# 重量大的会更容易抽到
mtcars %>% slice_sample(weight_by = wt, n = 5)
```
- 分组操作
```{r}
df % group_by(group) %>% slice_head(n = 2)
# 注意体会使用prop参数时的差异
df %>% group_by(group) %>% slice_head(prop = 0.4) #仅c组返回一个,因为c组3个数字的 40%的前面还有数
df %>% group_by(group) %>% slice_head(prop = 0.5) # 因为a组只有一个数字,前50%位没有数字
```
### group 系列
group 系列函数包含 group_by(),group_map(), group_nest(), group_split(), group_trim()等。
其中我常用group_by(),group_split()两个函数。group_by()是我们熟悉的动词,大部分数据操作中的分组操作由它完成。
- group_by()
```{r}
#group_by()不会改变数据框
by_cyl % group_by(cyl)
by_cyl
# It changes how it acts with the other dplyr verbs:
by_cyl %>% summarise(
disp = mean(disp),
hp = mean(hp)
)
# group_by中可以添加计算字段 即mutate操作
mtcars %>% group_by(vsam = vs + am) %>%
group_vars()
```
- group_split()
目前该函数是实验性的,group_split() 功能与 base::split()相似,按照数据集的分组变量切割数据集。如下,将 iris 数据集按照 Species 分组并切割成长度为3的列表,每个子元素是 Species 变量的子数据集。Species 变量包含`setosa`、`versicolor`、` virginica`三种情况。
```{r}
ir %
group_by(Species)
group_split(ir)
group_keys(ir)
```
- group_map()
group_map,group_modify,group_walk等三个函数是purrr类具有迭代风格的函数。简单关系数据的数据清洗一般不涉及,常用在建模等方面。
但是目前函数是实验性的,未来可能会发生变化。
```{r}
# return a list
# 返回列表
mtcars %>%
group_by(cyl) %>%
group_map(~ head(.x, 2L))
```
```{r}
iris %>%
group_by(Species) %>%
group_modify(~ {
.x %>%
purrr::map_dfc(fivenum) %>%
mutate(nms = c("min", "Q1", "median", "Q3", "max"))
})
```
分组后批量输出
```{r eval=FALSE}
# group_walk
dir.create(temp %
group_by(Species) %>%
group_walk(~ write.csv(.x, file = file.path(temp, paste0(.y$Species, ".csv"))))
list.files(temp, pattern = "csv$")
unlink(temp, recursive = TRUE)
```
- group_cols()
选择分组变量
```{r}
gdf % group_by(Species)
gdf %>% select(group_cols())
```
### 其它函数
有针对性学习函数可以有效提高学习效率,先浏览一遍 dplyr 中函数,再挑选觉得对自己有用的函数学习即可。
- between
```{r}
between(1:12, 7, 9)
```
- pull
```{r}
mtcars %>% pull(-1)
mtcars %>% pull(cyl)
```
- distinct
```{r eval=FALSE}
df 该函数在我们计算下单间隔天数时比较简洁方便
如需了解更多,可以通过学习[`slider`](https://github.com/DavisVaughan/slider)包知道更多"滑动窗口函数"。
- cummean cumsum cumall cumany
累计系列函数
```{r}
x %
mutate(a = coalesce(a,0))
```
## 表关联 {#dplyr:merge-two-table}
在我们做数据分析项目时,大多数时候数据源不止一个,我们需要通过表间共有信息字段将表关联,让表信息更加全面,从而方便做各种信息分类汇总。
dplyr 中表关联是通过一组函数实现,像`sql`中的`left join`,`inner join`等表格之间的操作,或者是Excel中`Power Piovt`建模的建立关系,从而实现不同表格间的关联。
### 两表关联 {#dplyr:merge-table-usage}
dplyr 中`left_join()`,`full_join`,`inner_join()`等动词关联两个表,详情请查看:`vignette("two-table")`。
`left_join()`,`right_join()`,`full_join()`,`inner_join`(),第一个以左表为主,第二个右表为主,第三个全连接,第四个内连接(只返回两表中都有的记录),和数据库中用法一致。
> `left_join()`实现类似 Excel中`VLOOKUP`函数功能,将匹配到的"右表"字段依据对应关系关联到“左表”上。
#### 基础用法
dplyr中的关联函数的关联条件可以分为:
- 同名列关联
- 不同名称列关联
##### 同名列关联
默认同名列关联,也可指定关联条件列,类似数据库中`on a.column = b.column `,但 R 代码看起来更加"优雅"。
1. 单个关联条件
```{r dplyr-32}
library("nycflights13")
flights2 % select(year:day, hour, origin, dest, tailnum, carrier)
# 默认同名
flights2 %>% left_join(airlines)
```
在某些时候,不同的表间可能出现同名的字段,但是并不能作为关联字段,这是就需要人为指定关联字段,并且建议在写代码的时候明确指定关联字段,能避免一些可能不必要的错误。
```{r}
# 或指定列名
flights2 %>% left_join(planes, by = "tailnum")
```
像 Sql 中指定关联字段,以下 Sql 代码和上面 R 代码 等效。
```{sql dplyr-33, eval=FALSE}
select * from flights2 a left join planes b on a.tailnum = b.tailnum
```
2. 多个关联条件
```{r}
dta % left_join(airports, by = c("dest" = "faa"))
# flights2 %>% left_join(airports, c("origin" = "faa"))
# 多条件关联时用逗号隔开
dta %
select(-id.x,-id.y)
```
##### 不唯一关联
如果关联表中值不是唯一的,连接将添加匹配表的所有可能组合(笛卡尔积):
右表重复
```{r}
df1 % left_join(df2)
df1 %>% right_join(df2)
```
两表重复
```{r}
df1 % left_join(df2)
```
在我们工作中可能需要求两个表之间的笛卡尔乘积[^笛卡尔乘积],并且没有可关联字段,该如何求?
[^笛卡尔乘积]:笛卡尔乘积是指在数学中,两个集合X和Y的笛卡尔积(Cartesian product),又称直积,表示为X × Y,第一个对象是X的成员而第二个对象是Y的所有可能有序对的其中一个成员
```{r}
dta %
glimpse()
```
以上是简单的构造笛卡尔积表的办法,我们还可以通过构建相同列达到效果,如下所示:
```{r}
dta %>%
mutate(col = 1) %>%
left_join(dtb %>% mutate(col=1)) %>%
mutate(col = NULL) %>%
glimpse()
```
#### 筛选关联
在某些时候筛选关联会比较方便,但是我个人比较少用。
`anti_join()`:删除所有左表中在右表中匹配到的行
`semi_join()`:保留所有左表在右表中匹配到的行
```{r dplyr-34}
df1 % anti_join(df1)
df1 %>% semi_join(df2)
```
#### 集合操作
以下动词要求x,y具有相同的变量,也就是x,y的格式一样。
1. `intersect(x,y)`返回x,y交集
2. `union(x,y)`返回x,y中唯一的值
3. `setdiff(x,y)`返回存在x中但是不存在y中的记录
4. `union_all(x,y)`返回全部值,不删除重复值
```{r dplyr-35}
(df1 %
left_join(dt2) %>%
left_join(dt3) %>%
left_join(dt4) %>%
left_join(dt5)
```
- 改进写法
当需要合并多个表时,可用`purrr::reduce()`实现
```{r}
dtlist
### 添加分组
```{r dplyr-groupby-1}
by_species % group_by(species)
by_sex_gender % group_by(sex, gender)
```
在添加分组后,print()打印时可以看到分组:
```{r}
by_species
```
计算每个分组的行数,可以通过`sort`参数控制排序方式。
```{r}
by_species %>% tally()
by_sex_gender %>% tally(sort = TRUE)
```
> 在数据探索时比较有用。
除了按照现有变量分组外,还可以按照函数处理后的变量分组,等效在`mutate()`之后执行`group_by`:
```{r dplyr-groupby-2}
bmi_breaks %
group_by(bmi_cat = cut(mass/(height/100)^2, breaks=bmi_breaks)) %>%
tally()
```
> group_by()可通过在group_by()计算产生的新字段分组
### 查看分组
使用`group_keys()`查看数据的分组,每个组一行,每个分组变量占一列:
```{r}
by_species %>% group_keys()
by_sex_gender %>% group_keys()
```
也可以使用命令`group_indices()`查看每行属于哪个组:
```{r}
by_species %>% group_indices()
```
该特性方便增加组别列。
```{r}
df %
group_by(a) %>%
mutate(组别列 = group_indices() )
```
上述用法在dplyr 1.0.0 中弃用,用`cur_group_id()`代替,如下所示:
```{r}
df %>%
group_by(a) %>%
mutate(组别列 = cur_group_id())
```
> dplyr 在我看来 API 变化较快,所以我个人习惯使用 data.table 包处理数据。所以在学习的时候用最新的版本学习。
`group_rows()`每个组包含哪些行:
```{r}
by_species %>% group_rows() %>% head()
```
`group_vars()`返回分组变量的名称,请使用:
```{r}
by_species %>% group_vars()
by_sex_gender %>% group_vars()
```
### 更改和添加分组变量
如果将`group_by()`应用已经分组的数据集,将覆盖现有的分组变量。例如,下面的分组变量是`homeworld`而不是之前的`species`。
```{r}
by_species %>%
group_by(homeworld) %>%
tally()
```
要增加分组变量,使用`.add=TRUE`参数即可。例如:
```{r}
by_species %>%
group_by(homeworld,.add = TRUE) %>%
tally()
```
### 删除分组变量
要删除所有分组变量,使用`ungroup()`:
```{r dplyr-groupby-3}
by_species %>%
ungroup() %>%
tally()
```
还可以通过列出要删除的变量来有选择的删除分组变量:
```{r}
by_sex_gender %>%
ungroup(sex) %>%
tally()
```
### 动词影响
`group_by()`是如何影响 dplyr 的主要动词。
- summarise
`summarise()` 计算每个组的汇总,表示从`group_keys()`开始,在右侧添加`summarise()`的汇总变量。
```{r dplyr-groupby-4}
by_species %>%
summarise(
n = n(),
height = mean(height, na.rm = TRUE)
)
```
该`.groups=`参数控制输出的分组结构。删除右侧分组变量的历史行为对应于`.groups =` "drop_last"没有提示消息,或.groups = NULL有消息(默认值)。
```{r}
by_sex_gender %>%
summarise(n = n()) %>%
group_vars()
by_sex_gender %>%
summarise(n = n(),.groups = 'drop_last') %>%
group_vars()
```
从1.0.0版开始,分组信息可以保留`(.groups = "keep")`或删除 `(.groups = 'drop)`
```{r dplyr-groupby-5}
a %
summarise(
n = n(),
height = mean(height, na.rm = TRUE),.groups='drop')
b %
summarise(
n = n(),
height = mean(height, na.rm = TRUE),.groups='keep')
object.size(a)
object.size(b)
```
以上,可以看到保留分组信息的比没保留的对象大了两倍多,在实际使用中,当数据较大时会占据更多内存,所以我们需要根据实际情况决定是否保留分组信息,我在大部分时候都会删除分组信息。
- arrange
默认情况下,分组和不分组的数据集应用在在`arrange()`效果相同。除非设置`.by_group = TRUE`,这时首先按照分组变量排序。
```{r}
df %
group_by(a)
df %>%
arrange(desc(b))
df %>%
arrange(desc(b),.by_group = TRUE)
```
实际上就是,默认情况下`arrange()`会忽略分组变量,除非通过设置参数`.by_group`参数。
- mutate and transmute
根据 mutate 中函数不同,返回值视情况而定。
```{r}
df %
mutate( d = mean(b))
res2 %
group_by(a) %>%
mutate(d = mean(b))
# 分组后返回不同值,此时的 mutate 相当于汇总函数
identical(res1,res2)
```
- filter
filter 受 group_by 影响。
```{r}
df %>%
group_by(a) %>%
filter(b == max(b))
```
以上代码本质是先执行 `mutate(max(b))`,再filter,最后只保留 TRUE 所在行。如下所示:
```{r}
df %>%
group_by(a) %>%
mutate(max_num = max(b)) %>%
filter(b==max_num) %>%
select(-max_num)
```
想想以下代码的含义?
```{r}
df %>%
group_by(a) %>%
filter(n()!=1)
```
~~删除只有一行记录的数据行~~
- slice 系列
选择每个分组变量的第一个观测值。
```{r}
by_species %>%
relocate(species) %>%
slice(1)
```
同样,我同样可以使用`slice_min()`来选择 变量的最大值。
```{r}
by_species %>%
filter(!is.na(height)) %>%
slice_max(height,n=3)
```
## 列操作 {#dplyr-column-manipulation}
在多列上执行相同的函数是常有的操作,但是通过复制和粘贴代码,麻烦并且容易错,如下所示:
```{r eval=FALSE}
df %>%
group_by(g1, g2) %>%
summarise(a = mean(a), b = mean(b), c = mean(c), d = mean(d))
```
通过`across()`函数可以更简洁地重写上面代码:
```{r eval=FALSE}
df %>%
group_by(g1, g2) %>%
summarise(across(a:d, mean))
```
假设我们要将表格中多列向上取整,代码如下:
```{r}
dt %
mutate(across(a:d,ceiling))
```
函数`across()`通过与`summarise()`和`mutate()`结合,很容易将某函数运用到多列上。函数`across()`取代了`summarise_all()`,`summarise_at()`,`summarise_if()`函数。
```{r}
starwars %>%
summarise_at(c("height", "mass"), mean, na.rm = TRUE)
starwars %>% summarise(across(c("height", "mass"), ~ mean(.x, na.rm = TRUE))) # purrr风格函数
```
### 基础用法 {#dplyr-column-manipulation-basic-usage}
across() 有两个主要参数:
- 第一个参数,.cols 选择要操作的列。它使用`tidyr`的方式选择(例如select()),因此您可以按位置,名称和类型选择变量。
- 第二个参数,.fns是要应用于每一列的一个函数或函数列表。也可以是 purrr 样式的公式(或公式列表),例如~ .x / 2。
```{r}
starwars %>%
summarise(across(where(is.character), ~ length(unique(.x))))
starwars %>%
group_by(species) %>%
filter(n() > 1) %>%
summarise(across(c(sex, gender, homeworld), ~ length(unique(.x))))
starwars %>%
group_by(homeworld) %>%
filter(n() > 1) %>%
summarise(across(where(is.numeric), ~ mean(.x, na.rm = TRUE)))
```
> ~ .x / 2是purrr包提供的函数式编程风格,等效于 function(x) (x/2)
`across()` 不会选择分组变量,如下所示:`group_by()`中的变量`g`不会被选中执行`sum()`函数。
```{r}
df %
group_by(g) %>%
summarise(across(where(is.numeric), sum))
```
### 多种函数功能
通过在第二个参数提供函数或 lambda 函数的命名列表,可是使用多个函数转换每个变量:
```{r}
min_max % summarise(across(where(is.numeric), min_max))
```
通过`.names`参数控制名称:
Note: 该参数的机制没有特别理解,需多练习体会,主要是运用到匿名函数时
以下是官方案例,但是报错(目前已修复):
```{r }
starwars %>% summarise(across(where(is.numeric), min_max, .names = "{.fn}.{.col}"))
```
修改后正常运行:
```{r}
starwars %>% summarise(across(where(is.numeric), min_max, .names = "{fn}.{col}"))
```
区别主要是`.names`参数的使用方式问题,`.`加不加的问题。
```{r }
starwars %>% summarise(across(where(is.numeric), min_max, .names = "{fn}——{col}"))
```
### 当前列
如果需要,可以通过调用访问内部的“当前”列的名称`cur_column()`,仅在`across()`使用。和 Excel 中power pivot的"上下文"概念类似。
该函数不是特别容易理解,需要多尝试使用加深认识。
```{r}
df % mutate(across(all_of(names(mult)), ~ .x * mult[[cur_column()]]))
```
代码解释:代码实现的是数据框 df 中列和 mult 中同名元素相乘得到新列。`mult[[cur_column()]]`依次返回mult[["x"]],mult[["y"]],mult[["z"]]。
以上部分是关于列操作的内容,详情查看`vignette("colwise")`。
## 行操作{#dplyr-row-manipulation}
行操作指不同字段间的计算,如`Excle`的列与列之间计算,`Excle`中的函数对行列不敏感,没有明显区别,但是`R`中`tidyverse`里列计算简单,行间计算依赖`rowwise()`函数实现。
![Excel-sum](picture/dplyr/dplyr-row-manipulation.png)
```{r}
df % rowwise()
df %>% rowwise() %>% mutate(total = sum(c(x, y, z))) #返回结果与Excel一致
df %>% mutate(total = sum(c(x, y, z))) # 返回结果不符合预期
```
通过自己定义函数实现:
```{r}
fun1 % mutate(total = fun1(x,y,z))
```
### 比较差异
像`group_by()`,`rowwise()`并没有做任何事情,它的作用是改变其他动词的工作方式。
注意以下代码返回结果不同:
```{r}
df %>% mutate(m = mean(c(x, y, z)))
df %>% rowwise() %>% mutate(m = mean(c(x, y, z)))
```
`df %>% mutate(m = mean(c(x, y, z)))`返回的结果是x,y,z散列全部数据的均值;`df %>% rowwise() %>% mutate(m = mean(c(x, y, z)))`通过rowwise改变了mean的作为范围,返回的某行x,y,z列3个数字的均值。两种动词的作用的范围因为rowwise完全改变。
可以选择在调用中提供“标识符”变量`rowwise()`。这些变量在您调用时被保留`summarise()`,因此它们的行为与传递给的分组变量有些相似`group_by()`:
```{r}
df %
rowwise() %>%
summarise(m = mean(c(x, y, z)))
df %>%
rowwise(name) %>%
summarise(m = mean(c(x, y, z)))
```
### 常用案例
```{r}
df % rowwise() %>% mutate(m = mean(c(x, y, z)))
# Compute the minimum of x and y in each row
df %>% rowwise() %>% mutate(m = min(c(x, y, z)))
# In this case you can use an existing vectorised function:
df %>% mutate(m = pmin(x, y, z))
```
键入每个变量名称很繁琐,通过`c_across()`使更简单。
```{r eval=FALSE}
df % rowwise(id)
rf %>% mutate(total = sum(c_across(w:z)))
rf %>% mutate(total = sum(c_across(where(is.numeric))))
rf %>%
mutate(total = sum(c_across(w:z))) %>%
ungroup() %>%
mutate(across(w:z, ~ . / total))
```
有关更多信息请查看 vignette("rowwise")。
## `dplyr`编程{#dplyr-programming}
[Programming with dplyr](https://cloud.r-project.org/web/packages/dplyr/vignettes/programming.html)
本小节概念性东西较多且复杂不易理解,先尝试会使用,概念再慢慢消化理解。虽然复杂,但是比较实用,尤其是当我们需要定义一些通用功能函数时。以下是对原文引用。
两种情况:
- When you have the data-variable in a function argument (i.e. an env-variable that holds a promise2), you need to ** embrace ** the argument by surrounding it in doubled braces, like `filter(df, {{ var }})`.
The following function uses embracing to create a wrapper around `summarise()` that computes the minimum and maximum values of a variable, as well as the number of observations that were summarised:
```{r eval=FALSE}
var_summary %
summarise(n = n(), min = min({{ var }}), max = max({{ var }}))
}
mtcars %>%
group_by(cyl) %>%
var_summary(mpg)
```
- When you have an env-variable that is a character vector, you need to index into the .data pronoun with [[, like summarise(df, mean = mean(.data[[var]])).
The following example uses .data to count the number of unique values in each variable of mtcars:
```{r eval=FALSE}
for (var in names(mtcars)) {
mtcars %>% count(.data[[var]]) %>% print()
}
```
Note that .data is not a data frame; it’s a special construct, a pronoun, that allows you to access the current variables either directly, with `.data$x` or indirectly with ` .data[[var]]`. Don’t expect other functions to work with it.
### 案例
当我们不知道接下来会用哪个变量汇总时:
```{r}
my_summarise %
group_by({{ group_var }}) %>%
summarise(mean = mean(mass))
}
```
如果在多个位置使用:
```{r}
my_summarise2 % summarise(
mean = mean({{ expr }}),
sum = sum({{ expr }}),
n = n()
)
}
```
当多个表达式时:
```{r}
my_summarise3 %
summarise(mean = mean({{ mean_var }}), sd = mean({{ sd_var }}))
}
```
如果要输出变量名时:
```{r}
my_summarise4 % summarise(
"mean_{{expr}}" := mean({{ expr }}),
"sum_{{expr}}" := sum({{ expr }}),
"n_{{expr}}" := n()
)
}
my_summarise5 %
summarise(
"mean_{{mean_var}}" := mean({{ mean_var }}),
"sd_{{sd_var}}" := mean({{ sd_var }})
)
}
```
任意个表达式,这种使用场景更多。
```{r}
my_summarise %
group_by(...) %>%
summarise(mass = mean(mass, na.rm = TRUE), height = mean(height, na.rm = TRUE))
}
starwars %>% my_summarise(homeworld)
starwars %>% my_summarise(sex, gender)
```
本小节做为拓展学习部分,建议完全掌握基础动词的用法后再学习,尤其是在有相关需求的时候再研究效果更好,甚至大部分的商业数据分析师并不需要掌握使用"dplyr 编程",可以直接跳过不学习本小节。
## 参考资料 {#dplyr:reference-material}
关于使用 dbplyr 包转换 dplyr 动词为 SQL 语句实现数据查询,请参考[R 包 dbplyr 学习](https://www.yuque.com/docs/share/2d5a9224-4be2-4043-9645-924892246bb2?# 《R包 dbplyr 学习》)。
1. dplyr 项目地址
2. dplyr programming
3. dplyr programming
4. dbplyr 包学习