在 R 语言中, for 循环不像 C 语言中的 for 那么快速. 因为在 R 的 for 循环中, 需要 R 解释器去解析每个字符, 而 C 则通过编译把代码转为了电脑可以直接运行的机器码. 在 R 中, 单纯地在循环中增加括号, 就能够使得循环的速度下降.
m <- 0
system.time({
for (i in 1:10000000) {
m <- m + i
}
})
# user system elapsed
# 4.266 0.014 4.289
m <- 0
system.time({
for (i in 1:10000000) {
(((((((m <- m + i)))))))
}
})
# user system elapsed
# 10.920 0.013 10.953
有的时候我们需要做的只是简单地对列表或者数据框的每一行或列数据使用同样的函数, 在 C 语言中, 同样的问题通过 for loop 或者 while 来解决是十分直观和方便高效的. 而由于上面提到了 R 的特点, 使用 for loop 尤其是多层 for loop 则会很慢. 这样的情况下, 就应该取考虑映射 (mapping), 即在一系列数据上重复地去使用同一个函数, 即把该函数映射到数据上.
还应该注意的是, R 中对数据集合的单个元素的改变并不是在原来的位置上直接变换, 而是重新产生一个数据集合, 和之前的数据集合在需要变换的地方不同. 这样的效率就相对来说比较低下. 所以应该尽量避免频繁的复制改变. 那么一般的解决办法是使用一些在中间计算过程中不会赋值的函数, 如下面说道的 apply 系列, 还有就是使用 C++ 语言 和 RCpp 包来完成需要频繁改变的代码部分.
apply 函数
在 R 的基础包中有一类函数叫 apply, 包括 apply, lapply, sapply, vapply, mapply, replacate 等. 这些函数相同的地方都是接受一个 list, matrix 或者 data.frame 作为参数, 同时接受一个函数作为参数, 然后对数据的每一行或列都使用函数进行处理, 相当于 "把函数应用 (apply) 到每一个单元".
apply
apply 函数本身的运算速度其实和 for loop 很接近, 但是由于其语句少, 结构简单, 在进行大量重复应用相同函数到同一个数据集的时候效率似乎要比 for loop 要高一些. apply 函数有三个主要参数: 一个是数据集, array, matrix, data.frame, list 都可以; 一个是 MARGIN, MARGIN 为 1 的时候对 matrix 的每一行运行函数, MARGIN 为 2 的时候对 matrix 的每一列运行函数, 如果是 array 则可能是更高维; 一个是要对数据集运行的函数.
有的时候我需要对一个向量计算若干项的平均值, 比如说要把这个向量按顺序分成不重和的若干组, 每组三个, 然后计算其平均值. 这个时候就可以把该向量先转换成一个有一个维度为 3 的矩阵, 然后使用 apply 进行计算.
x <- 1:12
apply(matrix(x,ncol=3,byrow=TRUE), 1, mean)
#[1] 2 5 8 11
除了使用 apply 可以使用不同的函数之外, 如果目的仅仅是需要得到几个值的和或者均值, 还可以在把向量转换为矩阵之后使用 colSums, rowSums, colMeans, rowMeans 等函数.
sweep
有的时候, 所需要对一个数据集映射的函数需要有两个值, 比如说 "/" 除法操作符等双目操作符, 对于一列或者一行, 所用的值也不太一样. 这个时候 apply 可能就不是很方便了, 这个时候, 使用 sweep 就更方便一些. sweep 相对于 apply 要多接受一个参数, STAT, STAT 就是用来接受如双目的操作符后面的一个数字的.
x <- 1:12
y <- seq(2, 24, by = 2)
sweep(matrix(x, nrow = 1), 2, y, "/")
# [,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9] [,10] [,11] [,12]
#[1,] 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5
lapply, sapply, vapply, replicate
lapply, sapply, vapply, replicate 有很多相似的地方, 实际上, lapply 是最为基础的, sapply 是对 lapply 的一种方便使用的包装, 可以理解为 "simple apply". 而 replicate 则是对 sapply 的一个更方便使用的包装.
lapply 可以理解为 "list apply", 其总会返回一个 list 结果, 这个 list 的长度和输入的 list x 的长度是一样. 其接受一个数据集, 列表 x, 并接受一个需要做映射的函数, 同时也可以接受该函数的参数.
call_fun <- function(f, ...) f(...)
f <- list(sum, mean, median, sd)
lapply(f, call_fun, x = runif(1e3))
sapply 是对 lapply 的包装, 其结果不再一定是一个 list, 也会根据方便程度返回向量等, 其有参数 simplify = TRUE
可以设置是否采取简化输出.
vapply 则是限定输出的结果为 vector, 其返回结果会和 FUN.VALUE 所设定的一致, 如设定每一项的名字 c("one" = 0, "two" = 0, "three" = 0)
. 有的时候 sapply 输出的值和 vapply 输出的值是相似的.
replicate 是简单地把一个函数重复若干次, 并把每次重复函数的输出的值整合起来, 如可以重复一个输出五个随机数的函数, 这样设置 replicate 函数的参数 n 为 100, 就能输出一个 5 * 100 的矩阵. replicate 可以用于去构建测试数据集.
在 parallel 包中, mclapply 是多线程版的 lapply.
mapply
mapply 可以理解为 "multi sapply", 它接受一个函数作为参数 FUN, 另外接受若干数据集做为输入, 同时每个数据集对应的行会作为 FUN 的参数.
mapply(sum, 1:4, 4:1)
#[1] 5 5 5 5
在 parallel 包中, mcmapply 是多线程版的 mapply.
Map
Map 函数也可以实现一个对一个数据集进行多参数函数的映射. Map 相当于设置 simplify = FALSE
的 mapply 函数. 例如, weighted.mean 接受变量和其权重, 来计算加权平均值.
x <- replicate(10, runif(10), simplify = FALSE)
w <- replicate(10, rpois(10, 5) + 1, simplify = FALSE)
Map(weighted.mean, x, w)
在 parallel 包中, mcMap 是多线程版的 mcMap.
aggregate, tapply
有的时候我们需要根据某种分组来对数据集进行处理, 比如说根据性别来计算相对身高等. 这个时候就可以使用 aggregate, tapply 等函数.
aggregate 函数也可以接受一个 list 或 data.frame 作为输入数据集, 也接受一个函数做参数. 相对前面提到的 sapply 有一点重要不同是, 接受一个分组的 list 作为 by 参数的值. 所有的计算将根据 by 的分组进行. 所输出的结果的维度将会和分组的维度相关.
tapply 和 aggregate 类似, 不过其 INDEX 参数可以接受若干对数据集进行分组的向量, 输出结果将和分组相关. 也可以使用面向对象的 tapply, by 函数, 其参数和 tapply 类似. 对于 tapply, 就是可以把数据集按照一定规则分成几类, 那么相似的结果也可以通过 sapply 函数和 split 函数实现.
n <- 17; fac <- factor(rep(1:3, length = n), levels = 1:5)
aggregate(1:n, list(fac), fivenum)
# Group.1 x.1 x.2 x.3 x.4 x.5
#1 1 1.0 4.0 8.5 13.0 16.0
#2 2 2.0 5.0 9.5 14.0 17.0
#3 3 3.0 6.0 9.0 12.0 15.0
tapply(1:n, list(fac), fivenum)
#$`1`
#[1] 1.0 4.0 8.5 13.0 16.0
#$`2`
#[1] 2.0 5.0 9.5 14.0 17.0
#$`3`
#[1] 3 6 9 12 15
#$`4`
#NULL
#$`5`
#NULL
plyr
plyr 包中有若干的具有不同特性的 apply 类的函数. 大神 Hadley Wickham 在其关于 plyr 包的文章中抽象出了数据分析中的范式: 分割-应用-整合 (split-apply-combine). 这个范式正是 plyr 包的基础. 同时也被 Python 的数据分析工具包 Pandas 非常好地采用了.
R 的 for 循环的效率是知名的低, plyr 中提供的 apply 类型函数可以很大程度上去替代在数据分析中出现的一些 for 循环. 然而, apply 函数的意义还不仅仅是提高了速度, 而是把数据处理的逻辑给抽象出来, 我们可以更关注数据整体是采用怎样的逻辑被处理的, 而不必把过多的精力放在每一个数据是怎么被计算出来的.
例子
我们不妨以著名的 iris 数据为例子. 当然, iris 数据经过处理后才好使用, 这就可以使用 Hadley Wickham 的另一个包 tidyr 来处理, 不过这里我们先使用 utils 中的 stack 函数, tidyr 的使用会在相应的笔记中说明.
long.iris <- stack(iris, select=-Species)
long.iris <- data.frame(
long.iris,
Species=rep(
iris[['Speices']], 4
)
)
colnames(long.iris)
# [1] "values" "ind" "Species"
如果我们需要去看一下对于不同物种的不同的属性的最大和最小值.
我们自然是可以使用 summary 这样的函数 summary(iris)
.
也可以使用 tapply 函数来根据 index 去计算.
tapply(
long.iris$values,
INDEX=paste(
long.iris$Species,
long.iris$ind,
sep='.'
),
function(x) {c(max(x), min(x))}
)
这样可以计算出来结果, 不过结果是 list 的数据结构, 不方便进一步的计算, 我们希望结果能够成为一个 data.frame. 如果使用 plyr 包中的 ddply 则可以很好解决问题.
ddply(
long.iris,
.(Species, ind),
function(x) {c(max(x$values), min(x$values))}
)
# Species ind V1 V2
# 1 setosa Petal.Length 1.9 1.0
# 2 setosa Petal.Width 0.6 0.1
# 3 setosa Sepal.Length 5.8 4.3
# 4 setosa Sepal.Width 4.4 2.3
# 5 versicolor Petal.Length 5.1 3.0
# 6 versicolor Petal.Width 1.8 1.0
# 7 versicolor Sepal.Length 7.0 4.9
# 8 versicolor Sepal.Width 3.4 2.0
# 9 virginica Petal.Length 6.9 4.5
# 10 virginica Petal.Width 2.5 1.4
# 11 virginica Sepal.Length 7.9 4.9
# 12 virginica Sepal.Width 3.8 2.2
plyr 包中的 apply 函数
首先, 我们先要回顾一下 R 中最基本的数据结构, 向量 (vector), 列表 (list), 和数组 (array). 另外, 矩阵 (matrix) 是特殊的仅仅有两维的数组, 而数据表 (data.frame) 是特殊的具有矩阵特性的列表. 我们用 a 代替数组, 用 l 代替列表, 用 d 代替数据表, 用 - 代替无. 那么我们就可以做一下排列组合, apply 函数的输入的数据结构可以有 a, l, d 三种, 输出的数据结构可以有 a, l, d, - 四种, - 代表不输出. 这样我们就有了 12 种组合, 这也是 plyr 中的 12 种 apply 函数.
Array | Data Frame | List | Discard | |
---|---|---|---|---|
Array | aaply |
adply |
alply |
a_ply |
Data Frame | daply |
ddply |
dlply |
d_ply |
List | laply |
ldply |
llply |
l_ply |
如 aaply 代表输入的是数组, 输出的是数组; adply 代表输入的是数组, 输出的是数据表; a_ply 输入的是数组, 而不输出.
a*ply
对于 a*ply 函数, 即 aaply, adply, alply 和 a_ply 函数, 有:
a*ply(.data, .margins, .fun, ..., .progress='none')
- .data 接受的是输出的多维数组
- .margins 是一个向量, 包含需要加以考虑的数组的维度,
例如对于三维数组, 要对前两维进行计算则
.margins=c(1, 2)
- .fun 接受对分组进行操作的函数.
a <- array(rnorm(27), c(3, 3, 3))
adply(a, c(1, 2), mean)
# X1 X2 V1
# 1 1 1 -0.48106276
# 2 2 1 0.69799472
# 3 3 1 -0.21132999
# 4 1 2 0.54177418
# 5 2 2 0.64463422
# 6 3 2 0.93857373
# 7 1 3 -0.86448199
# 8 2 3 -0.93706920
# 9 3 3 -0.08792167
对于对分组进行操作的 .fun 则需要注意根据所输入的数组和选用的 margin 进行匹配. 如果输入的是三维数组, 选择其中一个维度进行分组, 那么 .fun 则应该是对二维数组, 即矩阵的操作.
aaply(a, c(1), class)
# 1 2 3
# "matrix" "matrix" "matrix"
aaply(a, c(1, 2), class)
# X2
# X1 1 2 3
# 1 "numeric" "numeric" "numeric"
# 2 "numeric" "numeric" "numeric"
# 3 "numeric" "numeric" "numeric"
d*ply
对于 d*ply 函数, 即 daply, ddply, dlply 和 d_ply 函数, 有:
d*ply(.data, .variables, .fun, ..., .progress='none')
- .data 接受的是输出的数据表
- .variables 是一个向量, 包含需要加以考虑的列名,
可以用
c('Col1', 'Col2')
这样的形式或者.(Col1, Col2)
. 其中.()
这个函数是用来把没有引号标记的字符转为标记字符串. - .fun 接受对分组进行操作的函数.
偷个懒, 还是上面用过的例子咯.
daply(
long.iris,
c('Species', 'ind'),
function(x) {c(max(x$values), min(x$values))}
)
# , , = 1
#
# ind
# Species Petal.Length Petal.Width Sepal.Length Sepal.Width
# setosa 1.9 0.6 5.8 4.4
# versicolor 5.1 1.8 7.0 3.4
# virginica 6.9 2.5 7.9 3.8
#
# , , = 2
#
# ind
# Species Petal.Length Petal.Width Sepal.Length Sepal.Width
# setosa 1.0 0.1 4.3 2.3
# versicolor 3.0 1.0 4.9 2.0
# virginica 4.5 1.4 4.9 2.2
#
对于 d*ply
函数,
其 .fun
所接受的函数应该是对于和输入数据表同样的列数而不同行数的数据表进行操作的函数.
dim(long.iris)
# 600 3
ddply(
long.iris,
.(Species, ind),
function(x) {c(class(x), dim(x))}
)
# Species ind V1 V2 V3
# 1 setosa Petal.Length data.frame 50 3
# 2 setosa Petal.Width data.frame 50 3
# 3 setosa Sepal.Length data.frame 50 3
# 4 setosa Sepal.Width data.frame 50 3
# 5 versicolor Petal.Length data.frame 50 3
# 6 versicolor Petal.Width data.frame 50 3
# 7 versicolor Sepal.Length data.frame 50 3
# 8 versicolor Sepal.Width data.frame 50 3
# 9 virginica Petal.Length data.frame 50 3
# 10 virginica Petal.Width data.frame 50 3
# 11 virginica Sepal.Length data.frame 50 3
# 12 virginica Sepal.Width data.frame 50 3
50 * 12
# [1] 600
l*ply
对于 l*ply 函数, 即 laply, ldply, llply 和 l_ply 函数, 有:
l*ply(.data, .fun, ..., .progress='none')
- .data 接受的是输出的数据表
- .fun 接受对分组进行操作的函数.
举个简单的例子:
llply(long.iris, unique)
# $values
# [1] 5.1 4.9 4.7 4.6 5.0 5.4 4.4 4.8 4.3 5.8 5.7 5.2 5.5 4.5 5.3 7.0 6.4 6.9 6.5
# [20] 6.3 6.6 5.9 6.0 6.1 5.6 6.7 6.2 6.8 7.1 7.6 7.3 7.2 7.7 7.4 7.9 3.5 3.0 3.2
# [39] 3.1 3.6 3.9 3.4 2.9 3.7 4.0 3.8 3.3 4.1 4.2 2.3 2.8 2.4 2.7 2.0 2.2 2.5 2.6
# [58] 1.4 1.3 1.5 1.7 1.6 1.1 1.2 1.0 1.9 0.2 0.4 0.3 0.1 0.5 0.6 1.8 2.1
#
# $ind
# [1] Sepal.Length Sepal.Width Petal.Length Petal.Width
# Levels: Petal.Length Petal.Width Sepal.Length Sepal.Width
#
# $Species
# [1] setosa versicolor virginica
# Levels: setosa versicolor virginica
m*ply
在 plyr 包中还有一系列对应于 mapply 的函数,
包括 maply
, mdply
, mlply
, m_ply
等.
m*ply(.data, .fun, ..., .progress='none')
- .data 接受一个矩阵或者数据表
- .fun 是输入的函数
m*ply
函数接受一个矩阵, 或者数据表,
按照行的方式去处理数据,
每一行的一个列对应于函数的一个参数.
所以可以对应一个函数有不同的输入参数从而产生不同的结果.
例如生成若干随机数:
b <- data.frame(
n=c(5, 5, 5),
mean=c(1, 20, 300),
sd=c(1, 20, 300)
)
mdply(b, rnorm)
# n mean sd V1 V2 V3 V4 V5
# 1 5 1 1 1.13 1.23 0.39 3.34 0.19
# 2 5 20 20 17.37 -23.71 33.75 27.07 0.37
# 3 5 300 300 507.72 634.58 -70.71 643.29 555.39
r*ply
对应于 replicate 函数, plyr 中的函数是 r*ply
.
其也包括 raply
, rdply
, rlaply
, r_ply
等.
r*ply(.n, .expr, ..., .progress='none')
- .n 是要重复的个数
- .expr 是需要重复的表达式
例如生成随机数:
raply(5, rnorm(5))
# 1 2 3 4 5
# [1,] 0.74 0.05 -2.08 -0.57 0.12
# [2,] -1.23 0.32 -0.58 0.19 -0.31
# [3,] -1.10 0.32 0.07 -0.97 -0.05
# [4,] 0.82 -0.07 -0.12 1.53 -0.32
# [5,] 0.83 -0.20 -0.61 -0.42 -0.56
总结
于是这主要的十二个 apply 函数介绍完了.
我们可以发现, 对于 a*ply
, d*ply
, 和 l*ply
函数,
他们重要的区别在于如何确定分组.
a*ply
函数是根据 .margins
指定的输入数组的维度的值进行分组计算,
d*ply
函数是根据 .variables
指定的列名进行分组计算,
而 l*ply
函数类似于 lapply
对列表的每一个元素进行计算.
辅助函数
Hadley 大神把能想到的都做到了. 显然很多函数并不能直接用在 plyr 中, plyr 包中有很多辅助函数.
- splat: 把函数转换为可以接受一个列表进行输入, 配合
m*ply
有奇效
splat(rnorm)(list(n=5, mean=2))
# [1] 2.505678 2.944919 1.485397 1.536358 2.216418
- each: 同一个值传入不同的函数, 输出向量等, 可以配合
ddply
使用.
each(max, min)(seq(5))
# max min
# 5 1
- colwise: 把一个标量函数转为向量化的函数
class(iris)
# [1] "data.frame"
colwise(class)(iris)
# Sepal.Length Sepal.Width Petal.Length Petal.Width Species
# 1 numeric numeric numeric numeric factor
- failwith: 如果一个函数报错了, 整个计算就错了, 算的岂不可惜? 可以用 failwith 对于报错的函数返回特定的值, 而不中断计算.
sum(c(1, 2, '3'))
# Error in sum(c(1, 2, "3")) : invalid 'type' (character) of argument
failwith('错啦', sum)(c(1, 2, '3'))
# Error in f(...) : invalid 'type' (character) of argument
# [1] "错啦"
failwith('又错啦', sum, quiet=T)(c(1, 2, '3'))
# [1] "又错啦"