Tidyverse包中的循环和迭代-purrr

Author

Lee

Published

September 13, 2023

本章中,我们主要使用purrrdplyr两个包来实现循环和迭代。在R中,迭代通常与其他编程语言看起来截然不同,例如,如果您想要将数值向量x中的每个元素都加倍,在R中,您只需编写2 * x即可。而在大多数其他编程语言中,您需要使用某种形式的for循环来显式地将x的每个元素加倍。类似的情况在使用purrr的时候尤为明显。

1 purrr泛函式循环迭代

purrr包是一个非常强大的包,它提供了一种更简洁的方式来处理循环和迭代。它的核心思想是使用函数式编程的方式来处理数据,而不是使用传统的for循环。

循环迭代,本质上是将一个函数依次应用到序列的每一个元素上,在purrr中,这个函数就是map()map()函数的基本语法为purrr::map(.x, .f, ...)

在最新的purrr中,为使逻辑更清晰一致,已经建议弃用map_df/map_dfr/map_dfc这些一步到位的操作,改用map+list_bind/list_cbind

2 修改多列

2.1 选择多列并调用单个函数

library(tidyverse)
df <- tibble(
  a = rnorm(10),
  b = rnorm(10),
  c = rnorm(10),
  d = rnorm(10)
)

df |>
  summarise(
    n = n(),
    a = median(a),
    b = median(b),
    c = median(c),
    d = median(d),
  )
# A tibble: 1 × 5
      n     a     b       c      d
  <int> <dbl> <dbl>   <dbl>  <dbl>
1    10 0.529 0.108 -0.0540 -0.102

上述代码在计算df各列时,median()函数被重复执行了四次,意味着我们进行了多次的复制粘贴操作,这与“在代码中永远不要复制粘贴超过2次”的原则相违背。试想如果数据集中有10个以上的列,多次进行复制粘贴不仅繁琐,而且容易产生错误。幸好,在tidyverse中的acorss()函数就排上了用场。

across()是一个功能异常强大的函数,它有3个关键的参数:

  1. .cols,操作的目标列,可以是多列,where()which()everything()starts_with()等在此处同样适用。
  2. .fns,对目标列进行操作的函数,同样可以是多个函数,如果是多个函数,则需要以列表的形式传入。
  3. .names,控制输出列的名称,在与mutate()连用时非常有用。

那么上述代码可以使用across()进行改写。

df |>
  summarise(
    across(a:d, median)
  )
# A tibble: 1 × 4
      a     b       c      d
  <dbl> <dbl>   <dbl>  <dbl>
1 0.529 0.108 -0.0540 -0.102
Important

across()函数有两个使用注意点一定要牢记:

  1. across()不能单独使用,它必须在summarise()mutate()等函数中使用。
  2. across().fns传入函数时,函数后不需要加括号。如果函数中需要传入参数,则需要使用匿名函数。有关匿名函数,在下节中会有描述。

2.2 选择多列并调用多个函数

# 生成一个可操作的数据集
rnorm_na <- function(n, n_na, mean = 0, sd = 1) {
  sample(c(rnorm(n - n_na, mean = mean, sd = sd), rep(NA, n_na)))
} # 生成包含缺失值的正态分布数列,项数为n-n_na,其中有n-n_na项缺失值
df_miss <- tibble(
  a = rnorm_na(5, 1),
  b = rnorm_na(5, 1),
  c = rnorm_na(5, 2),
  d = rnorm(5)
)

# 计算df_miss各列的中数,计算时需要去掉缺失值
df_miss |>
  summarise(
    across(a:d, \(x) median(x, na.rm = TRUE))
  )
# A tibble: 1 × 4
        a     b       c      d
    <dbl> <dbl>   <dbl>  <dbl>
1 0.00670 0.492 -0.0768 -0.897
Warning

上述代码的匿名函数写作了\(x) median(x, na.rm = TRUE),而我们更熟悉的写法是~median(.x, na.rm = TRUE)。Hadley之所以修改写法基于以下两个方面:

  1. .x的写法只适用于tidyverse函数内。
  2. .x有时指代比较抽象,不便理解。

基于此,Hadley建议使用\(x)的写法,例如~.x + 1建议写为\(x) x + 1

