
关于 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() 类似。
不同之处在于:
- 它也可用于其他对象,如数据框、列表或向量;以及
- 其输出是一个列表(这也解释了函数名中的 “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, )
该操作显示在图 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 课程。