添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
首发于 R

R 数据处理(十九)

5. map 函数

遍历向量,并对向量中每个元素执行函数操作,最后返回结果的模式是非常普遍的。

因此, purrr 提供了一系列函数来帮你简化这些操作,每种类型的都有一个对应的函数

  • map() - list
  • map_lgl() - 逻辑值向量
  • map_int() - 整数向量
  • map_dbl() - double 向量
  • map_chr() - 字符串向量

每个函数都将一个向量作为输入,将一个函数应用于每个元素,然后返回一个与输入长度相同(名称相同)的新变量。向量的类型由 map 函数的后缀确定。

这样能够极大减少 for 循环的使用,使你的代码更加简洁和优雅。

当然并不是说 for 循环的速度很慢,我们避免使用 for 循环只是为了让代码更加的清晰,易于编写和阅读。

我们使用 map 函数来重写最后一个 for 循环,用更少的代码完成同样的工作。

因为我们的数据是双精度的,所以使用 map_dbl() 函数

> map_dbl(df, mean)
          a           b           c           d 
 0.41487173 -0.16774333 -0.05348092  0.01059490 
> map_dbl(df, median)
          a           b           c           d 
 0.20374050 -0.23739013  0.05839867  0.08679879 
> map_dbl(df, sd)
        a         b         c         d 
1.0889674 0.8172145 0.8357361 0.7092745

for 循环相比,我们突出的是对每个元素执行的函数,如果我们使用管道符,这一点会更加明显

> df %>% map_dbl(mean)
          a           b           c           d 
 0.41487173 -0.16774333 -0.05348092  0.01059490 
> df %>% map_dbl(median)
          a           b           c           d 
 0.20374050 -0.23739013  0.05839867  0.08679879 
> df %>% map_dbl(sd)
        a         b         c         d 
1.0889674 0.8172145 0.8357361 0.7092745 

map_*() col_summary() 之间有一些区别:

  • 所有 purrr 函数都是用 C 实现的。所以它们的速度更快一点,但牺牲了可读性
  • 第二个参数是要应用的函数 .f ,它可以是公式、字符向量或整数向量
  • map_*() 使用 ... 来解析额外的参数并将其传递给函数 .f
> map_dbl(df, mean, trim = 0.5)
          a           b           c           d 
 0.20374050 -0.23739013  0.05839867  0.08679879 
  • map 函数还保留原来向量的名称
> z <- list(x = 1:3, y = 4:5)
> map_int(z, length)

5.1 快捷键

有几个快捷方式可以与 .f 一起使用,以节省输入。

假设您希望将线性模型拟合到数据集中的每个组,下面的示例将 mtcars 数据集分割为三个部分(每个圆柱体的值一个),并对每个部分拟合相同的线性模型

models <- mtcars %>% 
  split(.$cyl) %>% 
  map(function(df) lm(mpg ~ wt, data = df))

这里 . 作为代词,表示当前列表元素

当我们想要提取一些汇总信息时,比如 R^2 。我们可以使用 summary() 获得汇总信息,然后提取出名为 r.squared 的组件。我们可以使用匿名函数的简写来完成

> models %>% 
+     map(summary) %>% 
+     map_dbl(~.$r.squared)
        4         6         8 
0.5086326 0.4645102 0.4229655

但是提取命名组件是一个常见的操作,因此 purrr 提供了一个更短的快捷方式:使用字符串。

> models %>% 
+     map(summary) %>% 
+     map_dbl("r.squared")
        4         6         8 
0.5086326 0.4645102 0.4229655

你也可以使用一个整数来按位置选择元素

> x <- list(list(1, 2, 3), list(4, 5, 6), list(7, 8, 9))
> x %>% map_dbl(2)
[1] 2 5 8

5.3 思考练习

  1. 使用 map 函数来实现
  • 计算 mtcars 中每列的平均值
  • 确定 nycflights13::flights 中每个列的类型
  • 计算 iris 每一列中唯一值的数量
  • 分别以 -10 0 10 100 的平均值生成 10 个正态随机数
  1. 在非 list 向量上使用 map 函数时会发生什么? map(1:5,runif) 有什么作用?为什么?
  2. map(-2:2, rnorm, n = 5) 有什么作用?为什么? map_dbl(-2:2, rmrm, n = 5) 有什么作用?为什么?

6. 处理错误

