這裡介紹 R 的幾種進階迴圈使用方式,善用這些 R 特有的迴圈技巧可以讓程式碼更簡潔。

R 語言除了提供一般性的 repeatwhilefor 迴圈之外,還有許多進階的迴圈使用方式,它可以讓您將特定的函數套用至列表、向量或陣列中的每一個元素,進行特定的運算後,傳回所有元素個別運算的結果。

replicate 函數

replicate 函數跟 rep 函數類似,但 rep 只是單純將輸入的值重複指定的次數,而 replicate 則是會對指定的運算式重複執行指定的次數。在大多數的情況之下,這兩個函數的作用是相同的:

rep(1.2, 3)
[1] 1.2 1.2 1.2
replicate(3, 1.2)
[1] 1.2 1.2 1.2

但如果遇到含有隨機變數的運算式時,就會有很大的差異,例如:

rep(rnorm(1), 3)
[1] -0.7170305 -0.7170305 -0.7170305
replicate(3, rnorm(1))
[1] -0.5230300  0.2188132  0.5704128

replicate 函數主要用於固定計算次數的蒙地卡羅(Monte Carlo)運算,也就是每一次的迭代運算都是完全獨立的狀況。

範例 1
以下是一個模擬通車時間的小程式,我們使用不同的隨機變數分佈,模擬搭乘不同交通工具所需要的時間。

traffic.time <- function() {
  # 選擇交通工具
  transportation <- sample(
    c("car", "bus", "train", "bike"),
    size = 1,
    prob = c(0.2, 0.3, 0.3, 0.2)
  )
  # 模擬通車時間
  time <- switch(
    transportation,
    car = rlnorm(1, log(30), 0.5),
    bus = rlnorm(1, log(40), 0.5),
    train = rnorm(1, 30, 10),
    bike = rnorm(1, 70, 5)
  )
  names(time) <- transportation
  time
}

這樣的程式結構中因為含有 switch 判斷式,所以比較難使用一般的向量化寫法,也就是說我們每一次要模擬通車時間的時候,就需要執行一次 traffic.time 函數,這樣的情況就可以使用 replicate 函數來進行模擬:

replicate(10, traffic.time())
    bike    train    train      car    train 
72.09280 38.57392 16.74473 18.24996 55.33025 
   train      bus     bike      bus     bike 
17.94697 13.19669 77.62744 49.96687 79.84052

範例 2
以下是另外一個簡單的 bootstrap 範例,使用重複的抽樣來計算母體平均數的 95% 的信賴區間模擬值,首先產生常態分配的樣本:

rn <- rnorm(1000, 10)

接著使用 replicate 函數重複取樣,然後計算母體平均數的 95% 的信賴區間的模擬值:

quantile(replicate(1000,
  mean(sample(rn, replace = TRUE))),
  probs = c(0.025, 0.975))
     2.5%     97.5% 
 9.952728 10.079374

我們可以拿標準的 95% 的信賴區間來跟模擬值比較:

t.test(rn)$conf.int
[1]  9.948975 10.074432
attr(,"conf.level")
[1] 0.95

列表的迭代

以 R 語言撰寫程式時,應該盡可能使用向量化運算的方式來處理各種重複性的運算,這樣除了可讓程式碼更容易閱讀之外,執行速度也會比一般性的迴圈高出許多。

然而並非所有的程式邏輯都可以很直接的使用向量化運算來處理,若遇到無法修改成向量化程式碼的情況時,可以改用 *apply 系列的函數,使用這類的函數雖然在執行效能上不會改變,但是至少可以讓程式碼比較整潔。

一般 R 語言的向量化運算是屬於 C 語言層級迴圈,執行速度較快,而 *apply 系列的函數是屬於 R 語言層級的迴圈,執行速度接近一般的 R 迴圈。

*apply 系列的函數中,最常被使用的就是 lapply 函數(list apply),它可以接受一個列表變數以及一個函數,然後將列表變數中的每個元素一一交給該函數處理,最後傳回所有的結果所組成的列表。

假設我們有一個列表的資料如下:

# 產生資料
x.list <- list(
  a = rgeom(6, prob = 0.1),
  b = rgeom(6, prob = 0.4),
  c = rgeom(6, prob = 0.7)
)
x.list
$a
[1]  1  2  2  9 10  1

$b
[1] 4 5 0 2 2 2

$c
[1] 1 0 0 0 2 0

若想要將此列表中每個元素都交由 unique 處理,刪除重複的數值,使用一般迴圈的做法會類似下面這樣:

