Ching-Chuan Chen's Blogger

Statistics, Machine Learning and Programming

0%

Introduce to foreach and iterators

Introduction *apply functions, Reduce and do.call

我一直沒時間寫*apply系列函數的介紹

不過最近用了foreachiterators

發現其實這兩個套件就可以完全取代*apply系列函數

而且對新手來說寫起來也不容易搞錯

這裡先簡介一下*apply系列函數

  1. apply
1
2
3
m <- matrix(1:6, 3)
# input matrix, output vector/matrix/list
apply(m, 1, sum) # output vector
1
## [1] 5 7 9
1
apply(m, 1, `*`, 2)  # output matrix
1
2
3
##      [,1] [,2] [,3]
## [1,] 2 4 6
## [2,] 8 10 12
1
apply(m, 1, function(v) rep(v[1], v[2]))  # output list
1
2
3
4
5
6
7
8
## [[1]]
## [1] 1 1 1 1
##
## [[2]]
## [1] 2 2 2 2 2
##
## [[3]]
## [1] 3 3 3 3 3 3
  1. lapply
1
2
# input vector/list, output list
lapply(1:3, `*`, 2) # input vector
1
2
3
4
5
6
7
8
## [[1]]
## [1] 2
##
## [[2]]
## [1] 4
##
## [[3]]
## [1] 6
1
lapply(as.list(1:3), `*`, 2)  # input list
1
2
3
4
5
6
7
8
## [[1]]
## [1] 2
##
## [[2]]
## [1] 4
##
## [[3]]
## [1] 6
  1. sapply
1
2
# input vector/list, output vector/matrix/list
sapply(1:3, `*`, 2) # input vector, output vector
1
## [1] 2 4 6
1
sapply(1:3, rep, each = 2)  # input vector, output matrix
1
2
3
##      [,1] [,2] [,3]
## [1,] 1 2 3
## [2,] 1 2 3
1
sapply(1:3, rep, x = 2)  # input vector, output list
1
2
3
4
5
6
7
8
## [[1]]
## [1] 2
##
## [[2]]
## [1] 2 2
##
## [[3]]
## [1] 2 2 2
1
sapply(as.list(1:3), `*`, 2)  # input list, output vector
1
## [1] 2 4 6
1
sapply(as.list(1:3), rep, each = 2)  # input list, output matrix
1
2
3
##      [,1] [,2] [,3]
## [1,] 1 2 3
## [2,] 1 2 3
1
sapply(as.list(1:3), rep, x = 2)  # input list, output list
1
2
3
4
5
6
7
8
## [[1]]
## [1] 2
##
## [[2]]
## [1] 2 2
##
## [[3]]
## [1] 2 2 2
  1. mapply
1
2
# multiple inputs, output vector/matrix/list stick list output by using `SIMPLIFY = FALSE`
mapply(function(x, y) x + y, 1:3, as.list(2:4)) # input vector and list, output vector
1
## [1] 3 5 7
1
mapply(function(x, y) rep(x * y, 2), 1:3, as.list(2:4))  # input vector and list, output matrix
1
2
3
##      [,1] [,2] [,3]
## [1,] 2 6 12
## [2,] 2 6 12
1
mapply(function(x, y) x + y, 1:3, as.list(2:4), SIMPLIFY = FALSE)  # input vector and list, output list
1
2
3
4
5
6
7
8
## [[1]]
## [1] 3
##
## [[2]]
## [1] 5
##
## [[3]]
## [1] 7
1
mapply(function(x, y) rep(x, y), 1:3, as.list(2:4))  # input vector and list, output list
1
2
3
4
5
6
7
8
## [[1]]
## [1] 1 1
##
## [[2]]
## [1] 2 2 2
##
## [[3]]
## [1] 3 3 3 3
  1. tapply