当您使用 map 函数来重复许多操作时,如果其中某一步出现错误,您将得到一条错误消息,并且没有输出。

这就很烦人了,为什么一次失败会阻碍你获得其他成功值? 你要如何确保一个坏苹果不会毁掉一整桶?

我们可以使用 safely 函数来处理这种情况,它接受一个函数并返回修改后的版本。

在这种情况下,修改后的函数将永远不会抛出错误,并且它总是返回一个包含两个元素的列表

  • result : 结果,如果出现错误,返回 NULL
  • error : error 对象,如果操作成功,返回 NULL

它与 try() 函数类似,但 try 有时返回原始结果,有时返回错误对象,所以处理起来比较困难

举个简单的例子来说明一下

> safe_log <- safely(log)
> str(safe_log(10))
List of 2
 $ result: num 2.3
 $ error : NULL
> str(safe_log("a"))
List of 2
 $ result: NULL
 $ error :List of 2
  ..$ message: chr "数学函数中用了非数值参数"
  ..$ call   : language .Primitive("log")(x, base)
  ..- attr(*, "class")= chr [1:3] "simpleError" "error" "condition"

函数执行成功, result 元素包含结果, error 元素为 NULL 。当函数执行失败时, result 元素为 NULL ,并且 error 元素包含错误对象

map 函数一起使用

> x <- list(1, 10, "a")
> y <- x %>% map(safely(log))
> str(y)
List of 3
 $ :List of 2
  ..$ result: num 0
  ..$ error : NULL
 $ :List of 2
  ..$ result: num 2.3
  ..$ error : NULL
 $ :List of 2
  ..$ result: NULL
  ..$ error :List of 2
  .. ..$ message: chr "数学函数中用了非数值参数"
  .. ..$ call   : language .Primitive("log")(x, base)
  .. ..- attr(*, "class")= chr [1:3] "simpleError" "error" "condition"

如果我们将其转换为包含两个元素的 list :一个存储所有错误一个存储所有输出。会更容易使用

purrr::transpose() 可以很容易做到

> y <- y %>% transpose()
> str(y)
List of 2
 $ result:List of 3
  ..$ : num 0
  ..$ : num 2.3
  ..$ : NULL
 $ error :List of 3
  ..$ : NULL
  ..$ : NULL
  ..$ :List of 2
  .. ..$ message: chr "数学函数中用了非数值参数"
  .. ..$ call   : language .Primitive("log")(x, base)
  .. ..- attr(*, "class")= chr [1:3] "simpleError" "error" "condition"

然后,来处理错误

> is_ok <- y$error %>% map_lgl(is_null)
> x[!is_ok]
[[1]]
[1] "a"
> y$result[is_ok] %>% flatten_dbl()
[1] 0.000000 2.302585

purrr 还提供了另外两个有用的函数:

  • 类似 safely() possibly() 总是成功的。它比 safely() 更简单,因为当出现错误时,您给它一个要返回的默认值
> x <- list(1, 10, "a")
> x %>% map_dbl(possibly(log, NA_real_))
[1] 0.000000 2.302585       NA
  • quietly() safety() 的作用类似,但不捕获错误,而是捕获打印的输出,消息和警告
> x <- list(1, -1)
> x %>% map(quietly(log)) %>% str()
List of 2
 $ :List of 4
  ..$ result  : num 0
  ..$ output  : chr ""
  ..$ warnings: chr(0) 
  ..$ messages: chr(0) 
 $ :List of 4
  ..$ result  : num NaN
  ..$ output  : chr ""
  ..$ warnings: chr "产生了NaNs"
  ..$ messages: chr(0) 

7. 多参数 map

到目前为止,我们只对 map 传入了一个输入参数,但通常需要对多个相关的输入并行迭代。这就是 map2() pmap() 函数的工作。

例如,假设您模拟一些不同均值的正态分布随机值。可以使用 map()

> mu <- list(5, 10, -3)
> mu %>% 
+     map(rnorm, n = 5) %>% 
+     str()
List of 3
 $ : num [1:5] 4.58 4.8 4.08 5.72 3.21
 $ : num [1:5] 8.58 11.45 10.65 9.48 10.81
 $ : num [1:5] -5.02 -5.25 -2.65 -2.74 -2.84

如果您还想改变标准差怎么办?一种方法是遍历索引并传入对应索引的均值和方差。

