跳至主要内容

R Apply 系列函数教程

在本教程中,您将了解 R 中 apply 系列函数的用法、其变体,以及与其相关的一些函数在不同数据结构中的应用。
· 14分钟 阅读

使用 AI 探索

learn r banner

关于 R Apply 系列的说明

R 的 Apply 系列是一组函数,允许用户将某个函数应用到向量、列表或矩阵的元素上。不过,它被视为遗留功能,不建议在新代码中使用。对于 R 中的所有循环,建议改用 purrr 包。purrr 为处理具有多个输入和输出的函数提供了一致的语法,使代码更易于编写和阅读。此外,purrr 提供了一套针对现代计算环境优化的函数,比 Apply 系列更快、更高效。总的来说,尽管 R 的 Apply 系列在某些场景下仍然可用,但对所有新的 R 项目,建议使用 purrr 包。

Apply 函数:循环的替代方案

本文将向您展示如何使用 R 的 apply() 函数、其变体如 mapply() ,以及 apply() 的一些近亲,并将它们应用于不同的数据结构。当然,不可能涵盖所有变体,但在可能的情况下,我们会通过几个更具分量的示例,介绍这些函数如何配合使用。

此外,您可能会发现这门 R 入门课程很有帮助,以更好地理解列表、向量、数组和数据框,尽管并不需要完成该课程就能阅读本文!

apply() 系列

apply() 系列属于 R 的 base 包,包含一组函数,用于以重复的方式处理矩阵、数组、列表和数据框中的数据切片。这些函数允许以多种方式遍历数据,避免显式使用循环结构。它们针对输入的列表、矩阵或数组,应用一个具名函数,并可附带一个或多个可选参数。

被调用的函数可以是:

  • 聚合函数,例如均值或求和(返回一个数值或标量);
  • 其他变换或子集化函数;以及
  • 其他向量化函数,返回更复杂的结构,如列表、向量、矩阵和数组。

apply() 系列是更复杂组合的基础,能用很少的代码完成操作。更具体地,这个系列包括 apply()、lapply()、sapply()、vapply()、mapply()、rapply() 和 tapply() 等函数。

但我们该如何、何时使用这些函数呢?

这取决于您要操作的数据结构,以及您需要的输出格式。

如何在 R 中使用 apply()

先从这个系列的“大家长” apply() 开始,它作用于数组。为简单起见,本教程仅限于二维数组,也就是矩阵。

R base 手册中说明其调用方式如下:apply(X, MARGIN, FUN, ...)

其中:

  • X 是一个数组;如果数组维度为 2,则是一个矩阵;
  • MARGIN 用于定义函数如何应用:当 MARGIN=1 时按行应用,而 MARGIN=2 时按列应用。注意,如果使用 MARGIN=c(1,2),则会同时对行和列应用;以及
  • FUN 是您希望应用到数据上的函数。它可以是任何 R 函数,包括用户自定义函数(UDF)。

初学者可能难以想象其过程,因此图示和一些代码会有助于理解。

我们先构造一个 5 x 6 的矩阵,假设您想要对每一列的数值求和。

您可以写出如下代码:

# Construct a 5 x 6 matrix
my_matrix <- matrix(1:30, nrow = 5, ncol = 6)

# Calculate the sum of each column
col_sums <- apply(my_matrix, 2, sum)

# Print the result
print(col_sums)

matrix() 函数创建了一个 5 x 6 的矩阵,取值范围为 1 到 30。接着我们使用 apply() 函数将 sum() 函数应用到矩阵的每一列(2 表示按列应用)。每列和的向量被存入变量 col_sums,并打印到控制台。

请记住,在 R 中,沿着 1 号维(边)从上到下遍历矩阵时,矩阵可以看作一组行向量;沿着 2 号维(边)从左到右遍历时,可以看作一组列向量。

这意味着,您刚刚输入的指令(图 1 所示)可理解为:“将函数 ‘sum’ 沿 2 号维(按列)应用到矩阵 X,对每列的数值求和”。

为避免图示杂乱,图中仅高亮显示了一列。

最终得到一个包含各列和的行向量。

上述代码的输出是一个行向量;如果按矩阵的行方向求和,R 也会以类似的方式显示结果。

补充说明:在大多数情况下,即使您没有显式指定返回值(更准确地说,没有将函数的返回值赋给变量),R 也会返回一个值——R 会返回最后被求值的对象。不过在实践中,当您想检查返回值并对其进一步操作时,最好显式地将结果赋给某个变量。

lapply() 函数

