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)
x y
3 2
5.1 快捷键
有几个快捷方式可以与 .f
一起使用,以节省输入。
假设您希望将线性模型拟合到数据集中的每个组,下面的示例将 mtcars
数据集分割为三个部分(每个圆柱体的值一个),并对每个部分拟合相同的线性模型
models <- mtcars %>%
split(.$cyl) %>%
map(function(df) lm(mpg ~ wt, data = df))
这里 .
作为代词,表示当前列表元素
当我们想要提取一些汇总信息时,比如 。我们可以使用 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 思考练习
- 使用
map
函数来实现
- 计算
mtcars
中每列的平均值 - 确定
nycflights13::flights
中每个列的类型 - 计算
iris
每一列中唯一值的数量 - 分别以
-10
、0
、10
和100
的平均值生成10
个正态随机数
-
在非
list
向量上使用 map 函数时会发生什么?map(1:5,runif)
有什么作用?为什么? -
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]], ...)
}
out
}
你可能会想,是不是也有 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
会执行如下操作
[ image.png如果不命名列表的元素,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
这样会产生更长但更安全的调用
image.png因为参数的长度是一样的,所以可以将它们存储在数据框中
> 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
image.png
第一个参数是函数列表或函数名的字符向量。第二个参数是一个列表,给出了每个函数的不同参数。随后的参数会传递给每个函数
同样,您可以使用 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)
> x
[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 思考练习
-
使用
for
循环实现您自己的every()
版本。与purrr::every()
进行比较,你的版本缺了啥? -
创建一个增强的
col_summary()
,将汇总函数应用于数据框中的每个数字列 -
与
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")
)
# OK
col_sum3(df, mean)
# Has problems: don't always return numeric vector
col_sum3(df[1:2], mean)
col_sum3(df[1], mean)
col_sum3(df[0], mean)
是什么导致了错误
总结
可以在速查表中看到更多的函数及使用方法,以及函数功能的图形示例
image.png image.png