# 初始化 x.uniq
x.uniq <- vector("list", length(x.list))
for ( i in seq_along(x.list) ) {
  # 對 x.list 的每個元素進行 unique 運算
  x.uniq[[i]] <- unique(x.list[[i]])
}
# 設定 x.uniq 的元素名稱
names(x.uniq) <- names(x.list)
x.uniq
$a
[1]  1  2  9 10

$b
[1] 4 5 0 2

$c
[1] 1 0 2

像這樣的動作無法直接使用向量化的寫法來處理,不過我們可以改用 lapply,讓程式碼比較乾淨一些:

# 改用 lapply
lapply(x.list, unique)
$a
[1]  1  2  9 10

$b
[1] 4 5 0 2

$c
[1] 1 0 2

lapply 所傳回的結果也是一個列表變數,這樣的好處是允許每個元素的計算結果都是長度不同的向量(或是其他各種類型的變數)。

如果每個元素的計算結果都是長度相同的向量,可以使用 vapply 函數,它的功能跟 lapply 相同,只是會以向量的方式傳回結果:

vapply(x.list, length, numeric(1))
a b c 
6 6 6

這裡的第三個參數是傳回值的樣板,vapply 會將計算的結果依照這個樣板傳回。以下是使用 fivenum 計算每個元素的 Tukey’s five number summary:

vapply(x.list, fivenum,
  c("Min." = 0, "1st Qu." = 0, "Median" = 0,
  "3rd Qu." = 0, "Max." = 0))
         a b c
Min.     1 0 0
1st Qu.  1 2 0
Median   2 2 0
3rd Qu.  9 4 1
Max.    10 5 2

另外還有一個 sapply 函數,它介於 lapplyvapply 之間,其使用的方式跟 lapply 相同:

sapply(x.list, unique)
$a
[1]  1  2  9 10

$b
[1] 4 5 0 2

$c
[1] 1 0 2

sapply 會嘗試簡化傳回的變數,在情況許可時會自動將結果轉為向量的形式傳回:

sapply(x.list, length)
a b c 
6 6 6

對於較高維度的資料,sapply 也可以自動處理:

sapply(x.list, summary)
             a   b    c
Min.     1.000 0.0 0.00
1st Qu.  1.250 2.0 0.00
Median   2.000 2.0 0.00
Mean     4.167 2.5 0.50
3rd Qu.  7.250 3.5 0.75
Max.    10.000 5.0 2.00

對於使用互動式操作來使用 R 的狀況來說,使用 sapply 函數會比較方便,就算使用者不確定執行結果會是什麼,它通常都可以自動將結果以最適合的方式呈現。

雖然 *apply 系列的函數主要是用來處理列表變數的,但它也可以接受一般的向量,而其對於向量的處理方式也是跟列表類似,逐一將元素交給指定的函數來處理。

source 這個函數可以從檔案中載入 R 的程式碼並且執行,但這個函數沒有支援向量化的運算,如果要一次載入多個 .R 指令稿,可以配合 lapply 一起使用:

r.files <- dir(pattern = "\\.R$")
lapply(r.files, source)

這裡我們使用 dir 指令加上正規表示法,取得所有檔名為 .R 結尾的指令稿,接著使用 lapply 逐一載入每個指令稿。

使用 *apply 系列的函數時,若需要傳遞一些額外的參數給指定的函數,可以將具名參數放在最後面,這樣該參數就會自動被傳入:

lapply(x.list, quantile, probs = 1:3/4)
$a
 25%  50%  75% 
1.25 2.00 7.25 

$b
25% 50% 75% 
2.0 2.0 3.5 

$c
 25%  50%  75% 
0.00 0.00 0.75

由於 *apply 系列的函數只會將列表或向量中的元素逐一取出,放在指定函數的第一個參數來執行,若遇到指定函數的輸入資料不是放在第一個參數時,就要改以自訂函數的方式處理:

x <- 1:3
my.seq <- function(by) seq(2, 10, by = by)
lapply(x, my.seq)
[[1]]
[1]  2  3  4  5  6  7  8  9 10

[[2]]
[1]  2  4  6  8 10

[[3]]
[1] 2 5 8

也可以使用匿名函數的寫法:

lapply(x, function(by) seq(2, 10, by = by))
[[1]]
[1]  2  3  4  5  6  7  8  9 10

[[2]]
[1]  2  4  6  8 10

[[3]]
[1] 2 5 8

如果需要對環境空間中的每一個變數做處理時,可以使用 eapply 函數:

my.env <- new.env()
my.env$foo <- 1:5
my.env$larry <- runif(8)
eapply(my.env, length)
$foo
[1] 5

$larry
[1] 8