如果我们系统同时计算数据集中每列的中数并求和呢?这就需要调用两个函数。

df_miss |>
  summarise(
    across(
      a:d, # 一定要指定列
      list(
        median = \(x) median(x, na.rm = TRUE),
        sum = \(x) sum(x, na.rm = TRUE)
      )
    )
  )
# A tibble: 1 × 8
  a_median a_sum b_median b_sum c_median  c_sum d_median d_sum
     <dbl> <dbl>    <dbl> <dbl>    <dbl>  <dbl>    <dbl> <dbl>
1  0.00670 -1.79    0.492  1.23  -0.0768 -0.187   -0.897 -2.91

上述代码输出的结果列的名称是使用一个类似于 {.col}_{.fn}glue 规范命名的,其中 .col 是原始列的名称,.fn 是函数的名称。我们可以使用 .names 参数来提供您自己的 glue 规范

df_miss |>
  summarise(
    across(
      a:d, # 一定要指定列
      list(
        median = \(x) median(x, na.rm = TRUE),
        sum = \(x) sum(x, na.rm = TRUE)
      ),
      .names = "{.fn}-{.col}"
    )
  )
# A tibble: 1 × 8
  `median-a` `sum-a` `median-b` `sum-b` `median-c` `sum-c` `median-d` `sum-d`
       <dbl>   <dbl>      <dbl>   <dbl>      <dbl>   <dbl>      <dbl>   <dbl>
1    0.00670   -1.79      0.492    1.23    -0.0768  -0.187     -0.897   -2.91

.names参数在与mutate()连用时非常有用,通常用来对比新生成的列与原有列的不同。

# 求数据集的绝对值
df_miss |>
  mutate(
    across(a:d, \(x) abs(x), .names = "{.col}_abs")
  )
# A tibble: 5 × 8
       a       b       c       d  a_abs   b_abs   c_abs  d_abs
   <dbl>   <dbl>   <dbl>   <dbl>  <dbl>   <dbl>   <dbl>  <dbl>
1 -0.242 -0.0278 NA      -1.06    0.242  0.0278 NA      1.06  
2  0.255  1.63   -1.59   -0.897   0.255  1.63    1.59   0.897 
3  0.266 NA      -0.0768  0.199   0.266 NA       0.0768 0.199 
4 NA      1.01   NA      -0.0204 NA      1.01   NA      0.0204
5 -2.07  -1.38    1.48   -1.13    2.07   1.38    1.48   1.13  

3 行操作

across()summarize()mutate()匹配度很好,但都是针对列的操作,有没有针对filter()的行的操作呢?

dplyrif_any()if_all()两个函数功能就是across()针对行操作的变种。我们具体来看:

# 选择包含缺失值的所有行
x <- df_miss |>
  filter(
    if_any(a:d, is.na)
  )

# 选择全是缺失值的所有行
df_miss |>
  filter(
    if_all(a:d, is.na)
  )
# A tibble: 0 × 4
# ℹ 4 variables: a <dbl>, b <dbl>, c <dbl>, d <dbl>

这部分更详细的说明可参看:https://bookdown.org/wangminjie/R4DS/tidyverse-beauty-of-across2.html#dplyr-1.0.4-if_any-and-if_all。

4 在自定义函数中使用across()

expand_dates <- function(df) {
  df |>
    mutate(across(where(is.Date), list(year = year, month = month, day = mday)))
}

上述函数的作用为:将所有日期列扩展为年、月和日列:

df_date <- tibble(
  name = c("Amy", "Bob"),
  date = ymd(c("2009-08-03", "2010-01-16"))
)
df_date
# A tibble: 2 × 2
  name  date      
  <chr> <date>    
1 Amy   2009-08-03
2 Bob   2010-01-16
df_date |>
  expand_dates()
# A tibble: 2 × 5
  name  date       date_year date_month date_day
  <chr> <date>         <dbl>      <dbl>    <int>
1 Amy   2009-08-03      2009          8        3
2 Bob   2010-01-16      2010          1       16

