r使用purrr实现迭代(下)
16.5 映射函数
purrr 包提供了一个函数族来完成这种循环操作。每种类型的输出都有一个相应的函数:
map() 用于输出列表;
map_lgl() 用于输出逻辑型向量;
map_int() 用于输出整型向量;
map_dbl() 用于输出双精度型向量;
map_chr() 用于输出字符型向量。
每个函数都使用一个向量作为输入,并对向量的每个元素应用一个函数,然后返回和输入 向量同样长度(同样名称)的一个新向量。向量的类型由映射函数的后缀决定。
可以使用这些函数来执行与 for 循环相同的操作。
library(tidyverse) df <- tibble( a = rnorm(10), b = rnorm(10), c = rnorm(10), d = rnorm(10) ) map_dbl(df, mean) map_dbl(df, median) map_dbl(df, sd)
> map_dbl(df, mean) a b c d 0.25308139 -0.02056859 -0.27513008 0.20992743 > map_dbl(df, median) a b c d 0.09989806 0.23524827 -0.42150709 0.24600297 > map_dbl(df, sd) a b c d 0.9880827 1.0419660 0.9359970 0.6146009
快捷方式
对于参数 .f,你可以使用几种快捷方式来减少输入量。假设你想对某个数据集中的每个分组都拟合一个线性模型。以下这个简单示例将 mtcars 数据集拆分成 3 个部分(按照气缸的值分类),并对每个部分拟合一个线性模型:
models <- mtcars %>% split(.$cyl) %>% map(function(df) lm(mpg ~ wt, data = df))
因为 R 中创建匿名函数的语法比较繁琐,所以 purrr 提供了一种更方便的快捷方式——单侧公式:
models <- mtcars %>% split(.$cyl) %>% map(~lm(mpg ~ wt, data = .))
我们在以上示例中使用了 . 作为一个代词:它表示当前列表元素(与 for 循环中用 i 表示当前索引是一样的)。
当检查多个模型时,有时你会需要提取出像 R^2 这样的摘要统计量。要想完成这个任务,需要先运行 summary() 函数,然后提取出结果中的 r.squared。我们可以使用匿名函数的快捷方式来完成这个操作:
models %>% map(summary) %>% map_dbl(~.$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")
你还可以使用整数按照位置来选取元素:
x <- list(list(1, 2, 3), list(4, 5, 6), list(7, 8, 9)) x %>% map_dbl(2)
> x %>% map_dbl(2) [1] 2 5 8
16.6 多参数映射
前面的映射函数都是对单个输入进行映射。但我们经常会有多个相关的输入需要同步迭代,这时候可以使用 map2() 和 pmap() 函数。例如,假设你想模拟几个均值不同的随机正态分布,我们已经知道了如何使用 map() 函数来完成这个任务:
> mu %>% + map(rnorm, n = 5) %>% + str() List of 3 $ : num [1:5] 4.79 3.73 7.17 6.21 3.88 $ : num [1:5] 9.6 9.53 10.78 9.92 10.25 $ : num [1:5] -3.03 -3.04 -1.63 -3.23 -1.48
> mu %>% + map(rnorm, n = 5) %>% + str() List of 3 $ : num [1:5] 4.79 3.73 7.17 6.21 3.88 $ : num [1:5] 9.6 9.53 10.78 9.92 10.25 $ : num [1:5] -3.03 -3.04 -1.63 -3.23 -1.48
如果还想让标准差也不同,那么该怎么办呢?其中一种方法是使用均值向量和标准差向量的索引进行迭代:
sigma <- list(1, 5, 10) seq_along(mu) %>% map(~rnorm(5, mu[[.]], sigma[[.]])) %>% str()
> seq_along(mu) %>% + map(~rnorm(5, mu[[.]], sigma[[.]])) %>% + str() List of 3 $ : num [1:5] 3.45 5.58 5.12 5.22 5.38 $ : num [1:5] 7.49 8.33 4.91 4.64 11.52 $ : num [1:5] 1.48 -2.47 6.22 17.5 -7.91
但是这种方法很难让人理解代码的本意。相反,我们应该使用 map2() 函数,它可以对两个向量进行同步迭代:
map2(mu, sigma, rnorm, n = 5) %>% str()
> map2(mu, sigma, rnorm, n = 5) %>% str() List of 3 $ : num [1:5] 2.69 6.01 4.29 4.31 6.03 $ : num [1:5] 8.58 3.9 10.91 9.31 10.03 $ : num [1:5] 0.853 -6.707 3.444 -5.205 0.318
map2() 函数可以生成以下一系列函数调用:
注意,每次调用时值发生变化的参数(这里是 mu 和 sigma)要放在映射函数(这里是 rnorm)的前面,值保持不变的参数(这里是 n)要放在映射函数的后面。
和 map() 函数一样,map2() 函数也是对 for 循环的包装:
map2 <- function(x, y, f, ...) { out <- vector("list", length(x)) for (i in seq_along(x)) { out[[i]] <- f(x[[i]], y[[i]], ...) } out }
如果你想生成均值、标准差和样本数量 都不相同的正态分布,那么就可以使用pmap()函数:
n <- list(1, 3, 5) args1 <- list(n, mu, sigma) args1 %>% pmap(rnorm) %>% str()
> args1 %>% + pmap(rnorm) %>% + str() List of 3 $ : num 6.1 $ : num [1:3] 12.18 8.37 15.74 $ : num [1:5] 6.935 2.484 -0.613 -9.279 10.607
如果没有为列表的元素命名,那么 pmap() 在调用函数时就会按照位置匹配。这样做比较容易出错,而且会让代码的可读性变差,因此最好使用命名参数:
args2 <- list(mean = mu, sd = sigma, n = n) args2 %>% pmap(rnorm) %>% str()
因为长度都是相同的,所以可以将各个参数保存在一个数据框中:
params <- tribble( ~mean, ~sd, ~n, 5, 1, 1, 10, 5, 3, -3, 10, 5 ) params %>% pmap(rnorm)
> params <- tribble( + ~mean, ~sd, ~n, + 5, 1, 1, + 10, 5, 3, + -3, 10, 5 + ) > params %>% + pmap(rnorm) [[1]] [1] 4.39974 [[2]] [1] 20.936665 17.663053 8.821498 [[3]] [1] -13.2642090 -10.1040656 -0.4311629 -5.4669188 [5] -6.4754260
调用不同函数
还有一种更复杂的情况:不但传给函数的参数不同,甚至函数本身也是不同的。
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()
> invoke_map(f, param, n = 5) %>% str() List of 3 $ : num [1:5] -0.6587 -0.6557 -0.0359 -0.4941 -0.5675 $ : num [1:5] 2.26 2.63 -1.15 6.99 8.82 $ : int [1:5] 11 9 10 14 9
第一个参数是一个函数列表或包含函数名称的字符向量。第二个参数是列表的一个列表, 其中给出了要传给各个函数的不同参数。随后的参数要传给每个函数。
第一个参数是一个函数列表或包含函数名称的字符向量。第二个参数是列表的一个列表, 其中给出了要传给各个函数的不同参数。随后的参数要传给每个函数。
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))
16.7 游走函数
如果调用函数的目的是利用其副作用,而不是返回值时,那么就应该使用游走函数,而不是映射函数。通常来说,使用这个函数的目的是在屏幕上提供输出或者将文件保存到磁 盘——重要的是操作过程,而不是返回值。以下是一个非常简单的示例:
x <- list(1, "a", 3) x %>% walk(print)
> x %>% + walk(print) [1] 1 [1] "a" [1] 3
16.8 for循环的其他模式
purrr 还提供了其他一些函数,可以对 for 循环的其他模式进行抽象。虽然它们的使用频率 比映射函数低,但了解一下还是有用的。
16.8.1 预测函数
一些函数可以与返回 TRUE 或 FALSE 的预测函数一同使用。
keep() 和 discard() 函数可以分别保留输入中预测值为 TRUE 和 FALSE 的元素:
iris %>% keep(is.factor) %>% str()
> 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()
> 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 x %>% some(is_character) x %>% every(is_vector) 1 2 3 4 5 6 > x [[1]] [1] 1 2 3 4 5 [[2]] [1] "a" "b" "c" "d" "e" "f" "g" "h" "i" "j" "k" "l" "m" [14] "n" "o" "p" "q" "r" "s" "t" "u" "v" "w" "x" "y" "z" [[3]] [[3]][[1]] [1] 10 > x %>% + some(is_character) [1] TRUE > x %>% + every(is_vector) [1] TRUE
detect() 函数可以找出预测值为真的第一个元素,detect_index() 函数则可以返回这个元素的位置:
x <- sample(10) x x %>% detect(~ . > 5) x %>% detect_index(~ . > 5)
> x [1] 7 1 8 2 9 10 6 4 3 5 > x %>% + detect(~ . > 5) [1] 7 > x %>% + detect_index(~ . > 5) [1] 1
head_while() 和 tail_while() 分别从向量的开头和结尾找出预测值为真的元素:
x %>% head_while(~ . > 5) x %>% tail_while(~ . > 5)
> x %>% + head_while(~ . > 5) [1] 7 > x %>% + tail_while(~ . > 5) integer(0)
16.8.2 归约与累计
对于一个复杂的列表,有时你想将其归约为一个简单列表,方式是使用一个函数不断将两个元素合成一个。如果想要将两表间的一个 dplyr 操作应用于多张表,那么这种方法是非常适合的。例如,如果你有一个数据框列表,并想要通过不断将两个数据框连接成一个的方式来最终生成一个数据框:
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
> dfs $age # A tibble: 1 x 2 name age <chr> <dbl> 1 John 30 $sex # A tibble: 2 x 2 name sex <chr> <chr> 1 John M 2 Mary F $trt # A tibble: 1 x 2 name treatment <chr> <chr> 1 Mary A
dfs %>% reduce(full_join)
> 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)
> vs %>% reduce(intersect) [1] 1 3 10