Advertisement

R 语言入门 —— tidyverse

阅读量:

R 语言入门 —— tidyverse

tidyverse 是 R语言中的集成包家族成员之一, 该家族成员均遵循一致的设计理念、统一的语法体系及数据架构. 广为人知的数据可视化工具 ggplot2 是其中最突出代表, 它通过管道操作实现的代码结构更为直观简洁. 为了更好地理解这一机制, 请先了解什么是管道操作——在此之前, 请确保已正确安装相关组件.

我们只需一键安装 tidyverse,就可以使用其下的所有包

复制代码
    # 从 CRAN 库中安装
    install.packages("tidyverse")
    
    # 安装开发者版本
    # install.packages("devtools")
    devtools::install_github("tidyverse/tidyverse")

导入 tidyverse

复制代码
    library(tidyverse)
    # ── Attaching packages ───────────────────────────────────────── tidyverse 1.3.1 ──
    # ✓ ggplot2 3.3.5     ✓ purrr   0.3.4
    # ✓ tibble  3.1.6     ✓ dplyr   1.0.7
    # ✓ tidyr   1.1.4     ✓ stringr 1.4.0
    # ✓ readr   2.1.0     ✓ forcats 0.5.1
    # ── Conflicts ──────────────────────────────────────────── tidyverse_conflicts() ──
    # x dplyr::filter() masks stats::filter()
    # x dplyr::lag()    masks stats::lag()

默认情况下会将选中标记的包一并导入;对于未被选中的其他包,则需手动进行导入操作;在Conflicts标签下的函数名称均存在冲突;建议采用格式为[包名].[函数名称]来访问这些函数

1. 管道操作

管道操作的本质就是模拟数据流经管道的过程;也可以理解为一种链式调用方式,在每一步处理过程中只对单个数据进行操作。与流水线处理机制相似,在每一步处理中只对单个数据进行操作。这与Linux shell中的管道符功能相仿,在每一步处理中只对单个数据进行操作。该功能由magrittr提供

安装完 tidyverse 之后,便可以导入了,如果要单独安装该包也非常简单

复制代码
    install.packages("magrittr")
    # 或
    devtools::install_github("tidyverse/magrittr")

导入包

复制代码
    library(magrittr)  # version 2.0.1

magrittr 主要提供了 4 个管道操作符:

  • %>%:右向操作符,在R语言中用于将数据传递给右侧的功能或函数
  • %T>%:Tee操作符,在管道运算中偏离了主流程后继续向下运行,并不影响数据源
  • %$%:解释操作符,在嵌套管道时会将中间结果存储起来供后续使用
  • %<>%:复合赋值管道操作符,在循环或递归场景中会将处理后的结果反馈至初始赋值变量

%>% 操作符

将左边的结果传递给右边的函数的第一个参数值,这些结果可以继续向右边传递,默认情况下,默认使用 %>% 符号左侧计算得到的值作为右侧函数的第一个参数。例如以下两种方式是等价的操作。

管道操作 嵌套函数
x %>% f f(x)
x %>% f(y) f(x, y)
x %>% f %>% g %>% h h(g(f(x)))
复制代码
    x <- 3
    f <- function(x, y=1) { return(x+y) }
    x %>% f
    # [1] 4
    x %>% f(2)
    # [1] 5

使用 . 符号标识来自左侧的数据变量...其对应的代码实现相同。

管道符 函数
x %>% f(y, .) f(y, x)
x %>% f(y, z = .) f(y, z = x)
复制代码
    x %>% f(y = .,x = 5)
    # [1] 8
    x %>% f(y = 3,x = .)
    # [1] 6

在右侧表达式中可以多次使用占位符。

复制代码
    x <- matrix(1:12, nrow = 3, ncol = 4)
    f <- function(x, y, z) { return(x * y * z) }
    x %>% f(y = nrow(.), z = ncol(.))
    #      [,1] [,2] [,3] [,4]
    # [1,]   12   48   84  120
    # [2,]   24   60   96  132
    # [3,]   36   72  108  144

但是,当占位符出现在嵌套表达式中时,第一个参数不能省略

复制代码
    x %>% {f(y = nrow(.), z = ncol(.))}
    # Error in f(y = nrow(.), z = ncol(.)) : 缺少参数"x",也没有缺省值
    x %>% {f(1, y = nrow(.), z = ncol(.))}
    # [1] 12

任何以 . 开头的管道序列将会返回一个一元函数

复制代码
    f <- . %>% cos %>% sin 
    f(10)
    # [1] -0.7440231
    # 等价于
    h <- function(.) sin(cos(.)) 
    h(10)
    # [1] -0.7440231

传递到代码块

复制代码
    rnorm(10) %>% multiply_by(5) %>% add(5) %>%
    {
     print("Mean:", mean(.))
     sort(.) %>% head(5)
    }
    # [1] "Mean:"
    # [1] -0.7076385 -0.3230133  0.9298034  0.9390084  2.5007525

传递到函数

复制代码
    rnorm(10) %>% add(1) %>% `*`(10) %>% (
      function(x) {
    if (x[1] > 5) {
      x-5
    } else x
      }
    )
    # [1] -5.056051  5.214681 16.122495 22.311603 22.431362 15.828250  1.785964 -2.969273 26.003416
    # [10] 12.304749

%T>% 操作符

它与 %>% 的主要区别在于前者无法将右侧计算结果直接传递给后续操作;相反可以通过管道符号将左侧的结果沿着管道符号向后传播至后续操作中进行处理。其中 %T> 常用于生成图形、打印输出至终端或者导出文件内容,并在此之后与其他管道命令配合使用形成完整的处理流程。具体而言左侧的操作相当于进行了两次信息传输过程:一次是将数据发送给右侧指定的功能模块完成特定功能并返回中间结果;另一次则是将中间结果作为下一条管道命令的基础输入完成后续操作任务

复制代码
    a <- 5
    a %>% cos %T>% print %>% sin
    # [1] 0.2836622
    # [1] 0.2798734

%$% 操作符

通常 %$% 的左边是数据框,而右边的函数可直接使用该数据框中的变量

复制代码
    x <- matrix(1:12, nrow = 3, ncol = 4)
    data.frame(x = x[,1], y = x[,2], z = x[,3]) %$% .[which(x > 5),]
    # [1] x y z
    # <0 行> (或0-长度的row.names)
    data.frame(x = x[,1], y = x[,2], z = x[,3]) %$% .[which(y > 5),]
    #   x y z
    # 3 3 6 9

相当于

复制代码
    df <- data.frame(x = x[,1], y = x[,2], z = x[,3])
    df[which(df$y>5),]
    #   x y z
    # 3 3 6 9

%<>% 操作符

必须位于管道连接处左侧的位置,在连续进行一系列管道操作后,将最终结果直接分配给左侧的对象。

复制代码
    a <- 1:10
    print(a)
    #  [1]  1  2  3  4  5  6  7  8  9 10
    a %<>% exp %>% sqrt
    print(a)
    # [1]   1.648721   2.718282   4.481689   7.389056  12.182494  20.085537  33.115452  54.598150  90.017131
    # [10] 148.413159

a 的值变为了 sqrt(exp(a))

通用函数操作符

除了以下提到的四个主要管道符外,在此 additionally, 这些系统中的一些常用操作符函数也被赋予了直观易懂的名字,并列于下表