across()还可以与非标准性评估(tidy eval)结合,实现给一个参数传入多个列的操作,只要注意将这些传入多个列的参数用两个大括号括起来。关于非标准性评估(tidy eval),可参看这里这里。还有我自己的blog

TODO: 补充自己blog链接

5 与pivot_longer()连用

pivot_longer()across()间有许多有趣的联系,它们可以实现相同的操作并得到一致的结果。我们看一个例子,例如我们需要计算df数据集每列的中数和众数。

  • 使用across():我们可以看到结果是一个宽表格,每列的名称的形式为”列名_函数名”。
df |>
  summarise(
    across(a:d, list(median = median, mean = mean))
  )
# A tibble: 1 × 8
  a_median a_mean b_median b_mean c_median  c_mean d_median d_mean
     <dbl>  <dbl>    <dbl>  <dbl>    <dbl>   <dbl>    <dbl>  <dbl>
1    0.529  0.389    0.108  0.527  -0.0540 -0.0844   -0.102 -0.243
long <- df |>
  pivot_longer(a:d) |>
  group_by(name) |>
  summarise(
    median = median(value),
    mean = mean(value)
  )
long
# A tibble: 4 × 3
  name   median    mean
  <chr>   <dbl>   <dbl>
1 a      0.529   0.389 
2 b      0.108   0.527 
3 c     -0.0540 -0.0844
4 d     -0.102  -0.243 
long |>
  pivot_wider(
    names_from = name,
    values_from = c(median, mean),
    names_vary = "slowest",
    names_glue = "{name}_{.value}"
  )
# A tibble: 1 × 8
  a_median a_mean b_median b_mean c_median  c_mean d_median d_mean
     <dbl>  <dbl>    <dbl>  <dbl>    <dbl>   <dbl>    <dbl>  <dbl>
1    0.529  0.389    0.108  0.527  -0.0540 -0.0844   -0.102 -0.243

这个操作在我们无法使用across()的时候非常有用:即当我们需要同时计算一组多列的时候。例如,数据框同时包含了数值列和权重列,我们需要计算加权平均值时(我理解其实就是数据不整洁的时候):

df_paired <- tibble(
  a_val = rnorm(10),
  a_wts = runif(10),
  b_val = rnorm(10),
  b_wts = runif(10),
  c_val = rnorm(10),
  c_wts = runif(10),
  d_val = rnorm(10),
  d_wts = runif(10)
)
df_paired
# A tibble: 10 × 8
     a_val a_wts  b_val   b_wts   c_val    c_wts  d_val   d_wts
     <dbl> <dbl>  <dbl>   <dbl>   <dbl>    <dbl>  <dbl>   <dbl>
 1  0.142  0.713  0.868 0.921   -1.04   0.711     0.104 0.913  
 2 -0.887  0.701 -1.07  0.00904  0.401  0.353    -0.394 0.854  
 3  1.22   0.482 -0.477 0.744   -0.763  0.533     2.04  0.402  
 4 -0.810  0.360 -0.596 0.0532   1.48   0.277    -1.08  0.672  
 5 -0.259  0.883 -0.659 0.896   -0.262  0.000178 -1.15  0.466  
 6  0.700  0.575  0.747 0.954   -0.601  0.383     1.44  0.402  
 7  0.0426 0.254  0.677 0.170   -0.848  0.997    -0.176 0.180  
 8  0.713  0.935  0.646 0.961    0.155  0.268     0.759 0.541  
 9 -0.364  0.851  1.41  0.234   -0.0721 0.736    -0.234 0.00948
10  0.543  0.443  1.87  0.254    1.12   0.0446    0.669 0.283  

df_paired数据框是不整洁的,我们无法使用across()函数,此时我们需要进行转换:

df_long <- df_paired |>
  pivot_longer(
    everything(),
    names_to = c("group", ".value"),
    names_sep = "_"
  )
df_long
# A tibble: 40 × 3
   group    val     wts
   <chr>  <dbl>   <dbl>
 1 a      0.142 0.713  
 2 b      0.868 0.921  
 3 c     -1.04  0.711  
 4 d      0.104 0.913  
 5 a     -0.887 0.701  
 6 b     -1.07  0.00904
 7 c      0.401 0.353  
 8 d     -0.394 0.854  
 9 a      1.22  0.482  
