# Create a vector filled with random normal values
u1 <- rnorm(30)
print("This loop calculates the square of the first 10 elements of vector u1")# Initialize `usq`
usq <-0.for(i in1:10){# i-th element of `u1` squared into `i`-th position of `usq`
usq[i]<- u1[i]* u1[i]
print(usq[i])}
print(i)技术支持
for 代码块包含在花括号中。花括号可以紧跟在测试条件之后,也可以另起一行,后面最好配合缩进。二者都非强制,但花括号确实能增强代码可读性,并便于定位循环块及其中可能的错误。
# Insert your own integer here
my_int <-42
nr <- as.integer(my_int)# Create a `n` x `n` matrix with zeros
mymat <- matrix(0, nr, nr)# For each row and for each column, assign values based on position# These values are the product of two indexesfor(i in1:dim(mymat)[1]){for(j in1:dim(mymat)[2]){
mymat[i, j]= i * j
}}技术支持
上面的代码中有两个嵌套的 for 循环,因此有两组花括号,每组都有自己的代码块并由各自的索引控制。也就是说,i 遍历行,j 遍历列。
生成了什么?
其实,您生成的是非常熟悉的乘法表,这应该已经烂熟于心。
您也可以自行选择一个整数,然后按您的选择生成对应的表:如果是方阵,可将该整数赋给一个变量;若是矩形表,则赋给两个变量。该变量随后将作为索引 i 与 j 的上界。
# Show the first 10x10 chunk or the first `nr` x `nr` chunkif(nr >10){
mymat[1:10,1:10]}else
mymat[1:nr,1:nr]技术支持
注意,为避免用户在屏幕上打印巨大表格,您在末尾添加了一个条件:仅当用户输入的整数大于 10 时,才打印前 10 x 10 的区域。否则,将打印 n x n 的区域。
完整代码如下:
# Insert your own integer here
my_int <-42
nr <- as.integer(my_int)# Create a `n` x `n` matrix with zeroes
mymat <- matrix(0, nr, nr)技术支持
# For each row and for each column, assign values based on position# These values are the product of two indexesfor(i in1:dim(mymat)[1]){for(j in1:dim(mymat)[2]){
mymat[i, j]= i * j
}}# Show the first 10x10 chunk or the first `nr` x `nr` chunkif(nr >10){
mymat[1:10,1:10]}else
mymat[1:nr,1:nr]技术支持
for 循环:流行但并非总是完美之选
for 循环无疑最为流行,其结构意味着迭代次数是固定且事先已知的,例如“生成前 200 个素数”或“列出最重要的 10 位客户”。
# Your User Defined Function
readinteger <-function(){
n <- readline(prompt="Please, enter your ANSWER: ")}
response <- as.integer(readinteger())while(response!=42){
print("Sorry, the answer to whatever the question MUST be 42");
response <- as.integer(readinteger());}技术支持
一开始,使用用户自定义函数在进入循环前获取用户输入。只要答案不是预期的 42,该循环就会继续。
换句话说,您这样做是因为否则 R 会抱怨缺失用于提供 True 或 False 的表达式——实际上,在循环中使用前并不知道“response”为何物。您也这样做是因为如果您在第一次尝试中就回答正确,循环将根本不会执行。
repeat 循环
repeat 循环位于上面流程图的最右侧。它类似 while 循环,但无论条件结果如何,指令块 i1 与 i2 至少都会执行一次。
readinteger <-function(){
n <- readline(prompt="Please, enter your ANSWER: ")}repeat{
response <- as.integer(readinteger());if(response ==42){
print("Well done!");break}else print("Sorry, the answer to whatever the question MUST be 42");}技术支持
在熟悉的输入函数之后,您看到 repeat 循环的代码块至少执行一次,并且当 if 条件满足时终止。
# Make a lower triangular matrix (zeroes in upper right corner)
m =10
n =10# A counter to count the assignment
ctr =0# Create a 10 x 10 matrix with zeroes
mymat = matrix(0, m, n)for(i in1:m){for(j in1:n){if(i == j){break}else{# you assign the values only when i<>j
mymat[i, j]= i * j
ctr = ctr +1}}
print(i * j)}# Print how many matrix cells were assigned
print(ctr)技术支持
最后,采用原生向量格式,能够利用非常高效的线性代数例程(如 BLAS,Basic Linear Algebra Subprograms)。因此在执行向量化指令时,R 可以借力这些高效的数值例程。由此可得出的建议是:若可能,尽量在单次函数调用中处理完整的数据结构,以避免各种复制操作。
闲话少说,先看一个向量化的通用示例,下一小节将深入更具体、常用的 R 向量化函数,以替代循环。
R 中向量化的一个示例
回到“增长数据”的概念,这是“更新”数据框的一种典型低效方式。
首先,使用 replicate(m, rnorm(n)) 创建一个 m x n 的矩阵,其中 m=10 个列向量、每个有 n=10 个元素,由 rnorm(n) 构造(生成正态随机数)。
然后将其转换为数据框(因此是 10 个观测、10 个变量),并使用嵌套的 for 循环对每个元素执行代数运算:在每次迭代中,用一个正弦函数为由两个索引指向的每个元素递增。
以下示例略显“人为”,但它可以代表在随机噪声上叠加一个信号:
# This is a bad loop with 'growing' data
set.seed(42)
m =10
n =10# Create matrix of normal random numbers
mymat <- replicate(m, rnorm(n))# Transform into data frame
mydframe <- data.frame(mymat)for(i in1:m){for(j in1:n){
mydframe[i, j]<- mydframe[i, j]+10* sin(0.75* pi)
print(mydframe)}}技术支持
在这里,大部分执行时间都消耗在复制与管理循环上。
来看一下向量化解法:
set.seed(42)
m =10
n =10
mymat <- replicate(m, rnorm(n))
mydframe <- data.frame(mymat)
mydframe <- mydframe +10* sin(0.75* pi)
mydframe技术支持
这看起来更简单:最后一行取代了嵌套的 for 循环。注意 使用 set.seed() 以确保两种实现给出完全相同的结果。
apply 命令(或更确切地说,一组命令)属于 R base 包。它由多个函数([s,l,m,r, t,v]apply)组成,以重复方式对矩阵或数组形式的数据切片进行操作,从而横向或纵向遍历数据,避免显式使用循环结构。这些函数作用于输入的矩阵或数组,并将一个指定的函数(可带一个或多个可选参数)应用于之上。
给定一个矩阵 M,调用 apply(M,1,fun) 或 apply(M, 2,fun) 会将指定的函数 fun 应用于 M 的每一行(当指定 1)或每一列(当指定 2)。这个数值参数称为“边(margin)”,对矩阵而言仅限 1 与 2;不过,若是数组,维度最多可达 8。
该函数可以是任何合法的 R 函数,也可以是用户自定义函数(UDF),甚至可以直接在 apply() 内联编写,这非常方便。
apply():一个示例
# define matrix `mymat` by replicating the sequence `1:5` for `4` times and transforming into a matrix
mymat <- matrix(rep(seq(5),4), ncol =5)# `mymat` sum on rows
apply(mymat,1, sum)# `mymat` sum on columns
apply(mymat,2, sum)# With user defined function within the apply that adds any number `y` to the sum of the row# `y` is set at `4.5`
apply(mymat,1,function(x, y)
sum(x)+ y, y =4.5)# Or produce a summary column wise for each column
apply(mymat,2,function(x, y)
summary(mymat))技术支持
至此,我们从编程中最基本的循环结构出发,介绍了(基础的)向量化概念,并展示了 apply() 家族中一个函数的用法,该家族在 R 中经常会用到。
在代码流程控制方面,我们只涉及了循环:for、while、repeat,以及如何中断和退出它们。
既然前面的小节暗示在 R 中应尽量避免使用循环,您也许会问,为何还要学习它们。
在我看来,您应该学习这些编程结构,因为:
R 很可能不会是您在数据科学或其他领域中唯一使用的语言,掌握循环等通用结构,是值得装进技能包的好东西。不同语言的循环语法可能不同,但一旦在一种语言中掌握,您就能迅速将其应用到遇到的其他语言中。
R 生态非常庞大,要对所有现有函数了然于胸非常困难,甚至几乎不可能。实现同一目标的方法很多,有的更高效或更优雅,您的学习曲线将是渐进的;在使用过程中,您会开始思考如何更高效地做事,并最终发现此前从未听说过的函数。当您读到本文时,也许已有一些新库被开发发布了;除非您把全部时间都投入到 R 专业研究,否则想要全盘掌握的概率很小。
如果在读完本教程后,R 中的 for 循环对您来说已不再是挑战,您可以考虑学习 DataCamp 的 Intermediate R 课程。该课程会通过一系列全新有趣的练习加强您在《R 进阶》中的相关知识。如果循环对您已毫无秘密,DataCamp 的 Writing Functions in R 课程(由 Hadley 和 Charlotte Wickham 授课)也许会引起您的兴趣。此外,别忘了看看For Loops in R 教程。
本 R 循环教程将探讨 R 中可用的循环结构、这些结构应在何时使用,以及如何利用 R 的向量化等替代方案,更高效地完成循环任务。
本文将先给出一些循环示例,随后对其加以批判,并转而推荐在 R 丰富库集中广泛可用且更受欢迎的向量化替代方案。
总体而言,本教程对循环的建议是:先学习循环。它能让您在基本层面上清楚了解程序会发生什么,同时帮助您理解手头所操作的数据。
在您清楚理解循环之后,再尽量摆脱它们。
把精力投入到学习向量化替代方案上。在效率方面,它会带来回报。
若想交互式练习,请尝试我们Intermediate R 课程中的循环章节。
什么是循环?
“循环”“轮转”“迭代”,或只是重复指令,是早在计算机发明之前就存在的一种做法。它无非是通过组织一系列动作或“批处理”流程,并将需要重复的部分归组,从而将多步过程自动化。
所有现代编程语言都提供了允许重复执行指令或指令块的特殊结构。
笼统地说,现代编程语言中有两类这样的特殊结构或循环。一些循环在计数器或索引的控制下执行预定次数,在每次迭代周期后自增。这些属于
for循环家族。另一方面,还有一些循环基于逻辑条件的出现与验证。条件在循环结构的开始或结束处进行测试。这些变体分别属于
while或repeat循环家族。R 中循环的介绍
根据 R 基础手册,在控制流命令中,循环结构包括
for、while和repeat,以及附加的break与next子句。请记住,控制流命令是让程序在不同备选方案之间分支,或者“做出决策”的命令。
您随时可以在 RStudio 命令行调用
?Control来查看这些控制流命令。下图展示了 R 的循环结构:
R 中的 for 循环
接下来的部分将更仔细地查看上图中展示的每种结构。我们将从左侧的结构开始讨论,随后逐步移动到右侧的结构。
若想观看 for 循环的视频介绍并完成后续练习,请在我们的 Intermediate R 课程中尝试For Loop 章节。
for 循环详解
这种循环结构由“init”(初始化)的矩形框、菱形的决策节点以及矩形框
i1组成,会被执行已知次数。在流程图语境中,矩形框表示“执行不涉及决策的某些操作”。而菱形(或称菱形决策符号)则是“决策符号”,因此对应只能有两个逻辑答案的问题,即 True (T) 或 False (F)。
注意,为简化起见,图中略去了其他可能的符号。
初始化矩形中的一条或多条指令之后,会对某个变量上的条件进行求值,该变量可以在某个给定序列内取值。图中菱形表示的就是:“变量
v的当前值是否属于序列seq?”。换言之,您在测试
v的当前值是否在指定范围内。通常您会在初始化中定义这个范围,例如用1:100来确保循环开始。如果条件不满足,结果为
False,则循环永不执行。这由for循环结构右侧的游离箭头表示。随后程序将执行循环块之后遇到的第一条指令。若条件满足,则会执行一条(或一组)指令
i1。该指令块也可能是另一个循环。在这种情况下,就称为嵌套循环。完成后,会再次对条件求值。这由从
i1返回到顶部、紧接初始化框之后的连线表示。在 R(以及 Python)中,可以用直白的自然语言来表达:询问我们的变量是否属于某个取值范围。注意,在其他语言中,例如 C,条件通常会显式使用逻辑运算符来表示,如大于、小于、等于等……
下面是一个简单的 for 循环示例:
for代码块包含在花括号中。花括号可以紧跟在测试条件之后,也可以另起一行,后面最好配合缩进。二者都非强制,但花括号确实能增强代码可读性,并便于定位循环块及其中可能的错误。注意,平方向量
usq在此处被初始化。在纯 RStudio 代码中这并非必要,但在标记化版本中,knitr 会因为在循环中使用前未找到该向量的引用而无法编译,从而在 RStudio 内抛出错误。关于 knitr 的更多信息,请访问R Markdown 页面。嵌套 for 循环
既然您已经知道 for 循环也可以嵌套,那么您可能会想,为什么以及何时需要在代码中这样做。
假设您希望通过将元素设置为特定值来操作一个二维数组。
那么您可能会写出如下代码:
提示:关于
上面的代码中有两个嵌套的 for 循环,因此有两组花括号,每组都有自己的代码块并由各自的索引控制。也就是说,matrix()函数的更多信息,请访问R Documentation。i遍历行,j遍历列。其实,您生成的是非常熟悉的乘法表,这应该已经烂熟于心。
您也可以自行选择一个整数,然后按您的选择生成对应的表:如果是方阵,可将该整数赋给一个变量;若是矩形表,则赋给两个变量。该变量随后将作为索引
i与j的上界。注意,为避免用户在屏幕上打印巨大表格,您在末尾添加了一个条件:仅当用户输入的整数大于
10时,才打印前10x10的区域。否则,将打印nxn的区域。完整代码如下:
for 循环:流行但并非总是完美之选
for循环无疑最为流行,其结构意味着迭代次数是固定且事先已知的,例如“生成前 200 个素数”或“列出最重要的 10 位客户”。但如果您并不知道或无法控制迭代次数,且一个或多个无法预见的条件可能会发生,该怎么办?
例如,您可能想统计居住在某个邮政编码区域内的客户数量,或最近两天网页横幅上的点击次数,或其他类似不可预先预测的事件。
在这类情况下,
while循环及其“亲戚”repeat可能会派上用场……while 循环
上图中居中的
while循环,与之前类似,由一个初始化块开始,随后是一个逻辑条件。该条件通常通过控制变量与某个值的比较来表达,使用大于、小于或等于等运算,但任何能返回逻辑值 True 或 False 的表达式都可行。如果结果为 False (F),如图右侧的游离箭头所示,循环将不会执行。随后程序会执行循环块之后遇到的第一条指令。
若结果为 True (T),则会执行指令或指令块
i1。注意,这里还添加了另一条指令或指令块
i2:它用于更新控制变量,从而可能改变循环开始处条件的结果,但这并非必需。或者,您也许想给计数器加一,以记录已执行的迭代次数。一旦条件求值为假,迭代就会停止。其格式为
while(cond) expr,其中cond是要测试的条件,expr是要执行的表达式。例如,下面的循环通过一个用户自定义函数(UDF)要求用户输入“宇宙以及一切”的正确答案。在用户答对之前,它会一直继续:
一开始,使用用户自定义函数在进入循环前获取用户输入。只要答案不是预期的
42,该循环就会继续。换句话说,您这样做是因为否则 R 会抱怨缺失用于提供 True 或 False 的表达式——实际上,在循环中使用前并不知道“response”为何物。您也这样做是因为如果您在第一次尝试中就回答正确,循环将根本不会执行。
repeat 循环
repeat循环位于上面流程图的最右侧。它类似while循环,但无论条件结果如何,指令块i1与i2至少都会执行一次。参照其他语言,可以把它称为“repeat until”,以强调指令
i1与i2会一直执行,直到条件保持为 False (F) 或等价地变为 True (T) 从而退出;但无论如何,至少执行一次。作为对上一个示例的变体,您可以这样写:
在熟悉的输入函数之后,您看到
repeat循环的代码块至少执行一次,并且当if条件满足时终止。请注意,您需要在循环中设置一个条件,并通过
break子句在该条件下退出。该子句引出了在循环中中断或退出的概念。在 R 中中断与退出循环
那么,如何从循环中退出?
换种说法,除去循环的“自然”结束(要么达到预定迭代次数(
for),要么满足某个条件(while、repeat)),您能否提前停止或中断循环?如果能,该如何做?
break语句回答了第一个问题:您已在上一个示例中见过它。用
break中断循环当 R 解释器遇到 break 时,它会将控制权交给循环结束之后的那条指令(若存在)。在嵌套循环的情况下,
break只会退出最内层的循环。下面是一个示例。
这段代码定义了一个
mxn的零矩阵,然后进入嵌套的for循环来填充矩阵位置,但仅在两个索引不相等时进行。其目的是创建一个下三角矩阵,即主对角线以下的元素为非零,上面的元素保持为初始化的 0。当索引相等时,内层循环(遍历列索引
j)的条件满足,执行break,最内层循环被中断,直接跳到内层循环之后的指令。这条指令是一个print()。然后,控制流回到外层 for 条件(行索引i),再进行求值。如果索引不同,则进行赋值,并将计数器加
1。最后,程序打印计数器ctr,其中包含被赋值的元素数量。注意,即便在并非绝对必要时加上花括号也许略显谨慎,但通常这样做能确保凡是用
{打开的,就会用}关闭。如果您发现{或}的数量不匹配,就知道有错误,尽管反之未必成立!在循环中使用
nextnext会终止某次具体的迭代并跳到下一轮。实际上,它会跳转到当前循环条件的求值处。在其他语言中,您可能会看到等价但略显混淆的叫法“continue”,含义相同:无论您身在何处,一旦条件触发,就跳到循环条件的求值。
一个更简单的示例是在条件发生时继续循环但丢弃某次迭代:
这段代码会打印区间
1:m(此处m=20)内的所有奇数。换句话说,所有除以2余数非零的整数(因此使用取模运算符%%)将被打印,如 if 测试所示。余数为零的数字将不会被打印,因为程序跳至
i in 1:m条件的求值,并忽略后续任何指令。在此例中,print(k)被忽略。小结:在 R 中使用循环
repeat时要小心:务必通过测试条件显式设置终止条件,否则可能陷入无限循环。何时使用 R 循环
这些内容都不错,但您究竟应该在何时使用 R 中的循环,又在何时不该使用?
每当需要重复执行某些操作时,循环都可能派上用场。
您只需指定需要执行的次数,或指定在何种条件下执行这些操作:为控制循环变量赋初值,执行循环,然后在循环结束后,通常对结果做一些处理。
但您何时应该使用循环?
难道不能直接把所需指令复制足够多次吗?
一个经验法则是,如果您需要执行某个动作(三次)或更多次,那么循环会更适合。它能让代码更紧凑、可读性更高、可维护性更好,也能省些打字:假如您发现某条指令需要比最初预期多重复一次,您无需重写整条指令,只需修改测试条件中某个变量的取值即可。
不过,R 的特殊性也提示:只要有替代方案,尽量不要使用循环(!)。
幸运的是,确实有替代方案!
R 拥有少数编程语言才具备的一个特性,称为向量化。
R 中循环的替代方案
什么是向量化?
顾名思义,向量化就是将对简单数字(“标量”)的重复操作,转换为对向量或矩阵的一次性操作。您已在上面各小节中看到过多个例子。
现在,向量是 R 中的基本数据结构,根据 R 基础手册,它是“由若干元素组成的单个实体”。
因此,一组数字就是一个数值向量。
如果将(长度相同的)向量组合起来,就能得到一个矩阵。您可以通过不同的 R 指令按列或按行进行组合。因此在 R 中,矩阵可视为按行或按列的向量集合。以此类推,您也可以将对向量的重复操作进行向量化。
许多上述循环结构都可以通过向量化而变得“隐式”。
我说“隐式”,是因为它们并未真正消失。在更低层,向量化的替代形式会被翻译为在其实现与编译所用的低层语言(Fortran、C 或 C++)中的一段包含一个或多个循环的代码。
这些对用户是隐藏的,且通常比等价的显式 R 代码更快。但除非您打算用这些语言来实现自己的 R 函数,否则这些细节对您是完全透明的。
最基本的例子,您可以将两个向量
v1和v2相加得到向量v3,既可以用for循环逐个元素相加:也可以使用“原生”的向量化形式:
注意,之所以称为“原生”,是因为 R 能识别所有算术运算符对向量与矩阵的运算。
类似地,对于两个矩阵
A和B,与其在对应位置逐个相加A[i,j]与B[i,j](这需要两个索引i与j),您可以直接让 R 这样做:确实非常简单!
R 中的向量化详解
既然看起来基本运算次数相同,为什么向量化运行更快?
最佳解释需要深入 R 的内部机制,这值得单开一文。简而言之:首先,R 是解释型语言,因此所有关于变量定义的细节都由解释器处理。例如,您无需指定数字是浮点型,或使用指针在内存中分配空间。
R 解释器会在您输入命令时从上下文“理解”这些问题,但它是逐条命令处理的。因此,即便重复同一命令,每次也都要处理这些定义。
相反,编译器会在编译阶段对整个代码的定义与声明“逐一清算”;代码会在您执行之前被翻译为紧凑、优化的二进制。因此,当 R 函数以这些底层语言编写时,它们更高效。
实际上,如果查看底层代码,您会发现对 C 或 C++ 的调用,通常封装在所谓的“包装器”代码中实现。
其次,在支持向量化的语言(如 R 或 Matlab)中,每条使用数值数据的指令,都是对一个天生定义为向量的对象进行操作,即便它只有一个元素。例如,当您定义一个单独的数值变量时,它在 R 内部的表示始终是一个向量,尽管仅包含一个数。
循环仍然存在于底层,但位于更底层且更快的 C/C++ 编译级别。拥有向量的优势在于:解释器只需对整个向量进行一次定义解析,而不管其大小;而对标量进行循环时,定义与分配需要逐元素进行,这就更慢。
最后,采用原生向量格式,能够利用非常高效的线性代数例程(如 BLAS,Basic Linear Algebra Subprograms)。因此在执行向量化指令时,R 可以借力这些高效的数值例程。由此可得出的建议是:若可能,尽量在单次函数调用中处理完整的数据结构,以避免各种复制操作。
闲话少说,先看一个向量化的通用示例,下一小节将深入更具体、常用的 R 向量化函数,以替代循环。
R 中向量化的一个示例
回到“增长数据”的概念,这是“更新”数据框的一种典型低效方式。
首先,使用
replicate(m, rnorm(n))创建一个mxn的矩阵,其中m=10个列向量、每个有n=10个元素,由rnorm(n)构造(生成正态随机数)。然后将其转换为数据框(因此是
10个观测、10个变量),并使用嵌套的for循环对每个元素执行代数运算:在每次迭代中,用一个正弦函数为由两个索引指向的每个元素递增。以下示例略显“人为”,但它可以代表在随机噪声上叠加一个信号:
在这里,大部分执行时间都消耗在复制与管理循环上。
来看一下向量化解法:
这看起来更简单:最后一行取代了嵌套的
for循环。注意 使用set.seed()以确保两种实现给出完全相同的结果。现在让我们量化两种方案的执行时间。
您可以使用 R 的系统命令(如
system.time())对一段代码进行计时,如下所示:提示:只需将要评估的代码放在
system.time()函数的括号中。在上述代码中,您仅在开始时完成选择
m与n、创建矩阵及其转换为数据框的工作,然后分别用两次system.time()调用,来比较for代码块与向量化“一行解”的耗时。您会发现,即便在
m=n=10的最小设置下,向量化版本也快约 7 倍,尽管对这种低规模场景而言,对用户的意义不大。当设置为
m=n=100时,差异在人感知层面变得明显;而增至 1000 时,for 循环会让人感觉卡顿数十秒,而向量化形式仍可在眨眼间完成。在
m=n=10000时,for 循环会卡顿一分钟以上,而向量化方案仅需 2.54 秒。当然,这些测量应谨慎对待,并会受硬件与软件配置影响;尽量避免在浏览器开几十个标签页、后台运行多个程序的情况下进行测试。不过,这些数字足以说明差异。公允地说,增大
m与n也会显著影响矩阵的生成,您可以通过在复制函数外再套一层system.time()来轻松观察到这一点。欢迎您通过改变
m与n来观察执行时间的变化,并将执行时间绘制为mxn乘积的函数。这是相关指标,因为它表示所创建矩阵的尺寸,也量化了通过nset-edfor循环完成任务所需的迭代次数。以上是一个向量化的示例。当然还有许多其他示例。在 R 项目通讯(R News)第 46 页中,介绍了一些对数组或矩阵特定维度计算求和与均值的高效函数,如:
rowSums()、colSums()、rowMeans()和colMeans()。此外,该通讯还提到:“……‘apply’ 家族的函数,命名为 [s,l,m,t]apply,用于将另一个函数应用到对象的元素/维度上。这些 ‘apply’ 函数为有时相当复杂的任务提供了简洁语法,比写得不好的循环更易读也更快。”
apply家族:只是隐藏的循环吗?功能丰富而强大的
apply函数家族,本质上是向量化的函数。如果乍一看它们似乎不包含任何循环,那么在仔细探究其实现后,这一点会变得显而易见。apply命令(或更确切地说,一组命令)属于 R base 包。它由多个函数([s,l,m,r, t,v]apply)组成,以重复方式对矩阵或数组形式的数据切片进行操作,从而横向或纵向遍历数据,避免显式使用循环结构。这些函数作用于输入的矩阵或数组,并将一个指定的函数(可带一个或多个可选参数)应用于之上。注意,这正是它们被称为“函数式”(可参见 Hadley Wickham 的 Advanced R 页面)的原因。
被调用的函数可以是聚合函数,如简单的均值,也可以是其它变换或子集函数。
需要指出的是,使用这些函数不一定会带来更快的执行:差异并不大;它们的优势在于避免编写繁琐的循环,从而减少出错概率。
该家族中的函数包括:
apply()、sapply()、lapply()、mapply()、rapply()、tapply()、vapply()。那么,您该在何时、如何使用它们?
值得注意的是,诸如
plyr的包涵盖了该家族的功能;尽管不查官方文档便记住它们可能有难度,但这些函数构成了更复杂且有用组合的基础。最常用的是前三个:
apply()您希望将某个函数应用到矩阵的行(索引“1”)或列(索引“2”)上。详情参见此页面。
lapply()您希望将某个函数应用到列表的每个元素上,并得到一个列表作为结果(这也解释了函数名中的“l”)。可在R 文档中阅读更多内容。
sapply()您希望将某个函数应用到列表的每个元素上,但希望得到一个向量而不是列表。想了解更多?请访问R 文档页面。
相关函数还有
sweep()、by()与aggregate(),有时会与apply()家族中的成员配合使用。我会将本篇博客的讨论限定在
apply()函数(更深入的讨论可能会留给未来的文章)。给定一个矩阵
M,调用apply(M,1,fun)或apply(M, 2,fun)会将指定的函数 fun 应用于M的每一行(当指定1)或每一列(当指定2)。这个数值参数称为“边(margin)”,对矩阵而言仅限1与2;不过,若是数组,维度最多可达 8。该函数可以是任何合法的 R 函数,也可以是用户自定义函数(UDF),甚至可以直接在
apply()内联编写,这非常方便。apply():一个示例您经常会使用数据框:在这种情况下,您必须确保数据具有相同类型,否则就可能发生强制类型转换,这很可能不是您想要的。例如,在包含文本与数字的混合数据框中,数值数据会被转换为字符串或字符型。
关于在 R 中使用循环及其替代方案的最后思考
至此,我们从编程中最基本的循环结构出发,介绍了(基础的)向量化概念,并展示了
apply()家族中一个函数的用法,该家族在 R 中经常会用到。在代码流程控制方面,我们只涉及了循环:
for、while、repeat,以及如何中断和退出它们。既然前面的小节暗示在 R 中应尽量避免使用循环,您也许会问,为何还要学习它们。
在我看来,您应该学习这些编程结构,因为:
如果在读完本教程后,R 中的 for 循环对您来说已不再是挑战,您可以考虑学习 DataCamp 的 Intermediate R 课程。该课程会通过一系列全新有趣的练习加强您在《R 进阶》中的相关知识。如果循环对您已毫无秘密,DataCamp 的 Writing Functions in R 课程(由 Hadley 和 Charlotte Wickham 授课)也许会引起您的兴趣。此外,别忘了看看For Loops in R 教程。