R 数据处理 —— purrr

1. 前言

本节我们将开始介绍 R 中的迭代。主要介绍两种重要的迭代:

  • 命令式编程:

    有像 forwhile 循环一样的工具,使迭代非常的明确以及比较容易理解。

    但是 for 循环一般代码较长,重复的代码较多

  • 函数式编程(FP,Functional programming):

    函数式编程提供了提取重复代码的工具,每个循环模式都是自己的函数。

1.1 导入

在这里我们将要介绍另一个 tidyverse 核心包 purrr。它提供了许多强大的编程工具

library(tidyverse)

2. for 循环

假设我们有下面一个简单的 tibble

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

计算每列的中位数

> median(df$a)
[1] 0.2037405
> median(df$b)
[1] -0.2373901
> median(df$c)
[1] 0.05839867
> median(df$d)
[1] 0.08679879

这样做可真不是个好选择。记住,只要复制粘贴代码两次以上,就要考虑使用 for 循环

> output <- vector("double", ncol(df))    # 1. output
> for (i in seq_along(df)) {              # 2. sequence
+     output[[i]] <- median(df[[i]])      # 3. body
+ }
> output
[1]  0.20374050 -0.23739013  0.05839867  0.08679879

对于每个循环有三个组成部分

  1. 输出:output <- vector("double", length(x))

在循环开始之前,必须为输出分配足够的空间。这是很重要的,如果每次都用 c() 来动态添加会极大拖慢程序的速度

通常使用 vector() 来创建给定长度的空向量。接受两个参数:向量的类型(logical, integer, double, character 等)和长度。

  1. 序列:i in seq_along(df)

遍历 1,2,3,4。你可能没见过 seq_along(),它是一个安全版本的 1:length(l)

它们之间的区别是,当传入的是一个空向量时,seq_along 是正确的

> y <- vector("double", 0)
> seq_along(y)
integer(0)
> 1:length(y)
[1] 1 0
  1. 循环体:output[[i]] <- median(df[[i]])

每次获取不同的 i 值,并执行同样的操作。

2.1 思考练习
  1. 编写循环
  • 计算 mtcars 每一列的均值
  • 确定 nycflights13::flights 每一列的类型
  • 计算 iris 每列唯一值的数目
  • 从均值为 -10010100 的分布中生成 10 个随机正态分布
  1. 将下面的代码改写为向量函数而不是 for 循环
out <- ""
for (x in letters) {
  out <- stringr::str_c(out, x)
}

x <- sample(100)
sd <- 0
for (i in seq_along(x)) {
  sd <- sd + (x[i] - mean(x)) ^ 2
}
sd <- sqrt(sd / (length(x) - 1))

x <- runif(100)
out <- vector("numeric", length(x))
out[1] <- x[1]
for (i in 2:length(x)) {
  out[i] <- out[i - 1] + x[i]
}

3. 变异 for 循环

for 循环主要包含 4 种变体:

  1. 修改一个现有对象,而不是创建一个新对象
  2. 遍历名称或值,而不是索引
  3. 处理长度未知的输出
  4. 处理长度未知的序列
3.1 修改现有对象
df <- tibble(
  a = rnorm(10),
  b = rnorm(10),
  c = rnorm(10),
  d = rnorm(10)
)
rescale01 <- function(x) {
  rng <- range(x, na.rm = TRUE)
  (x - rng[1]) / (rng[2] - rng[1])
}

df$a <- rescale01(df$a)
df$b <- rescale01(df$b)
df$c <- rescale01(df$c)
df$d <- rescale01(df$d)

改写为 for 循环

for (i in seq_along(df)) {
  df[[i]] <- rescale01(df[[i]])
}