> sigma <- list(1, 5, 10)
> seq_along(mu) %>% 
+     map(~rnorm(5, mu[[.]], sigma[[.]])) %>% 
+     str()
List of 3
 $ : num [1:5] 5.48 3.9 4.28 5.46 4.8
 $ : num [1:5] 7.913 9.208 9.664 -1.062 0.389
 $ : num [1:5] 19.8 5.22 -19.1 -2.8 -6.54

但是这混淆了代码的意图,我们可以使用 map2() 来并行迭代两个向量

> map2(mu, sigma, rnorm, n = 5) %>% str()
List of 3
 $ : num [1:5] 5.67 4.89 5.64 3.39 5.19
 $ : num [1:5] 8.513 11.702 13.607 0.998 -0.381
 $ : num [1:5] -19.4 -6.2 2.38 3.89 -16.89

map2() 生成以下一系列函数调用

注意 :每次调用的参数都在函数之前。固定参数在函数之后

map 一样, map2 只是函数的包装

map2 <- function(x, y, f, ...) {
  out <- vector("list", length(x))
  for (i in seq_along(x)) {
    out[[i]] <- f(x[[i]], y[[i]], ...)

你可能会想,是不是也有 map3 , map4 , map5 等函数呢?这种想法是不对的,像这种都会有一个可变参数列表来实现

purrr 提供了 pmap() ,它接受参数列表。例如,如果您想改变平均值、标准偏差和样本数量

> n <- list(1, 3, 5)
> args1 <- list(n, mu, sigma)
> args1 %>%
+     pmap(rnorm) %>% 
+     str()
List of 3
 $ : num 5.57
 $ : num [1:3] 16.34 19.49 6.76
 $ : num [1:5] 4.95 7.75 20 -18.65 4.66

会执行如下操作

如果不命名列表的元素, pmap() 将在调用函数时使用位置匹配。这容易出错,并且使代码更难阅读,所以最好命名参数

> args2 <- list(mean = mu, sd = sigma, n = n)
> args2 %>% 
+     pmap(rnorm) %>% 
+     str()
List of 3
 $ : num 5.2
 $ : num [1:3] 11.34 13.53 5.42
 $ : num [1:5] 13.41 -6.24 -2.85 -12.55 -4.16

这样会产生更长但更安全的调用

因为参数的长度是一样的,所以可以将它们存储在数据框中

> params <- tribble(
+     ~mean, ~sd, ~n,
+     5,     1,  1,
+     10,     5,  3,
+     -3,    10,  5
> params %>% 
+     pmap(rnorm)
[[1]]
[1] 3.567739
[[2]]
[1] 17.696317 17.825486  7.442771
[[3]]
[1] -8.619017 14.267712 -3.187630 -8.664932 15.264829

当代码变得非常复杂时,推荐使用数据框形式的参数,保证每列都有列名,且长度相同

7.1 调用不同的函数

进一步提高复杂性,除了更改函数的参数外,您还可以更改函数本身

f <- c("runif", "rnorm", "rpois")
param <- list(
  list(min = -1, max = 1), 
  list(sd = 5), 
  list(lambda = 10)

这种情况下,可以使用 invoke_map() 函数

> invoke_map(f, param, n = 5) %>% str()
List of 3
 $ : num [1:5] -0.8496 -0.1387 -0.0755 -0.2218 0.6228
 $ : num [1:5] 3.19 -1.3 5.11 -7.96 -1.04
 $ : int [1:5] 8 5 9 9 10

第一个参数是函数列表或函数名的字符向量。第二个参数是一个列表,给出了每个函数的不同参数。随后的参数会传递给每个函数

同样,您可以使用 tribble() 使创建这些配对变得更简单

sim <- tribble(
  ~f,      ~params,
  "runif", list(min = -1, max = 1),
  "rnorm", list(sd = 5),
  "rpois", list(lambda = 10)
sim %>% 
  mutate(sim = invoke_map(f, params, n = 10))

8. walk

walk map 函数的一种替代方法,它不关心返回值,而是操作本身。

通常这样做是因为要将输出呈现到屏幕上或将文件保存到磁盘上,重要的是操作,而不是返回值。

> x <- list(1, "a", 3)
> x %>% 
+     walk(print)
[1] 1
[1] "a"
[1] 3

通常相较于 walk2 pwalk walk 没那么有用。

例如,如果有绘图列表和文件名向量,可以使用 pwalk() 将每个文件保存到磁盘上的相应位置

library(ggplot2)
plots <- mtcars %>% 
  split(.$cyl) %>% 
  map(~ggplot(., aes(mpg, wt)) + geom_point())
paths <- stringr::str_c(names(plots), ".pdf")
pwalk(list(paths, plots), ggsave, path = tempdir())

9. for 循环的其他模式

purrr 提供了许多其他函数,这些函数抽象了其他类型的 for 循环。您使用它们的频率要低于 map 函数,但是了解它们很有用。

在这里我们简要说明每个函数,如果您将来看到类似的问题,希望您会想到它。然后你可以去查阅文档了解更多细节

9.1 谓词函数

许多函数返回单一的 TRUE FLASE

keep() discard() 分别保存谓词为真或假的输入元素

> iris %>% 
+     keep(is.factor) %>% 
+     str()
'data.frame': 150 obs. of  1 variable:
 $ Species: Factor w/ 3 levels "setosa","versicolor",..: 1 1 1 1 1 1 1 1 1 1 ...
> iris %>% 
+     discard(is.factor) %>% 
+     str()
'data.frame': 150 obs. of  4 variables:
 $ Sepal.Length: num  5.1 4.9 4.7 4.6 5 5.4 4.6 5 4.4 4.9 ...
 $ Sepal.Width : num  3.5 3 3.2 3.1 3.6 3.9 3.4 3.4 2.9 3.1 ...
 $ Petal.Length: num  1.4 1.4 1.3 1.5 1.4 1.7 1.4 1.5 1.4 1.5 ...
 $ Petal.Width : num  0.2 0.2 0.2 0.2 0.2 0.4 0.3 0.2 0.2 0.1 ...

some() every() 确定谓词是否对任何或所有元素都是正确的

> x <- list(1:5, letters, list(10))
> x %>% 
+     some(is_character)
[1] TRUE
> x %>% 
+     every(is_vector)
[1] TRUE

detect() 查找谓词为真的第一个元素; detect_index() 返回其位置

> x <- sample(10)
 [1]  4  3  8 10  7  2  1  9  6  5
> x %>% 
+     detect(~ . > 5)
[1] 8
> x %>% 
+     detect_index(~ . > 5)
[1] 3

head_while() tail_while() 从向量的开头或结尾获取满足谓词为 true 时的元素,直到出现 FALSE

> x %>% 
+   head_while(~ . > 5)
integer(0)
> x %>% 
+   tail_while(~ . > 5)
integer(0)

9.2 reduce 和 accumulate

有时你想重复应该一个函数将一个复杂列表转化为一个值,可以使用 reduce

> dfs <- list(
+     age = tibble(name = "John", age = 30),
+     sex = tibble(name = c("John", "Mary"), sex = c("M", "F")),
+     trt = tibble(name = "Mary", treatment = "A")
> dfs %>% reduce(full_join)
Joining, by = "name"
Joining, by = "name"
# A tibble: 2 x 4
  name    age sex   treatment
  <chr> <dbl> <chr> <chr>    
1 John     30 M     NA       
2 Mary     NA F     A 

或者你有一列向量,想要找到它们的交集

> vs <- list(
+     c(1, 3, 5, 6, 10),
+     c(1, 2, 3, 7, 8, 10),
+     c(1, 2, 3, 4, 8, 9, 10)
> vs %>% reduce(intersect)
[1]  1  3 10

reduce 接受一个具有两个主要输入的函数,并将其重复引用在列表中的相邻元素,直到剩下一个元素

accumulate() 与此类似,但它保留所有中间结果,你可以用它来实现一个累加和

 > (x <- sample(10))
 [1]  7  1  3 10  6  5  8  4  9  2
> x %>% accumulate(`+`)
 [1]  7  8 11 21 27 32 40 44 53 55

9.3 思考练习

  1. 使用 for 循环实现您自己的 every() 版本。与 purrr::every() 进行比较,你的版本缺了啥?
  2. 创建一个增强的 col_summary() ,将汇总函数应用于数据框中的每个数字列
  3. col_summary() 等价的基础 R 函数
col_sum3 <- function(df, f) {
  is_num <- sapply(df, is.numeric)
  df_num <- df[, is_num]
  sapply(df_num, f)

但是它有许多错误,对于如下输入

df <- tibble(
  x = 1:3, 
  y = 3:1,
  z = c("a", "b", "c")