10 b     -0.477 0.744  
# ℹ 30 more rows
df_long |>
  group_by(group) |>
  summarise(mean = weighted.mean(val, wts)) |>
  pivot_wider(
    names_from = group,
    values_from = mean,
    names_glue = "{group}_{.value}"
  )
# A tibble: 1 × 4
  a_mean b_mean c_mean d_mean
   <dbl>  <dbl>  <dbl>  <dbl>
1 0.0901  0.397 -0.380 0.0977

6 案例:探索ariquality数据集

ariquality数据集是一个关于美国各州空气质量和健康状况的数据集。我们将使用purrrdplyr来探索这个数据集。

  • 掌握 map 循环迭代解决问题的分解逻辑和一般流程,能够灵活运用 map 系列函数,通过调试解决各种具体问题。
  • 体会数据结构的重要性,特别是通过恰当的后缀,控制结果的返回类型。

我们主要从以下几个方面来探索airquality数据集:

  1. Temp 温度列为华氏度(𝐹 ),用 map 迭代转化为摄氏度(𝐶),保留 1 位小数。
  2. Month 和 Day 列分别是月和日,结合 1973 年,用 map 迭代从这两列计算日期列。
  3. 探索数据框 airquality 各列的基本信息:类型、缺失值个数。
  4. 对前 4 列做归一化,需要区分正向指标和逆向指标。
  5. 计算数据框 airquality 每一行缺失比例,保留 3 位小数。
  6. 批量写出数据到文件。
df <- airquality

6.1 温度列转换

# 取一个元素调试解决问题
x <- df$Temp[1]
x
[1] 67
round((x - 32) / 1.8, 1)
[1] 19.4
# 编写函数
temp_convert <- function(temp_f) {
  round((temp_f - 32) / 1.8, 1)
}

# 使用 map系列 函数将函数应用到数据框的 Temp 列
df %>%
  mutate(Temp_C = map_dbl(Temp, temp_convert)) %>%
  select(Temp, Temp_C)
    Temp Temp_C