当您想将某个函数应用到列表的每个元素,并获得一个列表作为结果时,可以使用 lapply()。执行 ?lapply 可见其语法与 apply() 类似。

不同之处在于:

  1. 它也可用于其他对象,如数据框、列表或向量;以及
  2. 其输出是一个列表(这也解释了函数名中的 “l”),且元素数量与传入对象的元素数量相同。

为了解其工作方式,先创建几个矩阵,并从每个矩阵中提取指定的一列。

这在实际数据中很常见,用于对不同数据框进行比较或聚合。

我们的示例(图 2 左侧)可编码如下:

# Create a list of matrices
MyList <- list(A,B,C)

# Extract the 2nd column from `MyList` with the selection operator `[` with `lapply()`
......(MyList,"[", , 2)

# Extract the 1st row from `MyList`
lapply(MyList,"[", 1, )

lapply 与 apply 函数对比

该操作显示在图 2 的左侧。

同样,先指定关注的对象,即列表 Mylist。使用标准的 R 选择操作符 [,省略第一个参数(因此表示“任意”,这就是为什么看到两个逗号)。

接着指定第二个参数 2:我们的边是“列”。因此您会从列表中每个矩阵提取第二列。

关于上述代码的几点说明:

  • [ 表示选择操作符。例如,提取矩阵 B 的第三行所有元素需要:B[3,];
  • [[ ]] 表明我们在处理列表:[[2]] 表示列表的第二个元素。这在 R 的输出中也可见;
  • 输出是一个列表,其元素数量与输入中的元素数量相同;以及
  • 注意,您也可以从每个矩阵中提取一个单个元素,例如:lapply(MyList,"[", 1, 2)

图 2 的右侧展示了另一种提取方式:这次省略第一个参数,从每个矩阵中提取第一行。

动手试试吧!从列表中每个矩阵选择第二列:

# Use `lapply()` to select the 2nd column from each matrix in `MyList`
......(MyList,"[", 1,)

sapply() 函数

sapply() 与 lapply() 的工作方式类似,但它会尽量将输出简化为尽可能基础的数据结构。实际上,sapply() 是 lapply() 的一层“封装”。

一个例子有助于理解:假设您想重复上一节中提到的单元素提取操作,但这次要对每个矩阵取“第二行的第一个元素”(索引 2 和 1)。

使用 lapply() 会返回列表;而如果使用 sapply() 并传入 simplify=FALSE 参数,也会返回列表。请看下面的代码片段是如何工作的:

# Return a list with `lapply()`
lapply(MyList,"[", 2, 1 )

# Return a vector with `sapply()`
......(MyList,"[", 2, 1 )

# Return a list with `sapply()`
......(MyList,"[", 2, 1, simplify=F)

# Return a vector with `unlist()`
......(lapply(MyList,"[", 2, 1 ))

相反,使用 unlist() 这样的函数,可以让 lappy() 返回一个向量!

无论如何,为避免混淆,最好以这些函数的“原生格式”使用它们,除非确有必要,尽量避免转换。

rep() 函数

apply() 系列常与 rep() 一起使用。将其应用于向量或因子 x 时,函数会按指定次数复制其值。

让我们取用前面通过 lapply() 生成并放入 MyList 的某个向量。

不过这次,您只选择列表 MyList 中每个元素的第一行第一列(并使用 sapply() 得到一个向量):

# Initialize `Z`
Z <- sapply(MyList,"[", 1,1 )

# Return `Z`
Z

# Replicate the values of `Z`
Z <- rep(Z,c(3,1,2))

# Return `Z`
Z

您会看到,上述代码按照 c(3,1,2) 指定的次数复制了 Z 的各个值:第一个复制三次,第二个一次,第三个两次。

是不是很方便?

mapply() 函数

mapply() 代表“多变量” apply。其目的是将本不接受向量作为参数的函数的参数向量化。

简言之,mapply() 将一个函数应用到多个列表或多个向量参数上。

来看一个 mapply() 的示例:通过多次调用 rep() 函数创建一个 4 x 4 的矩阵:

# Create a 4x4 matrix
Q1 <- ......(c(rep(1, 4), rep(2, 4), rep(3, 4), rep(4, 4)),4,4)

# Print `Q1`
.....(Q1)

# Or use `mapply()`
Q2 <- ......(rep,1:4,4)

# Print `Q2`
print(Q2)

不过您会发现,与其用 c() 绑定 rep() 的结果,不如用更高效的方式:调用 mapply() 时,您将 rep() 的动作向量化了。

aggregate() 函数

该函数位于 stats 包中,使用方式为:aggregate(x, by, FUN, ..., simplify = TRUE)。

换言之,它与 apply() 的工作方式类似:您指定对象、函数,以及是否需要像 sapply() 那样简化输出。关键差异在于 by 子句,它设置我们希望按其进行聚合的变量或数据框字段。

下一节将展示其工作方式。

aggregate() 的示例

考虑一个名为 Mydf 的示例数据集,其中包含某产品的销售信息,并且变量 DepPC 列中的某些值会重复。

该变量用于地理分类,例如邮编的一部分(这里的数字对应法兰西岛大区的省份编号,即包含巴黎的地区)。

您希望对销售相关的列做一些统计。这些列包括 DProgr(按时间递增的序号)、产品销量(数量 Qty),以及一个逻辑变量 Delivered,用于指示产品是否已送达(T)或未送达(F)。

首先,除了直接输入数据名(此处只有 120 条记录,但想象一下真实场景可能有成千上万行!)查看全部数据外,您还可以做一些非常简单的操作来熟悉数据集。

让我们来探索一下数据:

# Explore the first 15 records with `head()`
....(Mydf,15)

# See the last 5 records with `tail()`
....(Mydf,5) 

# Show data types for each column
sapply(Mydf, class)

# Return number of rows and columns with `dim()`
...(Mydf) 

# How many departments? 
unique(Mydf$DepPC)

注意,如果想查看数据框的行数和列数,也可以调用 nrow(Mydf) 和 ncol(Mydf)。

关于数据,您还可以提出许多其他问题。

例如,您可能想知道在哪个省份该产品卖得最好。因此,应按省份对数据进行分组,并借助 aggregate() 函数对每个省份的销量 Qty 求和:

aggregate(Mydf$Qty, by=Mydf["DepPC"], FUN=sum)

也就是说,aggregate() 告诉 R:希望对属于同一省份的所有 Qty 值进行求和。

注意,因为您没有另行指定,R 将求和结果赋给了变量 “x”。

输出本身已经比较易读,但当省份较多时,可能就不那么直观了。在这种情况下,您可以借助图形输出:结合 R 的图形系统与 aggregate() 函数绘图展示结果:

# Load in `ggplot2`
library(ggplot2)

# Plot the sales per department
ggpot(data=aggregate(Mydf$QMy,by=Mydf["DepPC"],FUN=sum), aes(x=DepPC, y=x)) +
  geom_point()+
  ggtitle("Sales per department - All")

这样我们就得到了各省份的销售情况。

您也可以提出同样的问题,但仅限于已送达的商品。为此,先使用熟悉的子集操作符 "[" 选择 Delivered 为真(T)的数据。

注意,这里将结果赋给了新变量 Y,这是一个新的数据框,继承了父数据框 Mydf 的列名。这样做是为了避免在绘图调用中重复写聚合语句,使代码更易读:

# select only for delivered=True
Y <- Mydf[Mydf$Delivered==TRUE,]

# Plot the sales by department
ggplot(data=aggregate(Y$Qty,by=Y["DepPC"],FUN=sum), aes(x=DepPC, y=x)) +
  geom_point()+
  ggtitle("Sales per department - Delivered")

因此,您可以像使用 aggregate() 一样,以向量化的方式向数据提出不同问题,并经常与便捷的绘图系统(如 ggplot2)配合使用,思路就是这样。

值得注意的是,为实现上述目标,您只需要非常少的代码。

向量化能否作为循环与 Apply 函数的替代?

上面展示了同一主题的一些变体,即“以重复方式作用于结构化数据集”。从这个意义上说,这些函数不仅是循环的替代方案,也是一种向量化的做法。

这里的“向量化”是宽泛意义上的,我们不讨论哪些 apply() 函数是否真正向量化(可参见例如此处的讨论)。

在实践中,为选择使用哪一个 apply() 函数,您需要考虑以下几点:

  • 输入的数据类型:即您将要操作的对象(向量、矩阵、数组、列表、数据框,或它们的组合);
  • 您的意图:要传入的 FUN 函数是什么;
  • 数据的子集:是行、列,还是全部?
  • 您希望函数返回的数据类型为何?因为您可能还要据此做进一步操作(以及您是想得到一个新对象,还是直接变换输入对象?)

这些都是可以用于相关函数的通用问题,我们已讨论了 aggregate()、by()、sweep() 等。

但还有更多可探索的内容!别止步于此!请查看 DataCamp 的 R 中的数组教程。

作为本教程的后续,建议学习 DataCamp 的 Introduction to R 或 Intermediate R 课程。

主题