14 Basic: apply family

数据分析任务中,常常会需要在数据的某个维度方向上执行一套操作,例如求一个 data.frame 中存储的所有变量的均值(列方向),如果用for结构可以写成如下的样子:

df <- data.frame(
  x = sample(1:5, 10, replace = TRUE),
  y = sample(1:5, 10, replace = TRUE, prob = c(0.5/5, 1/5, 2/5, 1/5, 0.5/5)), 
  z = sample(1:5, 10, replace = TRUE, prob = c(1.5/5, 0.9/5, 0.2/5, 0.9/5, 1.5/5))
)
df
#>    x y z
#> 1  5 3 1
#> 2  2 3 1
#> 3  1 3 5
#> 4  4 3 1
#> 5  2 2 5
#> 6  3 4 5
#> 7  2 1 2
#> 8  4 3 2
#> 9  5 3 4
#> 10 5 3 1
n_var <- ncol(df)
mean_var <- vector(mode = "numeric", length = n_var)
for (i in seq_along(mean_var)) {
  mean_var[i] <- mean(df[[i]])
}
mean_var
#> [1] 3.3 2.8 2.7
colMeans(df)
#>   x   y   z 
#> 3.3 2.8 2.7

这种同一套操作重复多次使用for结构可以解决,但会带来的问题是需要额外写不少代码,例如需要提前初始化输出结果,需要设定for结构中的varseq等。 R 的 base 包提供了一个apply()函数族,包括apply()lappy()sapply()等,能够在完成任务的同时,相比for结构简单很多。

14.1 Use function as argument

apply()函数族的核心思想是把 function 作为 argument,这是 R 这种函数式编程(functional programming)语言的一个重要特点。要理解这个做法,先来看一个简单的例子(来自“R for Data Science”一书的 For loops vs. functionals 小节)。

假定经常需要计算变量的均值,每次都要重写一遍上述含for结构的代码,会很麻烦,所以干脆把上述代码 define 成一个 function:

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

相应的,可以把中数(median)和标准差(standard deviation, sd)也都各自定义了一个函数,

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

但实际上可以看出来,上面 3 个自定义函数只有第 4 行才是不一样的,其他都是完全相同的,也就意味着这 3 个函数完全可以合并成 1 个,只不过需要将不同的部分变成一个 argument:

col_summary <- function(df, fun) {
  out <- vector("double", length(df))
  for (i in seq_along(df)) {
    out[i] <- fun(df[[i]])
  }
  return(out)
}
col_summary(df, mean)
#> [1] 3.3 2.8 2.7
col_summary(df, median)
#> [1] 3.5 3.0 2.0
col_summary(df, sd)
#> [1] 1.4944341 0.7888106 1.8287822

在 define function 章节讲过,当调用一个 function 时,其arglist部分相当于创建了一个只存在于该 function 内部的 object。当把另一个 function 作为arglist中的某个 argument 时,arglist部分相当于创建了一个只存在于被调用的 function 内部的 function object。所以,调用 function 时,argument 不论是用常见的 object (vector, data.frame, matrix, etc.) 还是用 function object,本质上是一回事,都是在 function 内部创建了一个 temporary object,随着 function 运行开始而出现,随着 function 运行结束和消失:

col_summary <- function(df, fun) {
  print(fun)
  out <- vector("double", length(df))
  for (i in seq_along(df)) {
    out[i] <- fun(df[[i]])
  }
  return(out)
}
results <- col_summary(df, mean)  # effectively create a function object "fun" whose value is the function body of mean()
#> function (x, ...) 
#> UseMethod("mean")
#> <bytecode: 0x0000022ee3d25a50>
#> <environment: namespace:base>

14.2 The basic usage of apply()

apply()函数族就是将需要重复多次的操作所对应的 function (可以是 anonymous function)作为 argument,来实现和for结构一样的效果。例如,想要实现与col_summary()相同的效果,使用apply()可以大幅缩短代码行数:

apply(X, MARGIN, FUN, ..., simplify = TRUE)

  • X: an array, including a matrix. If X is not an array but an object of a class with a non-null dim value (such as a data frame), apply attempts to coerce it to an array via as.matrix if it is two-dimensional (e.g., a data frame) or via as.array.
  • MARGIN: a vector giving the subscripts which the function will be applied over. E.g., for a matrix 1 indicates rows, 2 indicates columns.
  • simplify: a logical indicating whether results should be simplified if possible.
apply(df, MARGIN = 2, FUN = mean)
#>   x   y   z 
#> 3.3 2.8 2.7
apply(df, MARGIN = 2, FUN = median)
#>   x   y   z 
#> 3.5 3.0 2.0
apply(df, MARGIN = 2, FUN = sd)
#>         x         y         z 
#> 1.4944341 0.7888106 1.8287822

apply()本质上就是把FUN应用到df的指定维度上,实际操作的时候,apply()会先检测是要处理哪个 object,应用到哪个维度,然后把数据按照维度拆成一个个小的部分,然后把这些小的部分传递给FUN提供的函数作为输入参数来执行。

apply()的输出结果根据具体情况的不同,可以是 matrix、vector 或 list(simplify = FALSE)。

