1 、 apply函數
數據量比較大的時候,R 語言for循環非常的慢,
apply函數是最常用的代替for循環的函數。apply函數可以對矩陣、數據框、數組(二維、多維),按行或列進行循環計算,對子元素進行迭代,并把子元素以參數傳遞的形式給自定義的FUN函數中,并以返回計算結果。
apply(X, MARGIN, FUN, ...)
X:數組、矩陣、數據框
MARGIN:按行計算或按按列計算,1表示按行,2表示按列
FUN:自定義的調用函數
…:更多參數,可選
示例:對一個矩陣的每一行求和
> x<-matrix(1:12,ncol=4,nrow=3)
> x
[,1] [,2] [,3] [,4]
[1,] 1 4 7 10
[2,] 2 5 8 11
[3,] 3 6 9 12
> apply(x,1,sum)
[1] 22 26 30
計算一個稍微復雜點的例子,按行循環,讓數據框的x1列加1,并計算出x1,x2列的均值
# 生成data.frame
> x <- cbind(x1 = 3, x2 = c(4:1, 2:5)); x
x1 x2
[1,] 3 4
[2,] 3 3
[3,] 3 2
[4,] 3 1
[5,] 3 2
[6,] 3 3
[7,] 3 4
[8,] 3 5
# 自定義函數myFUN,第一個參數x為數據
# 第二、三個參數為自定義參數,可以通過apply的'...'進行傳入。
> myFUN<- function(x, c1, c2) {
+ c(sum(x[c1],1), mean(x[c2]))
+ }
# 把數據框按行做循環,每行分別傳遞給myFUN函數,設置c1,c2對應myFUN的第二、三個參數
> apply(x,1,myFUN,c1='x1',c2=c('x1','x2'))
[,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8]
[1,] 4.0 4 4.0 4 4.0 4 4.0 4
[2,] 3.5 3 2.5 2 2.5 3 3.5 4
通過這個上面的自定義函數myFUN就實現了,一個常用的循環計算
如果直接用for循環來實現,那么代碼如下:
# 定義一個結果的數據框
> df<-data.frame()
# 定義for循環
> for(i in 1:nrow(x)){
+ row<-x[i,] # 每行的值
+ df<-rbind(df,rbind(c(sum(row[1],1), mean(row)))) # 計算,并賦值到結果數據框
+ }
# 打印結果數據框
> df
V1 V2
1 4 3.5
2 4 3.0
3 4 2.5
4 4 2.0
5 4 2.5
6 4 3.0
7 4 3.5
8 4 4.0
通過for循環的方式,也可以很容易的實現上面計算過程,但是這里還有一些額外的操作需要自己處理,比如構建循環體、定義結果數據集、并合每次循環的結果到結果數據集。
對于上面的需求,還有第三種實現方法,那就是完成利用了R的特性,通過向量化計算來完成的
> data.frame(x1=x[,1]+1,x2=rowMeans(x))
x1 x2
1 4 3.5
2 4 3.0
3 4 2.5
4 4 2.0
5 4 2.5
6 4 3.0
7 4 3.5
8 4 4.0
``
那么,一行就可以完成整個計算過程了。
接下來,我們需要再比較一下3種操作上面性能上的消耗。
```{r}
# 清空環境變量
> rm(list=ls())
# 封裝fun1
> fun1<-function(x){
+ myFUN<- function(x, c1, c2) {
+ c(sum(x[c1],1), mean(x[c2]))
+ }
+ apply(x,1,myFUN,c1='x1',c2=c('x1','x2'))
+ }
# 封裝fun2
> fun2<-function(x){
+ df<-data.frame()
+ for(i in 1:nrow(x)){
+ row<-x[i,]
+ df<-rbind(df,rbind(c(sum(row[1],1), mean(row))))
+ }
+ }
# 封裝fun3
> fun3<-function(x){
+ data.frame(x1=x[,1]+1,x2=rowMeans(x))
+ }
# 生成數據集
> x <- cbind(x1=3, x2 = c(400:1, 2:500))
# 分別統計3種方法的CPU耗時。
> system.time(fun1(x))
用戶 系統 流逝
0.01 0.00 0.02
> system.time(fun2(x))
用戶 系統 流逝
0.19 0.00 0.18
> system.time(fun3(x))
用戶 系統 流逝
0 0 0
從CPU的耗時來看,用for循環實現的計算是耗時最長的,apply實現的循環耗時很短,而直接使用R語言內置的向量計算的操作幾乎不耗時。
通過上面的測試,對同一個計算來說,優先考慮R語言內置的向量計算,必須要用到循環時則使用apply函數,應該盡量避免顯示的使用for,while等操作方法。
2、lapply函數
lapply函數是一個最基礎循環操作函數之一,
用來對list、data.frame數據集進行循環,并返回和X長度同樣的list結構作為結果集,通過lapply的開頭的第一個字母’l’就可以判斷返回結果集的類型。
lapply(X, FUN, ...)
X:list、data.frame數據
FUN:自定義的調用函數
…:更多參數,可選
示例:計算list中的每個KEY對應該的數據的分位數。
# 構建一個list數據集x,分別包括a,b,c 三個KEY值。
> x <- list(a = 1:10, b = rnorm(6,10,5), c = c(TRUE,FALSE,FALSE,TRUE));x
$a
[1] 1 2 3 4 5 6 7 8 9 10
$b
[1] 0.7585424 14.3662366 13.3772979 11.6658990 9.7011387 21.5321427
$c
[1] TRUE FALSE FALSE TRUE
# 分別計算每個KEY對應該的數據的分位數。
> lapply(x,fivenum)
$a
[1] 1.0 3.0 5.5 8.0 10.0
$b
[1] 0.7585424 9.7011387 12.5215985 14.3662366 21.5321427
$c
[1] 0.0 0.0 0.5 1.0 1.0
lapply就可以很方便地把list數據集進行循環操作了,還可以用data.frame數據集按列進行循環,但如果傳入的數據集是一個向量或矩陣對象,那么直接使用lapply就不能達到想要的效果了。
比如,對矩陣的列求和。
# 生成一個矩陣
> x <- cbind(x1=3, x2=c(2:1,4:5))
> x; class(x)
x1 x2
[1,] 3 2
[2,] 3 1
[3,] 3 4
[4,] 3 5
[1] "matrix"
# 求和
> lapply(x, sum)
[[1]]
[1] 3
[[2]]
[1] 3
[[3]]
[1] 3
[[4]]
[1] 3
[[5]]
[1] 2
[[6]]
[1] 1
[[7]]
[1] 4
[[8]]
[1] 5
lapply會分別循環矩陣中的每個值,而不是按行或按列進行分組計算。
如果對數據框的列求和。
> lapply(data.frame(x), sum)
$x1
[1] 12
$x2
[1] 12
lapply會自動把數據框按列進行分組,再進行計算。
3、sapply函數
sapply函數是一個簡化版的lapply,sapply增加了2個參數simplify和USE.NAMES,主要就是讓輸出看起來更友好,返回值為向量,而不是list對象
sapply(X, FUN, ..., simplify=TRUE, USE.NAMES = TRUE)
X:數組、矩陣、數據框
FUN:自定義的調用函數
…:更多參數,可選
simplify:是否數組化,當值array時,輸出結果按數組進行分組
USE.NAMES:如果X為字符串,TRUE設置字符串為數據名,FALSE不設置
示例:計算list中的每個KEY對應該的數據的分位數。
> x <- cbind(x1=3, x2=c(2:1,4:5))
# 對矩陣計算,計算過程同lapply函數
> sapply(x, sum)
[1] 3 3 3 3 2 1 4 5
# 對數據框計算
> sapply(data.frame(x), sum)
x1 x2
12 12
# 檢查結果類型,sapply返回類型為向量,而lapply的返回類型為list
> class(lapply(x, sum))
[1] "list"
> class(sapply(x, sum))
[1] "numeric"
如果simplify=FALSE和USE.NAMES=FALSE,那么完全sapply函數就等于lapply函數了。
> lapply(data.frame(x), sum)
$x1
[1] 12
$x2
[1] 12
> sapply(data.frame(x), sum, simplify=FALSE, USE.NAMES=FALSE)
$x1
[1] 12
$x2
[1] 12
對于simplify為array時,我們可以參考下面的例子,構建一個三維數組,其中二個維度為方陣。
> a<-1:2
# 按數組分組
> sapply(a,function(x) matrix(x,2,2), simplify='array')
, , 1
[,1] [,2]
[1,] 1 1
[2,] 1 1
, , 2
[,1] [,2]
[1,] 2 2
[2,] 2 2
# 默認情況,則自動合并分組
> sapply(a,function(x) matrix(x,2,2))
[,1] [,2]
[1,] 1 2
[2,] 1 2
[3,] 1 2
[4,] 1 2
對于字符串的向量,還可以自動生成數據名。
> val<-head(letters)
# 默認設置數據名
> sapply(val,paste,USE.NAMES=TRUE)
a b c d e f
"a" "b" "c" "d" "e" "f"
# USE.NAMES=FALSE,則不設置數據名
> sapply(val,paste,USE.NAMES=FALSE)
[1] "a" "b" "c" "d" "e" "f"
4、vapply函數
vapply類似于sapply,提供了FUN.VALUE參數,用來控制返回值的行名,這樣可以讓程序更健壯。
vapply(X, FUN, FUN.VALUE, ..., USE.NAMES = TRUE)
X:數組、矩陣、數據框
FUN:自定義的調用函數
…:更多參數,可選
FUN.VALUE:定義返回值的行名row.names
USE.NAMES: 如果X為字符串,TRUE設置字符串為數據名,FALSE不設置
示例:對數據框的數據進行累計求和,并對每一行設置行名row.names
# 生成數據集
> x <- data.frame(cbind(x1=3, x2=c(2:1,4:5)))
# 設置行名,4行分別為a,b,c,d
> vapply(x,cumsum,FUN.VALUE=c('a'=0,'b'=0,'c'=0,'d'=0))
x1 x2
a 3 2
b 6 3
c 9 7
d 12 12
# 當不設置時,為默認的索引值
> a<-sapply(x,cumsum);a
x1 x2
[1,] 3 2
[2,] 6 3
[3,] 9 7
[4,] 12 12
# 手動的方式設置行名
> row.names(a)<-c('a','b','c','d')
> a
x1 x2
a 3 2
b 6 3
c 9 7
d 12 12
通過使用vapply可以直接設置返回值的行名,
這樣子做其實可以節省一行的代碼,讓代碼看起來更順暢,當然如果不愿意多記一個函數,那么也可以直接忽略它,只用sapply就夠了。
5、mapply函數
mapply也是sapply的變形函數,類似多變量的sapply,但是參數定義有些變化。
第一參數為自定義的FUN函數,第二個參數’…’可以接收多個數據,作為FUN函數的參數調用。
mapply(FUN, ..., MoreArgs = NULL, SIMPLIFY = TRUE,USE.NAMES = TRUE)
FUN:自定義的調用函數
…:更多參數,可選
MoreArgs:參數列表
SIMPLIFY:是否數組化,當值array時,輸出結果按數組進行分組
USE.NAMES:如果X為字符串,TRUE設置字符串為數據名,FALSE不設置
示例:比較3個向量大小,按索引順序取較大的值。
> set.seed(1)
# 定義3個向量
> x<-1:10
> y<-5:-4
> z<-round(runif(10,-5,5))
# 按索引順序取較大的值。
> mapply(max,x,y,z)
[1] 5 4 3 4 5 6 7 8 9 10
再看一個例子,生成4個符合正態分布的數據集,分別對應的均值和方差為c(1,10,100,1000)。
> set.seed(1)
# 長度為4
> n<-rep(4,4)
# m為均值,v為方差
> m<-v<-c(1,10,100,1000)
# 生成4組數據,按列分組
> mapply(rnorm,n,m,v)
[,1] [,2] [,3] [,4]
[1,] 0.3735462 13.295078 157.57814 378.7594
[2,] 1.1836433 1.795316 69.46116 -1214.6999
[3,] 0.1643714 14.874291 251.17812 2124.9309
[4,] 2.5952808 17.383247 138.98432 955.0664
由于mapply是可以接收多個參數的,所以我們在做數據操作的時候,就不需要把數據先合并為data.frame了,直接一次操作就能計算出結果了。
6、tapply函數
tapply用于分組的循環計算,通過INDEX參數可以把數據集X進行分組,相當于group by的操作。
tapply(X, INDEX, FUN = NULL, ..., simplify = TRUE)
X:向量
FUN:自定義的調用函數
…:更多參數,可選
INDEX:用于分組的索引
simplify:是否數組化,當值array時,輸出結果按數組進行分組
示例:計算不同品種的鳶尾花的花瓣(iris)長度的均值。
# 通過iris$Species品種進行分組
> tapply(iris$Petal.Length,iris$Species,mean)
setosa versicolor virginica
1.462 4.260 5.552
對向量x和y進行計算,并以向量t為索引進行分組,求和。
> set.seed(1)
# 定義x,y向量
> x<-y<-1:10;x;y
[1] 1 2 3 4 5 6 7 8 9 10
[1] 1 2 3 4 5 6 7 8 9 10
# 設置分組索引t
> t<-round(runif(10,1,100)%%2);t
[1] 1 2 2 1 1 2 1 0 1 1
# 對x進行分組求和
> tapply(x,t,sum)
0 1 2
8 36 11
由于tapply只接收一個向量參考,通過’…’可以把再傳給你FUN其他的參數,那么我們想去y向量也進行求和,把y作為tapply的第4個參數進行計算。
> tapply(x,t,sum,y)
0 1 2
63 91 66
得到的結果并不符合我們的預期,結果不是把x和y對應的t分組后求和,而是得到了其他的結果。
第4個參數y傳入sum時,并不是按照循環一個一個傳進去的,而是每次傳了完整的向量數據,那么再執行sum時sum(y)=55,所以對于t=0時,x=8 再加上y=55,
最后計算結果為63。那么,我們在使用’…’去傳入其他的參數的時候,一定要看清楚傳遞過程的描述,才不會出現的算法上的錯誤。
7、rapply函數
rapply是一個遞歸版本的lapply,它只處理list類型數據,對list的每個元素進行遞歸遍歷,如果list包括子元素則繼續遍歷。
rapply(object, f, classes = "ANY", deflt = NULL, how = c("unlist", "replace", "list"), ...)
object:list數據
f:自定義的調用函數
…:更多參數,可選
classes:匹配類型, ANY為所有類型
deflt:非匹配類型的默認值
how:3種操作方式,當為replace時,則用調用f后的結果替換原list中原來的元素;當為list時,新建一個list,類型匹配調用f函數,不匹配賦值為deflt;當為unlist時,會執行一次unlist(recursive = TRUE)的操作
…: 更多參數,可選
示例:對一個list的數據進行過濾,把所有數字型numeric的數據進行從小到大的排序
> x=list(a=12,b=1:4,c=c('b','a'))
> y=pi
> z=data.frame(a=rnorm(10),b=1:10)
> a <- list(x=x,y=y,z=z)
# 進行排序,并替換原list的值
> rapply(a,sort, classes='numeric',how='replace')
$x
$x$a
[1] 12
$x$b
[1] 4 3 2 1
$x$c
[1] "b" "a"
$y
[1] 3.141593
$z
$z$a
[1] -0.8356286 -0.8204684 -0.6264538 -0.3053884 0.1836433 0.3295078
[7] 0.4874291 0.5757814 0.7383247 1.5952808
$z$b
[1] 10 9 8 7 6 5 4 3 2 1
> class(a$z$b)
[1] "integer"
從結果發現,只有$z$a的數據進行了排序,檢查$z$b的類型,發現是integer,是不等于numeric的,所以沒有進行排序。
接下來,對字符串類型的數據進行操作,把所有的字符串型加一個字符串’++++’,非字符串類型數據設置為NA。
> rapply(a,function(x) paste(x,'++++'),classes="character",deflt=NA, how = "list")
$x
$x$a
[1] NA
$x$b
[1] NA
$x$c
[1] "b ++++" "a ++++"
$y
[1] NA
$z
$z$a
[1] NA
$z$b
[1] NA
只有$x$c為字符串向量,都合并了一個新字符串。那么,有了rapply就可以對list類型的數據進行方便的數據過濾了
8、eapply函數
對一個環境空間中的所有變量進行遍歷。如果我們有好的習慣,把自定義的變量都按一定的規則存儲到自定義的環境空間中,
那么這個函數將會讓你的操作變得非常方便。當然,可能很多人都不熟悉空間的操作,那么請參考文章 揭開R語言中環境空間的神秘面紗,
解密R語言函數的環境空間。
eapply(env, FUN, ..., all.names = FALSE, USE.NAMES = TRUE)
env:環境空間
FUN:自定義的調用函數
…:更多參數,可選
all.names: 匹配類型, ANY為所有類型
USE.NAMES: 如果X為字符串,TRUE設置字符串為數據名,FALSE不設置
示例:下面我們定義一個環境空間,然后對環境空間的變量進行循環處理。
# 定義一個環境空間
> env# 向這個環境空間中存入3個變量
> env$a <- 1:10
> env$beta <- exp(-3:3)
> env$logic <- c(TRUE, FALSE, FALSE, TRUE)
> env# 查看env空間中的變量
> ls(env)
[1] "a" "beta" "logic"
# 查看env空間中的變量字符串結構
> ls.str(env)
a : int [1:10] 1 2 3 4 5 6 7 8 9 10
beta : num [1:7] 0.0498 0.1353 0.3679 1 2.7183 ...
logic : logi [1:4] TRUE FALSE FALSE TRUE
計算env環境空間中所有變量的均值。
> eapply(env, mean)
$logic
[1] 0.5
$beta
[1] 4.535125
$a
[1] 5.5
再計算中當前環境空間中的所有變量的占用內存大小。
# 查看當前環境空間中的變量
> ls()
[1] "a" "df" "env" "x" "y" "z" "X"
# 查看所有變量的占用內存大小
> eapply(environment(), object.size)
$a
2056 bytes
$df
1576 bytes
$x
656 bytes
$y
48 bytes
$z
952 bytes
$X
1088 bytes
$env
56 bytes
eapply函數平時很難被用到,但對于R包開發來說,環境空間的使用是必須要掌握的。特別是當R要做為工業化的工具時,對變量的精確控制和管理是非常必要的。
本文全面地介紹了,R語言中的數據循環處理的apply函數族,基本已經可以應對所有的循環處理的情況了。同時,在apply一節中也比較了,3種數據處理方面的性能,R的內置向量計算,要優于apply循環,大幅優于for循環。那么我們在以后的R的開發和使用過程中,應該更多地把apply函數使用好。
忘掉程序員的思維,換成數據的思維,也許你就一下子開朗了。