1     67   19.4
2     72   22.2
3     74   23.3
4     62   16.7
5     56   13.3
6     66   18.9
7     65   18.3
8     59   15.0
9     61   16.1
10    69   20.6
11    74   23.3
12    69   20.6
13    66   18.9
14    68   20.0
15    58   14.4
16    64   17.8
17    66   18.9
18    57   13.9
19    68   20.0
20    62   16.7
21    59   15.0
22    73   22.8
23    61   16.1
24    61   16.1
25    57   13.9
26    58   14.4
27    57   13.9
28    67   19.4
29    81   27.2
30    79   26.1
31    76   24.4
32    78   25.6
33    74   23.3
34    67   19.4
35    84   28.9
36    85   29.4
37    79   26.1
38    82   27.8
39    87   30.6
40    90   32.2
41    87   30.6
42    93   33.9
43    92   33.3
44    82   27.8
45    80   26.7
46    79   26.1
47    77   25.0
48    72   22.2
49    65   18.3
50    73   22.8
51    76   24.4
52    77   25.0
53    76   24.4
54    76   24.4
55    76   24.4
56    75   23.9
57    78   25.6
58    73   22.8
59    80   26.7
60    77   25.0
61    83   28.3
62    84   28.9
63    85   29.4
64    81   27.2
65    84   28.9
66    83   28.3
67    83   28.3
68    88   31.1
69    92   33.3
70    92   33.3
71    89   31.7
72    82   27.8
73    73   22.8
74    81   27.2
75    91   32.8
76    80   26.7
77    81   27.2
78    82   27.8
79    84   28.9
80    87   30.6
81    85   29.4
82    74   23.3
83    81   27.2
84    82   27.8
85    86   30.0
86    85   29.4
87    82   27.8
88    86   30.0
89    88   31.1
90    86   30.0
91    83   28.3
92    81   27.2
93    81   27.2
94    81   27.2
95    82   27.8
96    86   30.0
97    85   29.4
98    87   30.6
99    89   31.7
100   90   32.2
101   90   32.2
102   92   33.3
103   86   30.0
104   86   30.0
105   82   27.8
106   80   26.7
107   79   26.1
108   77   25.0
109   79   26.1
110   76   24.4
111   78   25.6
112   78   25.6
113   77   25.0
114   72   22.2
115   75   23.9
116   79   26.1
117   81   27.2
118   86   30.0
119   88   31.1
120   97   36.1
121   94   34.4
122   96   35.6
123   94   34.4
124   91   32.8
125   92   33.3
126   93   33.9
127   93   33.9
128   87   30.6
129   84   28.9
130   80   26.7
131   78   25.6
132   75   23.9
133   73   22.8
134   81   27.2
135   76   24.4
136   77   25.0
137   71   21.7
138   71   21.7
139   78   25.6
140   67   19.4
141   76   24.4
142   68   20.0
143   82   27.8
144   64   17.8
145   71   21.7
146   81   27.2
147   69   20.6
148   63   17.2
149   70   21.1
150   77   25.0
151   75   23.9
152   76   24.4
153   68   20.0
# 当函数比较简单时,使用匿名函数直接计算
map_dbl(df$Temp, \(x) round((x - 32) / 1.8, 1))
  [1] 19.4 22.2 23.3 16.7 13.3 18.9 18.3 15.0 16.1 20.6 23.3 20.6 18.9 20.0 14.4
 [16] 17.8 18.9 13.9 20.0 16.7 15.0 22.8 16.1 16.1 13.9 14.4 13.9 19.4 27.2 26.1
 [31] 24.4 25.6 23.3 19.4 28.9 29.4 26.1 27.8 30.6 32.2 30.6 33.9 33.3 27.8 26.7
 [46] 26.1 25.0 22.2 18.3 22.8 24.4 25.0 24.4 24.4 24.4 23.9 25.6 22.8 26.7 25.0
 [61] 28.3 28.9 29.4 27.2 28.9 28.3 28.3 31.1 33.3 33.3 31.7 27.8 22.8 27.2 32.8
 [76] 26.7 27.2 27.8 28.9 30.6 29.4 23.3 27.2 27.8 30.0 29.4 27.8 30.0 31.1 30.0
 [91] 28.3 27.2 27.2 27.2 27.8 30.0 29.4 30.6 31.7 32.2 32.2 33.3 30.0 30.0 27.8
[106] 26.7 26.1 25.0 26.1 24.4 25.6 25.6 25.0 22.2 23.9 26.1 27.2 30.0 31.1 36.1
[121] 34.4 35.6 34.4 32.8 33.3 33.9 33.9 30.6 28.9 26.7 25.6 23.9 22.8 27.2 24.4
[136] 25.0 21.7 21.7 25.6 19.4 24.4 20.0 27.8 17.8 21.7 27.2 20.6 17.2 21.1 25.0
[151] 23.9 24.4 20.0
# 对于向量,可以直接使用向量计算,没必要使用map迭代核算
round((df$Temp - 32) / 1.8, 1)
  [1] 19.4 22.2 23.3 16.7 13.3 18.9 18.3 15.0 16.1 20.6 23.3 20.6 18.9 20.0 14.4
 [16] 17.8 18.9 13.9 20.0 16.7 15.0 22.8 16.1 16.1 13.9 14.4 13.9 19.4 27.2 26.1
 [31] 24.4 25.6 23.3 19.4 28.9 29.4 26.1 27.8 30.6 32.2 30.6 33.9 33.3 27.8 26.7
 [46] 26.1 25.0 22.2 18.3 22.8 24.4 25.0 24.4 24.4 24.4 23.9 25.6 22.8 26.7 25.0
 [61] 28.3 28.9 29.4 27.2 28.9 28.3 28.3 31.1 33.3 33.3 31.7 27.8 22.8 27.2 32.8
 [76] 26.7 27.2 27.8 28.9 30.6 29.4 23.3 27.2 27.8 30.0 29.4 27.8 30.0 31.1 30.0
 [91] 28.3 27.2 27.2 27.2 27.8 30.0 29.4 30.6 31.7 32.2 32.2 33.3 30.0 30.0 27.8