# matrix
apply(df, 2, \(x) x + 1)
#>       x y z
#>  [1,] 6 4 2
#>  [2,] 3 4 2
#>  [3,] 2 4 6
#>  [4,] 5 4 2
#>  [5,] 3 3 6
#>  [6,] 4 5 6
#>  [7,] 3 2 3
#>  [8,] 5 4 3
#>  [9,] 6 4 5
#> [10,] 6 4 2
# list
apply(df, 2, \(x) x + 1, simplify = FALSE)
#> $x
#>  [1] 6 3 2 5 3 4 3 5 6 6
#> 
#> $y
#>  [1] 4 4 4 4 3 5 2 4 4 4
#> 
#> $z
#>  [1] 2 2 6 2 6 6 3 3 5 2

14.3 lapply() and sapply()

lapply()sapply()可以视作是不同版本的apply()

  1. lapply(X, FUN)
  • X: a vector (atomic or list).
  • FUN: the function to be applied to each element of X.

lapply()输出的结果是一个 list。

lapply(df, mean)
#> $x
#> [1] 3.3
#> 
#> $y
#> [1] 2.8
#> 
#> $z
#> [1] 2.7
lapply(c(1, 2, 3), \(x) x + 1)
#> [[1]]
#> [1] 2
#> 
#> [[2]]
#> [1] 3
#> 
#> [[3]]
#> [1] 4
  1. sapply(X, FUN)

sapply()输出的结果视情况而定,可以是一个 vector、matrix 或 list。

# vector
sapply(df, mean)
#>   x   y   z 
#> 3.3 2.8 2.7
# matrix
df_list <- lapply(1:3, \(x) data.frame(
  x = sample(1:5, size = 10, replace = TRUE),
  y = sample(1:5, size = 10, replace = TRUE, prob = c(0.5/5, 1/5, 2/5, 1/5, 0.5/5)), 
  z = sample(1:5, size = 10, replace = TRUE, prob = c(1.5/5, 0.9/5, 0.2/5, 0.9/5, 1.5/5))
))
sapply(df_list, colMeans)
#>   [,1] [,2] [,3]
#> x  3.8  3.5  2.7
#> y  3.2  2.7  3.6
#> z  4.0  3.3  2.6
# list
sapply(3:9, seq)
#> [[1]]
#> [1] 1 2 3
#> 
#> [[2]]
#> [1] 1 2 3 4
#> 
#> [[3]]
#> [1] 1 2 3 4 5
#> 
#> [[4]]
#> [1] 1 2 3 4 5 6
#> 
#> [[5]]
#> [1] 1 2 3 4 5 6 7
#> 
#> [[6]]
#> [1] 1 2 3 4 5 6 7 8
#> 
#> [[7]]
#> [1] 1 2 3 4 5 6 7 8 9

14.4 The basic usage of lapply() and sapply()

因为lapply()sapply()本质上是用简单的写法解决需要多次重复相同操作的问题,所以使用思路上和for中的seq很类似,

  1. 循环element
set.seed(123)
scores <- list(
  class_1 = sample(100, size = 32, replace = TRUE),
  class_2 = sample(100, size = 36, replace = TRUE),
  class_3 = sample(100, size = 30, replace = TRUE)
)
lapply(scores, mean)
#> $class_1
#> [1] 53.34375
#> 
#> $class_2
#> [1] 52.94444
#> 
#> $class_3
#> [1] 50.4
  1. 循环subscript
set.seed(123)
scores <- list(
  class_1 = sample(100, size = 32, replace = TRUE),
  class_2 = sample(100, size = 36, replace = TRUE),
  class_3 = sample(100, size = 30, replace = TRUE)
)
lapply(names(scores), \(x) mean(scores[[x]]))
#> [[1]]
#> [1] 53.34375
#> 
#> [[2]]
#> [1] 52.94444
#> 
#> [[3]]
#> [1] 50.4
set.seed(123)
scores <- list(
  class_1 = sample(100, size = 32, replace = TRUE),
  class_2 = sample(100, size = 36, replace = TRUE),
  class_3 = sample(100, size = 30, replace = TRUE)
)
lapply(seq_along(scores), \(x) mean(scores[[x]]))
#> [[1]]
#> [1] 53.34375
#> 
#> [[2]]
#> [1] 52.94444
#> 
#> [[3]]
#> [1] 50.4

apply()函数族中使用匿名函数的小技巧

lapply()sapply()的第二种基本用法的例子中,scores没虽然没有作为参数和x一并传入给 anonymous function,但运行并不会受影响。结合 define function 章节(详见13)的知识可以知道,这是因为在定义的 anonymous function 运行时自动创建的独立 environment 中没有scores, R 自动到上一层 environment 里面找,就自然找到了在lapply()外定义的scores。这也就是为什么apply()函数族中使用 anonymous function 时,虽然只能接受一个由apply()函数族传递进来的输入参数x,但也可以使用在 anonymous function 之前就已经存在的 object,从而完成较为复杂的任务。

14.5 Recap

  1. apply()函数族的核心思想是把 function 作为 argument;
  2. 比较简单的重复任务可以考虑使用apply()函数族代替for结构;
  3. 需要重复多次,但又不需要保存下供后续使用的操作,请使用 anonymous function。