为什么在每个 for 循环内部我都使用 [[ 而不是 [ 呢?因为它清楚地表明,我想处理的是单个元素。

3.2 循环模式

遍历向量主要有三种基本方式,上面讲的是最常用的方式。还有另外两种:

  1. 遍历向量的元素:for (x in xs)
  2. 遍历向量的名称:for (n in names(xs))

一般遍历索引是最通用的形式,可以根据索引位置提取出名称和值

for (i in seq_along(x)) {
  name <- names(x)[[i]]
  value <- x[[i]]
}
3.3 输出长度未知

有时您可能不知道输出结果有多长,可以使用动态添加的方式

> means <- c(0, 1, 2)
> 
> output <- double()
> for (i in seq_along(means)) {
+     n <- sample(100, 1)
+     output <- c(output, rnorm(n, means[[i]]))
+ }
> str(output)
 num [1:153] -0.479 0.612 1.231 1.243 0.583 ...

但是会增加程序耗时。

一个改进的方法是,将结果保存在 list 当中,循环之后再合并为一个向量。

> out <- vector("list", length(means))
> for (i in seq_along(means)) {
+     n <- sample(100, 1)
+     out[[i]] <- rnorm(n, means[[i]])
+ }
> str(out)
List of 3
 $ : num -1.52
 $ : num [1:30] 0.163 -0.411 0.144 0.613 2.449 ...
 $ : num [1:65] 3.037 1.725 1.879 3.329 0.978 ...
> str(unlist(out))
 num [1:96] -1.522 0.163 -0.411 0.144 0.613 ...

在这里,我们使用 unlist 将一个列表向量展开为单个向量。

这种模式先考虑将输出保存在更复杂的对象中,在循环结束后合并到一起。

3.4 序列长度未知

有时你可能甚至不知道序列有多长,可以考虑使用 while 循环。

例如,计算连续得到三个 H 需要多少次数

> flip <- function() sample(c("T", "H"), 1)
> 
> flips <- 0
> nheads <- 0
> 
> while (nheads < 3) {
+     if (flip() == "H") {
+         nheads <- nheads + 1
+     } else {
+         nheads <- 0
+     }
+     flips <- flips + 1
+ }
> flips
[1] 58
3.5 思考练习
  1. 如果你使用 for (nm in names(x)) 遍历,但是 x 没有名称时会发生什么?如果只有一些元素有名称呢?如果名字不是唯一的呢?

  2. 编写一个函数来打印数据框中每个数字列的均值及其名称。例如,show_mean(iris) 将打印

show_mean(iris)
#> Sepal.Length: 5.84
#> Sepal.Width:  3.06
#> Petal.Length: 3.76
#> Petal.Width:  1.20
  1. 下面的代码的作用是什么?它是如何工作的?
trans <- list( 
  disp = function(x) x * 0.0163871,
  am = function(x) {
    factor(x, labels = c("auto", "manual"))
  }
)
for (var in names(trans)) {
  mtcars[[var]] <- trans[[var]](mtcars[[var]])
}

4. for VS 函数

for 循环在 R 中可能没有在其他语言中那么重要,因为 R 是函数式编程语言。

这意味着可以在函数中将 for 循环封装起来,然后调用该函数,而不是直接使用 for 循环。

为了理解这一点的重要性,让我们考虑下面这个数据框

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

你可以使用 for 循环来计算每列的均值

> output <- vector("double", length(df))
> for (i in seq_along(df)) {
+     output[[i]] <- mean(df[[i]])
+ }
> output
[1]  0.41487173 -0.16774333 -0.05348092  0.01059490

你将会意识到,这一操作是会频繁的发生,所以我们将它封装为一个函数

col_mean <- function(df) {
  output <- vector("double", length(df))
  for (i in seq_along(df)) {
    output[i] <- mean(df[[i]])
  }
  output
}

但同时,你认为计算中位数和标准差也会有所帮助,所以你复制并粘贴 col_mean() 函数,然后用 mediansd 替换 mean

col_median <- function(df) {
  output <- vector("double", length(df))
  for (i in seq_along(df)) {
    output[i] <- median(df[[i]])
  }
  output
}
col_sd <- function(df) {
  output <- vector("double", length(df))
  for (i in seq_along(df)) {
    output[i] <- sd(df[[i]])
  }
  output
}

你看,类似的代码虽然被包装为不同的函数,但是大部分代码还是复制粘贴,那我们该怎么改进呢?

考虑一下下面的简单例子

f1 <- function(x) abs(x - mean(x)) ^ 1
f2 <- function(x) abs(x - mean(x)) ^ 2
f3 <- function(x) abs(x - mean(x)) ^ 3

我们可以将这三个函数再抽象出来

f <- function(x, i) abs(x - mean(x)) ^ i

这样不仅减少了代码量,同时提高了函数的可扩展性。

现在,让我们来更改上面的三个函数

col_summary <- function(df, fun) {
  out <- vector("double", length(df))
  for (i in seq_along(df)) {
    out[i] <- fun(df[[i]])
  }
  out
}
col_summary(df, median)
#> [1] -0.51850298  0.02779864  0.17295591 -0.61163819
col_summary(df, mean)
#> [1] -0.3260369  0.1356639  0.4291403 -0.2498034

将一个函数传递给另一个函数,是 R 中非常重要的思想。

在后续的章节中,将介绍并使用 purrr 包中的函数来消除常见的 for 循环。

当然 R 提供的原生的 apply(), lapply(), tapply() 也可以解决类似的问题,但是 purrr 更容易学习使用。

使用 purrr 中的函数而不是 for 循环的目的是为了让你将常见的列表操作分解成独立的部分

  1. 如何解决列表中单个元素的问题?解决该问题后,purrr 会将您的解决方案推广到列表中的每个元素

  2. 如果你正在解决一个复杂的问题,你如何把它分解成多个小块,从而让你更容易解决问题。然后使用 purrr 将许多小部件通过管道组合在一起

4.1 思考练习
  1. 阅读 apply() 的文档。在 2d 情况下,它泛化了哪两个 for 循环?

  2. 调整 col_summary(),使其仅适用于数值列。您可能需要使用 is_numeric() 函数,该函数返回一个逻辑向量,每个数值列对应 TRUE

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))

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

当我们想要提取一些汇总信息时,比如 R 2 R^2 R2。我们可以使用 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 每一列中唯一值的数量
  • 分别以 -10010100 的平均值生成 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() 生成以下一系列函数调用

image.png

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

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

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

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

> x <- list(1, "a", 3)

> x %>% 
+     walk(print)
[1] 1
[1] "a"
[1] 3

通常相较于 walk2pwalkwalk 没那么有用。

例如,如果有绘图列表和文件名向量,可以使用 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 谓词函数

许多函数返回单一的 TRUEFLASE

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 思考练习
  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")
)
# 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

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

名本无名

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值