這裡介紹 R 的幾種進階迴圈使用方式,善用這些 R 特有的迴圈技巧可以讓程式碼更簡潔。
R 語言除了提供一般性的 repeat
、while
與 for
迴圈之外,還有許多進階的迴圈使用方式,它可以讓您將特定的函數套用至列表、向量或陣列中的每一個元素,進行特定的運算後,傳回所有元素個別運算的結果。
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
系列的函數,使用這類的函數雖然在執行效能上不會改變,但是至少可以讓程式碼比較整潔。
*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
函數,它介於 lapply
與 vapply
之間,其使用的方式跟 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
Page: 1 2