函数 操作符 函数 操作符
extract [(索引取值) inset [<-(索引赋值)
extract2 [[ inset2 ``[[<-
add + subtract -
multiply_by * divide_by /
use_series $(属性访问) raise_to_power ^
multiply_by_matrix %*% divide_by_int %/%
mod %% is_in %in%
and & or `` `
equals == not !
is_greater_than > is_less_than <
is_weakly_greater_than >= is_weakly_less_than <=
set_colnames colnames<- set_rownames rownames<-
set_names names<- set_class class<-
set_attributes attributes<- set_attr attr<-

在上表中可以看到,在之前介绍的一些运算符之外,默认情况下还会观察到一些特殊的操作符:其中以 [ 表示方括号索引取值为特点的基本操作符;而与之相对应的则是带有赋值功能的 [<-` `;对于类似 [[ 这样的组合符号,则代表着双层方括号的操作;最后则有专门用于属性访问的单层方括号 ``$`

复制代码
    a <- list(x = 3, y = 4, r = 10)
    a$x
    # [1] 3
    `$`(a, "x")      # 用函数获取属性值
    # [1] 3
    `$`(a, "y")      
    # [1] 4
    `[[`(a, 3)       # 用函数获取第三个值
    # [1] 10
    `[[<-`(a, 3, 8)  # 设置值第三个值
    # $x
    # [1] 3
    # 
    # $y
    # [1] 4
    # 
    # $r
    # [1] 8
    names(a)          # 获取名称
    # [1] "x" "y" "r"
    `names<-`(a, c("p", "q", "r"))  # 设置名称
    #  p  q  r 
    #  3  4 10 
    names(a) <- c("p", "q", "r")
    a
    #  p  q  r 
    #  3  4 10

我们可以看出,在采用操作符函数设置时会产生一个新的对象实例;而采用操作符的方式会对当前对象进行直接修改。正是由于这一特性,在引入上述函数别名后能够确保流程能够持续进行而不出现提前终止的情况

复制代码
    seq(10) %>% `*`(5) %>% `+`(5)
    # [1] 10 15 20 25 30 35 40 45 50 55
    seq(10) %>% multiply_by(5) %>% add(5)
    # [1] 10 15 20 25 30 35 40 45 50 55

2. 数据结构

R 语言自诞生以来已经经历了相当长的时间,在过去的 1020 年期间许多曾经被广泛采用的功能和技术也可能已经逐渐被新的工具和方法取代或替代了。尽管如此,在不破坏现有代码库的前提下希望修改 R 的基础数据结构仍然是一项极具挑战性的任务;因此大部分创新工作都集中在开发新的软件包中。

在介绍tidyverse包的基础知识时,我们会重点讲解其核心数据模型——tibble这一概念。它构成了tidyverse体系的重要组成部分,并广泛应用于各种数据处理场景。

其结构与 R 现代化的 tibble 结构相匹配,在大多数情况下,在 data\textbackslash{}frame 函数均可适用于该格式,并且反过来也是一样;两者之间能够互相转换。

首先,我们导入该包

复制代码
    library(tidyverse)
    # 或者
    library(tibble)     # version 3.1.6

构造 tibble

大部分R包专为常规的数据框对象设计,在需要将数据框转换为Tibbles时,请调用as_tibble()函数。

复制代码
    as_tibble(iris)
    # # A tibble: 150 × 5
    #    Sepal.Length Sepal.Width Petal.Length Petal.Width Species
    #           <dbl>       <dbl>        <dbl>       <dbl> <fct>  
    #  1          5.1         3.5          1.4         0.2 setosa 
    #  2          4.9         3            1.4         0.2 setosa 
    #  3          4.7         3.2          1.3         0.2 setosa 
    #  4          4.6         3.1          1.5         0.2 setosa 
    #  5          5           3.6          1.4         0.2 setosa 
    #  6          5.4         3.9          1.7         0.4 setosa 
    #  7          4.6         3.4          1.4         0.3 setosa 
    #  8          5           3.4          1.5         0.2 setosa 
    #  9          4.4         2.9          1.4         0.2 setosa 
    # 10          4.9         3.1          1.5         0.1 setosa 
    # … with 140 more rows
    # # … with 140 more rows

可以看到,在这种情况下,默认情况下对象的值不会全部显示出来,默认会展示前10行数据,并且可以看出每一列的数据类型。

通过 tibble() 函数可以从一个向量生成一个新的 tibble。该函数会自动处理长度为 1 的输入,并将其扩展至与现有数据长度一致。从而允许您引用新生成的变量。这种机制使得在处理不同长度的数据时更加灵活和高效。

复制代码
    tibble(
      a = 3:5, 
      b = 2, 
      c = 2 * a + b
    )
    # # A tibble: 3 × 3
    #       a     b     c
    #   <int> <dbl> <dbl>
    # 1     3     2     8
    # 2     4     2    10
    # 3     5     2    12

tibble 严格遵循数据结构规范,在处理过程中始终保持输入数据的原有类型(例如:字符串始终作为字符型存储),同时也不会对变量命名系统进行任何修改或引入额外的索引属性

tibble 支持使用非法的 R 变量名称作为列名。即被视为非合法名称。例如,在某些情况下变量名可能不符合命名规范或包含特殊字符(如空格),此时为了引用这些变量需用反引号将它们括起来

复制代码
    tb <- tibble(
      `+` = "plus",
      `2000` = "integer",
      `:)` = "smile", 
    )
    tb
    # # A tibble: 1 × 3
    #   `+`   `2000`  `:)` 
    #   <chr> <chr>   <chr>
    # 1 plus  integer smile
    tb$`+`
    # [1] "plus"
    tb$`:)`
    # [1] "smile"

引用非正规变量名通常需以反引号括起来。通过tribble函数可以构造tibble对象;此函数主要用于构建小型数据集并具有良好的易读性;列名通常采用公式表示法,并以~符号开始;各条目之间则以逗号分隔开;所见所得即是如此;此外,在表格顶部通常会添加一行以#号开头的注释来标识表头和数据区域;该功能对于处理小型数据集非常有用并且易于阅读;

复制代码
    tribble(
      ~a, ~b, ~c,
      #--|--|----
      "x", 3, 12.5,
      "y", 1.2, 0.123
    )
    # # A tibble: 2 × 3
    #   a         b      c
    #   <chr> <dbl>  <dbl>
    # 1 x       3   12.5  
    # 2 y       1.2  0.123
    tr <- tribble(
      ~a,  ~b,
      "x", 9:7,
      "y", -3:-1
    )
    tr
    # # A tibble: 2 × 2
    #   a     b        
    #   <chr> <list>   
    # 1 x     <int [3]>
    # 2 y     <int [3]>
    tr$b[[1]]
    # [1] 9 8 7

行名与列的转换

在处理过程中,在操作大数据时会发现一个问题:即行列命名有时会嵌入其中作为一个组成部分。为了便于操作,并非总是可行的办法是将行列名称从矩阵中分离出来,并脱离其所属的数据体系单独存在;但总是必要的功能则是通过它们快速定位并访问所需的数据内容。

tibble 提供了几个用于操作行名和列名的函数

函数 功能
has_rownames 数据中是否包含行名
remove_rownames 删除行名
rownames_to_column 将行名转换成一个列变量
rowid_to_column 将行号转换成一个列变量
column_to_rownames 将一列转换为行名
复制代码
    head(mtcars)
    #                    mpg cyl disp  hp drat    wt  qsec vs am gear carb
    # Mazda RX4         21.0   6  160 110 3.90 2.620 16.46  0  1    4    4
    # Mazda RX4 Wag     21.0   6  160 110 3.90 2.875 17.02  0  1    4    4
    # Datsun 710        22.8   4  108  93 3.85 2.320 18.61  1  1    4    1
    # Hornet 4 Drive    21.4   6  258 110 3.08 3.215 19.44  1  0    3    1
    # Hornet Sportabout 18.7   8  360 175 3.15 3.440 17.02  0  0    3    2
    # Valiant           18.1   6  225 105 2.76 3.460 20.22  1  0    3    1
    has_rownames(mtcars)
    # [1] TRUE
    remove_rownames(mtcars) %>% has_rownames()
    # [1] FALSE
    rownames_to_column(mtcars, var = "car") %>% 
      as_tibble() %>%
      `[`(1:3, 1:5)
    # # A tibble: 3 × 5
    #   car             mpg   cyl  disp    hp
    #   <chr>         <dbl> <dbl> <dbl> <dbl>
    # 1 Mazda RX4      21       6   160   110
    # 2 Mazda RX4 Wag  21       6   160   110
    # 3 Datsun 710     22.8     4   108    93
    rownames_to_column(mtcars, var = "car") %>% 
      column_to_rownames(var = "car") %>%
      `[`(1:3, 1:5)
    #                mpg cyl disp  hp drat
    # Mazda RX4     21.0   6  160 110 3.90
    # Mazda RX4 Wag 21.0   6  160 110 3.90
    # Datsun 710    22.8   4  108  93 3.85
    iris %>% head(3) %>%
      rowid_to_column()
    #   rowid Sepal.Length Sepal.Width Petal.Length Petal.Width Species
    # 1     1          5.1         3.5          1.4         0.2  setosa
    # 2     2          4.9         3.0          1.4         0.2  setosa
    # 3     3          4.7         3.2          1.3         0.2  setosa

与 data.frame 的区别

tibbledata.frame 之间区别主要在于打印方式和取子集操作。

tibble能够展示前十个记录,并且在处理不同大小的数据集时会根据屏幕尺寸自动调整各部分的位置。与常规的数据框不同,在处理大规模数据集时,默认不会覆盖整个终端界面;而R语言在这方面确实存在一些不足之处。然而,在某些情况下,默认的展示方式可能无法满足需求;幸运的是,这些选项提供了灵活的配置方式以调整输出格式。

首先明确地调用Python中的print函数以输出dataframe,并指定其行高和列宽以确保所有内容可见

复制代码
    as_tibble(iris) %>% print(n=5, width=Inf)
    # # A tibble: 150 × 5
    #   Sepal.Length Sepal.Width Petal.Length Petal.Width Species
    #          <dbl>       <dbl>        <dbl>       <dbl> <fct>  
    # 1          5.1         3.5          1.4         0.2 setosa 
    # 2          4.9         3            1.4         0.2 setosa 
    # 3          4.7         3.2          1.3         0.2 setosa 
    # 4          4.6         3.1          1.5         0.2 setosa 
    # 5          5           3.6          1.4         0.2 setosa 
    # # … with 145 more rows

还可以通过设置以下选项来控制默认的打印行为

复制代码
    # 如果多于 n 行,只打印 m 行。
    options(tibble.print_max = n, tibble.print_min = m)
    # 总是显示所有行
    options(tibble.print_min = Inf)
    # 无论屏幕的宽度如何,始终打印所有列
    options(tibble.width = Inf)

使用$[[ 操作符可以提取数据中的变量

复制代码
    df <- as_tibble(iris)
    df$Species %>% head()
    # [1] setosa setosa setosa setosa setosa setosa
    # Levels: setosa versicolor virginica
    df[['Sepal.Length']]
    # [1] 5.1 4.9 4.7 4.6 5.0 5.4
    df[[1]]
    # [1] 5.1 4.9 4.7 4.6 5.0 5.4

使用管道操作

复制代码
    df %>% .$Sepal.Width
    # [1] 3.5 3.0 3.2 3.1 3.6 3.9
    df %>% .[['Sepal.Width']]
    [1] 3.5 3.0 3.2 3.1 3.6 3.9

相较于 data.frametibble 更严格,如果你访问的列不存在,将抛出警告

与旧代码交互

部分较古老的代码无法兼容于 tibble 数据类型,在这种情况下,请通过调用 as.data.frame() 函数将这些代码转换为 data.frame 格式。无需顾虑地进行转换是可行的。

复制代码
    as.data.frame(df)
    #   Sepal.Length Sepal.Width Petal.Length Petal.Width Species
    # 1          5.1         3.5          1.4         0.2  setosa
    # 2          4.9         3.0          1.4         0.2  setosa
    # 3          4.7         3.2          1.3         0.2  setosa
    # 4          4.6         3.1          1.5         0.2  setosa
    # 5          5.0         3.6          1.4         0.2  setosa
    # 6          5.4         3.9          1.7         0.4  setosa

3. 文件读写

文件的读取操作主要依赖于readr包,在导入了tidyverse包的情况下,默认即可使用;否则可单独引入readr

复制代码
    library(readr)  # version 2.1.0

该软件包专门为处理不同格式的文件而设计,并主要提供若干功能模块。其中包含 7 个以read_开头命名的功能函数

函数 功能
read_csv 读取 csv 文件
read_csv2 读取分隔符为 ; 的文件
read_tsv 读取分隔符为 \t 的文件,tsv 文件
read_delim 读取固定分隔符的文件
read_fwf 固定宽度的文件
read_table 读取表格文件,列之间用空格隔开
read_log 读取 web 日志文件

这些函数的主要使用方法基本相同。一般情况下,在提供文件路径后即可实现读取。相比之下,在处理像 read_delim 这样的函数时,则需要特别设置分隔符参数。为了便于说明,请我们暂时专注于 read_csv 这个函数进行介绍。例如加载测试示例文件

复制代码
    mtcars <- read_csv(readr_example("mtcars.csv"))
    # Rows: 32 Columns: 11                                                                                                 
    # ── Column specification ──────────────────────────────────────────────────────────
    # Delimiter: ","
    # dbl (11): mpg, cyl, disp, hp, drat, wt, qsec, vs, am, gear, carb
    # 
    # ℹ Use `spec()` to retrieve the full column specification for this data.
    # ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.

当您调用 read_csv 函数时会输出每一列的名称及其数据类型该函数自动生成数据字段的数据类型通过查看输出结果可以看出分隔符确定为逗号 , 共有11个字段被解析为双精度数值型 double 类型你可以通过调用 spec 方法获取各字段的具体数据类型的详细信息

当然在读取文件时,也可以为每列指定类型

复制代码
    mtcars <- read_csv(
      readr_example("mtcars.csv"), 
      col_select = 1:2,     # 只读取前两列,可以是向量或 list,可以是数值索引或列名
      col_types = cols(
    mpg = col_double(),  # 解析为 double
    cyl = col_integer()  # 解析为 integer
    )
    )

甚至可以提供一个内联的字符串

复制代码
    read_csv("1,2,3
    a,b,c
    o,p,q")

这两种读取方式都是默认将第一行作为列名,也可以调整这种模式:

如果文件开头几行包含描述性数据并非所需内容,则可以通过设置参数 skip = n 来跳过前 n 行内容;亦可借助参数设置将所有以 # 开头的注释行予以删除。

复制代码
    read_csv("first line
      second line
      a,b,c
      1,2,3", skip = 2, 
      show_col_types = FALSE  # 不显示列类型信息
    )
    # # A tibble: 1 x 3
    #       a     b     c
    #   <dbl> <dbl> <dbl>
    # 1     1     2     3
    read_csv("# comment line
      a,b,c
      1,2,3", comment = "#", 
      show_col_types = FALSE
    )
    # # A tibble: 1 x 3
    #       a     b     c
    #   <dbl> <dbl> <dbl>
    # 1     1     2     3

如果您的数据文件没有预设的列标题,请在调用函数时设置参数 col_names = FALSE,并按照顺序从 X1 至 Xn 自动分配变量名

复制代码
    read_csv("1,2,3\n4,5,6", col_names = FALSE)
    # # A tibble: 2 x 3
    #      X1    X2    X3
    #   <dbl> <dbl> <dbl>
    # 1     1     2     3
    # 2     4     5     6

或者使用 col_names 参数设置列名

复制代码
    read_csv("1,2,3\n4,5,6", col_names = c("x", "y", "z"))
    # # A tibble: 2 x 3
    #       x     y     z
    #   <dbl> <dbl> <dbl>
    # 1     1     2     3
    # 2     4     5     6

另一个需要调整的选项是 na,用来标识缺省的值,即什么值会被解析为 NA

复制代码
    read_csv("1,2,3\n4,5,*", na = "*", show_col_types = FALSE)
    # # A tibble: 1 × 3                                                                                                    
    #     `1`   `2` `3`  
    #   <dbl> <dbl> <lgl>
    # 1     4     5 NA

其他函数的读取方式也是类似的,就不再做详细介绍了。

相较于 R 自带的 read.csv 等函数 readr 中的函数具有如下优势

该算法在读取速度上显著优于 R 基础函数(约十倍于之),同时在读取过程中提供一个进度表示踪,帮助追踪程序的执行进展。

其次,不会自动将字符串转换为 factor

最后,更好的移植,一些基础的 R 函数是与操作系统相关联的

解析向量

在深入研究 readr 读取文件机制之前,有必要先探讨一下那些遵循 parse_* 格式的函数。这些函数虽然在使用方法上基本一致,但它们各自的功能定位和适用场景还是有所不同。其中 na 参数则明确了哪些特定的字符串应被视为缺失值

复制代码
    parse_logical(c("TRUE", "FALSE"))
    # [1]  TRUE FALSE
    parse_integer(c("1", "2", "3"))
    # [1] 1 2 3
    parse_date(c("2022-05-04", "1949-10-01"))
    # [1] "2022-05-04" "1949-10-01"
    parse_integer(c("123", "456", "-", "789"), na = "-")
    # [1] 123 456  NA 789

如果解析失败,将会抛出一个警告

复制代码
    a <- parse_integer(c("123", "456", "abc", "789"))
    # Warning: 1 parsing failure.
    # row col               expected actual
    #   3  -- no trailing characters    abc

但是错误信息并不会包含在对象的输出中,无法解析的值会转换为 NA

复制代码
    a
    # [1] 123 456  NA 789
    # attr(,"problems")
    # # A tibble: 1 × 4
    #     row   col expected               actual
    #   <int> <int> <chr>                  <chr> 
    # 1     3    NA no trailing characters abc

当遇到多个值解析失败时,建议您使用 problems() 方法以获取详细信息。其结果仍然是一个 tibble

复制代码
    problems(a)
    # # A tibble: 1 × 4
    #     row   col expected               actual
    #   <int> <int> <chr>                  <chr> 
    # 1     3    NA no trailing characters abc

解析器主要用于处理不同类型的输入,常用的解析器如下

函数 描述
parse_logical 解析逻辑值
parse_integer 解析整数
parse_double 解释浮点数,严格型
parse_number 灵活型数字解析器
parse_character 解析字符串
parse_factor 解析因子
parse_datetimeparse_dateparse_time 解析日期

解析器

对数值的解析看起来挺简单的,但其实有几个问题需要解决:

在全球范围内的人们书写数字的方式存在显著差异,在某些国家中,默认采用点号·作为整数部分与小数部分的分隔符;而在其他一些国家,则普遍使用逗号:来进行同类数字的区分。

数字通常还带有单位符,比如 45%$10

而且较大的数字通常还包含分组字符,例如 10,000

为了处理第一个问题,可以通过调用该函数来实现;通过设置小数点字符参数的值来覆盖默认的小数点字符 .

复制代码
    parse_double("3.1415")
    # [1] 3.1415
    parse_double("3,1415", locale = locale(decimal_mark = ","))
    # [1] 3.1415

该函数被用来负责解决第二个问题。它跳过这些位于数字前后的非数字字符,并且对处理货币数值和百分比数值非常有帮助。同样也能提取隐藏在文字中的数值数据。

复制代码
    parse_number("$10")
    # [1] 10
    parse_number("45%")
    # [1] 45
    parse_number("I have $1234567890")
    # [1] 1234567890

组合 parse_numberlocale 来忽略分组标记就可以解决最后一个问题

复制代码
    parse_number("123,456,789")
    # [1] 123456789
    parse_number("123.456.789", locale = locale(grouping_mark = "."))
    # [1] 123456789
    parse_number("123'456'789", locale = locale(grouping_mark = "'"))
    # [1] 123456789

通过调用 parse_character 这一函数来解析字符串可能显得异常直接——它能够立即返回原始输入数据。然而事实并非看上去那么简单——由于一个字符串可能存在多种编码形式,在不同环境下可能会导致不同的结果表现出来。值得注意的是,默认情况下无论是读取还是写入操作都会采用 UTF-8 编码格式进行处理——如果您所使用的系统无法正确处理 UTF-8 格式(假如您的硬件配置并不算老旧),您将看到一堆乱码字符。举个例子说明:

复制代码
    (a <- "je ne connais pas le fran\xe7ai")
    # [1] "je ne connais pas le fran\xe7ai"
    (b <- "\xd7\xe1\xdf\xf1\xe5\xf4\xe5")
    # [1] "\xd7\xe1\xdf\xf1\xe5\xf4\xe5"

未将非英文字符正确转译;通过调用 parse_character 并指定相应的编码格式能够准确解析字符串

复制代码
    parse_character(a, locale = locale(encoding = "Latin1"))
    # [1] "je ne connais pas le françai"
    parse_character(b, locale = locale(encoding = "iso-8859-7"))
    # [1] "Χαίρετε"

当无法确定字符串的编码方式时

复制代码
    guess_encoding(charToRaw(a))
    # # A tibble: 2 × 2
    #   encoding   confidence
    #   <chr>           <dbl>
    # 1 ISO-8859-1       0.98
    # 2 ISO-8859-2       0.51
    guess_encoding(charToRaw(b))
    # # A tibble: 0 × 2
    # # … with 2 variables: encoding <chr>, confidence <dbl>

parse_factor 函数负责解析因子变量,并接受 levels 参数作为一个预定义的向量输入用于分类分析;当输入数据超出预期范围时会触发警告提示。

复制代码
    sex <- c("male", "formale")
    parse_factor(c("male", "formale", "male"), levels = sex)
    # [1] male    formale male   
    # Levels: male formale

时间和日期的解析可以从下面三个解析器中选择

parse_datetime 用于解析日期和时间,默认情况下将日期分为年月日时分秒六个部分进行排列,并且允许用户自定义时间格式。

复制代码
    parse_datetime("2022-05-01T13:14")
    # [1] "2022-05-01 13:14:00 UTC"
    parse_datetime("2022-05-01")
    # [1] "2022-05-01 UTC"
    parse_datetime("01/01/2010", format = "%d/%m/%Y")
    # [1] "2010-01-01 UTC"
  • parse_date() 用于解析日期,默认的形式 yyyy-mm-ddyyyy/mm/dd
复制代码
    parse_date("2022-03-04")
    # [1] "2022-03-04"
    parse_date("2022/03/04")
    # [1] "2022-03-04"
    parse_date("2022 03 04", format = "%Y %m %d")
    # [1] "2022-03-04"
  • parse_time() 用于解析时间,默认形式为 hh:mm(:ss am/pm)
复制代码
    parse_time("01:10 pm")
    # 13:10:00
    parse_time("10:10:10")
    # 10:10:10
    parse_time("13:14:15 US/Central", format = "%H:%M:%S %Z")
    # 13:14:15

日期时间的格式,可以由以下几种格式进行组成

符号 含义 符号 含义
%Y 4 位数字的年份 %y 2 位数字的年份
%m 2 位数字的月份 %b 月份简写,如 Jan
%B 月份全称,如 January %d 一个月内的 2 位数字的天数
%H 24 小时制 %I 12 小时制,必须包含 %p
%p AM/PM %M 2 位数字的分钟数
%S 整数秒 %OS 实数秒
%Z 时区 %z 相对于标准时间的偏移,如 +0800
%. 跳过一个非数字字符 %* 跳过任意个非数字字符

解析文件

到了这里,你已经很清楚如何分析单个向量了。现在让我们回头探讨 readr 在文件上的解析过程。

readr 利用获取的数据前 1000 行并借助一些启发式算法识别每一列的数据类型;该工具能够帮助我们推测列数据类型。

复制代码
    guess_parser(c("1", "3", "5"))
    # [1] "double"
    guess_parser("1561,123")
    # [1] "number"
    guess_parser("2022-05-01")
    # [1] "date"
    guess_parser("13:14")
    # [1] "time"
    guess_parser(c("TRUE", "FALSE"))
    # [1] "logical"
    > 
    > str(parse_guess("2010-10-10"))
     Date[1:1], format: "2010-10-10"

使用 parse_guess 函数可以用推测到的最优类型去解析数值

复制代码
    guess_parser(c("2022-02-20"))
    # [1] "date"
    parse_guess(c("2022-02-20"))
    # [1] "2022-02-20"

基于启发式的策略将识别一系列分类规则,并在所有特定分类规则无法识别时以字符串形式表示。

类型 规则
logical 只包含 F, T, FALSE, 或 TRUE
integer 只包含数字和符号 -
double 只包含有效双精度数字
number 相较于 double,多了分组标记
time 匹配默认的 time_format
date 匹配默认的 date_format
date-time 任何 ISO8601 格式

启发式的应用并非普遍无例外,在涉及大数据文件的情境下

在这时,可能需要我们对数据进行观测,手动为每一列设置类型,例如

复制代码
    mtcars <- read_csv(
      readr_example("mtcars.csv"), 
      col_select = 1:2,     # 只读取前两列,可以是向量或 list,可以是数值索引或列名
      col_types = cols(
    mpg = col_double(),  # 解析为 double
    cyl = col_integer()  # 解析为 integer
    )
    )

所有 parse_* 函数都对应一个 col_* 函数,在读取文件过程中作为参数传递给 col_types 参数以指定解析方式。

还有其他一些通用策略可以帮助您解析文件,例如

设置最大猜测行数 guess_max

或者设置默认解析为字符串,然后使用 type_convert 进行类型推断

在分析大文件时,通过调整 n_max 参数为一个较小的值,每次读取一点数据可以消除潜在的问题,并加快模型的训练迭代速度。

或者使用 read_lines 每次读取一行

写入文件

readr 提供了一系列具有 write_* 类型功能的函数用于将数据写入文件中,并且其中包含如 write_csvwrite_tsv 等常用函数。特别地,在需要将数据以 Excel 格式保存为 CSV 文件时,则应调用 write_excel_csv() 函数来实现这一目标。

这些函数有两个主要参数需要设置:数值和路径。此外,在na参数中,请指定一个字符串来表示缺失值被写入到文件中的值,并可以选择性地追加至现有文件中。

复制代码
    write_csv(mtcars, "mtcars.csv")

提示:在数据以CSV格式保存时,在保存过程中可能会导致CSV文件在缓存中间断数据时存在一定的不可靠性。当需要获取类型信息时,请考虑使用其他两种方法。

这些功能模块是针对基本函数 ... 进行的封装。它们负责将数据存储为 ... 二进制文件。

复制代码
    write_rds(mtcars, "mtcars.rds")
    read_rds("mtcars.rds")
    # # A tibble: 32 × 2
    #      mpg   cyl
    #    <dbl> <int>
    #  1  21       6
    #  2  21       6
    #  3  22.8     4
    #  4  21.4     6
    #  5  18.7     8
    #  6  18.1     6
    #  7  14.3     8
    #  8  24.4     4
    #  9  22.8     4
    # 10  19.2     6
    # # … with 22 more rows
  • feather包能够支持将数据编码为一种与其他编程语言兼容地传输的高效二进制文件格式。相较于现有标准格式而言更加高效,并且可以在不依赖R的情况下运行。
复制代码
    library(feather)
    write_feather(mtcars, "mtcars.feather")
    read_feather("mtcars.feather")
    # # A tibble: 32 × 2
    #      mpg   cyl
    #    <dbl> <int>
    #  1  21       6
    #  2  21       6
    #  3  22.8     4
    #  4  21.4     6
    #  5  18.7     8
    #  6  18.1     6
    #  7  14.3     8
    #  8  24.4     4
    #  9  22.8     4
    # 10  19.2     6
    # # … with 22 more rows

4. dplyr 基础

一般情况下,在获取到的数据可能无法完全满足后续分析工作的需求时,则需要采取相应的处理措施以解决这一问题。例如,在实际操作中可能会有以下几种情况:一是生成新的变量;二是仅需更改变量名称或重新排列数据以观察结果变化;三是为了便于后续分析而进行必要的预处理工作等。在本章及随后的内容中,我们将详细介绍如何利用tidyverse包中的dplyr功能来实现这些数据操作任务。

先导入包

复制代码
    library(dplyr)  # version 1.0.7
    # library(tidyverse)

在R语言中存在部分与基础功能库产生冲突的情况,在dplyr包中也存在类似问题。为了避免此类问题的发生并保证功能的一致性,在调用相关功能时建议采用包名加函数名的方式进行调用。例如,在统计包中调用过滤操作时可以写成stats::filter的形式

本节的重点是介绍 dplyr 包的基本功能。 以便深入理解与操作, 我们选择该数据集的原因在于, 该数据集记录了自2013年在纽约机场起飞的所有航班, 这些航班总数为 336776 架次, 全部来自美国交通统计局.

复制代码
    # 安装 nycflights13 包,版本为 1.0.2
    install.packages("nycflights13")
    # 导入
    library(nycflights13)

flights 是一个 tibble 类型,每架航班包含 19 列信息

复制代码
    dim(flights)
    # [1] 336776     19
    colnames(flights)
    #  [1] "year"           "month"          "day"            "dep_time"      
    #  [5] "sched_dep_time" "dep_delay"      "arr_time"       "sched_arr_time"
    #  [9] "arr_delay"      "carrier"        "flight"         "tailnum"       
    # [13] "origin"         "dest"           "air_time"       "distance"      
    # [17] "hour"           "minute"         "time_hour" 
    class(flights)
    # [1] "tbl_df"     "tbl"        "data.frame"

我们先介绍了几个关键的 dplyr 动词函数,在不同的情境下这些动词有多种变体形式。这些功能有助于解决大部分数据处理问题,并对目标变量进行特定的操作后会生成一个新的数据框。将这些功能组合起来使用会使得完成复杂操作变得更加容易一些。动词主要有

函数 功能 函数 功能
filter 逻辑条件过滤行 slice 按照位置获取行
distinct 行数据去重 arrange 行数据排序
select 列选择 pull 将列值转换为向量
mutate 添加新列 transmute 只保留添加的新列
rename 列重命名 summarise 汇总函数
add_row 添加行 count 统计唯一值的数量

行过滤(filter

filter 函数用于基于特定条件对行数据实施过滤以实现子集的抽取,并具有相似的功能性于内置 subset 函数。重点在于构建过滤条件。

制作过滤规则的过程非常简单,在操作过程中可以直接利用变量名称来进行计算。除了上述方法之外,还可以通过逻辑操作符将多个条件组合起来。经过尝试你会发现这种方法与其他内置判断机制并无明显差异。比如我们可以按照如下步骤查询 20133 月的具体航班数据。

复制代码
    filter(flights, year == 2013, month == 3) %>% nrow()
    # [1] 28834

dplyr 函数不会更改输入数据,并且每个函数都会生成一个新的数据集。这意味着如果想要保存结果的话,则必须通过赋值操作来实现。

复制代码
    a <- filter(flights, year == 2013, month == 3) %>% nrow()

注意,上面的筛选参数用的是 == 而不是 =,如果你用错了将会抛出异常

对于浮点数,由于精度的问题,有时候可能不太适合 == 来判断数值相等,例如

复制代码
    sqrt(3) ^ 3 == 3
    # [1] FALSE

计算机的精度是受限的,并且由于精度限制而无法精确表示一个无限大的数值。然而,在大多数情况下,我们可以使用 near() 函数来替代 == 运算符,在进行数值比较时实现近似判断。

复制代码
    near(sqrt(3) ^ 2,  3)
    # [1] TRUE

多个字段对应的过滤操作相当于执行逻辑与运算;只有当所有条件都被满足时才进行筛选;而对于其他类型的组合关系,则可采用相应的布尔运算符:& \mid !。例如,在搜索 20133-4 月之间的航班数据时

复制代码
    filter(flights, year == 2013, month == 3 | month == 4) %>% nrow()
    # [1] 57164
    filter(flights, year == 2013, month %in% c(3, 4) ) %>% nrow()  # 成员判断
    # [1] 57164

注意,该过滤条件不能写成

复制代码
    filter(flights, year == 2013, month == (3 | 4))

由于运算符 3 | 4 执行后会返回 TRUE 值。而 TRUE 值等价于整数值 1。因此该表达式的结果会包括所有属于 2013 年 January 的航班记录。

对于 NA 值的过滤也是一样的

复制代码
    tibble(x = c(1, NA, 3)) %>% filter(!is.na(x))
    # # A tibble: 2 × 1
    #       x
    #   <dbl>
    # 1     1
    # 2     3

与内置的条件判断基本一样,不需要更多的说明了。

行去重(distinct

对行数据去重,可以使用 distinct 函数

复制代码
    df <- tibble(
      g = c(1, 1, 2, 2),
      x = c(1, 1, 2, 1)
    ) 
    df %>% distinct(df)
    # # A tibble: 3 × 2
    #       g     x
    #   <dbl> <dbl>
    # 1     1     1
    # 2     2     2
    # 3     2     1
    distinct(df, x)
    # # A tibble: 2 × 1
    #       x
    #   <dbl>
    # 1     1
    # 2     2

行子集(slice

该函数可根据行号提取数据片段,并可从列中执行提取、删除或去重操作;此外它还有几种不同的变体以适应不同场景

  • 分别由 $\texttt{slice\_head}$``\texttt{}$\texttt{slice\_tail}$``\texttt{} 从数据集中提取首部和尾部数据。
  • $\texttt{slice\_sample}$``\texttt{} 被用来代替传统的 $\texttt{sample\_n}$``\texttt{}$\texttt{sample\_frac}$``\texttt{} ,它能随机抽取指定数量或比例的样本。
  • $\texttt{slice\_min}$``\texttt{}$\texttt{slice\_max}$``\texttt{} 被用来替代原来的 $\texttt{top\_n}$``\texttt{}$\texttt{top\_frac}$``\texttt{} ,它们分别用于确定最小值和最大值所在的具体行号。
复制代码
    slice(flights[,1:5], 1)
    # # A tibble: 1 × 5
    #    year month   day dep_time sched_dep_time
    #   <int> <int> <int>    <int>          <int>
    # 1  2013     1     1      517            515
    slice_head(flights[,1:5], n = 3)
    # # A tibble: 3 × 5
    #    year month   day dep_time sched_dep_time
    #   <int> <int> <int>    <int>          <int>
    # 1  2013     1     1      517            515
    # 2  2013     1     1      533            529
    # 3  2013     1     1      542            540
    slice_sample(flights[,1:5], n = 3)
    # # A tibble: 3 × 5
    #    year month   day dep_time sched_dep_time
    #   <int> <int> <int>    <int>          <int>
    # 1  2013    12    29     1527           1529
    # 2  2013    11     9     1653           1700
    # 3  2013     1    23      854            855
    flights[,1:5] %>% 
      slice_max(order_by = dep_time, with_ties = FALSE, n = 2)
    # # A tibble: 2 × 5
    #    year month   day dep_time sched_dep_time
    #   <int> <int> <int>    <int>          <int>
    # 1  2013    10    30     2400           2359
    # 2  2013    11    27     2400           2359

当我们试图获取最大值所在的行时, 因为一个最大值可能分布在多行中, 这会导致返回的结果数量超过预设的 n 行. 通过将参数 with_ties 设置为 FALSE, 可以限制结果的数量为指定的数目

行重排(arrange

arrange 函数用于对数据进行排序操作(默认按升序排列)。当调用此函数时,默认会将数据按照升序排列。如果指定单个列名,则按照该列进行排序;如果传递多个列名,则按照从左到右的顺序依次作为优先级依据,并根据前面指定的列为优先级依据。

复制代码
    df <- tibble(
      x = c(9, 2, 6, NA, 3, 8),
      y = c(NA, 1, 6, 9, 4, NA)
    )
    df
    # # A tibble: 6 × 2
    #       x     y
    #   <dbl> <dbl>
    # 1     9    NA
    # 2     2     1
    # 3     6     6
    # 4    NA     9
    # 5     3     4
    # 6     8    NA
    arrange(df, x)
    # # A tibble: 6 × 2
    #       x     y
    #   <dbl> <dbl>
    # 1     2     1
    # 2     3     4
    # 3     6     6
    # 4     8    NA
    # 5     9    NA
    # 6    NA     9

使用 desc() 按降序重新排序

复制代码
    arrange(df, desc(y))
    # # A tibble: 6 × 2
    #       x     y
    #   <dbl> <dbl>
    # 1    NA     9
    # 2     6     6
    # 3     3     4
    # 4     2     1
    # 5     9    NA
    # 6     8    NA

通过观察发现,在数据集中通常情况下我们会发现缺失值位于末尾位置。为了将 NA 值放置在前面以便后续处理流程更加高效和有序,请考虑采用以下方法:通过内置函数 rank 来实现这一目标。

复制代码
    arrange(df, rank(y, na.last = FALSE))
    # # A tibble: 6 × 2
    #       x     y
    #   <dbl> <dbl>
    # 1     9    NA
    # 2     8    NA
    # 3     2     1
    # 4     3     4
    # 5     6     6
    # 6    NA     9

行添加(add_row

复制代码
    df <- tibble(x = 1:2, y = 8:9)
    add_row(df, x = 1, y = 0)
    # # A tibble: 3 × 2
    #       x     y
    #   <dbl> <dbl>
    # 1     1     8
    # 2     2     9
    # 3     1     0
    add_row(df, x = 1, y = 0, .before = 1)
    # # A tibble: 3 × 2
    #       x     y
    #   <dbl> <dbl>
    # 1     1     0
    # 2     1     8
    # 3     2     9
    add_row(df, x = -1:0, y = 5:6)
    # # A tibble: 4 × 2
    #       x     y
    #   <int> <int>
    # 1     1     8
    # 2     2     9
    # 3    -1     5
    # 4     0     6

列选择(select

在大数据处理中,在面对大量数据时(通常会遇到数据量非常庞大的情况),我们需要关注的数据仅占整体的一小部分(此时),提取所需的数据具有重要意义(这有助于降低存储空间需求和内存占用),从而显著提升程序运行效率(select函数可以快速提取列的子集)。

例如,我们只对航班出发地和到达地的距离感兴趣

复制代码
    select(flights, origin, dest, distance)
    # # A tibble: 336,776 × 3
    #    origin dest  distance
    #    <chr>  <chr>    <dbl>
    #  1 EWR    IAH       1400
    #  2 LGA    IAH       1416
    #  3 JFK    MIA       1089
    #  4 JFK    BQN       1576
    #  5 LGA    ATL        762
    #  6 EWR    ORD        719
    #  7 EWR    FLL       1065
    #  8 LGA    IAD        229
    #  9 JFK    MCO        944
    # 10 LGA    ORD        733
    # # … with 336,766 more rows

有许多辅助函数可以在 select 中使用,用于筛选列

函数 功能 函数 功能
: 范围选择 ! 补集
&、` ` 交集或并集 c
everything 选择所有列 last_col 选择倒数第几列,0 表示最后一列
starts_with 匹配列名开头规则 ends_with 匹配列名结尾规则
contains 匹配包含某条件的列名 matches 正则表达式匹配列名
num_range 匹配范围 where 使用函数,选择函数返回值为真的列
all_of 所有在向量中的列名 any_of 主要用于负向选择

范围和集合选择

复制代码
    select(flights, last_col(0)) %>% head()
    # # A tibble: 6 × 1
    #   time_hour          
    #   <dttm>             
    # 1 2013-01-01 05:00:00
    # 2 2013-01-01 05:00:00
    # 3 2013-01-01 05:00:00
    # 4 2013-01-01 05:00:00
    # 5 2013-01-01 06:00:00
    # 6 2013-01-01 05:00:00
    
    # 1:3 等价于 year:day、c(1, 2, 3)、year | month | day
    select(flights, 1:3) %>% head()
    # # A tibble: 6 × 3
    #    year month   day
    #   <int> <int> <int>
    # 1  2013     1     1
    # 2  2013     1     1
    # 3  2013     1     1
    # 4  2013     1     1
    # 5  2013     1     1
    # 6  2013     1     1

列名匹配

复制代码
    select(flights, ends_with("time")) %>% head()
    # # A tibble: 6 × 5
    #   dep_time sched_dep_time arr_time sched_arr_time air_time
    #      <int>          <int>    <int>          <int>    <dbl>
    # 1      517            515      830            819      227
    # 2      533            529      850            830      227
    # 3      542            540      923            850      160
    # 4      544            545     1004           1022      183
    # 5      554            600      812            837      116
    # 6      554            558      740            728      150
    select(flights, contains("rr")) %>% head()
    # # A tibble: 6 × 4
    #   arr_time sched_arr_time arr_delay carrier
    #      <int>          <int>     <dbl> <chr>  
    # 1      830            819        11 UA     
    # 2      850            830        20 UA     
    # 3      923            850        33 AA     
    # 4     1004           1022       -18 B6     
    # 5      812            837       -25 DL     
    # 6      740            728        12 UA  
    df <- tibble(
      x1 = c(9, 2, 6, NA),
      x2 = c(NA, 1, 6, 9),
      y = c(NA, 1, 6, 9)
    ) 
    df %>% select(num_range("x", 1:2))
    # # A tibble: 6 × 2
    #      x1    x2
    #   <dbl> <dbl>
    # 1     9    NA
    # 2     2     1
    # 3     6     6
    # 4    NA     9

列向量判断

复制代码
    columns <- c("x1", "x2")
    df %>% select(all_of(columns))  # 相当于 df %>% select({{ columns }})
    # # A tibble: 4 × 2
    #      x1    x2
    #   <dbl> <dbl>
    # 1     9    NA
    # 2     2     1
    # 3     6     6
    # 4    NA     9
    df %>% select(-any_of(columns))
    # # A tibble: 4 × 1
    #       y
    #   <dbl>
    # 1    NA
    # 2     1
    # 3     6
    # 4     9

应用函数

复制代码
    select(flights, where(~ !is.numeric(.x))) %>% head()
    # # A tibble: 6 × 5
    #   carrier tailnum origin dest  time_hour          
    #   <chr>   <chr>   <chr>  <chr> <dttm>             
    # 1 UA      N14228  EWR    IAH   2013-01-01 05:00:00
    # 2 UA      N24211  LGA    IAH   2013-01-01 05:00:00
    # 3 AA      N619AA  JFK    MIA   2013-01-01 05:00:00
    # 4 B6      N804JB  JFK    BQN   2013-01-01 05:00:00
    # 5 DL      N668DN  LGA    ATL   2013-01-01 06:00:00
    # 6 UA      N39463  EWR    ORD   2013-01-01 05:00:00

上面的 formula 相当于 function(x) !is.numeric(x)

当使用 select 时,默认会排除那些未明确指定的字段;通过与 everything 函数配合使用,则能实现仅筛选出需要保留的关键字段。此外,在实际操作中还能够灵活地将某些字段重新排列至数据框的顶部或底部位置。

复制代码
    select(df, y, everything())
    # # A tibble: 4 × 3
    #       y    x1    x2
    #   <dbl> <dbl> <dbl>
    # 1    NA     9    NA
    # 2     1     2     1
    # 3     6     6     6
    # 4     9    NA     9

列向量化(pull

无论选中多少列,“select”函数都会返回一个 tibble 对象;通过 pull 方法可以使对象被转换为向量。“无论是输入具体的列名还是索引位置,“pull”操作都会生成一个带有名称的向量

复制代码
    pull(df, y)
    # [1] NA  1  6  9
    pull(df, 1)
    # [1]  9  2  6 NA
    pull(df, -1)
    # [1] NA  1  6  9
    pull(df, y, x1)
    #    9    2    6 <NA> 
    #   NA    1    6    9

列重命名(rename

通过调用 rename 函数可以实现列名的更改。该函数作为 select 的一个变体,在数据处理过程中负责管理所有未显式提到的变量。通过这种方式,新的列名会被设定为 $new\_name = old\_name$。此外,在这种情况下,还可以利用变形形式 $rename\_with$ 来对所需的列名执行相应的操作。

复制代码
    flights[1:3,1:5] %>% 
      rename(Year = year, Month = month)
    # # A tibble: 3 × 5
    #    Year Month   day dep_time sched_dep_time
    #   <int> <int> <int>    <int>          <int>
    # 1  2013     1     1      517            515
    # 2  2013     1     1      533            529
    # 3  2013     1     1      542            540
    flights[1:3,1:5] %>% 
      rename_with(toupper)
    # # A tibble: 3 × 5
    #    YEAR MONTH   DAY DEP_TIME SCHED_DEP_TIME
    #   <int> <int> <int>    <int>          <int>
    # 1  2013     1     1      517            515
    # 2  2013     1     1      533            529
    # 3  2013     1     1      542            540
    flights[1:3,1:5] %>% 
      rename_with(~ gsub("_", ".", .x, fixed = TRUE))
    # # A tibble: 3 × 5
    #    year month   day dep.time sched.dep.time
    #   <int> <int> <int>    <int>          <int>
    # 1  2013     1     1      517            515
    # 2  2013     1     1      533            529
    # 3  2013     1     1      542            540

通过 .cols 选项设置来指定要选中的列(若无特别设置,则默认选中所有列),随后采用带有 ~ 标识符的函数格式来获取一个包含所需列名的向量(此方式与直接调用 function(...) 并传递 "A", "B", "C" 的结果一致)。

复制代码
    flights[1:3,1:5] %>% 
      rename_with(.cols = 1:3, ~ c("A", "B", "C")) %>%
      colnames()
    # [1] "A"              "B"              "C"              "dep_time"      
    # [5] "sched_dep_time"
    LETTERS[1:5]  # 内置字符串向量
    # [1] "A" "B" "C" "D" "E"
    flights[1:3,1:5] %>% 
      `names<-`(LETTERS[1:5]) %>%  # 等同于 rename_with(~ LETTERS[1:5])
      colnames()
    # [1] "A" "B" "C" "D" "E"

列创建(mutate

mutator函数能够为数据集新增字段或修改现有字段;它默认在数据集末尾新增字段,并可通过.before.after参数指定插入位置;该功能可以直接引用现有字段名,在函数内部创建的新字段也可以直接用于各种向量化运算。

复制代码
    select(flights, distance, air_time) %>% head() %>%
      mutate(
    hours = air_time / 60,
    speed = distance / hours
      )
    # # A tibble: 6 × 4
    #   distance air_time hours speed
    #      <dbl>    <dbl> <dbl> <dbl>
    # 1     1400      227  3.78  370.
    # 2     1416      227  3.78  374.
    # 3     1089      160  2.67  408.
    # 4     1576      183  3.05  517.
    # 5      762      116  1.93  394.
    # 6      719      150  2.5   288.

mutate 将会将原有的数据与新增的数据合并后返回结果。类似地使用函数 transmute(如.keep参数设置为"none"时的mutate`行为)则可以直接返回新增的列。

复制代码
    select(flights, distance, air_time) %>% head() %>%
      transmute(
    hours = air_time / 60,
    speed = distance / hours
      )
    # # A tibble: 6 × 2
    #   hours speed
    #   <dbl> <dbl>
    # 1  3.78  370.
    # 2  3.78  374.
    # 3  2.67  408.
    # 4  3.05  517.
    # 5  1.93  394.
    # 6  2.5   288.

如果新建的列名与现有列相同,将会覆盖现有列

复制代码
    select(flights, distance, air_time) %>% head() %>%
      transmute(
    hours = air_time / 60,
    speed = distance / hours
      ) %>%
      mutate(speed = paste0(round(speed, 2), "km/h"))
    # # A tibble: 6 × 2
    #   hours speed     
    #   <dbl> <chr>     
    # 1  3.78 370.04km/h
    # 2  3.78 374.27km/h
    # 3  2.67 408.38km/h
    # 4  3.05 516.72km/h
    # 5  1.93 394.14km/h
    # 6  2.5  287.6km/h

mutate 也可以通过应用向量化函数来处理列变量。这些函数能够接受列变量并输出一个向量。常见的功能包括

函数 功能 函数 功能
cumsum 累加和 between 范围内的数
near 浮点数忽略精度的等于 case_when 多条件的 if-else
if_else 条件判断 na_if 将值替换为 NA
pmax 逐元素比较最大值 pmin 逐元素比较最小值
coalesce 寻找向量集合中各位置第一个非 NA recode 向量化的 switch

获取飞行时长在 2.5 ~ 3.5 小时之间的航班数

复制代码
    flights %>%
      mutate(
    hours = air_time / 60,
    times = between(hours, 2.5, 3.5)
    ) %>%
      filter(times) %>%
      nrow()
    # [1] 56477

将超过 3.5 小时的航班标记为 'long',其他标记为 'short'

复制代码
    flights %>%
      transmute(
    hours = air_time / 60,
    label = if_else(hours > 3.5, 'long', "short")
    ) %>%
      slice_head(n = 3)
    # # A tibble: 3 × 2
    #   hours label
    #   <dbl> <chr>
    # 1  3.78 long 
    # 2  3.78 long 
    # 3  2.67 short

多条件判断流程中,在运用 case_when 函数将时间段划分为不同的等级区间后,在所有指定条件都不满足的情况下会返回 TRUE。

复制代码
    flights %>%
      transmute(
    hours = air_time / 60,
    label = case_when(
      hours < 1 ~ "short",
      between(hours, 1, 2) ~ "mid",
      between(hours, 2, 3) ~ "long",
      TRUE ~ "long long"
      )
      ) %>%
      slice_head(n = 5)
    # # A tibble: 5 × 2
    #   hours label    
    #   <dbl> <chr>    
    # 1  3.78 long long
    # 2  3.78 long long
    # 3  2.67 long     
    # 4  3.05 long long
    # 5  1.93 mid

计算预计起飞时间和真实起飞时间的最大值

复制代码
    flights %>%
      select(tailnum, dep_time, sched_dep_time) %>%
      mutate(max = pmax(dep_time, sched_dep_time)) %>%
      slice_head(n = 3)
    # # A tibble: 3 × 4
    #   tailnum dep_time sched_dep_time   max
    #   <chr>      <int>          <int> <int>
    # 1 N14228       517            515   517
    # 2 N24211       533            529   533
    # 3 N619AA       542            540   542

允许 recode 替换指定的字段值。其设置方式与 rename 不同。具体来说, 该操作允许逐一指定替代值(可选配默认替代方案)。此外, 也可通过提供名称向量的形式进行配置

复制代码
    recode(1:3, `2` = 20, .default = NA_real_)
    # [1] NA 20 NA
    m <- month.abb
    names(m) <- 1:12
    m
    #     1     2     3     4     5     6     7     8     9    10    11    12 
    # "Jan" "Feb" "Mar" "Apr" "May" "Jun" "Jul" "Aug" "Sep" "Oct" "Nov" "Dec" 
    flights %>%
      distinct(month) %>%
      mutate(month = recode(month, !!!m)) %>%  # !!! 符号用于解引用对象并拼接在一起
      pull()
    #  [1] "Jan" "Oct" "Nov" "Dec" "Feb" "Mar" "Apr" "May" "Jun" "Jul" "Aug" "Sep"

分组汇总(summarise

最后一个关键性的动词 summarise ,它能够整合所有的信息 ,例如 统计数据显示的数据总量

复制代码
    summarise(flights, count = n())
    # # A tibble: 1 × 1
    #    count
    #    <int>
    # 1 336776

但是,在大多数情况下我们较少独自使用 summarise 而是将其与 group_by 函数结合使用以实现分组计算或汇总统计这样可以便于直观比较各组间的差异情况而这一组合也是 dplyr 最常用的功能之一

在我们对数据进行分组后(段落开始),所有用于统计的数据函数会自动针对每个分组执行计算(段落内部第一句话)。例如(段落内部第二句话)计算2013年各个月份的航班数量以及平均延误时间(具体数值)。因为(第三句话)数据中存在缺失值(具体数值),当输入中含有任何缺失值时(具体数值),这样会导致输出结果也出现缺失(具体数值)。因此,在计算前通常会提供一个na.rm参数来排除这些缺失值(具体数值)。

复制代码
    flights %>%
      filter(year == 2013) %>%
      group_by(month) %>%
      summarise(num = n(), delay = mean(dep_delay, na.rm = TRUE))
    # # A tibble: 12 × 3
    #    month   num delay
    #    <int> <int> <dbl>
    #  1     1 27004 10.0 
    #  2     2 24951 10.8 
    #  3     3 28834 13.2 
    #  4     4 28330 13.9 
    #  5     5 28796 13.0 
    #  6     6 28243 20.8 
    #  7     7 29425 21.7 
    #  8     8 29327 12.6 
    #  9     9 27574  6.72
    # 10    10 28889  6.24
    # 11    11 27268  5.44
    # 12    12 28135 16.

我们也可以在统计前先删除 NA 值,在这种情况下,缺失值表示取消的航班。

复制代码
    arrived <- flights %>%
      filter(year == 2013, !is.na(dep_delay))
    arrived %>%
      group_by(month) %>%
      summarise(num = n(), delay = mean(dep_delay))
    # # A tibble: 12 × 3
    #    month   num delay
    #    <int> <int> <dbl>
    #  1     1 26483 10.0 
    #  2     2 23690 10.8 
    #  3     3 27973 13.2 
    #  4     4 27662 13.9 
    #  5     5 28233 13.0 
    #  6     6 27234 20.8 
    #  7     7 28485 21.7 
    #  8     8 28841 12.6 
    #  9     9 27122  6.72
    # 10    10 28653  6.24
    # 11    11 27035  5.44
    # 12    12 27110 16.6

R 提供了很多汇总函数

函数 功能 函数 功能
mean 均值 median 中位值
sd 标准差 var 方差
mad 绝对中位差 min 最小值
max 最大值 quantile 分位数
first 第一个值 last 最后一个值
nth n 个值 n 值的数量
n_distinct 唯一值的数量 sum(!is.na) NA 值的数

计算每个月起飞延误时间的均值和标准差

复制代码
    flights %>%
      filter(year == 2013, !is.na(dep_delay)) %>%
      group_by(month) %>%
      summarise(
    mean = mean(dep_delay),
    std = sd(dep_delay)
      ) %>%
      slice_head(n = 3)
    # # A tibble: 3 × 3
    #   month  mean   std
    #   <int> <dbl> <dbl>
    # 1     1  10.0  36.4
    # 2     2  10.8  36.3
    # 3     3  13.2  40.1

计算每个月最长起飞延误时间和最早提前到达时间

复制代码
    flights %>%
      filter(year == 2013, !is.na(dep_delay), !is.na(arr_delay)) %>%
      group_by(month) %>%
      summarise(
    max = max(dep_delay),
    min = min(arr_delay)
      ) %>%
      slice_head(n = 3)
    # # A tibble: 3 × 3
    #   month   max   min
    #   <int> <dbl> <dbl>
    # 1     1  1301   -70
    # 2     2   853   -70
    # 3     3   911   -68

计算每月最早和最晚起飞的一趟航班

复制代码
    flights %>%
      filter(year == 2013, !is.na(dep_time)) %>%
      group_by(month) %>%
      summarise(
    first = first(dep_time),
    last = last(dep_time)
      ) %>%
      slice_head(n = 3)
    # # A tibble: 3 × 3
    #   month first  last
    #   <int> <int> <int>
    # 1     1   517  2354
    # 2     2   456  2359
    # 3     3     4  2358

基于多个变量进行分组操作时,ungroup() 函数能够取消当前的分组设置;随后定义的函数将直接作用于整个数据集。

复制代码
    flights %>%
      filter(year == 2013) %>%
      group_by(tailnum, month) %>%
      summarise(
    count = sum(!is.na(dep_time))
      ) %>%
      ungroup() %>%
      slice_max(order_by = count, n = 3)
    # # A tibble: 3 × 3
    #   tailnum month count
    #   <chr>   <int> <int>
    # 1 N298JB      7    76
    # 2 N723MQ      5    76
    # 3 N730MQ      1    72

分组与 summarise() 结合使用最为强大,并非唯一的选择;同样地,你还可以将其与其他命令如 mutatefilter 一起运用。

提取航班数超过 10000 次的机场的所有航班

复制代码
    flights %>%
      group_by(dest) %>%
      filter(n() > 10000

提取平均晚点数少于 3 分钟的所有航班

复制代码
    flights %>%
      group_by(tailnum) %>%
      mutate(
    delay = if_else(arr_delay < 0, 0, arr_delay)
      ) %>%
      filter(mean(delay) < 3))

统计计数(count

该函数用于快速统计数据集中某字段的唯一取值数量。它等价于先按分组键进行分组然后计算每组的计数。举例而言,在涉及航班到达量的数据集中最多的是前三个机场

复制代码
    flights %>%
      filter(!is.na(dep_time)) %>%
      count(dest) %>%
      slice_max(n, n = 3)
    # # A tibble: 3 × 2
    #   dest      n
    #   <chr> <int>
    # 1 ATL   16898
    # 2 ORD   16642
    # 3 LAX   16076

通过添加权重系数 wt,计算每架航班飞行的总英里数最少的 3 架航班

复制代码
    flights %>%
      filter(!is.na(dep_time)) %>%
      count(tailnum, wt = distance, name = "Mile") %>%
      slice_min(Mile, n = 3)
    # # A tibble: 3 × 2
    #   tailnum  Mile
    #   <chr>   <dbl>
    # 1 N505SW    185
    # 2 N746SK    229
    # 3 N881AS    292

在配对时,tally 函数假设输入的数据已进行了分组处理;其其他用法与 count 函数相同。

复制代码
    flights %>%
      filter(!is.na(dep_time)) %>%
      tally()
    # # A tibble: 1 × 1
    #        n
    #    <int>
    # 1 328521
    flights %>%
      filter(!is.na(dep_time)) %>%
      group_by(tailnum) %>%
      tally(wt = distance, name = "Mile") %>%
      slice_min(Mile, n = 3)
    # # A tibble: 3 × 2
    #   tailnum  Mile
    #   <chr>   <dbl>
    # 1 N505SW    185
    # 2 N746SK    229
    # 3 N881AS    292

这两个函数默认仅输出统计后的结果;此外还提供了一个 add 版本, 用于将统计结果整合到现有数据中。

复制代码
    df <- tribble(
      ~name,    ~gender,   ~score,
      "Telly",  "male",       100,
      "Rose",   "female",      95,
      "Lux",    "female",      77
    )
    df %>% add_count(gender, wt = score)
    # # A tibble: 3 × 4
    #   name  gender score     n
    #   <chr> <chr>  <dbl> <dbl>
    # 1 Telly male     100   100
    # 2 Rose  female    95   172
    # 3 Lux   female    77   172
    df %>% 
      group_by(gender) %>%
      add_tally(wt = score)
    # # A tibble: 3 × 4
    #   name  gender score     n
    #   <chr> <chr>  <dbl> <dbl>
    # 1 Telly male     100   100
    # 2 Rose  female    95   172
    # 3 Lux   female    77   172

强制部分表达式

有时,在对整个表达式的分析处理之前进行部分表达式的分析处理,则可为我们的操作提供更为灵活的选择,在Tidy框架中提供了几个用于强制某些表达式提前被解析的强制运算符

该符号即为bang-bang operator,在编程中被用来强迫操作对象,并常用于将数据框内的变量从环境中替换出来。

复制代码
    var <- sym("dep_delay")
    flights %>%
      summarise(avg = mean(!!var, na.rm = TRUE))
    # # A tibble: 1 × 1
    #     avg
    #   <dbl>
    # 1  12.6

ang operator`):强行连接一个对象集合,在此过程中会将集合中的各个元素依次作为独立的参数传递给调用函数。这种操作等同于将该集合解析为带有名称的参数形式,并通过解压功能实现统一处理机制。

复制代码
    var <- c("tailnum", "dep_time", "sched_dep_time")
    flights %>%
      select(!!!var) %>%
      slice_head(n = 3)
    # # A tibble: 3 × 3
    #   tailnum dep_time sched_dep_time
    #   <chr>      <int>          <int>
    # 1 N14228       517            515
    # 2 N24211       533            529
    # 3 N619AA       542            540
  • {{ }}curly-curly operator):用于传递数据变量参数
复制代码
    mean_by <- function(data, by, var) {
      data %>%
    group_by({{ by }}) %>%
    summarise(avg = mean({{ var }}, na.rm = TRUE))
    }
    mean_by(flights, by = month, var = dep_delay) %>%
      slice_head(n = 3)
    # # A tibble: 3 × 2
    #   month   avg
    #   <int> <dbl>
    # 1     1  10.0
    # 2     2  10.8
    # 3     3  13.2

上下文表达式

与上下文环境相关联的函数的主要功能是获取当前分组或变量信息,并涵盖应用于 summarisemutate 函数。

函数 功能
n 当前分组大小
cur_data 当前分组的数据(不包括分组变量)
cur_data_all 当前分组的数据(包括分组变量)
cur_group 当前分组的键,为一行一列的 tibble
cur_group_id 当前分组的唯一数值标识符
cur_group_rows 当前分组的行索引
cur_column 当前列的名称,只能在 across 中使用

获取当前分组的数据

复制代码
    set.seed(1234)
    df <- iris %>%
      select(starts_with("Sepal"), Species) %>%
      slice_sample(n = 10) %>%
      group_by(Species)
    
    df %>% summarise(n = n())
    # # A tibble: 3 × 2
    #   Species        n
    #   <fct>      <int>
    # 1 setosa         3
    # 2 versicolor     4
    # 3 virginica      3
    df %>% summarise(data = list(cur_data()))
    # # A tibble: 3 × 2
    #   Species    data            
    #   <fct>      <list>          
    # 1 setosa     <tibble [3 × 2]>
    # 2 versicolor <tibble [4 × 2]>
    # 3 virginica  <tibble [3 × 2]>
    df %>% summarise(data = list(cur_data_all()))
    # # A tibble: 3 × 2
    #   Species    data            
    #   <fct>      <list>          
    # 1 setosa     <tibble [3 × 3]>
    # 2 versicolor <tibble [4 × 3]>
    # 3 virginica  <tibble [3 × 3]>
    df %>% summarise(data = list(cur_group()))
    # # A tibble: 3 × 2
    #   Species    data            
    #   <fct>      <list>          
    # 1 setosa     <tibble [1 × 1]>
    # 2 versicolor <tibble [1 × 1]>
    # 3 virginica  <tibble [1 × 1]>

添加分组 ID 和分组数据所在的行索引

复制代码
    df %>% mutate(rows = cur_group_id())
    # # A tibble: 10 × 4
    # # Groups:   Species [3]
    #    Sepal.Length Sepal.Width Species     rows
    #           <dbl>       <dbl> <fct>      <int>
    #  1          5.5         2.5 versicolor     2
    #  2          5.6         2.5 versicolor     2
    #  3          6           2.9 versicolor     2
    #  4          6.4         3.2 virginica      3
    #  5          4.3         3   setosa         1
    #  6          7.2         3.2 virginica      3
    #  7          5.9         3   versicolor     2
    #  8          4.6         3.1 setosa         1
    #  9          7.9         3.8 virginica      3
    # 10          5.1         3.4 setosa         1
    df %>% summarise(rows = cur_group_rows())
    # `summarise()` has grouped output by 'Species'. You can override using the `.groups` argument.
    # # A tibble: 10 × 2
    # # Groups:   Species [3]
    #    Species     rows
    #    <fct>      <int>
    #  1 setosa         5
    #  2 setosa         8
    #  3 setosa        10
    #  4 versicolor     1
    #  5 versicolor     2
    #  6 versicolor     3
    #  7 versicolor     7
    #  8 virginica      4
    #  9 virginica      6
    # 10 virginica      9

获取当前列的名称

复制代码
    cols <- c(Sepal.Length = "SL", Sepal.Width = "SW")
    df %>% mutate(across(everything(), ~ paste(cols[cur_column()], .x,  sep = ":")))
    # # A tibble: 10 × 3
    # # Groups:   Species [3]
    #    Sepal.Length Sepal.Width Species   
    #    <chr>        <chr>       <fct>     
    #  1 SL:5.5       SW:2.5      versicolor
    #  2 SL:5.6       SW:2.5      versicolor
    #  3 SL:6         SW:2.9      versicolor
    #  4 SL:6.4       SW:3.2      virginica 
    #  5 SL:4.3       SW:3        setosa    
    #  6 SL:7.2       SW:3.2      virginica 
    #  7 SL:5.9       SW:3        versicolor
    #  8 SL:4.6       SW:3.1      setosa    
    #  9 SL:7.9       SW:3.8      virginica 
    # 10 SL:5.1       SW:3.4      setosa

5. 应用函数

对数据的行列进行函数操作是一种常见的需求,在涉及行为基因型数据时,则具体表现为对包含样本特征的矩阵进行每行或每列的统计计算。例如,在一个包含样本特征的行为基因型矩阵中,需要计算每个样本对应的特征均值或每个基因的表现水平平均值。

如前所述,在之前的示例中

列函数(across

across 函数可以同时在多列应用一个或多个函数,例如,对于下面的代码

复制代码
    flights %>%
      group_by(tailnum) %>%
      summarise(
    dep_delay = mean(dep_delay),
    arr_delay = mean(arr_delay),
    air_time = mean(air_time)
      )

我们可以使用 across 将求函数应用到指定的列中,简化代码

复制代码
    flights %>%
      group_by(tailnum) %>%
      summarise(across(c(dep_delay, arr_delay, air_time), mean))

该函数的主要参数为

  • .cols: 与 .select() 函数相同的字段选择方式,在分组操作中不允许使用分组字段
  • .fns: 可以接受 null 值(直接返回 null)、单个函数或函数列表;其中的函数也可以是以 lambda 表达式的形式定义的(例如 purrr 包中的 lambda 函数)
  • ...: 表示其他额外的参数
  • .names: 使用 glue 类型命名规则生成列名,在此命名规则中 {.col} 表示选定的列名,《.fn》表示所使用的功能/变换;当仅应用一个功能/变换时,默认名称为 {.col};当应用多个功能/变换时,默认名称为 {.col}_{.fn}

该函数通常可应用于绝大多数动词函数;然而不能同时与 selectrename 运算符配合使用;这些运算符均采用相同的列选择规范。在实际操作中;它通常作为 summarise 的常用搭档出现;在分析数据时通常用于计算字符串类型字段中唯一的数值数量;这有助于提高数据汇总的效率和准确性。

复制代码
    flights %>%
      summarise(across(where(is.character), n_distinct))
    # # A tibble: 1 × 4
    #   carrier tailnum origin  dest
    #     <int>   <int>  <int> <int>
    # 1      16    4044      3   105

计算相同目的地包含多少不同的航班

复制代码
    flights %>%
      group_by(dest) %>%
      summarise(across(tailnum, n_distinct)) %>%
      slice_head(n = 5)
    # # A tibble: 5 × 2
    #   dest  tailnum
    #   <chr>   <int>
    # 1 ABQ       108
    # 2 ACK        58
    # 3 ALB       172
    # 4 ANC         6
    # 5 ATL      1180

应用多个函数

复制代码
    extremum <- list(
      min = ~min(.x, na.rm = TRUE), 
      max = ~max(.x, na.rm = TRUE)
    )
    var <- c("dep_time", "arr_time", "air_time")
    flights %>%
      summarise(
    across(all_of(var), extremum, .names = "{.fn}.{.col}"),
    )
    # # A tibble: 1 × 6
    #   min.dep_time max.dep_time min.arr_time max.arr_time min.air_time max.air_time
    #          <int>        <int>        <int>        <int>        <dbl>        <dbl>
    # 1            1         2400            1         2400           20          695

可以用多个应用单个函数的 across 函数,达到类似的效果

复制代码
    flights %>%
      summarise(
    across(all_of(var), ~min(.x, na.rm = TRUE), .names = "min.{.col}"),
    across(all_of(var), ~max(.x, na.rm = TRUE), .names = "max.{.col}")
    )
    # # A tibble: 1 × 6
    #   min.dep_time min.arr_time min.air_time max.dep_time max.arr_time max.air_time
    #          <int>        <int>        <dbl>        <int>        <int>        <dbl>
    # 1            1            1           20         2400         2400          695

然而,在后续函数处理过程中需要注意的是,在实际操作中可能会出现一个潜在的问题:当对数据进行处理时,“time” 结尾的列被特别关注,并可能导致对之前新创建的列也会进行相关处理。这会使得一些原本不需要参与的操作也被进行了转换。举个例子来说,在这种情况下,“time” 结尾的字段原本预计只生成 10 个指标列(即从某个字段名到 time字段名之间有十个字段),但实际情况却是指标数量意外地增长到了 15 个。

复制代码
    flights %>%
      summarise(
    across(ends_with("time"), extremum$min, .names = "min.{.col}"),
    across(ends_with("time"), extremum$max, .names = "max.{.col}")
      )
    # # A tibble: 1 × 15
    #   min.dep_time min.sched_dep_time min.arr_time min.sched_arr_time min.air_time
    #          <int>              <int>        <int>              <int>        <dbl>
    # 1            1                106            1                  1           20
    # # … with 10 more variables: max.dep_time <int>, max.sched_dep_time <int>,
    # #   max.arr_time <int>, max.sched_arr_time <int>, max.air_time <dbl>,
    # #   max.min.dep_time <int>, max.min.sched_dep_time <int>, max.min.arr_time <int>,
    # #   max.min.sched_arr_time <int>, max.min.air_time <dbl>

也可以改变变量的顺序,或者在选择列的时候排除不需要的列

复制代码
    flights %>%
      select(var) %>%
      summarise(n = n(), across(everything(), ~ sd(.x, na.rm = TRUE)))
    # # A tibble: 1 × 4
    #       n dep_time arr_time air_time
    #   <dbl>    <dbl>    <dbl>    <dbl>
    # 1    NA     488.     533.     93.7
    flights %>%
      select(var) %>%
      summarise(across(everything(), ~ sd(.x, na.rm = TRUE)), n = n())
    # # A tibble: 1 × 4
    #   dep_time arr_time air_time      n
    #      <dbl>    <dbl>    <dbl>  <int>
    # 1     488.     533.     93.7 336776
    flights %>%
      select(var) %>%
      summarise(n = n(), across(everything() & !n, ~ sd(.x, na.rm = TRUE)))
    # # A tibble: 1 × 4
    #        n dep_time arr_time air_time
    #    <int>    <dbl>    <dbl>    <dbl>
    # 1 336776     488.     533.     93.7

使用 relocate 可以更改列的位置

复制代码
    flights %>%
      summarise(
    across(all_of(var), extremum, .names = "{.fn}.{.col}"),
      ) %>%
      relocate(starts_with("min"))
    # # A tibble: 1 × 6
    #   min.dep_time min.arr_time min.air_time max.dep_time max.arr_time max.air_time
    #          <int>        <int>        <dbl>        <int>        <int>        <dbl>
    # 1            1            1           20         2400         2400          695

使用 cur_column 函数获取当前列名

复制代码
    fold <- list(dep_time = 1, arr_time = 2, air_time = 3)
    flights %>%
      select(var) %>%
      mutate(across(everything(), ~ .x * fold[[cur_column()]]))
    # # A tibble: 336,776 × 3
    #    dep_time arr_time air_time
    #       <dbl>    <dbl>    <dbl>
    #  1      517     1660      681
    #  2      533     1700      681
    #  3      542     1846      480

across 同样不能直接在 filter 函数中使用,因为它必须返回布尔值(boolean),所以有两个特别的函数可用

  • if_all:保留在选择的列均为真的行
复制代码
    flights %>%
      select(contains("delay")) %>%
      filter(if_all(everything(), ~ .x > 10)) %>%
      slice_head(n = 3)
    # # A tibble: 3 × 2
    #   dep_delay arr_delay
    #       <dbl>     <dbl>
    # 1        11        14
    # 2        24        12
    # 3        47        30
  • If_any:保留在选择的列中至少有一个为真的行
复制代码
    flights %>%
      select(contains("delay")) %>%
      filter(if_any(everything(), ~ .x > 10)) %>%
      slice_head(n = 3)
    # # A tibble: 3 × 2
    #   dep_delay arr_delay
    #       <dbl>     <dbl>
    # 1         2        11
    # 2         4        20
    # 3         2        33

across 函数能够替代带有的 _if_ 、_at_() 和 all 后缀的所有动词函数使用;因此,在实际应用中可以直接采用 across 函数完成相应的操作。

这两种方案之间可以实现相互转换。如果将旧代码迁移至 across 区域,则可实现相应的功能迁移。

  • 如果是 _if ,应将其包裹在 $where()$() 调用内部以实现条件筛选功能
    • 如果是 _at ,无需引用变量即可执行操作 ,其作用类似于选择特定列的功能
    • 如果是 _all ,直接使用 $everything()$() 即可完成所有列的操作

下面使用几个简单的例子来说明

复制代码
    df <- tribble(
      ~x1, ~x2, ~x3,
      1,  2,  3,
      4,  5,  6,
      7,  8,  9
    )
    # if
    df %>%
      mutate_if(is.numeric, mean, na.rm = TRUE)
    df %>%
      mutate(across(where(is.numeric), mean, na.rm = TRUE))
    # at
    df %>%
      mutate_at(vars(starts_with("x")), mean, na.rm = TRUE)
    df %>%
      mutate(across(starts_with("x"), mean, na.rm = TRUE))
    # all
    df %>%
      mutate_all(mean, na.rm = TRUE)
    df %>%
      mutate(across(everything(), mean, na.rm = TRUE))
    # 上面的输出结果都是下面这个样子
    # # A tibble: 3 × 3
    #      x1    x2    x3
    #   <dbl> <dbl> <dbl>
    # 1     4     5     6
    # 2     4     5     6
    # 3     4     5     6

行函数(rowwise

tidyverse 中的 tidy 原理视列为变量、行视为观察值,则我们可较为便捷地执行列操作、面对每条记录的操作则相对麻烦。该库通过 rowwise() 函数实现逐条处理逻辑、相当于一种特殊的 group_by 操作、它会将每个记录单独分组。如可计算每条记录数据的平均值。

复制代码
    set.seed(1234)
    df <- iris %>%
      select(!Species) %>%
      slice_sample(n = 10)
    
    df %>%
      rowwise() %>%
      mutate(m = mean(c(Sepal.Length, Sepal.Width, Petal.Length, Petal.Width)))
    # # A tibble: 5 × 5
    # # Rowwise: 
    #   Sepal.Length Sepal.Width Petal.Length Petal.Width     m
    #          <dbl>       <dbl>        <dbl>       <dbl> <dbl>
    # 1          5.8         2.6          4           1.2  3.4 
    # 2          5.6         2.8          4.9         2    3.82
    # 3          6.4         2.8          5.6         2.2  4.25
    # 4          6.7         3.1          4.4         1.4  3.9 
    # 5          7.7         2.8          6.7         2    4.8

这种方法仍然存在诸多不便,在编程过程中每次都需要逐一传递所有列名来完成计算。编程过程可能会显得异常单调乏味,在涉及大量列名的情况下尤其如此。值得庆幸的是还有一种专门适用于rowwise操作中的列选择功能叫做c_across其工作原理与across函数相同。对于上述情况我们也可以提供一个简明扼要的解决方案

复制代码
    df %>%
      rowwise() %>%
      mutate(m = mean(c_across()))
    # # A tibble: 5 × 5
    # # Rowwise: 
    #   Sepal.Length Sepal.Width Petal.Length Petal.Width     m
    #          <dbl>       <dbl>        <dbl>       <dbl> <dbl>
    # 1          5.8         2.6          4           1.2  3.4 
    # 2          5.6         2.8          4.9         2    3.82
    # 3          6.4         2.8          5.6         2.2  4.25
    # 4          6.7         3.1          4.4         1.4  3.9 
    # 5          7.7         2.8          6.7         2    4.8

该标识符可以通过 rowwise 这一机制进行输入,在调用 summarise 时会被记录下来。

复制代码
    iris %>%
      slice_sample(n = 3) %>%
      rowwise(Species) %>%
      summarise(m = mean(c_across(where(is.numeric))))
    # `summarise()` has grouped output by 'Species'. You can override using the `.groups` argument.
    # # A tibble: 3 × 2
    # # Groups:   Species [2]
    #   Species       m
    #   <fct>     <dbl>
    # 1 setosa     2.45
    # 2 virginica  4.55
    # 3 virginica  4.8

该函数对于 list 类型的列非常有用,例如

复制代码
    df <- iris %>%
      slice_sample(n = 10) %>%
      group_by(Species) %>%
      summarise(data = list(cur_data()))
    df
    # # A tibble: 3 × 2
    #   Species    data            
    #   <fct>      <list>          
    # 1 setosa     <tibble [1 × 4]>
    # 2 versicolor <tibble [6 × 4]>
    # 3 virginica  <tibble [3 × 4]>

为了获取数据列的长度,在代码中可以直接调用内置函数 length 来完成这一操作。通过内置函数 length 计算数据列的长度。此外借助 rowwise 方法将函数作用于每行数据,则可以得到准确的结果。

复制代码
    df %>%
      mutate(l = length(data))
    # # A tibble: 3 × 3
    #   Species    data                 l
    #   <fct>      <list>           <int>
    # 1 setosa     <tibble [1 × 4]>     3
    # 2 versicolor <tibble [6 × 4]>     3
    # 3 virginica  <tibble [3 × 4]>     3
    df %>%
      rowwise() %>%
      mutate(l = length(data))
    # # A tibble: 3 × 3
    #   Species    data                 l
    #   <fct>      <list>           <int>
    # 1 setosa     <tibble [1 × 4]>     4
    # 2 versicolor <tibble [6 × 4]>     4
    # 3 virginica  <tibble [3 × 4]>     4
    df %>%
      rowwise() %>%
      mutate(l = dim(data)[1])
    # # A tibble: 3 × 3
    # # Rowwise: 
    #   Species    data                 l
    #   <fct>      <list>           <int>
    # 1 setosa     <tibble [1 × 4]>     1
    # 2 versicolor <tibble [6 × 4]>     6
    # 3 virginica  <tibble [3 × 4]>     3

rowwise 在分组建模方面具有便利性,在调用 nest_by 函数生成嵌套数据框后所得结果仍以 rowwise 格式呈现其操作方式与 group_by 类似 但其输出数据结构存在差异

复制代码
    mtcars %>% 
      nest_by(cyl)
    # # A tibble: 3 × 2
    # # Rowwise:  cyl
    #     cyl                data
    #   <dbl> <list<tibble[,10]>>
    # 1     4           [11 × 10]
    # 2     6            [7 × 10]
    # 3     8           [14 × 10]

对每个 cyl 分组的 mpg 列和 wt 列进行线性关系建模,并对数据做出预测

复制代码
    models <- mtcars %>% nest_by(cyl) %>%
      mutate(mod = list(lm(mpg ~ wt, data = data))) %>%
      mutate(pred = list(predict(mod, data)))
    # # A tibble: 3 × 4
    # # Rowwise:  cyl
    #     cyl                data mod    pred      
    #   <dbl> <list<tibble[,10]>> <list> <list>    
    # 1     4           [11 × 10] <lm>   <dbl [11]>
    # 2     6            [7 × 10] <lm>   <dbl [7]> 
    # 3     8           [14 × 10] <lm>   <dbl [14]>

使用 broom 包的 glance 函数来计算模型的各种指标

复制代码
    models %>% summarise(broom::glance(mod))
    # `summarise()` has grouped output by 'cyl'. You can override using the `.groups` argument.
    # # A tibble: 3 × 13
    # # Groups:   cyl [3]
    #     cyl r.squared adj.r.squared sigma statistic p.value    df logLik   AIC   BIC
    #   <dbl>     <dbl>         <dbl> <dbl>     <dbl>   <dbl> <dbl>  <dbl> <dbl> <dbl>
    # 1     4     0.509         0.454  3.33      9.32  0.0137     1 -27.7   61.5  62.7
    # 2     6     0.465         0.357  1.17      4.34  0.0918     1  -9.83  25.7  25.5
    # 3     8     0.423         0.375  2.02      8.80  0.0118     1 -28.7   63.3  65.2
    # # … with 3 more variables: deviance <dbl>, df.residual <int>, nobs <int>

事实上,在这些行中执行的函数操作都可以通过 purrr 包内部提供的函数来完成。关于该包的具体使用方法,我们将稍后进行讲解。

分组(group_by

除了在行和列上使用函数外,在对数据进行分组(即使用group_by)时也可以对每个分组应用相应的功能。之前我们还介绍了如何将此功能与汇总计算相结合;然而需要注意的是,并非所有的分组操作仅限于统计计算,在某些场景下还可以执行其他多种数据处理任务。

提取每个分组中 Sepal.Length 最大的数据行

复制代码
    group_by(iris, Species) %>%
      filter(Sepal.Length == max(Sepal.Length))
    # # A tibble: 3 × 5
    # # Groups:   Species [3]
    #   Sepal.Length Sepal.Width Petal.Length Petal.Width Species   
    #          <dbl>       <dbl>        <dbl>       <dbl> <fct>     
    # 1          5.8         4            1.2         0.2 setosa    
    # 2          7           3.2          4.7         1.4 versicolor
    # 3          7.9         3.8          6.4         2   virginica

对每个分组的数据进行标准化

复制代码
    group_by(iris, Species) %>%
      mutate(across(everything(), ~ (.x - mean(.x))/ sd(.x))) %>%
      head(3)
    # # A tibble: 3 × 5
    # # Groups:   Species [1]
    #   Sepal.Length Sepal.Width Petal.Length Petal.Width Species
    #          <dbl>       <dbl>        <dbl>       <dbl> <fct>  
    # 1        0.267       0.190       -0.357      -0.436 setosa 
    # 2       -0.301      -1.13        -0.357      -0.436 setosa 
    # 3       -0.868      -0.601       -0.933      -0.436 setosa

除了这三个函数之外,还有其他三个函数可用于对分组数据应用功能;它们基本上都是按分组对每个子集执行操作,并且主要区别在于返回值的具体类型。

group_map:执行函数并返回 list

该函数将被运行,并生成 tibble 结果;其中需要注意的是该函数的所有输出结果也必须是 tibble

group_walk:该函数实现了某种功能,并且不会打印输入数据但可以将它们赋值给变量。

复制代码
    iris %>%
      group_by(Species) %>%
      group_map(~ fivenum(.x$Sepal.Width))
    # [[1]]
    # [1] 2.3 3.2 3.4 3.7 4.4
    # [[2]]
    # [1] 2.0 2.5 2.8 3.0 3.4
    # [[3]]
    # [1] 2.2 2.8 3.0 3.2 3.8
    iris %>%
      group_by(Species) %>%
      group_modify(~ {
    fivenum(iris$Sepal.Width) %>%
      purrr::set_names(c("min", "Q1", "median", "Q3", "max")) %>%
      as.data.frame() %>% t() %>% as_tibble()
      })
    # # A tibble: 3 × 6
    # # Groups:   Species [3]
    #   Species      min    Q1 median    Q3   max
    #   <fct>      <dbl> <dbl>  <dbl> <dbl> <dbl>
    # 1 setosa         2   2.8      3   3.3   4.4
    # 2 versicolor     2   2.8      3   3.3   4.4
    # 3 virginica      2   2.8      3   3.3   4.4
    iris %>%
      group_by(Species) %>%
      group_walk(~ print(fivenum(.x$Sepal.Width)))
    # [1] 2.3 3.2 3.4 3.7 4.4
    # [1] 2.0 2.5 2.8 3.0 3.4
    # [1] 2.2 2.8 3.0 3.2 3.8

借助这些函数能够实现更为复杂的分组计算,并非局限于原始数据形态的限制,在此基础之上提供更多样化的选择。

6. 关系型数据

在数据分析工作中, 除了处理单一表格外, 有时还需要根据各表格间的特定联系整合数据. 这种基于特定联系形成的关系型多表格集合可称为关系型数据. 其核心关注点在于各表格间的联系, 这里的许多概念与 SQL 关系型数据库具有相似之处.

主键与外键

我们首先需要了解一下用于合并数据的键是什么,主要包括两种键:

主键:在自己的表中唯一标识每一个观察值。

外键:另一个表的主键。

当一个表的主键被用作另一个表的外键时,则会建立一种映射关系。这种情形通常表现为'一对多'模式。例如,在航空业中,'航班'与'飞机'之间的联系即属于这种模式:每个航班只能与一架飞机相关联,而每架飞机则可承担多个航班的任务。我们也能发现一些一对一的情况,这可以视为'一对 多'模式的一个特殊情况。通过建立' 多对一'和' 一对 多'的关系网络,则能够实现' 多对 多'连接的构建。例如,'航空公司'与 '机场 '之间的联系就是一个典型的 ' 多对 多 '例子:每家航空公司会在多个机场部署飞机,而每个机场也会拥有多家不同的航空公司。

在关系型数据库中,这个方法仅限于特定场景的应用.然而,我们处理的数据类型多样,有些表并未明确指明哪列为外码.有时可能需要用多列字段组合作为外码标识,或者某些情况下完全缺乏外码字段.对于缺乏相应外码字段的数据实体,可以通过 mutate+row_number 方法在数据中自动生成并命名一个代理外码字段.

我们还是使用 nycflights13 包的数据,包含 4 个与 flights 表相关的数据

  • 将航空公司的代码映射为其全称。
  • 通过 faa 机场代码提供的信息。
  • 记录了每架飞机的基本信息。
  • 以各自的 tailnum 作为标识符。
  • 记录了纽约市各个机场在每一小时内的天气状况。

用于处理关系型数据的函数主要包括

函数 功能 函数 功能
inner_join 内连接 left_join 左连接
right_join 右连接 full_join 全连接
semi_join 半连接 anti_join 反连接

可变连接

灵活整合两套数据集中的信息的可变连接操作能够实现多源数据的融合功能

复制代码
    set.seed(1234)
    data <- flights %>% select(origin, dest, tailnum, carrier) %>%
      slice_sample(n = 1000)
    head(data, 3)
    # # A tibble: 3 × 4
    #   origin dest  tailnum carrier
    #   <chr>  <chr> <chr>   <chr>  
    # 1 EWR    LAS   N447UA  UA     
    # 2 LGA    MCO   N373NW  DL     
    # 3 EWR    SNA   N38727  UA

我们获取的这个表中所有列都是外键

内连接(inner_join):它仅在键相等的情况下将对应的行进行匹配,并将这些匹配到的行组合到一起形成新的结果集;不会有相应的行出现在最终的输出结果中;如图所示

在这里插入图片描述

具体来说,在内部采用了等于运算符来匹配键。这一操作被称为等值连接操作。其输出结果形成一个新的数据框,
其中包含了键字段、x字段和y字段。
通过设置参数by,
我们可以指定该连接操作所基于的关键字是什么列名称。
比如指定by参数为某一列名称。

复制代码
    x <- tibble(
      key = 1:3,
      x = paste0("x", 1:3)
    )
    y <- tibble(
      key = c(1, 2, 4),
      y = paste0("y", 1:3)
    )
    x %>% inner_join(y, by = "key")
    # # A tibble: 2 × 3
    #     key x     y    
    #   <dbl> <chr> <chr>
    # 1     1 x1    y1   
    # 2     2 x2    y2

我们使用该函数来连接 dataairlines 两个表,获取航空公司的全名

复制代码
    data %>% 
      inner_join(airlines, by = "carrier") %>%
      slice_head(n = 3)
    # # A tibble: 3 × 5
    #   origin dest  tailnum carrier name                 
    #   <chr>  <chr> <chr>   <chr>   <chr>                
    # 1 EWR    LAS   N447UA  UA      United Air Lines Inc.
    # 2 LGA    MCO   N373NW  DL      Delta Air Lines Inc. 
    # 3 EWR    SNA   N38727  UA      United Air Lines Inc.

该操作在airlines表中将name字段追加到data字段前面,并这也是我们将其命名为可变连接的主要原因。当然地,在R语言中通过调用mutate函数并提取相应子集也能达到相同的效果。

复制代码
    data %>%
      mutate(name = airlines$name[match(carrier, airlines$carrier)]) %>%
      slice_head(n = 3)
    # # A tibble: 3 × 5
    #   origin dest  tailnum carrier name                 
    #   <chr>  <chr> <chr>   <chr>   <chr>                
    # 1 EWR    LAS   N447UA  UA      United Air Lines Inc.
    # 2 LGA    MCO   N373NW  DL      Delta Air Lines Inc. 
    # 3 EWR    SNA   N38727  UA      United Air Lines Inc.

内连接要求键共同存在于两个表中,在外连接操作中会包含至少存在于其中一个表中的键,并包含三种类型的外连接方式

左连接(left_join)操作包含第一个表的所有信息,并将键值一致的部分追加到最后面的位置。对于无法匹配的部分则标记为 NA 值。以下图为例

在这里插入图片描述
复制代码
    x %>% left_join(y, by = "key")
    # # A tibble: 3 × 3
    #     key x     y    
    #   <dbl> <chr> <chr>
    # 1     1 x1    y1   
    # 2     2 x2    y2   
    # 3     3 x3    NA

planes 表中的飞机信息添加到 data 后面

复制代码
    data %>% 
      left_join(planes, by = "tailnum") %>%
      select(1:7) %>%
      slice_head(n = 3)
    # # A tibble: 3 × 7
    #   origin dest  tailnum carrier  year type                    manufacturer    
    #   <chr>  <chr> <chr>   <chr>   <int> <chr>                   <chr>           
    # 1 EWR    LAS   N447UA  UA       1998 Fixed wing multi engine AIRBUS INDUSTRIE
    # 2 LGA    MCO   N373NW  DL       2001 Fixed wing multi engine AIRBUS INDUSTRIE
    # 3 EWR    SNA   N38727  UA       1999 Fixed wing multi engine BOEING
  • 右连接(right_join):类似 left_join, 但保留的是第二个表的所有值,如下图
在这里插入图片描述
复制代码
    x %>% right_join(y, by = "key")
    # # A tibble: 3 × 3
    #     key x     y    
    #   <dbl> <chr> <chr>
    # 1     1 x1    y1   
    # 2     2 x2    y2   
    # 3     4 NA    y3

data 表与 airports 进行右连接,获取出发地的信息

复制代码
    data %>% 
      right_join(airports, by = c("origin" = "faa")) %>%
      select(1:10) %>% slice_head(n = 3)
    # # A tibble: 3 × 10
    #   origin dest  tailnum carrier name                  lat   lon   alt    tz dst  
    #   <chr>  <chr> <chr>   <chr>   <chr>               <dbl> <dbl> <dbl> <dbl> <chr>
    # 1 EWR    LAS   N447UA  UA      Newark Liberty Intl  40.7 -74.2    18    -5 A    
    # 2 LGA    MCO   N373NW  DL      La Guardia           40.8 -73.9    22    -5 A    
    # 3 EWR    SNA   N38727  UA      Newark Liberty Intl  40.7 -74.2    18    -5 A
  • 全连接(full_join): 保留两个表的所有值,匹配的键会合并在一起,不匹配的将会添加 NA 值,如下图
在这里插入图片描述
复制代码
    x %>% full_join(y, by = "key")
    # # A tibble: 4 × 3
    #     key x     y    
    #   <dbl> <chr> <chr>
    # 1     1 x1    y1   
    # 2     2 x2    y2   
    # 3     3 x3    NA   
    # 4     4 NA    y3

在实际应用中,左连接是一个非常常用的连接方式,在不同的场景和时间段都能发挥其优势。无论是从哪个表中进行查询和获取关联数据时,在任何情况下都可以考虑使用此选项。这是因为即使在查询过程中没有找到匹配项时,默认也会将原始观测值保留在结果中。

在上面的例子中,当指定的键不唯一时会出现什么问题?我们可以将其分为两种情形来分析。

  • 其中一个表中存在重复的值,将会进行对重复值匹配两次
在这里插入图片描述
复制代码
    x <- tibble(
      key = c(1, 2, 2, 1),
      x = paste0("x", 1:4)
    )
    y <- tibble(
      key = c(1, 2),
      y = paste0("y", 1:2)
    )
    left_join(x, y, by = "key")
    # # A tibble: 4 × 3
    #     key x     y    
    #   <dbl> <chr> <chr>
    # 1     1 x1    y1   
    # 2     2 x2    y2   
    # 3     2 x3    y2   
    # 4     1 x4    y1

由于在两个表中键都无法唯一标识观测值,在这种情况下连接后会产生所有可能的组合即笛卡尔积往往会导致问题

在这里插入图片描述
复制代码
    x <- tibble(
      key = c(1, 2, 2, 3),
      x = paste0("x", 1:4)
    )
    y <- tibble(
      key = c(1, 2, 2, 4),
      y = paste0("y", 1:4)
    )
    # # A tibble: 7 × 3
    #     key x     y    
    #   <dbl> <chr> <chr>
    # 1     1 x1    y1   
    # 2     2 x2    y2   
    # 3     2 x2    y3   
    # 4     2 x3    y2   
    # 5     2 x3    y3   
    # 6     3 x4    NA   
    # 7     4 NA    y4

在前面的例子中,在通过一个键将两个表连接起来的情况下, 具体来说包括以下几种

  • 当设置为by=NULL(默认值)时,则会采用两个表中共同存在的字段进行自然连接。
  • 当设置为by='x'时,则会指定分隔符为单个字段名。
  • 当设置为by=c('a'='b')时,则会采用通过名称指代的方式进行匹配。

这些连接都可以通过 base::merge() 实现,其对应关系为

dplyr merge
inner_join(x, y) merge(x, y)
left_join(x, y) merge(x, y, all.x = TRUE)
right_join(x, y) merge(x, y, all.y = TRUE)
full_join(x, y) merge(x, y, all.x = TRUE, all.y = TRUE)
过滤连接

过滤连接与可变连接计算结果的方法是一样的;其中一个是对数据进行整合处理的过程;另一个则是对数据进行筛选处理的过程;两者之间存在对立的应用方式

  • semi_join(x, y):保留 x 中所有与 y 匹配的观察值,如下图
在这里插入图片描述
复制代码
    x <- tibble(
      key = c(1, 2, 3),
      x = paste0("x", 1:3)
    )
    y <- tibble(
      key = c(1, 2, 4),
      y = paste0("y", 1:3)
    )
    semi_join(x, y)
    # Joining, by = "key"
    # # A tibble: 2 × 2
    #     key x    
    #   <dbl> <chr>
    # 1     1 x1   
    # 2     2 x2

而当处理具有重复键的情况时也类似地进行操作相当于从两个表中提取共同的键并将这些键对应的第一张表中的行输出

在这里插入图片描述
复制代码
    x <- tibble(
      key = c(1, 2, 2, 3),
      x = paste0("x", 1:4)
    )
    y <- tibble(
      key = c(1, 2, 2, 4),
      y = paste0("y", 1:4)
    )
    semi_join(x, y)
    # Joining, by = "key"
    # # A tibble: 3 × 2
    #     key x    
    #   <dbl> <chr>
    # 1     1 x1   
    # 2     2 x2   
    # 3     2 x3

获取飞行次数最多的三架飞机的飞行记录

复制代码
    flights %>%
      filter(!is.na(tailnum)) %>%
      count(tailnum) %>%
      slice_max(order_by = n, n = 3) %>%
      semi_join(flights, .) %>%
      `[`(1:3, 1:5)
    # Joining, by = "tailnum"
    # # A tibble: 3 × 5
    #    year month   day dep_time sched_dep_time
    #   <int> <int> <int>    <int>          <int>
    # 1  2013     1     1      656            705
    # 2  2013     1     1      832            840
    # 3  2013     1     1     1157           1205
  • anti_join(x, y):删除 x 中所有与 y 匹配的观察值,如下图
在这里插入图片描述
复制代码
    x <- tibble(
      key = c(1, 2, 3),
      x = paste0("x", 1:3)
    )
    y <- tibble(
      key = c(1, 2, 4),
      y = paste0("y", 1:3)
    )
    anti_join(x, y)
    # Joining, by = "key"
    # # A tibble: 1 × 2
    #     key x    
    #   <dbl> <chr>
    # 1     3 x3

删除飞行次数少于 500 的飞机的飞行记录

复制代码
    flights %>%
      filter(!is.na(tailnum)) %>%
      count(tailnum) %>%
      filter(n < 500) %>%
      anti_join(flights, .) %>%
      `[`(1:3, 1:5)
    # Joining, by = "tailnum"
    # # A tibble: 3 × 5
    #    year month   day dep_time sched_dep_time
    #   <int> <int> <int>    <int>          <int>
    # 1  2013     1     1      656            705
    # 2  2013     1     1      832            840
    # 3  2013     1     1     1157           1205

前面的示例中所使用的数据都经过了清洗处理,并具有较为清晰的主键和外键结构。然而,在你的数据集中可能不够规范。因此建议首先对自身数据集进行清洗处理。例如

首先,要确定每个表中的主键和外键

确保主键中没有任何缺失值

检查您的外键是否与另一个表中的主键匹配,主要是名称的对应关系

集合操作

除了之前所述的关系之外,在分析数据集之间的重叠情况是否存在时主要涉及哪些方面

函数 intersect(x, y) 计算 x 与 y 的共同元素集合。
函数 union(x, y) 汇总 x 和 y 所有元素的总和。
函数 setdiff(x, y) 计算 x 中不属于 y 的元素集合。

集合操作必须满足两个数据表具有相同的字段名称,并且这些字段必须匹配对应的位置和类型。针对以下数据集

复制代码
    a <- tribble(
      ~x, ~y,
      1,  1,
      2,  1,
      3, 2
    )
    b <- tribble(
      ~x, ~y,
      1,  1,
      2,  2,
      3, 2
    )

我们可以应用集合操作

复制代码
    intersect(a, b)
    # # A tibble: 2 × 2
    #       x     y
    #   <dbl> <dbl>
    # 1     1     1
    # 2     3     2
    union(a, b)
    # # A tibble: 4 × 2
    #       x     y
    #   <dbl> <dbl>
    # 1     1     1
    # 2     2     1
    # 3     3     2
    # 4     2     2
    setdiff(a, b)
    # # A tibble: 1 × 2
    #       x     y
    #   <dbl> <dbl>
    # 1     2     1
    setdiff(b, a)
    # # A tibble: 1 × 2
    #       x     y
    #   <dbl> <dbl>
    # 1     2     2

7. 数据清理

如何以简洁明了且结构清晰的方式呈现杂乱无章的数据呢?这不仅要求我们具备对数据有清晰认识的能力,并且在数据转换操作方面也有较强的能力。 tidyverse 中的重要核心包 tidyr 就是为此目的而设计的,在其丰富的一系列工具函数中能够帮助我们将复杂的数据整理得条理分明。

复制代码
    library(tidyr)  # version 1.1.4

数据的整洁之道主要有下面三个相互关联的规则

  1. 每个变量为一列
  2. 行表示每个观察值
  3. 每个值必须在单元格内

确保数据干净整洁,并使数据结构更加明确以便于梳理各变量间的关联关系;同时将每一个变量独立成列为一列以充分体现R语言的强大向量化能力

确保数据干净整洁,并使数据结构更加明确以便于梳理各变量间的关联关系;同时将每一个变量独立成列为一列以充分体现R语言的强大向量化能力

数据透视表

在真实的数据处理过程中,在于明确具体有哪些变量及其观察值的本质属性是什么。为了实现这一目标,在于全面掌握其基本特征,并根据实际需求进行筛选和优化处理。

处理数据时,经常会遇到下面两个问题

  • 数据中有一个变量分布在多列上
  • 一个观测值分散在多行中

tidyr 包含了两个核心功能:即 pivot_longerpivot_wider, 这些功能旨在应对上述两种常见场景

举个例子,在这个包中有一个表 table4a ,它的列名分别是1999和2000年份数据。这些列分别代表了各年份对应的 cases 变量数值,并且每条记录包含两个变量的数据信息而非单一数据点。

复制代码
    table4a
    # # A tibble: 3 x 3
    #   country     `1999` `2000`
    #   <chr>        <dbl>  <dbl>
    # 1 Afghanistan    745   2666
    # 2 Brazil       37737  80488
    # 3 China       212258 213766

为了整理这类数据集,在处理过程中需要将有问题的列拆分到一对新的变量中,并对这一过程进行详细描述,则需要三个参数来完成这一操作。

  • 代表变量值的字段名称,在本案例中涉及的是1999年和2000年的数据
  • 将这些字段重命名为新的字段名称(new field name)
  • 将数值移动至指定的目标字段(target field),以便于后续的数据管理

找到上面这些变量,并将其传递到 pivot_longer 函数中即可实现转换

复制代码
    table4a %>% 
    pivot_longer(c(`1999`, `2000`), names_to = "year", values_to = "cases")
    # # A tibble: 6 x 3
    #   country     year   cases
    #   <chr>       <chr>  <dbl>
    # 1 Afghanistan 1999     745
    # 2 Afghanistan 2000    2666
    # 3 Brazil      1999   37737
    # 4 Brazil      2000   80488
    # 5 China       1999  212258
    # 6 China       2000  213766

可以通过 dplyr::select 语法来指定字段名。然而,在当前情况下仅包含两列。因此需要将这两项单独提取出来。由于新的字段名称尚未被定义,因此我们需要为这些字段名称加上双引号以避免混淆。

pivot_longer 函数通过将行数增加、列数减少从而扩大数据规模,并将两列被整合为单列以节省空间;值得注意的是,在这种情况下'长'通常指的是相对于原始数据而言;其中两列被整合为单列,并且另一列为原有字段名称提供存储空间。

还有一个数据框 table4b 也是类似的格式,但是存储的是人口变量的值

复制代码
    var <- c("1999", "2000")
    a <- table4a %>% 
      pivot_longer(var, names_to = "year", values_to = "cases")
    table4b %>% 
      pivot_longer(var, names_to = "year", values_to = "population") %>%
      left_join(a)
    # Joining, by = c("country", "year")
    # # A tibble: 6 x 4
    #   country     year   cases population
    #   <chr>       <chr>  <dbl>      <int>
    # 1 Afghanistan 1999     745   19987071
    # 2 Afghanistan 2000    2666   20595360
    # 3 Brazil      1999   37737  172006362
    # 4 Brazil      2000   80488  174504898
    # 5 China       1999  212258 1272915272
    # 6 China       2000  213766 1280428583

pivot_wilderpivot_longer 运算的逆向过程,在某一列的数据能够分解为多个变量时,则可运用此方法将行数据转换为列数据。例如,在 table2 中 type 观察值主要分布在国家和地区上

复制代码
    table2
    # # A tibble: 12 x 4
    #    country      year type            count
    #    <chr>       <int> <chr>           <int>
    #  1 Afghanistan  1999 cases             745
    #  2 Afghanistan  1999 population   19987071
    #  3 Afghanistan  2000 cases            2666
    #  4 Afghanistan  2000 population   20595360
    #  5 Brazil       1999 cases           37737
    #  6 Brazil       1999 population  172006362
    #  7 Brazil       2000 cases           80488
    #  8 Brazil       2000 population  174504898
    #  9 China        1999 cases          212258
    # 10 China        1999 population 1272915272
    # 11 China        2000 cases          213766
    # 12 China        2000 population 1280428583

这里我们只要设置两个参数

  • 将数值字段转为字段名的列,在这里是 type
  • 将数值字段转为字段值的列,在这里是 count

一旦我们弄明白了这一点,我们就可以使用 pivot_wilder

复制代码
    table2 %>% pivot_wider(names_from = type, values_from = count)
    # # A tibble: 6 x 4
    #   country      year  cases population
    #   <chr>       <int>  <int>      <int>
    # 1 Afghanistan  1999    745   19987071
    # 2 Afghanistan  2000   2666   20595360
    # 3 Brazil       1999  37737  172006362
    # 4 Brazil       2000  80488  174504898
    # 5 China        1999 212258 1272915272
    # 6 China        2000 213766 1280428583

pivot_wider 将一列数据转换成多列,使长表变短和变宽。

嵌套数据框

嵌套式数据框是指单列或多列为由数据框列表构成的数据框。我们之前也接触过类似的数据类型,并且也可以通过手动操作来生成这些结构。

复制代码
    tibble(
      a = c(1, 2, 3),
      data = list(
    tibble(x = 1),
    tibble(x = 3:5, y = 6:8),
    tibble(x = 1:10)
      )
    )
    # # A tibble: 3 × 2
    #       a data             
    #   <dbl> <list>           
    # 1     1 <tibble [1 × 1]> 
    # 2     2 <tibble [3 × 2]> 
    # 3     3 <tibble [10 × 1]>

采用 nest 函数进行数据嵌套设置,在函数体内定义需要嵌套的具体数据内容。该操作会依据非键列的值对数据进行分组处理。

复制代码
    df <- tibble(
      a = c(1, 2, 2, 3),
      b = 4:7,
      c = c(1, 6, 9, 4)
    )
    nest(df, data = c(b, c))
    # # A tibble: 3 × 2
    #       a data            
    #   <dbl> <list>          
    # 1     1 <tibble [1 × 2]>
    # 2     2 <tibble [2 × 2]>
    # 3     3 <tibble [1 × 2]>

或者使用 group_by 来创建

复制代码
    group_by(df, a) %>%
      nest()
    # # A tibble: 3 × 2
    #       a data            
    #   <dbl> <list>          
    # 1     1 <tibble [1 × 2]>
    # 2     2 <tibble [2 × 2]>
    # 3     3 <tibble [1 × 2]>

嵌套列表可以用于分组数据建模

复制代码
    mtcars %>% 
      group_by(cyl) %>% 
      nest() %>%
      mutate(model = lapply(data, function(df) lm(mpg ~ wt, data = df)))
    # # A tibble: 3 × 3
    # # Groups:   cyl [3]
    #     cyl data               model 
    #   <dbl> <list>             <list>
    # 1     6 <tibble [7 × 10]>  <lm>  
    # 2     4 <tibble [11 × 10]> <lm>  
    # 3     8 <tibble [14 × 10]> <lm>

unnest 函数是 nest 的逆操作,可以将指定的数据框列表列按行合并起来

复制代码
    nest(df, data = c(b, c)) %>%
      unnest(data)
    # # A tibble: 4 × 3
    #       a     b     c
    #   <dbl> <int> <dbl>
    # 1     1     4     1
    # 2     2     5     6
    # 3     2     6     9
    # 4     3     7     4

其他几个函数同样能够用来从嵌套在数据框中的数据中提取信息,并且这些格式通常被存储在JSON或XML格式中

unnest_longer:将嵌套数据框列中的每个元素提取出来并创建一个新行

unnest_wider:将嵌套数据框列中的每个元素提取出来并创建一个新列

unnest_auto:猜测你想要使用的是 unnest_longer 还是 unnest_wider

hoist:类似于 unnest_wider,但是只返回选择的列

例如,对于如下数据

复制代码
    df <- tibble(
      character = c("student", "china"),
      person = list(
    list(
      name = "Peach",
      age = 19,
      hobby = c(
        "basketball",
        "reading",
        "movie"
      )
    ),
    list(
      name = "Mario",
      age = 20,
      hobby = c("music", "play game")
    )
      )
    )
    df
    # # A tibble: 2 × 2
    #   character person          
    #   <chr>     <list>          
    # 1 student   <named list [3]>
    # 2 china     <named list [3]>

我们可以通过获取 person 数据来新增一列。系统默认地解析了所有的列,并且同时会自动地将长度为1的向量转换为对应的值。

复制代码
    df %>% unnest_wider(person)
    # # A tibble: 2 × 4
    #   character name    age hobby    
    #   <chr>     <chr> <dbl> <list>   
    # 1 student   Peach    19 <chr [3]>
    # 2 china     Mario    20 <chr [2]>
    df %>% unnest_wider(person, simplify = FALSE)
    # # A tibble: 2 × 4
    #   character name      age       hobby    
    #   <chr>     <list>    <list>    <list>   
    # 1 student   <chr [1]> <dbl [1]> <chr [3]>
    # 2 china     <chr [1]> <dbl [1]> <chr [2]>

解析嵌套列表中的各项值至外部数据区域,则其余字段的数据会随之复制对应次数的数量。

复制代码
    df %>%
      unnest_wider(person) %>%
      unnest_longer(hobby)
    # # A tibble: 5 × 4
    #   character name    age hobby     
    #   <chr>     <chr> <dbl> <chr>     
    # 1 student   Peach    19 basketball
    # 2 student   Peach    19 reading   
    # 3 student   Peach    19 movie     
    # 4 china     Mario    20 music     
    # 5 china     Mario    20 play game

使用 hoist 直接将最内层的数据提取出来

复制代码
    df %>% 
      hoist(
    person,
    "name",
    first_hobby = list("hobby", 1L),
    third_hobby = list("hobby", 3L)
    )
    # # A tibble: 2 × 5
    #   character name  first_hobby third_hobby person          
    #   <chr>     <chr> <chr>       <chr>       <list>          
    # 1 student   Peach basketball  movie       <named list [2]>
    # 2 china     Mario music       NA          <named list [2]>

字符串向量

有时会将多个变量整合到同一列表中,并以特定分隔符分隔开来;而我们的需求则相反:我们需要将多列合并为一;提供了一系列工具或函数功能可以让这一操作变得简单高效

  • extract:该操作通过分组正则表达式实现一个字段被拆分成多个字段,并且默认情况下会分离出字母和数字部分。
  • separate:采用正则表达式或位置索引方法来完成字段划分。
  • separate_rows:该功能将折叠的数据字段分解为独立的行。
  • unite:此功能通过字符串连接的方式将多列整合成一列。

例如,对于下面的数据框

复制代码
    df <- tibble(
      x = c(NA, "a,b", "c,d", "b,c", "d,e"),
      y = c("x:1", "x:2", "y:4:6", "z", NA)
    )
    # # A tibble: 5 × 4
    #   x     y
    #   <chr> <chr>
    # 1 NA    x:1
    # 2 a,b   x:2
    # 3 c,d   y:4:6
    # 4 b,c   z
    # 5 d,e   NA

通过 extract 函数基于正则表达式匹配从字符串值中提取所需数据,并将每个捕获组依次放置在目标列中

复制代码
    df %>% extract(x, c("A", "B"), "(\ w+),(\ w+)")
    # # A tibble: 5 × 5
    #   A     B     y
    #   <chr> <chr> <chr>
    # 1 NA    NA    x:1
    # 2 a     b     x:2
    # 3 c     d     y:4:6
    # 4 b     c     z
    # 5 d     e     NA

通过调用 separate 方法对字符串进行分割。若分割后的子字符串数量与预期的列数不符,则会触发警告提示。例如以下情况:

复制代码
    df %>% separate(y, into = c("key", "value"), sep = ":")
    # # A tibble: 5 × 5
    #   x     key   value
    #   <chr> <chr> <chr>
    # 1 NA    x     1
    # 2 a,b   x     2
    # 3 c,d   y     4
    # 4 b,c   z     NA
    # 5 d,e   NA    NA
    # Warning messages:
    # 1: Expected 2 pieces. Additional pieces discarded in 1 rows [3]. 
    # 2: Expected 2 pieces. Missing pieces filled with `NA` in 1 rows [4].

注意到出现了两个警示信息,在第三条和第四条指令中分别出现了异常情况。具体来说,在第三条指令中生成的字段数量有所增加,在第四条指令中则减少了。为了应对这些变化情况,可以通过设置两个不同的参数来定义应对策略。

  • extra:当数值超出范围时进行设置以处理多余的数值情况,默认情况下会发出警告信息并选择性地将其删除或合并到其他数据中。
    • fill:在数据不足的情况下进行设置以定义缺失值的填充策略,默认情况下会发出警告信息并提供右对齐或左对齐两种填充方式供选择。
复制代码
    df %>% separate(y, into = c("key", "value"), sep = ":",
                extra = "merge", fill = "right")
    # # A tibble: 5 × 5
    #   x     key   value
    #   <chr> <chr> <chr>
    # 1 NA    x     1
    # 2 a,b   x     2
    # 3 c,d   y     4:6
    # 4 b,c   z     NA
    # 5 d,e   NA    NA

我们可以观察到提取出来的数据均为字符串类型,并且可以通过设置 convert = TRUE 来自动推断它们的类型。

复制代码
    df %>% separate(y, into = c("key", "value"), sep = ":",
                extra = "drop", fill = "right", convert = TRUE)
    # # A tibble: 5 × 3
    #   x     key   value
    #   <chr> <chr> <int>
    # 1 NA    x     1    
    # 2 a,b   x     2    
    # 3 c,d   y     4
    # 4 b,c   z     NA   
    # 5 d,e   NA    NA

通过 sep 参数指定分隔符的位置信息,默认情况下采用字符位置索引的方式,默认值为 -1 表示末尾字符,默认值为 0 表示第一个字符,默认值为 1 表示第二个字符等

复制代码
    tibble(
      x = c("1999", "2000", "2001")
    ) %>% separate(x, into = c("century", "year"), sep = 2)
    # # A tibble: 3 × 2
    #   century year 
    #   <chr>   <chr>
    # 1 19      99   
    # 2 20      00   
    # 3 20      01

使用 separate_rows 可以将拆分出来的值与其他列组成新的行

复制代码
    df %>% separate_rows(y, sep = ":")
    # # A tibble: 9 × 4
    #   x     y
    #   <chr> <chr>
    # 1 NA    x
    # 2 NA    1
    # 3 a,b   x
    # 4 a,b   2
    # 5 c,d   y
    # 6 c,d   4
    # 7 c,d   6
    # 8 b,c   z
    # 9 d,e   NA

使用 unite 函数合并多列数据

复制代码
    df <- tibble(
      first_name = c("Paul", "John", "George", "Mother", "Elvis"),
      last_name = c("McCartney", "Lennon", "Harrison", "Teresa", "Presley")
    )
    df %>% unite(full_name, 1:2, sep = " ")
    # # A tibble: 5 × 1
    #   full_name      
    #   <chr>          
    # 1 Paul McCartney 
    # 2 John Lennon    
    # 3 George Harrison
    # 4 Mother Teresa  
    # 5 Elvis Presley

缺失值处理

可以认为,在数据集中缺失值是一种常见的现象。因为各种原因导致无法获得相应的数值。这些数值可能以显式的格式标记为 NA(即显式的),或者根本未被记录下来(隐式的)。针对这种情况的数据集,《tidyr》包提供了若干工具函数用于将缺失值的显性或隐性表示方式进行转换,并且专门处理显性缺失值的方法。

函数 功能
complete NA 填补数据中缺失的组合
drop_na 删除包含缺失值的行
replace_na 使用指定值替换缺失值
fill 用前一个或后一个值来填补缺失值
full_seq 填充序列中的缺失值,使其构成一个完整的等差序列
expand_grid 创建输入数据的所有组合
expand 返回数据集中所有变量的组合
crossing expand_grid 的封装,会对其输入进行去重和排序
nesting 只查找在数据集中已经存在的组合

本文将介绍如何将不同类型的数据显示组合在一起,并重点讲解其中最常用的方式——笛卡尔积。该方法通过生成所有可能的有序对构成一个集合,并可利用现成的expand_grid函数来实现这一过程。与R内置的expand.grid函数相比,该方法无需将字符串转化为因子、无需增加额外字段,并且支持数据框格式并按顺序排列结果。

复制代码
    expand_grid(x = 1:2, y = 3:4)
    # # A tibble: 4 × 2
    #       x     y
    #   <int> <int>
    # 1     1     3
    # 2     1     4
    # 3     2     3
    # 4     2     4
    expand_grid(
      data = data.frame(x = 1:2, y = 3:4),
      z = c("A", "B")
      )
    # # A tibble: 4 × 2
    #   data$x    $y z    
    #    <int> <int> <chr>
    # 1      1     3 A    
    # 2      1     3 B    
    # 3      2     4 A    
    # 4      2     4 B 
    expand_grid(
      mat1 = matrix(1:4, nrow = 2),
      mat2 = matrix(9:6, ncol = 2)
      )
    # # A tibble: 4 × 2
    #   mat1[,1]  [,2] mat2[,1]  [,2]
    #      <int> <int>    <int> <int>
    # 1        1     3        9     7
    # 2        1     3        8     6
    # 3        2     4        9     7
    # 4        2     4        8     6

expand 则用于将 tibble 数据框里面的列变量进行组合

复制代码
    clothes <- tibble(
      color   = c("green", "green", "blue", "blue", "green", "blue"),
      year   = c(2020, 2021, 2022, 2020, 2021, 2022),
      size  =  factor(
    c("S",  "M", "S", "S", "M", "L"),
    levels = c("S", "M", "L", "XL")
      ),
      weights = rnorm(6, as.numeric(size) + 2)
    )
    clothes %>% expand(color)
    # # A tibble: 2 × 1
    #   color
    #   <chr>
    # 1 blue 
    # 2 green
    clothes %>% expand(color, size)
    # # A tibble: 8 × 2
    #   color size 
    #   <chr> <fct>
    # 1 blue  S    
    # 2 blue  M    
    # 3 blue  L    
    # 4 blue  XL   
    # 5 green S    
    # 6 green M    
    # 7 green L    
    # 8 green XL 
    clothes %>% expand(color, size, 2022:2024)
    # # A tibble: 24 × 3
    #    color size  `2022:2024`
    #    <chr> <fct>       <int>
    #  1 blue  S            2022
    #  2 blue  S            2023
    #  3 blue  S            2024
    #  4 blue  M            2022
    #  5 blue  M            2023
    #  6 blue  M            2024
    #  7 blue  L            2022
    #  8 blue  L            2023
    #  9 blue  L            2024
    # 10 blue  XL           2022
    # # … with 14 more rows

其内部可以使用nestingcrossing,扩展其功能

复制代码
    clothes %>% expand(nesting(color, size))
    # # A tibble: 4 × 2
    #   color size 
    #   <chr> <fct>
    # 1 blue  S    
    # 2 blue  L    
    # 3 green S    
    # 4 green M 
    clothes %>% expand(nesting(color, size, year))
    # # A tibble: 5 × 3
    #   color size   year
    #   <chr> <fct> <dbl>
    # 1 blue  S      2020
    # 2 blue  S      2022
    # 3 blue  L      2022
    # 4 green S      2020
    # 5 green M      2021
    clothes %>% expand(crossing(color, size))
    # # A tibble: 8 × 2
    #   color size 
    #   <chr> <fct>
    # 1 blue  S    
    # 2 blue  M    
    # 3 blue  L    
    # 4 blue  XL   
    # 5 green S    
    # 6 green M    
    # 7 green L    
    # 8 green XL

等一下,这不是在讲解缺失值的问题吗?为何会引入数据组合的问题呢?经过将所有组合与实际数据进行整合后,确实会出现 NA 值的现象,请问这是什么意思?这反映出我们的数据存在不足,并遗漏了一些重要的信息点。

复制代码
    clothes %>% expand(color, size, year) %>%
      left_join(clothes)
    # Joining, by = c("color", "size", "year")
    # # A tibble: 25 × 4
    #    color size   year weights
    #    <chr> <fct> <dbl>   <dbl>
    #  1 blue  S      2020    1.63
    #  2 blue  S      2021   NA   
    #  3 blue  S      2022    1.41
    #  4 blue  M      2020   NA   
    #  5 blue  M      2021   NA   
    #  6 blue  M      2022   NA   
    #  7 blue  L      2020   NA   
    #  8 blue  L      2021   NA   
    #  9 blue  L      2022    5.30
    # 10 blue  XL     2020   NA   
    # # … with 15 more rows

该方法能够明确地标记缺失值,并且是由以下组件组成的:.expand, .full_join, 和 .replace_na.

复制代码
    clothes %>% complete(color, size, year, fill = list(weights = "null"))
    # # A tibble: 25 × 4
    #    color size   year weights         
    #    <chr> <fct> <dbl> <chr>           
    #  1 blue  S      2020 1.63063568676183
    #  2 blue  S      2021 null            
    #  3 blue  S      2022 1.4122919268352 
    #  4 blue  M      2020 null            
    #  5 blue  M      2021 null            
    #  6 blue  M      2022 null            
    #  7 blue  L      2020 null            
    #  8 blue  L      2021 null            
    #  9 blue  L      2022 5.30084155983022
    # 10 blue  XL     2020 null            
    # # … with 15 more rows

你可以使用 fill 来填充缺失值,可以指定填充方向,可以为

  • 下移赋值:将序列中的缺失值替换为其前一个有效数据点的值
  • 上移赋值:将序列中的缺失值替换为其后一个有效数据点的值
  • 依次下移再上移:首先将序列中的缺失值逐项向前一个有效数据点赋值后再进行后续处理
  • 依次上移再下移:首先将序列中的缺失值逐项向后一个有效数据点赋值后再进行后续处理
复制代码
    clothes %>% complete(color, size, year) %>%
      fill(weights)
    # # A tibble: 25 × 4
    #    color size   year weights
    #    <chr> <fct> <dbl>   <dbl>
    #  1 blue  S      2020    1.63
    #  2 blue  S      2021    1.63
    #  3 blue  S      2022    1.41
    #  4 blue  M      2020    1.41
    #  5 blue  M      2021    1.41
    #  6 blue  M      2022    1.41
    #  7 blue  L      2020    1.41
    #  8 blue  L      2021    1.41
    #  9 blue  L      2022    5.30
    # 10 blue  XL     2020    5.30
    # # … with 15 more rows

8. 解析因子

在R语言中使用因子时主要针对的是具有明确且完全已知类别情况的数据字段。基于实践经验,在大多数情况下factor比string更容易操作和管理因此在众多内置函数中factor类型的参数往往期望接收到此类数据形式然而这些转化通常没用因为它们无法有效提升分析效率并且在tidymes的框架下这种方法也不会被采用

为了更倾向于采用 tidyverse 提供的工具包来管理分类数据,在数据分析过程中会遇到多种类型的变量需要特别关注

复制代码
    library(forcats)  # version 0.5.1

对因子调整而言,主要依据其 levles 属性进行相应的改变;其中绝大多数都用于绘图方面,在数据预处理阶段将大量数据转为因子类型的情况相对较少。

因子的增删查

函数 功能 函数 功能
fct_c 合并多个因子 fct_expand 添加因子水平
fct_drop 删除未使用的因子水平 fct_match 因子是否存在某一水平
fct_count 统计每个水平的值的数量 fct_unique 对因子的值去重

合并两个不同的因子

复制代码
    f1 <- factor(c(1:2))
    f2 <- factor(c('A', 'B'))
    fct_c(f1, f2)
    # [1] 1 2 A B
    # Levels: 1 2 A B
    fct_c(!!!list(f1, f2))
    # [1] 1 2 A B
    # Levels: 1 2 A B

添加一个水平

复制代码
    fct_expand(f1, "A")
    # [1] 1 2
    # Levels: 1 2 A

删除未使用的水平

复制代码
    fct_drop(fct_expand(f1, "A"))
    # [1] 1 2
    # Levels: 1 2

因子的搜索、统计数量和去重

复制代码
    f <- factor(c("A", "C", "B", "B", "D", "A"))
    fct_match(f, "A")
    # [1]  TRUE FALSE FALSE FALSE FALSE  TRUE
    fct_count(f)
    # # A tibble: 4 × 2
    #   f         n
    #   <fct> <int>
    # 1 A         2
    # 2 B         2
    # 3 C         1
    # 4 D         1
    fct_unique(f)
    # [1] A B C D
    # Levels: A B C D

修改因子顺序

函数 功能 函数 功能
fct_relevel 重新排列因子水平 fct_infreq 通过频率对因子水平重排
fct_inorder 根据因子出现顺序重排 fct_rev 将因子逆序
fct_shift 将因子水平向左或向右移动 fct_shuffle 对因子随机重排
fct_reorder 根据另一个变量重排因子 fct_reorder2 用另外两个变量重排因子

使用 fct_relevel 可以根据传入因子水平的值或函数重新排列因子水平

复制代码
    f <- factor(letters[1:4], levels = c("b", "a", "d", "c"))
    fct_relevel(f, "d")
    # [1] a b c d
    # Levels: d b a c
    fct_relevel(f, "a", after = 3)
    # [1] a b c d
    # Levels: b d c a
    fct_relevel(f, rev)  # 相当于 fct_rev(f)
    # [1] a b c d
    # Levels: c d a b

根据出现顺序和频率重排因子水平

复制代码
    f <- factor(c("b", "a", "b", "b", "d", "b", "c", "a", "c", "a"))
    fct_inorder(f)
    #  [1] b a b b d b c a c a
    # Levels: b a d c
    fct_infreq(f)
    #  [1] b a b b d b c a c a
    # Levels: b a c d

对于绘图函数来说,在传递参数时采用单个参数更为灵活;而当绘制图形时,在传递参数方面双参数的应用更具灵活性。

复制代码
    df <- tibble(
      x = c(1, 5, 4),
      y = c(2, 3, 1),
      species = factor(c("setosa", "versicolor", "virginica"))
    )
    fct_reorder(df$species, df$x)
    # [1] setosa     versicolor virginica 
    # Levels: setosa virginica versicolor
    fct_reorder2(df$species, df$x, df$y)
    # [1] setosa     versicolor virginica 
    # Levels: versicolor setosa virginica

随机和平移

复制代码
    f <- factor(letters[1:4])
    fct_shift(f, -1)
    # [1] a b c d
    # Levels: d a b c
    fct_shift(f, 1)
    # [1] a b c d
    # Levels: b c d a
    fct_shuffle(f)
    # [1] a b c d
    # Levels: c d b a
    fct_shuffle(f)
    # [1] a b c d
    # Levels: d b a c

修改水平的值

函数 功能 函数 功能
fct_recode 手动修改因子水平 fct_relabel 自动修改因子水平
fct_annon 用数值将因子水平匿名 fct_collapse 将因子水平折叠
fct_lump_* 将符合条件的因子水平归为 other fct_other 指定 other

替换因子水平的值

复制代码
    f <- factor(letters[1:4])
    fct_recode(f, "get A" = "a", NULL = "c", B = "b")
    # [1] get A B     <NA>  d    
    # Levels: get A B d
    l <- c(high = "a", low = "d")
    fct_recode(f, !!!l)
    # [1] high b    c    low 
    # Levels: high b c low
    f <- factor(
      c("a123", "45abc", "bb12cc", "155oo44")
    )
    convert <- function(x) {
      num <- as.numeric(gsub(".*?([0-9]+).*", "\ 1", x))
      paste0("Num: ", num)
    }
    fct_relabel(f, convert)
    # [1] Num: 123 Num: 45  Num: 12  Num: 155
    # Levels: Num: 155 Num: 45 Num: 123 Num: 12

通过调用 fct_annon 方法能够实现分类变量(factor levels)向数字类型赋值,并赋予它们与前缀一致的连续增量值。

复制代码
    iris$Species %>% fct_anon() %>% fct_count()
    # # A tibble: 3 × 2
    #   f         n
    #   <fct> <int>
    # 1 1        50
    # 2 2        50
    # 3 3        50

fct_collapse 可以将因子折叠,可以理解为对因子进行分组

复制代码
    month.abb %>% fct_collapse(
      first = c("Jan", "Feb", "Mar"),
      second = c("Apr", "May", "Jun"),
      third = c("Jul", "Aug", "Sep"),
      fourth = c("Oct", "Nov", "Dec")
    ) %>% fct_count()
    # # A tibble: 4 × 2
    #   f          n
    #   <fct>  <int>
    # 1 second     3
    # 2 third      3
    # 3 fourth     3
    # 4 first      3
    f <- factor(letters[1:4])
    fct_collapse(f, head = "a", other_level = "other")
    # [1] head  other other other
    # Levels: head other

相当于上面方法的特立,只需指定合并或不被合并为 other 的值

复制代码
    f <- factor(rep(letters[1:4], times = c(3, 2, 1, 4)))
    fct_other(f, keep = c("a", "d"))
    #  [1] a     a     a     Other Other Other d     d     d     d    
    # Levels: a d Other
    fct_other(f, drop = c("a", "d"))
    #  [1] Other Other Other b     b     c     Other Other Other Other
    # Levels: b c Other

将满足某些标准的因子合并为 other

复制代码
    fct_lump_lowfreq(f)
    #  [1] a     a     a     b     b     Other d     d     d     d    
    # Levels: a b d Other
    fct_lump_n(f, 2)
    #  [1] a     a     a     Other Other Other d     d     d     d    
    # Levels: a d Other
    fct_lump_min(f, 3)
    #  [1] a     a     a     Other Other Other d     d     d     d    
    # Levels: a d Other

9. 解析时间日期

此外,在Tidyverse系列包中还包含了一个特别有用的工具箱:lubridate包集成了大量实用的功能模块。这些功能可以被用来生成和分析各种形式的时间与日期对象,并执行一系列运算操作。在处理涉及时间和日期的数据时,默认情况下我们会将一年视为365天、每日24小时以及每小时60分钟来进行估算。这种估算方式相对简单粗略,在实际情况中往往不够精确;然而,在考虑到地球自转与公转周期、不同地区时区差异以及夏令时等因素的影响后,默认采用这种方式会大大简化相关计算过程。尽管如此,在实际应用中我们通常忽略这些细节因素带来的复杂性变化,在生物数据处理领域涉及时间和日期的操作较为少见;例如生存分析等统计方法虽然会涉及到时间变量的计算问题。

下面我们就简单介绍一些函数的用法,先导入包

复制代码
    library(lubridate)  # version 1.8.0

解析与创建

日期由年(y)、月(m)、日(d)构成,并非固定遵循特定顺序;其格式可为三个字段的各种排列组合。由此可知有6个函数用于解析日期格式;此外还可以通过计算从1970-01-01开始的天数来实现这一功能。

复制代码
    ymd("2022-02-02")
    # [1] "2022-02-02"
    ydm("2022-31-12")
    # [1] "2022-12-31"
    mdy("01/06/2022")
    # [1] "2022-01-06"
    mdy("July 4th, 2000")
    # [1] "2000-07-04"
    dmy("4th of July '2022")
    # [1] "2022-07-04"
    as_date(18500)
    # [1] "2020-08-26"

时间由时(h)、分(m)、秒(s)构成,在变量表示中体现出来;通常情况下时间格式遵循时-分-秒的顺序排列,并允许省略其中一个或两个部分以适应不同需求

复制代码
    hms("11:11:11")
    # [1] "11H 11M 11S"
    hm("7 6")
    # [1] "7H 6M 0S"
    ms("6,5")
    # [1] "6M 5S"
    hms("Finished in 9 hours, 20 min and 4 seconds")
    # [1] "9H 20M 4S"

时间与日期可以结合使用,并非单独存在;通过在函数名称前后添加下划线的方式进行整合,从而实现对相应时间日期格式的解析

复制代码
    ymd_hms("2022-06-06T14:02:00")
    # [1] "2022-06-06 14:02:00 UTC"
    ydm_hms("2022-31-05T14:02:00")
    # [1] "2022-05-31 14:02:00 UTC"
    mdy_hms("01/06/2022 1:15:01")
    # [1] "2022-01-06 01:15:01 UTC"
    dmy_hm("1 Jul 2022 23:59")
    # [1] "2022-07-01 23:59:00 UTC"
    dmy_h("1 Jul 2022 23")
    # [1] "2022-07-01 23:00:00 UTC"
    as_datetime(1651870400)  # 解析时间戳
    # [1] "2022-05-06 20:53:20 UTC"

获取当前时间和日期

复制代码
    now()
    # [1] "2022-06-10 20:11:10 CST"
    today()
    # [1] "2022-06-10"

通过调用 make_date 方法可以生成日期对象,而调用 make_datetime 方法则能够生成包含时间信息的日期对象。例如,在处理 flights 数据集时,请注意这些方法的应用场景

复制代码
    flights %>% 
      select(year, month, day, hour, minute) %>% 
      transmute(
    date = make_date(year, month, day),
    departure = make_datetime(year, month, day, hour, minute)) %>%
      head(3)
    # # A tibble: 3 × 2
    #   date       departure          
    #   <date>     <dttm>             
    # 1 2013-01-01 2013-01-01 05:15:00
    # 2 2013-01-01 2013-01-01 05:29:00
    # 3 2013-01-01 2013-01-01 05:40:00

计算 2013-01-10 之前出发的航班数

复制代码
    flights %>% 
      filter(!is.na(dep_time), !is.na(arr_time)) %>% 
      mutate(date = make_date(year, month, day)) %>%
      filter(date < ymd(20130110)) %>%
      nrow()
    # [1] 7851

访问器

下面的函数可以用于获取和设置时间日期对象的不同组件

函数 功能 函数 功能
date 获取日期 year 年份
month 月份 wday 星期几
qday 季度日 hour 小时
minute 分钟 second
tz 时区 week 第几周
am 是否上午 pm 是否下午
leap_year 是否为闰年 update 修改时间

获取时间

复制代码
    d <- now()
    d
    # [1] "2022-06-10 21:44:54 CST"
    year(d)
    # [1] 2022
    month(d)
    # [1] 6
    date(d)
    # [1] "2022-06-10"
    wday(d)
    # [1] 6
    week(d)
    # [1] 23
    pm(d)
    # [1] TRUE
    am(d)
    # [1] FALSE
    leap_year(d)
    # [1] FALSE

设置时间

复制代码
    update(d, year = 2020, month = 1, mday = 10)
    # [1] "2020-01-10 21:44:54 CST"
    day(d) <- 12
    d
    # [1] "2022-06-12 21:44:54 CST"
    month(d) <- 12
    d
    # [1] "2022-12-12 21:44:54 CST"

时间跨度

时间跨度包含三种类型

  • periods:时间周期,代表有单位的日期,如几周和几个月
函数 功能 函数 功能
years 几年 months 几个月
weeks 几周 days 几天
hours 几小时 minutes 几分钟
seconds 几秒 period 自动解析
period_to_seconds 周期对象转换为秒 seconds_to_period 秒转换为周期对象
复制代码
    years(3)
    # [1] "3y 0m 0d 0H 0M 0S"
    weeks(1)
    # [1] "7d 0H 0M 0S"
    months(6) + days(12)
    # [1] "6m 12d 0H 0M 0S"
    hours(10) + minutes(10) + seconds(10)
    # [1] "10H 10M 10S"
    hours(1:3)
    # [1] "1H 0M 0S" "2H 0M 0S" "3H 0M 0S"
    period(c(3, 1, 2, 13, 1), c("second", "minute", "hour", "day", "week"))
    # [1] "20d 2H 1M 3S"
    period_to_seconds(minutes(1))
    # [1] 60
    seconds_to_period(3661)
    # [1] "1H 1M 1S"
  • durations:持续时间,表示精确的秒数

时间周期函数前面添加 d 就可以将对应的周期数变成持续时间的秒数

复制代码
    dhours(1)
    # [1] "3600s (~1 hours)"
    ddays(1)
    # [1] "86400s (~1 days)"
    today() - ddays()
    # [1] "2022-06-10"
    today() - dyears()
    # [1] "2021-06-10 18:00:00 UTC"
    duration(second = 3, minute = 1.5, hour = 2, day = 6, week = 1)
    # [1] "1130493s (~1.87 weeks)"
  • intervals:时间间隔,具有起点和终点的时间段
函数 功能 函数 功能
interval/%--% 创建时间间隔 a %within% b a 是否包含在 b
int_diff 时间日期向量相邻值的间隔 int_overlaps 两个时间间隔是否存在交集
int_start/int_end 起始和终止时间 int_flip 翻转时间间隔方向
int_length 时间间隔的秒数 int_shift 将时间间隔平移
as.interval 根据起始时间转换为时间间隔 is.interval 是否为时间间隔对象

创建时间间隔对象

复制代码
    date1 <- ymd_hms("2021-05-20 01:59:59")
    date2 <- ymd_hms("2022-06-05 12:00:00")
    (span <- interval(date1, date2))
    # [1] 2021-05-20 01:59:59 UTC--2022-06-05 12:00:00 UTC
    date1 %--% date2
    # [1] 2021-05-20 01:59:59 UTC--2022-06-05 12:00:00 UTC
    as.interval(days(1), start = now())
    # [1] 2022-06-11 10:33:30 CST--2022-06-12 10:33:30 CST
    (b <- int_diff(c(date1, date1+months(2), date2)))
    # [1] 2021-05-20 01:59:59 UTC--2021-07-20 01:59:59 UTC
    # [2] 2021-07-20 01:59:59 UTC--2022-06-05 12:00:00 UTC

对时间间隔对象的操作

复制代码
    int_start(span)
    # [1] "2021-05-20 01:59:59 UTC"
    int_end(span)
    # [1] "2022-06-05 12:00:00 UTC"
    int_flip(span)
    # [1] 2022-06-05 12:00:00 UTC--2021-05-20 01:59:59 UTC
    int_length(span)
    # [1] 32954401
    int_shift(span, days(2))
    # [1] 2021-05-22 01:59:59 UTC--2022-06-07 12:00:00 UTC
    int_shift(span, days(-2))
    # [1] 2021-05-18 01:59:59 UTC--2022-06-03 12:00:00 UTC

两个时间间隔对象的关系

复制代码
    int_overlaps(span, b)
    # [1] TRUE TRUE
    span %within% b
    # [1] FALSE FALSE
    b %within% span
    # [1] TRUE TRUE

根据具体情况采用最为基础的模式来解决相关问题

  • 如仅关注物理时间,则采用持续时间;
  • 如需增减由人类定义的时间,则采用周期;
  • 如需计算由人类定义的时间长度,则采用时间间隔。

格式化

lubridate 包还包含用于格式化时间日期对象的功能模块 stamp。通过将格式化模板作为字符串传递给该模块会生成一个专门的格式化函数;这个由模板定义的函数可用于对时间日期对象进行相应的格式化处理;例如

复制代码
    date_list <- now() - days(1:5)
    f <- stamp("March 1, 1999")
    # Multiple formats matched: "March %Om, %Y"(1), "%Om %d, %Y"(1), "March%b, %d%y"(1), "March%b, %Y"(0)
    # Using: "March%b, %d%y"
    f(date_list)
    # [1] "March 6, 1022" "March 6, 0922" "March 6, 0822" "March 6, 0722"
    # [5] "March 6, 0622"

从输出的提示可以看出,识别出三种不同的数据格式,随后系统会根据检测结果自动生成相应的处理规则,此外用户还可以根据需求自行配置

复制代码
    f <- stamp("March 1, 2000", orders = "%B %d, %Y")
    # Using: "%Ob %d, %Y"
    f(date_list)
    # [1] " 6 10, 2022" " 6 09, 2022" " 6 08, 2022" " 6 07, 2022" " 6 06, 2022"

指定其他格式

复制代码
    stamp("12/31/99")(date_list)
    # Multiple formats matched: "%Om/%d/%y"(1), "%m/%d/%y"(1)
    # Using: "%Om/%d/%y"
    # [1] "06/10/22" "06/09/22" "06/08/22" "06/07/22" "06/06/22"
    stamp("21 Aug 2011, 11:15:34 pm")(date_list)
    # Multiple formats matched: "%y Aug %d%Om, %H:%M:%S pm"(1), "%y Aug %d%m, %H:%M:%S pm"(1), "%d Aug %Y, %m:%H:%M pm"(1), "%y Aug %d%Om, %H:%M:%S %Op"(1), "%d %Om %Y, %H:%M:%S %Op"(1), "%d Aug %y%Om, %H:%M:%S %Op"(1), "%y Aug %d%m, %H:%M:%S %Op"(1), "%d %Om %Y, %H:%M:%S pm"(1), "%d Aug %y%Om, %H:%M:%S pm"(1), "%d Aug %Y, %Om:%H:%M pm"(1), "%d Aug %Y,%b%H:%M:%S pm"(1), "%d Aug %Y,%b%H:%M:%S %Op"(0), "%d Aug %Y, %Om:%H:%M %Op"(0), "%d Aug %Y, %m:%H:%M %Op"(0)
    # Using: "%y Aug %d%Om, %H:%M:%S %Op"
    # [1] "22 Aug 1006, 10:43:38 上午" "22 Aug 0906, 10:43:38 上午"
    # [3] "22 Aug 0806, 10:43:38 上午" "22 Aug 0706, 10:43:38 上午"
    # [5] "22 Aug 0606, 10:43:38 上午"

当处理时间日期时

10. 函数式编程

在函数式编程中,核心理念是将一系列操作封装成独立的、可重用的模块,通过将重复性操作封装为特定功能的子程序(如Python中的function),从而减少冗余代码量并提高程序的整体可读性和维护效率.对于像dataframe这样的数据结构,在实际编程中我们特别常见于对每一行或每一列执行特定的操作,这些操作通常包括统计计算、数据匹配与筛选、格式转换等功能.

在 R 的内建生态系统中,在完成该操作时可使用 apply 函数集合。而 tidyverse 环境中,则整合了 purrr 包的一系列功能齐全的工具函数支持多种迭代方式,并可替代传统的 for 循环结构以简化代码逻辑。

导入包

复制代码
    library(purrr)  # 0.3.4

map 函数族

purrr 库中进行循环处理时会生成一系列结果,在这种情况下最终结果会按照特定模式组织起来的方式被称为 map 函数族的应用方式;基于输入向量的数量及其类型可划分为四种不同的模式

  • map_*: 该操作符会逐个作用于单个 list 或向量的所有元素,并生成一个新的 list。
  • map2_*: 该操作符会应用到两个 list 或向量的对应位置,并生成一个新的 list。
  • pmap_*: 该操作符会作用于多个 list 或向量的所有对应位置,并生成一个新的 list。
  • imap_*: 不仅会作用于每个数据项本身(即数组中的每个数据项),还会包含其索引信息(即数组中的下标)。当变量有命名时等价于 map2(x, names(x), ...);而当变量无命名时则等价于 map2(x, seq_along(x), ...)

各类函数名称都具有相同的后缀特征,并且根据输出结果的类型可以分为不同的类别(如单变量操作中的 map 函数)。

函数 返回类型 函数 返回类型
map 返回 list walk 不显示返回值的 map
map_dbl double 向量 map_lgl 逻辑向量
map_chr 字符串向量 map_int 整数向量
map_dfc 列绑定的数据框 map_dfr 行绑定的数据框

这些函数的参数基本类似,根据其输入变量的数量,具有不同的形式

  • 单变量:map(.x, .f, ...)
  • 双变量:map2(.x, .y, .f, ...)
  • 多变量:pmap(.l, .f, ...)

该参数为.f格式。它不仅支持直接赋值(赋值类型),还包括了多种数据形式的选择。具体来说,它可以是以下几种类型之一:例如:公式、字符串向量、整数向量以及列表形式。当输入为公式时,则根据输入的参数数量不同而采用三种不同的引用方法。

  • 仅一个变量遵循以下引用规则:仅使用. .x`
    形式。
    • 双个量词必须按照.x

      .y
      的顺序进行引用。
    • 多于两个量词应依次采用. ..1 .y.
      .z
      `等排列格式。

如果输入的是字符串列表、整数列表或 list 类型的数据,则等价于获取其对应名称或索引处的元素。当无法找到对应的项时,默认可以用 .default 来设置。

其他额外的参数都会传入到 .f 指定的函数中进行解析。

例如,我们构造如下数据

复制代码
    set.seed(1234)
    df <- tibble(
      a = rnorm(5), b = rnorm(5),
      c = rnorm(5), d = rnorm(5)
    )
    df
    # # A tibble: 5 × 4
    #        a      b       c      d
    #    <dbl>  <dbl>   <dbl>  <dbl>
    # 1 -1.21   0.506 -0.477  -0.110
    # 2  0.277 -0.575 -0.998  -0.511
    # 3  1.08  -0.547 -0.776  -0.911
    # 4 -2.35  -0.564  0.0645 -0.837
    # 5  0.429 -0.890  0.959   2.42

计算每列的均值

复制代码
    map_dbl(df, function(x) mean(x, trim = 0.5))
    #          a          b          c          d 
    #  0.2774292 -0.5644520 -0.4771927 -0.5110095 
    map_dbl(df, mean, trim = 0.5)
    #          a          b          c          d 
    #  0.2774292 -0.5644520 -0.4771927 -0.5110095 
    map_dfr(df, ~ mean(.x, trim = 0.5))
    # # A tibble: 1 × 4
    #       a      b      c      d
    #   <dbl>  <dbl>  <dbl>  <dbl>
    # 1 0.277 -0.564 -0.477 -0.511
    # 计算 list 的均值
    map(1:3, ~ rnorm(n = 10)) %>%
      map_dbl(mean)
    # [1] -0.5837484 -0.4257427  0.4947986

修改数据

复制代码
    map_dbl(df$a, ~ .x + 1)
    # [1] -0.2070657  1.2774292  2.0844412 -1.3456977  1.4291247
    set_names(LETTERS[1:3]) %>% map_chr(~ paste(.x, "1", sep = "-"))
    #     A     B     C 
    # "A-1" "B-1" "C-1"

获取数据时支持通过整数索引、字符串名称或嵌套列表结构结合字符串名称标识和数值索引定位的方式进行输入

复制代码
    l1 <- list(
      a = 1:3,
      b = set_names(4:5, nm = letters[1:2]),
      c = 7
    )
    map_int(l1, 2, .default = NA)
    #  a  b  c 
    #  2  5 NA 
    map_int(l1, "a", .default = NA)
    #  a  b  c 
    # NA  4 NA
    l1 <- list(
      a = list(),
      b = list(a = 5:9, "a"),
      c = list(a = 1:3, b = 7:8)
    )
    map_int(l1, list("a", 2), .default = NA)
    #  a  b  c 
    # NA  6  2

按行或按列合并,

复制代码
    l2 <- list(
      a = list(
    x = rnorm(n = 10),
    y = rnorm(n = 10)
    ),
      b = list(
    x = rnorm(n = 10),
    y = rnorm(n = 10)
      )
    ) 
    
    l2 %>% map_dfr(~ map_dbl(.x, mean))
    # # A tibble: 2 × 2
    #       x      y
    #   <dbl>  <dbl>
    # 1 0.418 -0.193
    # 2 0.855 -0.616
    l2 %>% map_dfc(~ map_dbl(.x, mean))
    # # A tibble: 2 × 2
    #        a      b
    #    <dbl>  <dbl>
    # 1  0.418  0.855
    # 2 -0.193 -0.616

针对矩阵形式的数据,在具体操作步骤中首先运用split函数将其划分为若干个子列表。接着按照行或列的方向汇总统计信息。

复制代码
    mtcars %>%
      split(.$cyl) %>%
      map_dfr(~ map_dbl(.x, mean))
    # # A tibble: 3 × 11
    #     mpg   cyl  disp    hp  drat    wt  qsec    vs    am  gear  carb
    #   <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
    # 1  26.7     4  105.  82.6  4.07  2.29  19.1 0.909 0.727  4.09  1.55
    # 2  19.7     6  183. 122.   3.59  3.12  18.0 0.571 0.429  3.86  3.43
    # 3  15.1     8  353. 209.   3.23  4.00  16.8 0     0.143  3.29  3.5

对于双变量或多变量也是类似的

复制代码
    map2_dbl(df$a, df$b, sum)  # 两个变量对应元素之和
    # [1] -0.7010099 -0.2973107  0.5378093 -2.9101497 -0.4609131
    pmap_dbl(df, sum)          # 多个变量对应元素之和
    # [1] -1.288488 -1.806707 -1.149640 -3.682863  2.914416
    map2_dbl(df$a, df$b, min)
    # [1] -1.2070657 -0.5747400 -0.5466319 -2.3456977 -0.8900378
    pmin(df$a, df$b)
    # [1] -1.2070657 -0.5747400 -0.5466319 -2.3456977 -0.8900378
    df %>% pmap_dbl(~ (..1 + ..2) * ..3 - ..4)
    # [1]  0.4448023  0.8078405  0.4937188  0.6495869 -2.8580786

在对同一函数进行多次调用并传递不同参数向量时,pmap 就变得很有用,在这种情况下将这些参数值作为第一个自定义输入提供给该函数。

复制代码
    pat <- data.frame(
      x = c("apple", "banana", "orange"),
      pattern = c("a", "b", "o"),
      replacement = c("A", "B", "O")
    )
    pmap_chr(pat, gsub)
    # [1] "Apple"  "Banana" "Orange"
    list(mean = list(5, 10, -3), sd = list(1, 5, 10), n = list(1, 3, 5)) %>%
      pmap(rnorm)
    # [[1]]
    # [1] 4.031486
    # [[2]]
    # [1] 4.463409 3.740071 7.380859
    # [[3]]
    # [1]  -7.968500 -21.060313  -8.820759 -14.088896 -13.149620

mutate 函数联用

复制代码
    count(iris, Species) %>%
      mutate(merge = map2_chr(Species, n, ~ paste(.y, .x, sep = ": ")))
    #      Species  n          merge
    # 1     setosa 50     50: setosa
    # 2 versicolor 50 50: versicolor
    # 3  virginica 50  50: virginica

作为替代方案中使用的另一种方式,“walk”工具或方法专注于执行特定的操作而不考虑其结果。
它主要用于打印输出或直接将文件存储在磁盘上。
核心关注点在于操作本身的执行而非其最终的返回值。

复制代码
    list(1, "a", 1:3) %>% walk(print)
    # [1] 1
    # [1] "a"
    # [1] 1 2 3

过滤函数

过滤函数会对每个元素进行检测并返回检测结果,主要函数包括

函数 功能 函数 功能
keep 保留元素 discard 删除元素
compact 删除空元素 has_element 是否含有某对象
head_while 所有满足的头部元素 tail_while 所有满足条件的尾部元素
detect/detect_index 查找第一个匹配项的值或位置 every 所有元素满足条件
some 部分元素满足条件 none 没有元素满足条件
prepend 添加向量 negate 函数结果取反

匹配和搜索元素

复制代码
    month.abb
    # [1] "Jan" "Feb" "Mar" "Apr" "May" "Jun" "Jul" "Aug" "Sep" "Oct" "Nov" "Dec"
    month.abb %>% detect(~ startsWith(.x, "M"))
    # [1] "Mar"
    month.abb %>% detect_index(~ startsWith(.x, "M"), .dir = "backward")
    # [1] 5
    has_element(month.abb, "May")
    # [1] TRUE
    head_while(5:-5, ~ .x > 0)
    # [1] 5 4 3 2 1
    tail_while(5:-5, negate(~ .x > 0))
    # [1]  0 -1 -2 -3 -4 -5
    head_while(c(1:5, -1, 2:3), ~ .x > 0)
    # [1] 1 2 3 4 5

条件判断

复制代码
    every(month.abb, is.character)
    # [1] TRUE
    none(month.abb, is.numeric)
    # [1] TRUE
    some(month.abb, is.integer)
    # [1] FALSE

筛选元素,compact 会删除空元素或长度为 0 的元素

复制代码
    set.seed(1234)
    (a <- sample(1:10, size = 10))
    # [1] 10  6  7  9  4  8  5  1  2  3
    keep(a, ~ .x %% 2 == 0)
    # [1] 10  6  4  8  2
    discard(a, ~ .x %% 2 == 0)
    # [1] 5 1 7 9 3
    list(a = NULL, b = 1:3, c = NA, d = integer(0)) %>%
      compact()
    # $b
    # [1] 1 2 3
    # $c
    # [1] NA

添加元素

复制代码
    (a <- letters[1:5])
    [1] "a" "b" "c" "d" "e"
    append(a, "z")
    # [1] "a" "b" "c" "d" "e" "z"
    prepend(a, "0")
    # [1] "0" "a" "b" "c" "d" "e"
    prepend(a, letters[23:26], before = 3)
    # [1] "a" "b" "w" "x" "y" "z" "c" "d" "e"
    prepend(list(1:3), list("a"))
    # [[1]]
    # [1] "a"
    # [[2]]
    # [1] 1 2 3

向量变换

还有一些专门用于向量(包括 list)操作的函数,主要包括

函数 功能 函数 功能
flatten 删除一个嵌套层级 cross 所有元素之间的笛卡尔积
set_names 设置向量的名称 vec_depth 计算嵌套 list 的最大深度
transpose list 转置 splice 合并到 list
list_modify 修改 list 的值 list_merge 合并 list
accumulate 返回每次计算的累积值 reduce 重复应用函数并返回一个值

累积运算,依次将向量中相邻的两个值传递到函数中

复制代码
    accumulate(1:10, sum)
    # [1]  1  3  6 10 15 21 28 36 45 55
    accumulate(1:10, sum, .dir = "backward")  # 从右侧开始累积
    # [1] 55 54 52 49 45 40 34 27 19 10
    reduce(1:5, sum)
    # [1] 15
    letters[1:4] %>% reduce(paste, sep = '.')
    # [1] "a.b.c.d"
    paste2 <- function(x, y, sep = " ") paste(x, y, sep = sep)
    letters[1:4] %>% accumulate(paste2)
    # [1] "a"       "a b"     "a b c"   "a b c d"
    letters[1:4] %>% reduce(paste2)
    # [1] "a b c d"

带三个参数的形式会将累积运算的第二个操作数作为操作函数中的第三个操作数处理。例如,在以下情况中使用的c(".", "-", "_")表达式将会被赋值给sep参数

复制代码
    letters[1:4] %>% accumulate2(c(".", "-", "_"), paste2)
    # [[1]]
    # [1] "a"
    # [[2]]
    # [1] "a.b"
    # [[3]]
    # [1] "a.b-c"
    # [[4]]
    # [1] "a.b-c_d"
    letters[1:4] %>% reduce2(c(".", "-", "_"), paste2)
    # [1] "a.b-c_d"

该函数用于将输入的列表展平,默认返回类型为列表,并包含一系列与 map 类型相似的功能模块。

复制代码
    l1 <- list(
      a = 1:3,
      b = list(4:6)
      ) 
    l1%>% flatten() %>% str()
    # List of 4
    #  $  : int 1
    #  $  : int 2
    #  $  : int 3
    #  $ b: int [1:3] 4 5 6
    l1 %>% map(1) %>% flatten_int()
    # [1] 1 4 5 6
    list(
      a = letters[1:3],
      b = LETTERS[10:12]
      ) %>% flatten_chr()
    # [1] "a" "b" "c" "J" "K" "L"

构建数据集之间笛卡尔积的操作符该函数也具有多样化的参数形式包括cross2 cross3和cross_df

复制代码
    list(
      a = 1:2,
      b = c(2, 3)
    ) %>% cross(.filter = ~ .x < .y) %>% str()
    # List of 1
    #  $ :List of 2
    #   ..$ a: int 2
    #   ..$ b: num 2
    list(
      a = 1:2,
      b = c(2, 3)
    ) %>% cross(.filter = `==`) %>% str()
    # List of 3
    #  $ :List of 2
    #   ..$ a: int 1
    #   ..$ b: num 2
    #  $ :List of 2
    #   ..$ a: int 1
    #   ..$ b: num 3
    #  $ :List of 2
    #   ..$ a: int 2
    #   ..$ b: num 3
    # 相当于 cross2(.x = 1:2, .y = c(2, 3), .filter = `==`) %>% str()

list 数据的修改与合并

复制代码
    l1 <- list(
      a = 1:2,
      b = list(6:8)
    )
    splice(l1, d = letters[1:3]) %>% str()
    # List of 3
    #  $ a: int [1:2] 1 2
    #  $ b:List of 1
    #   ..$ : int [1:3] 6 7 8
    #  $ d: chr [1:3] "a" "b" "c"
    list_modify(l1, a = list(1:2)) %>% str()
    # List of 2
    #  $ a:List of 1
    #   ..$ : int [1:2] 1 2
    #  $ b:List of 1
    #   ..$ : int [1:3] 6 7 8
    list_modify(l1, b = list(x = 1:2)) %>% str()
    # List of 2
    #  $ a: int [1:2] 1 2
    #  $ b:List of 2
    #   ..$  : int [1:3] 6 7 8
    #   ..$ x: int [1:2] 1 2
    l2 <- list_merge(l1, c = list(x = letters[1:3], y = "z")) 
    l2 %>% str()
    # List of 3
    #  $ a: int [1:2] 1 2
    #  $ b:List of 1
    #   ..$ : int [1:3] 6 7 8
    #  $ c:List of 2
    #   ..$ x: chr [1:3] "a" "b" "c"
    #   ..$ y: chr "z"
    l2 %>% vec_depth()
    # [1] 3
    l2 %>% map_int(vec_depth)
    # a b c 
    # 1 2 2

list 数据的转置类似于矩阵,在运算过程中,默认基于 list 中的第一个元素来进行转换,并且可以通过 .name 参数设置需要转换的具体字段名。

复制代码
    ll <- list(
      list(x = 1, y = "one"),
      list(z = "two", x = 2)
    )
    ll %>% transpose() %>% str()
    # List of 2
    #  $ x:List of 2
    #   ..$ : num 1
    #   ..$ : num 2
    #  $ y:List of 2
    #   ..$ : chr "one"
    #   ..$ : NULL
    ll %>% transpose(.names = letters[24:26]) %>% str()
    # List of 3
    #  $ x:List of 2
    #   ..$ : num 1
    #   ..$ : num 2
    #  $ y:List of 2
    #   ..$ : chr "one"
    #   ..$ : NULL
    #  $ z:List of 2
    #   ..$ : NULL
    #   ..$ : chr "two"

索引元素

purrr 包提供了几个专门用于获取或设置单个元素值的函数

函数 功能 函数 功能
pluck 根据位置和名称获取和设置元素值 attr_getter 获取对象属性
chuck 类似 pluck,元素不存在会抛出异常 modify_in 使用函数修改值
assign_in 根据数据结果或位置设置对象的值

pluckchuck[[]] 的基础实现功能模块,在对象中提取单一元素的同时实现了对复杂数据结构的深入索引能力

复制代码
    l1 <- list(1, list("a", x = 4:6))
    l2 <- list(2, list("b", y = 7:9))
    x <- list(l1, l2)
    pluck(x, "a") %>% str()
    # List of 2
    #  $ : num 1
    #  $ :List of 2
    #   ..$  : chr "a"
    #   ..$ x: int [1:3] 4 5 6
    pluck(x, "b", 1)
    # [1] 2
    pluck(x, "b", 2, "y")
    # [1] 7 8 9
    pluck(x, "b", 2, "z")  # 不存在元素返回 NULL
    # NULL
    chuck(x, 1, 2, "x")
    # [1] 4 5 6
    chuck(x, 1, 2, "y")    # 不存在元素抛出异常
    # 错误: Can't find name `y` in vector

设置对象元素的值

复制代码
    x <- assign_in(x, list(1, 1), "abc")
    pluck(x, 1, 1)
    # [1] "abc"
    x <- modify_in(x, list(1, 1), ~ paste(.x, "1213"))
    pluck(x, 1, 1)
    # [1] "abc 1213"

获取对象的属性值

复制代码
    x <- list(
      a = structure("obj", obj_attr = "name"),
      b = structure("obj", obj_attr = "age")
      )
    x %>% pluck("b", attr_getter("obj_attr"))
    # [1] "age"

其他函数

此外,在已知 map 函数家族之外,还存在一些衍生产品类别的变种函数。例如 map_if, map_at, 和 map_depth, 以及另外两个独特的类型函数——invoke_maplmap.

invoke_map 函数负责将多个函数及其对应的参数进行整合处理;而 invoke 函数则专为单一函数操作设计,并基于 do.call 实现了更为便捷的操作流程。

复制代码
    invoke(paste, list("a", "b"), sep = "-")
    # [1] "a-b"
    invoke(paste, list("a", "b", sep = "-"))
    # [1] "a-b"
    list(a = 1:3, b = 4:6) %>% invoke(rbind, .)
    #   [,1] [,2] [,3]
    # a    1    2    3
    # b    4    5    6
    list(mean, sum) %>% invoke_map(list(1:5, 6:10))
    # [[1]]
    # [1] 1
    # [[2]]
    # [1] 40
    list(mean, sum) %>% invoke_map_int(list(1:5, 6:10))
    # [1]  1 40
    df <- tibble::tibble(
      f = c("runif", "rpois", "rnorm"),
      params = list(
    list(n = 5),
    list(n = 5, lambda = 10),
    list(n = 5, mean = -3, sd = 10)
      )
    )
    invoke_map(df$f, df$params)
    # [[1]]
    # [1] 0.14916805 0.58352601 0.01615866 0.84491405 0.01821047
    # [[2]]
    # [1]  7  6  9 16 20
    # [[3]]
    # [1]  -1.367514   9.577663   5.136080  -3.414958 -22.902536

这些工具(如)被称为基于条件的映射功能模块,并非全局应用于所有变量;而是针对特定变量施加函数影响。例如

复制代码
    set.seed(1234)
    df <- tibble(
      a = 1:5,
      b = letters[1:5],
      c = rnorm(n = 5)
    )
    df %>% map_if(is.numeric, exp) %>% str()
    # List of 3
    #  $ a: num [1:5] 2.72 7.39 20.09 54.6 148.41
    #  $ b: chr [1:5] "a" "b" "c" "d" ...
    #  $ c: num [1:5] 0.2991 1.3197 2.9578 0.0958 1.5359
    df %>% map_if(is.numeric, exp, .else = ~ paste0(.x, "1")) %>% str()
    # List of 3
    #  $ a: num [1:5] 2.72 7.39 20.09 54.6 148.41
    #  $ b: chr [1:5] "a1" "b1" "c1" "d1" ...
    #  $ c: num [1:5] 0.2991 1.3197 2.9578 0.0958 1.5359
    df %>% map_at(c("a", "c"), ~ .x + 3) %>% str()
    # List of 3
    #  $ a: num [1:5] 4 5 6 7 8
    #  $ b: chr [1:5] "a" "b" "c" "d" ...
    #  $ c: num [1:5] 1.793 3.277 4.084 0.654 3.429
    x <- list(a = list(x = 1:2, y = 3:4), b = list(z = 5:6))
    map_depth(x, 2, sum) %>% str()
    # List of 2
    #  $ a:List of 2
    #   ..$ x: int 3
    #   ..$ y: int 7
    #  $ b:List of 1
    #   ..$ z: int 11

这三个函数在功能上类似于 ... 函数。但它们会根据输入的类型而变化。当传递一个列表时结果仍为列表;当传递数据框时结果则为数据框。

复制代码
    df %>% lmap_if(is.numeric, exp)
    # # A tibble: 5 × 3
    #        a b          c
    #    <dbl> <chr>  <dbl>
    # 1   2.72 a     0.299 
    # 2   7.39 b     1.32  
    # 3  20.1  c     2.96  
    # 4  54.6  d     0.0958
    # 5 148.   e     1.54
    f <- function(x) {
      name <- names(x)
      x <- as.character(x[[1]])
      out <- list(paste(x, 1, sep = "-"))
      names(out) <- name
      out
    }
    df %>% lmap_at(.at = "b", .f = f)
    # # A tibble: 5 × 3
    #       a b          c
    #   <int> <chr>  <dbl>
    # 1     1 a-1   -1.21 
    # 2     2 b-1    0.277
    # 3     3 c-1    1.08 
    # 4     4 d-1   -2.35 
    # 5     5 e-1    0.429
    x <- list(a = 1:4, b = letters[5:7], c = 8:9)
    f <- function(x) {
      name <- names(x)
      out <- list(as.numeric(x[[1]]) * 10)
      names(out) <- name
      out
    }
    x %>% lmap_at(c("a", "c"), f)
    # $a
    # [1] 10 20 30 40
    # $b
    # [1] "e" "f" "g"
    # $c
    # [1] 80 90

请特别注意:用户自定义的函数应返回一个数据格式为列表的结构;在调用时需通过双层方括号的方式提取所需数据进行格式转换操作

11. excel 读写

Microsoft Office Excel 也被广泛应用于数据存储领域,在R语言中存在多个用于解析Excel文件的扩展包。例如,我们可以使用gdata、xlsx以及xlsReadWrite等工具进行操作。特别地,在这些扩展包中,“readxl”以其独立于外部库的独特优势脱颖而出——它无需额外依赖项即可运行,在不同操作系统上均可轻松安装。

该工具兼容旧版 ..xls 格式以及基于 XML 开发的 .xlsx 格式文件。其底层架构采用了 LibXml 的 C 语言版本来进行 ..xls 文件的支持工作,并采用 RapidXML 的 C++ 版本来进行 .xlsx 文件解析。

该包不是 tidyverse 的核心包,需要显式导入

复制代码
    library(readxl)  # version 1.3.1

读取文件

readxl 包中有一些示例文件,在调用无参数的 readxl_example 方法时会列出这些例子,在传递具体例子名称时则能确定其路径

复制代码
    readxl_example()
    # [1] "clippy.xls"    "clippy.xlsx"   "datasets.xls"  "datasets.xlsx"
    # [5] "deaths.xls"    "deaths.xlsx"   "geometry.xls"  "geometry.xlsx"
    # [9] "type-me.xls"   "type-me.xlsx"
    readxl_example("clippy.xls")
    # [1] "/library/readxl/extdata/clippy.xls"

进而可以通过调用 read_excel 函数来处理 xls 和 xlsx 格式的文件,默认情况下,默认设置下 first 表格会被读取。在 Excel 中通常每个文件都包含多个表格,在这种情况下如何访问其他表格?

通过配置 sheet 参数可以指定从 Excel 文件中读取所需的表名。例如,在文件路径 $ datasets.xlsx $中存在一个名为 $ mtcars $的工作表。

复制代码
    readxl_example("datasets.xlsx") %>% read_excel(sheet = 'mtcars') %>% head(3)
    # # A tibble: 3 × 11                                                              
    #     mpg   cyl  disp    hp  drat    wt  qsec    vs    am  gear  carb
    #   <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
    # 1  21       6   160   110  3.9   2.62  16.5     0     1     4     4
    # 2  21       6   160   110  3.9   2.88  17.0     0     1     4     4
    # 3  22.8     4   108    93  3.85  2.32  18.6     1     1     4     1

那我是否只是受限于必须打开 Excel 文件才能了解所有的表名呢?实际上该程序包含了这个函数——excel_sheets——能够返回所有表格名称。

复制代码
    readxl_example("datasets.xlsx") %>% excel_sheets()
    # [1] "iris"     "mtcars"   "chickwts" "quakes"

既然已经获得了所有的表名,请问是否可以通过输入这些表名的索引来访问对应的数据库表中的数据呢?

复制代码
    readxl_example("datasets.xlsx") %>% read_excel(sheet = 3) %>% head(3)
    # # A tibble: 3 × 2                                                               
    #   weight feed     
    #    <dbl> <chr>    
    # 1    179 horsebean
    # 2    160 horsebean
    # 3    136 horsebean

完全没问题,读取到了 chickwts 表的内容

设置表名

默认情况下会将第一行作为表头。即 col_names=TRUE 。如果将该参数设为 FALSE ,则会以递增的数值自动生成表头编号。

复制代码
    readxl_example("datasets.xls") %>% 
      read_excel(skip = 1, sheet = "chickwts", col_names = FALSE) %>%
      head(3)
    # New names:                                                                      
    # * `` -> ...1
    # * `` -> ...2
    # # A tibble: 3 × 2
    #    ...1 ...2     
    #   <dbl> <chr>    
    # 1   179 horsebean
    # 2   160 horsebean
    # 3   136 horsebean

此外,在col_names参数中还可以接受一个字符串向量,并表示我们需要指定的那些列名

复制代码
    readxl_example("datasets.xls") %>% 
      read_excel(
    skip = 1, sheet = "chickwts", 
    col_names = c("chick_weight", "chick_ate_this")
    ) %>%
      head(3)
    # # A tibble: 3 × 2                                                               
    #   chick_weight chick_ate_this
    #          <dbl> <chr>         
    # 1          179 horsebean     
    # 2          160 horsebean     
    # 3          136 horsebean

习惯性地手动指定列名确实是一个费时费力的过程。尤其是那些不符合R语言变量命名规范的名称更是让人头疼。不过请不要担心,在读取Excel文件时通过.name_repair参数可以实现自动检查或修复不规范的列名功能。其功能类似于在tibbles包中的tibblify或.as_tibble函数中使用的机制。

默认情况下 .name_repair = "unique" 会自动赋予每一列一个唯一的名称,并无需进行额外验证;若选中 .name_repair = "universal" 则会生成符合语言规范的名称,并避免使用被禁止字符及关键字。当选择 unique 选项时 列名允许包含空格 若选择 universal 则会将所有空格替换为点号

复制代码
    readxl_example("deaths.xlsx") %>%
      read_excel(range = "arts!A5:D8")
    # # A tibble: 3 × 4                                                               
    #   Name          Profession   Age `Has kids`
    #   <chr>         <chr>      <dbl> <lgl>     
    # 1 David Bowie   musician      69 TRUE      
    # 2 Carrie Fisher actor         60 TRUE      
    # 3 Chuck Berry   musician      90 TRUE
    readxl_example("deaths.xlsx") %>%
      read_excel(range = "arts!A5:D8", .name_repair = "universal")
    # New names:                                                                      
    # * `Has kids` -> Has.kids
    # # A tibble: 3 × 4
    #   Name          Profession   Age Has.kids
    #   <chr>         <chr>      <dbl> <lgl>   
    # 1 David Bowie   musician      69 TRUE    
    # 2 Carrie Fisher actor         60 TRUE    
    # 3 Chuck Berry   musician      90 TRUE

除此之外 .name_repair 参数还可以设置为函数

复制代码
    readxl_example("clippy.xlsx") %>%
      read_excel(.name_repair=toupper)
    # # A tibble: 4 × 2                                                               
    #   NAME                 VALUE    
    #   <chr>                <chr>    
    # 1 Name                 Clippy   
    # 2 Species              paperclip
    # 3 Approx date of death 39083    
    # 4 Weight in grams      0.9
    readxl_example("datasets.xlsx") %>%
      read_excel(
    n_max = 3,
    .name_repair = function(x) tolower(gsub("[.]", "_", x))
      )
    # # A tibble: 3 x 5                                                                                    
    #   sepal_length sepal_width petal_length petal_width species
    #          <dbl>       <dbl>        <dbl>       <dbl> <chr>  
    # 1          5.1         3.5          1.4         0.2 setosa 
    # 2          4.9         3            1.4         0.2 setosa 
    # 3          4.7         3.2          1.3         0.2 setosa

对于 purrr 风格的匿名函数,只能在 purrr 环境下使用

复制代码
    readxl_example("datasets.xlsx") %>%
      read_excel(
    n_max = 3, sheet = 'chickwts', 
    .name_repair = ~ substr(.x, start = 1, stop = 3)
    )
    # # A tibble: 3 x 2                                                                                    
    #     wei fee      
    #   <dbl> <chr>    
    # 1   179 horsebean
    # 2   160 horsebean
    # 3   136 horsebean

读取范围

在常见的工作中,我们可能会遇到Excel表格并非完美呈现矩阵格式的情况。这种情况下,内部可能包含多个独立的表格。为了方便高效地提取相关信息,在处理这些复杂结构时,建议设定数据读取范围并合理配置数据提取界限。

  • n_max 参数设置读取的行数,当该值为 0 时表示只读取表头
复制代码
    readxl_example("datasets.xls") %>% read_excel(n_max = 3)
    # # A tibble: 3 × 5                                                               
    #   Sepal.Length Sepal.Width Petal.Length Petal.Width Species
    #          <dbl>       <dbl>        <dbl>       <dbl> <chr>  
    # 1          5.1         3.5          1.4         0.2 setosa 
    # 2          4.9         3            1.4         0.2 setosa 
    # 3          4.7         3.2          1.3         0.2 setosa
  • range 参数来设置矩形的范围,范围值可以使用 Excel 的行列号
复制代码
    readxl_example("datasets.xls") %>% read_excel(range = "C1:E4")
    # # A tibble: 3 x 3                                                                                    
    #   Petal.Length Petal.Width Species
    #          <dbl>       <dbl> <chr>  
    # 1          1.4         0.2 setosa 
    # 2          1.4         0.2 setosa 
    # 3          1.3         0.2 setosa

我们设置了 rangeC1-E4 的矩形区域,并且注意到该区域中 E4 不在该范围之内。另外,在行或列的读取范围内也可以分别调用 cell_rowscell_cols 函数来实现相应的操作。

复制代码
    readxl_example("datasets.xls") %>% read_excel(range = cell_rows(1:4))
    # # A tibble: 3 x 5                                                                                    
    #   Sepal.Length Sepal.Width Petal.Length Petal.Width Species
    #          <dbl>       <dbl>        <dbl>       <dbl> <chr>  
    # 1          5.1         3.5          1.4         0.2 setosa 
    # 2          4.9         3            1.4         0.2 setosa 
    # 3          4.7         3.2          1.3         0.2 setosa
    readxl_example("datasets.xls") %>% 
      read_excel(range = cell_cols('B:D')) %>%
      head(3)
    # # A tibble: 3 × 3                                                               
    #   Sepal.Width Petal.Length Petal.Width
    #         <dbl>        <dbl>       <dbl>
    # 1         3.5          1.4         0.2
    # 2         3            1.4         0.2
    # 3         3.2          1.3         0.2

或者使用 anchoredcell_limits 函数来锚定一个矩形范围

复制代码
    readxl_example("geometry.xlsx") %>% 
      read_excel(
    col_names = paste0("var", 1:4),
    range = anchored("C3", c(3, 4))
    )
    # # A tibble: 3 × 4                                                               
    #   var1  var2  var3  var4 
    #   <chr> <chr> <lgl> <lgl>
    # 1 C3    D3    NA    NA   
    # 2 C4    D4    NA    NA   
    # 3 C5    D5    NA    NA 
    readxl_example("geometry.xlsx") %>%
      read_excel(
    col_names = FALSE,
    range = cell_limits(c(3, 3), c(5, NA))
      )
    # New names:                                                                      
    # * `` -> ...1
    # * `` -> ...2
    # # A tibble: 3 × 2
    #   ...1  ...2 
    #   <chr> <chr>
    # 1 C3    D3   
    # 2 C4    D4   
    # 3 C5    D5

其中 anchored 函数的第一个参数指定矩形范围的左上角位置, 第二个参数指定行数和列数, 而 cell_limits 函数其两个参数分别为左上角位置和右下角位置, 其中符号表示未设置边界, 即所有数值的有效范围.

也可以为 range 指定字符串格式的表格以及范围,注意为左闭右开区间

复制代码
    readxl_example("datasets.xls") %>% read_excel(range = "mtcars!B1:D5")
    # # A tibble: 4 x 3               
    #     cyl  disp    hp
    #   <dbl> <dbl> <dbl>
    # 1     6   160   110
    # 2     6   160   110
    # 3     4   108    93
    # 4     6   258   110

建议使用 skip 参数来指定在读取前应跳过的行数;当选择 range 选项时,则此参数将不再起作用。

复制代码
    readxl_example("datasets.xls") %>% 
      read_excel(skip = 1, sheet = "chickwts") %>% 
      head(3)
    # # A tibble: 3 × 2                                                               
    #   `179` horsebean
    #   <dbl> <chr>    
    # 1   160 horsebean
    # 2   136 horsebean
    # 3   227 horsebean

类型推测

通常情况下,在调用 read_excel 时,默认会识别并分类每一列的数据类型。然而,在某些特定需求下,默认行为可能无法满足您的预期结果。幸运的是,在调用该函数时,默认行为可能无法满足您的预期结果。在这种情况下,则可以通过指定 col_types 参数来实现更具体的控制。这一参数的设计非常灵活多样,在实际应用中提供了极大的便利性。具体来说,在处理数据时,默认值会被重复应用以确保操作的一致性和稳定性吗?

前面我们介绍过 readr 的类型推测,但是 readxlreadr 有点不太一样

  • readr:根据数据猜测列类型
  • readxl:根据 Excel 单元格类型猜测列类型

其可选的值有

col_types 含义 取值 含义
skip 跳过该列 guess 自动推断
logical 逻辑值 numeric 数值型
date 日期格式 text 字符串类型
list 长度为 1list NULL 默认值,自动推断所有列的类型

如果某一列包含不同类型的数据,可以使用 list 方式解析,例如

复制代码
    readxl_example("clippy.xlsx") %>% read_excel(col_types = c("text", "list"))
    # # A tibble: 4 × 2                                                               
    #   name                 value     
    #   <chr>                <list>    
    # 1 Name                 <chr [1]> 
    # 2 Species              <chr [1]> 
    # 3 Approx date of death <dttm [1]>
    # 4 Weight in grams      <dbl [1]>

处理流程

我们是否总是需要从 Excel 文件加载数据后再进行处理?实际上并非如此——我们可以选择将对我们有帮助的数据直接提取并存储在一个新的文件中以备后续使用

当有一个需要处理的 Excel 文件时, 我们通常可以通过其他软件来打开并了解其基本组成情况, 包括具体有多少张表格以及每张表中存储了什么类型的字段等信息. 然而, 在 Linux 服务器上的这类文档通常无法直接查看其内容. 那么如何获取相关信息呢?

  • 首先,查看文件中包含几个数据表及其名称
复制代码
    path <- readxl_example("deaths.xlsx")
    (sheets <- path %>% excel_sheets() %>% set_names())
    #    arts   other 
    #  "arts" "other"

采用purrr::map函数依次遍历所有数据表的结构。鉴于数据量庞大且内容复杂,在此不做详细展示。观察输出结果后可知:前几项与后几项的数据分布异常;各列名称通常位于第四行。

复制代码
    path %>% excel_sheets() %>%
      set_names() %>%
      map(read_excel, path = path) %>%
      str()

为了获取所需数据,请识别目标表格并选择 arts 表。随后将跳过的前四行记录予以忽略,并限定为10条有效数据进行处理。

复制代码
    arts <- sheets[1] %>% 
      read_excel(path = path, skip = 4, n_max = 10)

再次审视数据信息时会发现,在现有字段中存在几项字段名称不符合规范的情况;然而,在最后两个字段即出生日期和死亡日期中仅需年月日信息即可满足需求,并无需具体的时间戳;因此我们可以将字段名称中的空格替换为下划线,并随后调整字段的数据类型

复制代码
    arts <- sheets[1] %>% 
      read_excel(
    path = path, skip = 4, n_max = 10,                     # 跳过 4 行,读取 10 行
    .name_repair = function(x) gsub(" ", "_", x)) %>%      # 转换列名
      mutate(across(starts_with("Date"), lubridate::as_date))  # 转换类型
    
    head(arts, 3)
    # # A tibble: 3 × 6
    #   Name          Profession   Age Has_kids Date_of_birth Date_of_death
    #   <chr>         <chr>      <dbl> <lgl>    <date>        <date>       
    # 1 David Bowie   musician      69 TRUE     1947-01-08    2016-01-10   
    # 2 Carrie Fisher actor         60 TRUE     1956-10-21    2016-12-27   
    # 3 Chuck Berry   musician      90 TRUE     1926-10-18    2017-03-18

当前输出的数据已达到高度清晰的状态,并且如今我们有能力将这些数据存储至一个 csv 文件中

复制代码
    path %>% basename() %>%            # 获取文件名
      tools::file_path_sans_ext() %>%  # 切除文件名的后缀
      write_csv(x = arts, file = paste0(., "-", sheets[1], ".csv"))

全部评论 (0)

还没有任何评论哟~