1
2
# input two vectors, one is value and the other one is group.  output vector/matrix
tapply(1:6, rep(1:2, 3), sum)
1
2
##  1  2 
## 9 12
1
tapply(1:6, rep(1:2, 3), `+`, 1)
1
2
3
4
5
## $`1`
## [1] 2 4 6
##
## $`2`
## [1] 3 5 7

簡單掃過apply系列之後,有沒有覺得光要弄懂輸出什麼格式就很折磨人了…

這裡額外提一下do.call這個指令

do.calldo a function call的意思

所以如果我們要把list的資料合併再一起

那我們就可以執行一個c/cbind/rbind的function call

list裡面的element當成input丟入這個function call,例子如下:

1
do.call(c, list(1, 2, 3))
1
## [1] 1 2 3
1
do.call(cbind, list(1:2, 2:3, 3:4))
1
2
3
##      [,1] [,2] [,3]
## [1,] 1 2 3
## [2,] 2 3 4
1
do.call(rbind, list(1:2, 2:3, 3:4))
1
2
3
4
##      [,1] [,2]
## [1,] 1 2
## [2,] 2 3
## [3,] 3 4

這裡可能有人學過Reduce這個函數

雖然寫法也差不多,像是:Reduce(c, list(1, 2, 3))這樣

但是其實差異還是滿大的,我這邊簡單呈現一下do.callReduce的差異

假設一個list有四個elements,那Reducedo.call分別的做法如下:

1
c(c(c(1, 2), 3), 4)  # <= Reduce的做法
1
## [1] 1 2 3 4
1
c(1, 2, 3, 4)  # <= do.call的做法
1
## [1] 1 2 3 4

可以看出Reduce是兩兩做,所以他在內部處理的時候需要不斷擴充output vector/matrix的長度

但是do.call是一次做完,所以do.call就不需要擴充長度

效率方面可以自己動手測測看,會比較有感

Reduce要幹嘛用?其實像是operator都只能輸入兩個參數,這情境就只能用Reduce

或是有一個list,element都是data.frame,你要根據每個element的id欄位做合併

那這時候你能用的函數也只有merge而已,簡單範例如下:

1
2
listDF <- list(data.frame(id = 1:5, V1 = rnorm(5)), data.frame(id = 1:5, V2 = rnorm(5)), data.frame(id = 1:5, V3 = rnorm(5)))
Reduce(function(x, y) merge(x, y, by = "id"), listDF)
1
2
3
4
5
6
##   id         V1         V2         V3
## 1 1 0.5019189 1.9036192 -1.6574498
## 2 2 -1.2667358 0.3022502 0.1295988
## 3 3 -0.7827332 -3.4698064 0.4290014
## 4 4 -1.6435900 1.0705252 2.0689225
## 5 5 -0.4572341 -0.1567469 -0.1389343

講那麼久,還沒到正題Orz

foreach

接下來,我們來進入正題吧!

foreach的用法相當直覺,我們從最簡單的case開始:

1
2
3
foreach(x = 1:3) %do% {
x + 1
}
1
2
3
4
5
6
7
8
## [[1]]
## [1] 2
##
## [[2]]
## [1] 3
##
## [[3]]
## [1] 4

輸入一個vector: 1:3,對每一個element做加一的動作,然後輸出是list

除非調整foreach的參數,不然輸出絕對是list,這是foreach第一個特性

foreach這個函數會得到一個物件,而透過%do%這個operator就可以執行後面的命令

{}是可以省略的,但是只限於一行,而且那一行不能是用operator的運算,不然會出現像是下面的錯誤

1
2
3
tryCatch({
foreach(x = 1:3) %do% x + 1
}, error = function(e) e)
1
## <simpleError in foreach(x = 1:3) %do% x + 1: 二元運算子中有非數值引數>

如果還是不要{},可以用下面這樣,把operator當成函數來用即可:

1
foreach(x = 1:3) %do% (x + 1)
1
2
3
4
5
6
7
8
## [[1]]
## [1] 2
##
## [[2]]
## [1] 3
##
## [[3]]
## [1] 4

