tidyverse介绍
tidyverse
tidyverse 是由 Hadley Wickham 及其团队引入的 R 编程语言的一系列开源软件包 , 这些软件包共享整洁数据 (tidy data) 的底层设计理念. tidyverse 系列包的特征是广泛利用管道操作以及 R 的非标准求值.
简而言之, tidyverse 提供了数据分析的一整套流程, 包括
- 数据读取
- 数据整理
- 数据变换
- 可视化
此外, tidyverse 的风格也影响了一大批 R 包:
要使用 tidyverse 中的各个包, 只需 library(tidyverse)
magrittr
magrittr 是 tidyverse 管道操作的基础包, 提供了多种管道符, 最常用的是 %>%. 管道符允许将前一步的结果作为下一个函数的第一个参数, 极大提升了代码的可读性.
常用管道符 (已被 tidyverse 引入):
%>%:将左侧作为右侧函数的第一个参数%T>%:将左侧传递给右侧函数但返回左侧值, 常用于插入中间操作 (如 plot, print 这类依赖副作用的函数)
此外, magrittr 还提供了两个实用的管道符:
%$%:将左侧包含的 names 暴露给右侧%<>%:在%>%的基础上, 将结果赋值回左侧
magrittr 总是将左侧变量传递给右侧函数作为第一个参数, 但有时我们需要传给其他参数. 此时可以使用 . 代表左侧变量:
tibble
tibble 是对 R 基础数据框的重构, 仅保留了多年来被证明实用的特性, 由 tibble 包提供.
有许多种方式创建一个 tibble:
使用
tibble直接创建使用
as_tibble从现有的 data.frame 创建 (支持的更多数据结构见文档)
此外, 还有 tribble, as_tibble_row, as_tibble_col, enframe 等函数可创建 tibble.
readr: 数据读取
readr 提供了一系列风格统一的函数以读取各种文件并转换为 tibble.
read_csv(): 读取逗号分隔文件read_tsv(): 读取制表符分隔文件read_delim(): 读取任意分隔符的文本文件read_table(): 读取空白字符分隔的表格- …
其中, read_csv() 和 read_tsv() 可以看作 read_delim() 的特例.
readr 会自动推断列的类型, 但也允许手动修正.
另外常用的参数还包括
col_names: 是否把第一行当作列名skip: 跳过文件开头若干行n_max: 限制最大读取行数
另一个常见的需求是读取 excel 文件. readxl 包中的 read_excel() 实现了这个功能.
此外, 还存在与 read_*() 对应的 write_*() 函数族, 用于将 tibble 保存至各种格式的文件.
tidyr: 整洁数据
Hadley Wickham 将 tidy data 定义为满足如下条件的数据格式:
- Each variable is a column; each column is a variable.
- Each observation is a row; each row is an observation.
- Each value is a cell; each cell is a single value.
这种结构很接近我们熟悉的 \(n \times p\) 矩阵. 典型的 tidy data 如
| student_id | subject | score |
|---|---|---|
| 1 | math | 85 |
| 1 | english | 90 |
| 2 | math | 78 |
| 2 | english | 88 |
| 3 | math | 90 |
| 3 | english | 95 |
tidyr 提供了一系列函数, 用于将不同形式的数据转换为 tidy data.
pivot_longer
最常见的非 tidy 数据是宽表, 即分类变量成为列名.
注意其中每个 cell 中的 count 唯一对应一个 religion 和一个 income. 此时可以使用 pivot_longer() 将宽表转化为长表, 成为 tidy data:
pivot_wider
有时长表中的列仅作为不同变量的 indicator 而没有实际意义, 此时应将其包含的每个变量作为一列.
此时可以使用 pivot_wider() 将宽表转化为长表, 成为 tidy data:
separate_longer_delim
有时候同一个 cell 中以字符串形式存储了同一变量的多个值:
此时可以使用 separate_longer_delim() 将 cell 含多个值的一行拆分成 cell 含一个值的多行, 成为 tidy data:
separate_wider_delim
有时候同一个 cell 中以字符串形式存储了多个变量的值:
此时可以使用 separate_wider_delim() 将含多个变量的一列拆分成含一个变量的多列, 成为 tidy data:
数据清洗
此外, tidyr 还包含一些数据清洗的函数:
complete(): 补全所有可能的组合drop_na(): 删除含缺失值的行replace_na(): 用指定值替换缺失值fill(): 用前/后非缺失值填充缺失值
dplyr: 数据变换
dplyr 是 tidyverse 的核心包. 其提供了一组风格一致的动词, 以完成数据处理中所有常见操作.
- 行操作
slice(): 行切片filter(): 行筛选distinct(): 行去重arrange(): 行排序
- 列操作
select(): 选择列mutate(): 修改/新增列rename(): 修改列名relocate(): 列排序
- 分组计算:
group_by(),summarise() - 合并数据框:
bind_rows(),bind_cols(),*_join()
行操作
slice
slice() 根据索引向量对行进行切片.
filter
filter() 根据条件过滤出符合要求的行, 参数是作为条件的逻辑向量 (通常由某些列向量运算得到).
distinct
distinct() 去除重复行, 参数是作为去重依据的向量.
arrange
arrange() 对行排序, 参数是作为排序依据的向量.
列操作
select
select() 用于提取 tibble 的列子集. 其接受一个 <tidy-select> 参数
mutate
mutate() 用于修改列/新增列/删除列. 注意
mutate()会返回一个新的 tibble 而不是原地修改mutate()不会改变 tibble 的行数, 即只能新建/修改列为等长向量
rename
rename() 用于重命名 tibble 的列. 其语法为 rename(new_name = old_name).
rename_with() 用于批量修改 tibble 的列名. 它对选定的列名应用同一个函数以生成新的列名.
relocate
relocate() 用于调整 tibble 中列的位置. 可以将指定的列移动到表头/表尾/某一列的前后.
分组操作
group_by() 用于将 tibble 按一个或多个变量分组, 通常与 summarise() 搭配使用, 实现分组统计.
group_by
group_by() 用于按指定变量对 tibble 分组, 返回被分组的 tibble.
被分组的 tibble 不会立即改变, 但 mutate, filter, arrange 等函数作用于其上时会自动对每个组分别执行操作.
分组进行某个操作可以理解为:
- 将原 tibble 按照分组变量的值分割成多个行数较小的 tibble
- 对每个小 tibble 执行某个操作
- 将各个小 tibble 在行方向合并, 得到的结果形状与原 tibble 一致
使用 ungroup() 可以取消分组:
summarise
对于已使用 group_by 分组的 tibble, 使用 summarise() 可进行分组聚合. 使用 summarise() 后 tibble 默认将退出分组状态 (可由参数 .groups 控制).
与之前的分组计算不同, 使用 summarise() 的分组聚合会改变结果的行数. summarise() 要求每组的计算结果必须是长度为 1 的向量或行数为 1 的数据框.
reframe
使用 group_by + mutate/summarise 要求每组的计算结果为等长向量/标量, 这对分组计算造成了很大的限制.
比如, 如果我们希望计算每组的多个分位数, 此时每组计算结果的行数取决于分位数的数量.
此时我们可以使用 reframe(), 其允许每组的计算结果
注意: 这里我们在单个 verb 中使用了 .by 参数. 这与 group_by 的效果一致. 事实上, 可以与 group_by 联动的许多操作都支持直接传入 .by 参数, 无需显式 group_by (如 mutate, filter, summarise).
合并数据框
bind_rows / bind_cols
bind_rows(): 按行拼接多个数据框 (要求列名一致或自动补齐缺失列)bind_cols(): 按列拼接多个数据框 (要求行数一致)
left_join / right_join
left_join(x, y, by = ...): 将x与y按.by列匹配, 保留x的行但可能丢弃y的行left_join(x, y, by = ...): 将x与y按.by列匹配, 保留y的行但可能丢弃x的行full_join(x, y, by = ...): 将x与y按.by列匹配, 保留x和y中所有的行, 必要时用缺失值填充结果
<tidy-select> 语法
tidyverse 中常常出现参数需要提供列名子集 (通常是 cols 参数) 的情况. tidyselect 包提供了一套 DSL (domain specific language) 以快速构建 tibble 中特定的列名子集.
我们创建一个 tibble 用作演示:
按位置/列名构建
c(col1, col2, col3): 选择特定列 (col*可以是数字/列名)start:end: 选择从start到end的所有列 (start和end可以是数字/列名)everything(): 选择所有列last_col(): 选择最后一列
按字符向量构建
可以用已存在的字符向量选择列
all_of(vars): 严格匹配, 选择vars中的所有列any_of(vars): 宽松匹配, 选择vars中存在的列
正则匹配构建
starts_with("match"): 匹配以 “match” 开头的列名ends_with("match"): 匹配以 “match” 结尾的列名contains("match"): 匹配包含 “match” 的字符串的列名matches("match"): 匹配符合正则表达式的列名num_range("prefix", range): 匹配带有数字后缀的列名
条件匹配构建
使用 where() 结合返回 bool 的函数 (如 is.numeric, is.character, is.logical 等) 可以按条件选择列
结合多个 selection
可以像操作逻辑运算多个 selection
-/!: 取反&: 交集|: 并集c(): combine 多个 selection 对应的列
<data-masking> 参数
tidyverse 中的许多参数被标记为 <data-masking>, 这代表该参数位置我们输入的表达式中的所有 symbol 会在 tibble 环境中求值, 而不是在全局环境中求值, 即 tibble 中的变量 “mask” 了全局环境中的变量.
通常, <data-masking> 参数收到的值是一个向量, 如刚刚的例子. 但有时这个参数上需要传入多个列的组合, 如 distinct() 中可能需要根据多列的值去重. 为此, 存在两个函数以在 <data-masking> 参数中以 <tidy-select> 语法选择多个列, 返回一个 tibble.
purrr
purrr 提供的 map_* 族函数可以对某个向量的所有元素应用同一个函数, 并得到结果.
使用 for 循环往往是需要对某个向量执行同样的操作. 由于 R 中一切操作都是函数调用, 因此 purrr 可以完全替代 for 循环.
purrr 包含:
map(.x, .f, ...): 对.x的每个元素应用函数.f, 返回列表map_lgl(.x, .f, ...): 返回逻辑向量map_int(.x, .f, ...): 返回整数向量map_dbl(.x, .f, ...): 返回数值向量map_chr(.x, .f, ...): 返回字符向量map_dfr(.x, .f, ...): 返回按行拼接的数据框map_dfc(.x, .f, ...): 返回按列拼接的数据框
furrr: 并行计算
使用 purrr 的好处除了提高代码可读性以外, 还在于其可以无缝切换到并行计算.
furrr 基于 future 提供的并行计算后端, 实现了一系列 future_map_* 函数, 与 purrr 中 map_* 函数的用法几乎一致.
stringr
stringr 是 tidyverse 中用于字符串处理的包, 提供了一套一致、易用的字符串操作函数。常用函数包括:
str_sub(): 提取/替换字符串的子串.str_glue(): 使用花括号{}在字符串中内嵌变量str_extract(),str_extract_all(): 提取匹配的子串str_replace(),str_replace_all(): 替换匹配的子串str_split(): 按分隔符拆分字符串- …
forcats
forcats 提供了各种对因子的操作. 之所以出现这个包是因为 base R 中对因子的常见需求操作起来十分繁杂.
lubridate
lubridate 的产生原因与 forcats 类似, 主要处理日期类数据.
Cheatsheets
tidyverse 的回顾与反思
tidyverse 将数据操作拆分成很多动词, 以符合自然语言习惯与其设计哲学. 但这也导致了学习成本的增加. 例如, 行筛选其实就是基于逻辑向量对行索引, 行排序就是基于 order 向量对行索引, 但 tidyverse 却拆分成了 slice()/filter()/arrange() 三个动词.