[106] 26.7 26.1 25.0 26.1 24.4 25.6 25.6 25.0 22.2 23.9 26.1 27.2 30.0 31.1 36.1
[121] 34.4 35.6 34.4 32.8 33.3 33.9 33.9 30.6 28.9 26.7 25.6 23.9 22.8 27.2 24.4
[136] 25.0 21.7 21.7 25.6 19.4 24.4 20.0 27.8 17.8 21.7 27.2 20.6 17.2 21.1 25.0
[151] 23.9 24.4 20.0

6.2 日期列计算

date_convert <- function(month, day) {
  as.Date(paste("1973", month, day, sep = "-"))
}

map2_vec(df$Month, df$Day, date_convert)
  [1] "1973-05-01" "1973-05-02" "1973-05-03" "1973-05-04" "1973-05-05"
  [6] "1973-05-06" "1973-05-07" "1973-05-08" "1973-05-09" "1973-05-10"
 [11] "1973-05-11" "1973-05-12" "1973-05-13" "1973-05-14" "1973-05-15"
 [16] "1973-05-16" "1973-05-17" "1973-05-18" "1973-05-19" "1973-05-20"
 [21] "1973-05-21" "1973-05-22" "1973-05-23" "1973-05-24" "1973-05-25"
 [26] "1973-05-26" "1973-05-27" "1973-05-28" "1973-05-29" "1973-05-30"
 [31] "1973-05-31" "1973-06-01" "1973-06-02" "1973-06-03" "1973-06-04"
 [36] "1973-06-05" "1973-06-06" "1973-06-07" "1973-06-08" "1973-06-09"
 [41] "1973-06-10" "1973-06-11" "1973-06-12" "1973-06-13" "1973-06-14"
 [46] "1973-06-15" "1973-06-16" "1973-06-17" "1973-06-18" "1973-06-19"
 [51] "1973-06-20" "1973-06-21" "1973-06-22" "1973-06-23" "1973-06-24"
 [56] "1973-06-25" "1973-06-26" "1973-06-27" "1973-06-28" "1973-06-29"
 [61] "1973-06-30" "1973-07-01" "1973-07-02" "1973-07-03" "1973-07-04"
 [66] "1973-07-05" "1973-07-06" "1973-07-07" "1973-07-08" "1973-07-09"
 [71] "1973-07-10" "1973-07-11" "1973-07-12" "1973-07-13" "1973-07-14"
 [76] "1973-07-15" "1973-07-16" "1973-07-17" "1973-07-18" "1973-07-19"
 [81] "1973-07-20" "1973-07-21" "1973-07-22" "1973-07-23" "1973-07-24"
 [86] "1973-07-25" "1973-07-26" "1973-07-27" "1973-07-28" "1973-07-29"
 [91] "1973-07-30" "1973-07-31" "1973-08-01" "1973-08-02" "1973-08-03"
 [96] "1973-08-04" "1973-08-05" "1973-08-06" "1973-08-07" "1973-08-08"
[101] "1973-08-09" "1973-08-10" "1973-08-11" "1973-08-12" "1973-08-13"
[106] "1973-08-14" "1973-08-15" "1973-08-16" "1973-08-17" "1973-08-18"
[111] "1973-08-19" "1973-08-20" "1973-08-21" "1973-08-22" "1973-08-23"
[116] "1973-08-24" "1973-08-25" "1973-08-26" "1973-08-27" "1973-08-28"
[121] "1973-08-29" "1973-08-30" "1973-08-31" "1973-09-01" "1973-09-02"
[126] "1973-09-03" "1973-09-04" "1973-09-05" "1973-09-06" "1973-09-07"
[131] "1973-09-08" "1973-09-09" "1973-09-10" "1973-09-11" "1973-09-12"
[136] "1973-09-13" "1973-09-14" "1973-09-15" "1973-09-16" "1973-09-17"
[141] "1973-09-18" "1973-09-19" "1973-09-20" "1973-09-21" "1973-09-22"
[146] "1973-09-23" "1973-09-24" "1973-09-25" "1973-09-26" "1973-09-27"
[151] "1973-09-28" "1973-09-29" "1973-09-30"

Footnotes

  1. 序列由一系列可以根据位置索引的元素构成,向量、列表、数据框都是经常用作序列的数据结构。↩︎