再來,我們介紹怎麼改成vector/matrix輸出

我們只要在foreach這個命令裡面加上.combine這個屬性

那他裡面放的是我們要把結果合併的function,舉例如下:

1
2
3
foreach(x = 1:3, .combine = c) %do% {
x + 1
} # vector
1
## [1] 2 3 4
1
2
3
foreach(x = 1:3, .combine = cbind) %do% {
c(x, x + 1)
} # matrix
1
2
3
##      result.1 result.2 result.3
## [1,] 1 2 3
## [2,] 2 3 4
1
2
3
foreach(x = 1:3, .combine = rbind) %do% {
c(x, x + 1)
} # matrix
1
2
3
4
##          [,1] [,2]
## result.1 1 2
## result.2 2 3
## result.3 3 4

那這裡的.combine用的是Reduce,如果要用do.call的話,要在加上一個參數,如:

1
2
3
foreach(x = 1:3, .combine = c, .multicombine = TRUE) %do% {
x + 1
} # vector
1
## [1] 2 3 4
1
2
3
foreach(x = 1:3, .combine = cbind, .multicombine = TRUE) %do% {
c(x, x + 1)
} # matrix
1
2
3
##      result.1 result.2 result.3
## [1,] 1 2 3
## [2,] 2 3 4
1
2
3
foreach(x = 1:3, .combine = rbind, .multicombine = TRUE) %do% {
c(x, x + 1)
} # matrix
1
2
3
4
##          [,1] [,2]
## result.1 1 2
## result.2 2 3
## result.3 3 4

foreach還有幾個參數,稍微帶過,有些參數之後會再細講

.inorder是輸出是否要照順序,.maxcombine是最大結合數量

.errorhandling則是對錯誤的處理方式,verbose則是顯示更多訊息已提供user debug用

至於.package, .export.noexport,我們留到後面再解釋

剛剛上面看到foreach可以input vector, output list/vector/matrix了

但是我們還沒試過是不是list也行,舉個小範例:

1
foreach(x = as.list(1:3)) %do% rep(x, x)  # list
1
2
3
4
5
6
7
8
## [[1]]
## [1] 1
##
## [[2]]
## [1] 2 2
##
## [[3]]
## [1] 3 3 3
1
foreach(x = as.list(1:3), .combine = c, .multicombine = TRUE) %do% rep(x, x)  # vector
1
## [1] 1 2 2 3 3 3
1
foreach(x = as.list(1:3), .combine = cbind, .multicombine = TRUE) %do% rep(x, 2)  # matrix
1
2
3
##      result.1 result.2 result.3
## [1,] 1 2 3
## [2,] 1 2 3

那這樣其實就可以完全取代sapplylapply的功能了

再來是foreach也可以支援多個input,把mapply的範例重新用foreach寫一次:

1
2
3
foreach(x = 1:3, y = as.list(2:4)) %do% {
x + y
}
1
2
3
4
5
6
7
8
## [[1]]
## [1] 3
##
## [[2]]
## [1] 5
##
## [[3]]
## [1] 7
1
foreach(x = 1:3, y = as.list(2:4)) %do% rep(x * y, 2)
1
2
3
4
5
6
7
8
## [[1]]
## [1] 2 2
##
## [[2]]
## [1] 6 6
##
## [[3]]
## [1] 12 12
1
2
3
foreach(x = 1:3, y = as.list(2:4)) %do% {
x + y
}
1
2
3
4
5
6
7
8
## [[1]]
## [1] 3
##
## [[2]]
## [1] 5
##
## [[3]]
## [1] 7
1
foreach(x = 1:3, y = as.list(2:4)) %do% rep(x, y)
1
2
3
4
5
6
7
8
## [[1]]
## [1] 1 1
##
## [[2]]
## [1] 2 2 2
##
## [[3]]
## [1] 3 3 3 3

但是foreach除了取代sapplylapply之外

其實foreach還有很棒的debug功能:

1
print("i" %in% ls())
1
## [1] FALSE
1
2
3
4
result <- foreach(i = 1:3) %do% {
i + 1
}
if ("i" %in% ls()) print(i)
1
## [1] 3

可以看出來foreach會自動assign i,讓我們能夠直接把i帶入{}裡面直接執行裡面的程式,看最後一次的執行結果

另外,foreach還會提示是在哪一個task中失敗的:

1
2
3
4
5
6
tryCatch({
foreach(x = 1:3) %do% {
if (x == 2)
stop("")
}
}, error = function(e) e)
1
## <simpleError in {    if (x == 2)         stop("")}: task 2 failed - "">

上面的輸出可以看的出來就是task 2 failed,所以我們只要用x=2帶進{}裡面就可以找到問題了!

介紹到這,foreach還只是用到30%而已,這裡我要再介紹搭配同為Revolution R Analysis出的iterators來使用

iterators

一開頭介紹的*apply系列函數還有兩個沒有用foreach取代

這節就是要來用iteratos的功能來取代applytapply

要取代apply就是我們要讓foreach能一次存取一個列或是一個行

那麼就是用iter這個函數,然後藉由by這個參數來控制要算列還是行

1
2
3
m <- matrix(1:6, 3)
# input matrix, output vector/matrix/list
foreach(v = iter(m, by = "row"), .combine = c, .multicombine = TRUE) %do% sum(v)
1
## [1] 5 7 9
1
foreach(v = iter(m, by = "row"), .combine = cbind, .multicombine = TRUE) %do% (v * 2)
1
2
##      [,1] [,2] [,3] [,4] [,5] [,6]
## [1,] 2 8 4 10 6 12
1
foreach(v = iter(m, by = "row")) %do% rep(v[1], v[2])
1
2
3
4
5
6
7
8
## [[1]]
## [1] 1 1 1 1
##
## [[2]]
## [1] 2 2 2 2 2
##
## [[3]]
## [1] 3 3 3 3 3 3
1
foreach(v = iter(m, by = "column"), .combine = c, .multicombine = TRUE) %do% sum(v)
1
## [1]  6 15
1
foreach(v = iter(m, by = "column"), .combine = cbind, .multicombine = TRUE) %do% (v * 2)
1
2
3
4
##      [,1] [,2]
## [1,] 2 8
## [2,] 4 10
## [3,] 6 12
1
foreach(v = iter(m, by = "column")) %do% rep(v[1], v[2])
1
2
3
4
5
## [[1]]
## [1] 1 1
##
## [[2]]
## [1] 4 4 4 4 4

如果是array的話就用iapply這個函數來取代iter,舉例如下:

1
2
3
4
arr <- array(1:8, rep(2, 3))
foreach(v = iapply(arr, 1)) %do% {
v
} # iterate over rows
1
2
3
4
5
6
7
8
9
## [[1]]
## [,1] [,2]
## [1,] 1 5
## [2,] 3 7
##
## [[2]]
## [,1] [,2]
## [1,] 2 6
## [2,] 4 8
1
2
3
foreach(v = iapply(arr, 2)) %do% {
v
} # iterate over columns
1
2
3
4
5
6
7
8
9
## [[1]]
## [,1] [,2]
## [1,] 1 5
## [2,] 2 6
##
## [[2]]
## [,1] [,2]
## [1,] 3 7
## [2,] 4 8
1
2
3
foreach(v = iapply(arr, 3)) %do% {
v
} # iterate over slice
1
2
3
4
5
6
7
8
9
## [[1]]
## [,1] [,2]
## [1,] 1 3
## [2,] 2 4
##
## [[2]]
## [,1] [,2]
## [1,] 5 7
## [2,] 6 8
1
2
3
foreach(v = iapply(arr, 2:3)) %do% {
v
} # # iterate over all the columns of all the matrices
1
2
3
4
5
6
7
8
9
10
11
## [[1]]
## [1] 1 2
##
## [[2]]
## [1] 3 4
##
## [[3]]
## [1] 5 6
##
## [[4]]
## [1] 7 8

tapply的部分則是要引進isplit這個函數

只是要注意isplititerator跟前面使用方式不同

要用值的話,要用iteratorvalue,不能直接拿來用

它另外一個info是包含key,也就是用來分割的group value是什麼

1
str(isplit(1:6, rep(1:2, 3))$nextElem())
1
2
3
4
## List of 2
## $ value: int [1:3] 1 3 5
## $ key :List of 1
## ..$ : int 1
1
2
3
foreach(it = isplit(1:6, rep(1:2, 3)), .combine = c, .multicombine = TRUE) %do% {
sum(it$value)
}
1
## [1]  9 12
1
2
3
foreach(it = isplit(1:6, rep(1:2, 3))) %do% {
it$value + 1
}
1
2
3
4
5
## [[1]]
## [1] 2 4 6
##
## [[2]]
## [1] 3 5 7

多組group vector的話,可以這樣做:

1
2
splitList <- list(rep(1:2, 3), c(1, 1, 1, 3, 3, 3))
str(isplit(1:6, splitList)$nextElem())
1
2
3
4
5
## List of 2
## $ value: int [1:2] 1 3
## $ key :List of 2
## ..$ : int 1
## ..$ : num 1
1
2
3
foreach(it = isplit(1:6, splitList), .combine = c, .multicombine = TRUE) %do% {
sum(it$value)
}
1
## [1]  4  2  5 10
1
2
3
foreach(it = isplit(1:6, splitList)) %do% {
it$value + 1
}
1
2
3
4
5
6
7
8
9
10
11
## [[1]]
## [1] 2 4
##
## [[2]]
## [1] 3
##
## [[3]]
## [1] 6
##
## [[4]]
## [1] 5 7

但是isplit除了搭配vector之外,也可以搭配data.frame來使用:

1
2
3
4
5
DF <- data.frame(x = rep(1:3, 3), y = c(rep(1:2, 4), 3))
foreach(it = isplit(DF, with(DF, list(x = x, y = y)))) %do% {
print(it$key)
nrow(it$value)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
## $x
## [1] 1
##
## $y
## [1] 1
##
## $x
## [1] 2
##
## $y
## [1] 1
##
## $x
## [1] 3
##
## $y
## [1] 1
##
## $x
## [1] 1
##
## $y
## [1] 2
##
## $x
## [1] 2
##
## $y
## [1] 2
##
## $x
## [1] 3
##
## $y
## [1] 2
##
## $x
## [1] 1
##
## $y
## [1] 3
##
## $x
## [1] 2
##
## $y
## [1] 3
##
## $x
## [1] 3
##
## $y
## [1] 3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
## [[1]]
## [1] 2
##
## [[2]]
## [1] 1
##
## [[3]]
## [1] 1
##
## [[4]]
## [1] 1
##
## [[5]]
## [1] 2
##
## [[6]]
## [1] 1
##
## [[7]]
## [1] 0
##
## [[8]]
## [1] 0
##
## [[9]]
## [1] 1

這裡需要注意的是isplit不是現有組合有存在才會取值

而是將你輸入的group vector分別取unique後做expand.grid的動作展開

所以會有組合是沒有資料的,要記得在script裡面寫如果零列要怎麼處理:

1
2
3
4
5
6
DF <- data.frame(x = rep(1:3, 3), y = c(rep(1:2, 4), 3))
foreach(it = isplit(DF, with(DF, list(x = x, y = y))), .combine = rbind, .multicombine = TRUE) %do% {
if (nrow(it$value) == 0)
return(NULL)
data.frame(it$key, sum(it$value))
}
1
2
3
4
5
6
7
8
##   x y sum.it.value.
## 1 1 1 4
## 2 2 1 3
## 3 3 1 4
## 4 1 2 3
## 5 2 2 8
## 6 3 2 5
## 7 3 3 6