這裡介紹 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)運算,也就是每一次的迭代運算都是完全獨立的狀況。

範例一

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

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

範例二

以下是另外一個簡單的 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

陣列的迭代

lapplyvapply 以及 sapply 這幾個函數也可以用在矩陣與陣列的迭代上,不過這樣使用的結果通常不如使用者所預期,它會將矩陣或陣列直接視為一般的向量,將每個元素逐一交給指定的函數來處理。

x.mat <- matrix(1:9, 3, 3)
lapply(x.mat, sum)
[[1]]
[1] 1

[[2]]
[1] 2

[[3]]
[1] 3

[[4]]
[1] 4

[[5]]
[1] 5

[[6]]
[1] 6

[[7]]
[1] 7

[[8]]
[1] 8

[[9]]
[1] 9

若要對矩陣的一整個行(column)或列(row)來處理,可以使用 matlab 這個套件:

install.packages("matlab")
library(matlab)

R 的 matlab 套件中包含一些運作方式類似 Matlab 的函數,而該套件在載入之後,會將 basestatsutils 內建套件中的一些函數覆蓋掉,在使用完畢之後,若要讓這些函數恢復,可以執行 detach("package:matlab")matlab 卸載即可。

matlab 套件所提供的 apply 函數其功能類似 lapply,不過它可以允許對矩陣的行或列進行指定的運算,例如計算矩陣每個列的總和:

apply(x.mat, 1, sum)
[1] 12 15 18

apply 的第一個參數是輸入的矩陣或陣列,第二個參數是指定如何迭代矩陣或陣列中的資料,若指定為 1 則代表以列(row)的方式迭代,而若指定為 2 則代表以行(column)的方式迭代。而上面這行效果就等同於 rowSums

rowSums(x.mat)
[1] 12 15 18

matlabapply 函數可以跟任意的函數結合,產生各式各樣的變化:

apply(x.mat, 2, summary)
        [,1] [,2] [,3]
Min.     1.0  4.0  7.0
1st Qu.  1.5  4.5  7.5
Median   2.0  5.0  8.0
Mean     2.0  5.0  8.0
3rd Qu.  2.5  5.5  8.5
Max.     3.0  6.0  9.0

apply 也可以套用在 data frame 上:

(x.df <- data.frame(
  name = c("foo", "bar", "foo.bar"),
  value = c(1.2, 6.9, 2.4)
))
     name value
1     foo   1.2
2     bar   6.9
3 foo.bar   2.4
apply(x.df, 1, toString)
[1] "foo, 1.2"     "bar, 6.9"     "foo.bar, 2.4"
apply(x.df, 2, toString)
               name               value
"foo, bar, foo.bar"     "1.2, 6.9, 2.4"

apply 以行的方式迭代時,其作用會跟 sapply 相同(data frame 可以視為一種巢狀的列表,而其每個列表元素的長度都相等):

sapply(x.df, toString)
               name               value
"foo, bar, foo.bar"     "1.2, 6.9, 2.4"

mapplyVectorize 函數

lapply 有一個缺點就是它一次只能對一個列表變數的元素做迭代處理,另外在指定的處理函數當中,也無法獲取每個列表元素的名稱。

mapply 是一個可以同時處理多個列表變數迭代的函數,而若需要取得列表元素的名稱,也可以透過第二個參數把名稱傳入。

x.list <- list(
  foo = 12,
  bar = 34
)
my.fun <- function(name, value) {
  paste(name, "is", value)
}
mapply(my.fun, names(x.list), x.list)
        foo         bar
"foo is 12" "bar is 34"

mapply 預設會跟 sapply 函數一樣將傳回值簡化,若要改變這個行為,可以加上 SIMPLIFY = FALSE 參數。

Vectorize 是一個可以將非向量化函數轉換為向量化寫法的函數(但效能不會特別的改善),假設我們有一個無法以向量化表示的函數:

scalar.fun <- function(x) {
  switch(x, foo = "FOO", bar = "BAR", "Other")
}

這個函數若直接傳入一個向量參數,會產生錯誤:

input <- c("foo", "bar", "foo.bar")
scalar.fun(input)
Error in switch(x, foo = "FOO", bar = "BAR", "Other") :
  EXPR 必須是長度為 1 的向量

這種狀況就可以使用 Vectorize 將其包裝成可以處理向量的形式:

vectorize.fun <- Vectorize(scalar.fun)
vectorize.fun(input)
foo     bar foo.bar
  "FOO"   "BAR" "Other"

資料分組與迭代

在審視資料時,時常會需要將資料先分組後再計算各組的某些統計量,以 iris 資料為例:

head(iris)
  Sepal.Length Sepal.Width Petal.Length Petal.Width Species
1          5.1         3.5          1.4         0.2  setosa
2          4.9         3.0          1.4         0.2  setosa
3          4.7         3.2          1.3         0.2  setosa
4          4.6         3.1          1.5         0.2  setosa
5          5.0         3.6          1.4         0.2  setosa
6          5.4         3.9          1.7         0.4  setosa

假設我們要依據 Species 來分組,計算 Sepal.Length 的平均,首先將資料分組:

(group.data <- with(iris, split(Sepal.Length, Species)))
$setosa
 [1] 5.1 4.9 4.7 4.6 5.0 5.4 4.6 5.0 4.4 4.9 5.4 4.8
[13] 4.8 4.3 5.8 5.7 5.4 5.1 5.7 5.1 5.4 5.1 4.6 5.1
[25] 4.8 5.0 5.0 5.2 5.2 4.7 4.8 5.4 5.2 5.5 4.9 5.0
[37] 5.5 4.9 4.4 5.1 5.0 4.5 4.4 5.0 5.1 4.8 5.1 4.6
[49] 5.3 5.0

$versicolor
 [1] 7.0 6.4 6.9 5.5 6.5 5.7 6.3 4.9 6.6 5.2 5.0 5.9
[13] 6.0 6.1 5.6 6.7 5.6 5.8 6.2 5.6 5.9 6.1 6.3 6.1
[25] 6.4 6.6 6.8 6.7 6.0 5.7 5.5 5.5 5.8 6.0 5.4 6.0
[37] 6.7 6.3 5.6 5.5 5.5 6.1 5.8 5.0 5.6 5.7 5.7 6.2
[49] 5.1 5.7

$virginica
 [1] 6.3 5.8 7.1 6.3 6.5 7.6 4.9 7.3 6.7 7.2 6.5 6.4
[13] 6.8 5.7 5.8 6.4 6.5 7.7 7.7 6.0 6.9 5.6 7.7 6.3
[25] 6.7 7.2 6.2 6.1 6.4 7.2 7.4 7.9 6.4 6.3 6.1 7.7
[37] 6.3 6.4 6.0 6.9 6.7 6.9 5.8 6.8 6.7 6.7 6.3 6.5
[49] 6.2 5.9

接著再以 mean 配合 lapply 來處理:

lapply(group.data, mean)
$setosa
[1] 5.006

$versicolor
[1] 5.936

$virginica
[1] 6.588

或是使用 sapply 等函數亦可:

sapply(group.data, mean)
    setosa versicolor  virginica
     5.006      5.936      6.588

像這樣典型的資料分組與迭代的動作,可以使用 tapply 函數來處理:

with(iris, tapply(Sepal.Length, Species, mean))
    setosa versicolor  virginica
     5.006      5.936      6.588

plyr 套件

R 內建的 *apply 函數對於許多的迭代問題而言是一個很好用的工具,不過它們還是有一些小缺點,像函數的名稱並沒有清楚表明其作用(例如 tapplyt 到底代表什麼意思我們也不是很清楚),而不同函數的參數順序也沒有一致性,另外其傳回值的變數型態並沒有很彈性,造成有時候在使用上會不太方便。

plyr 套件提供了一系列完整的 **ply 函數,不僅參數格式統一,輸入與輸出的變數型態也很齊全,它的函數名稱有一定的規則,第一個字母代表輸入的變數型態,而第二個字幕則代表輸出的變數型態,例如 llply 函數就是輸入與輸出都是列表變數(list),所以它可以直接用來替代 lapply 函數:

x.list <- list(
  a = rgeom(6, prob = 0.1),
  b = rgeom(6, prob = 0.4),
  c = rgeom(6, prob = 0.7)
)
llply(x.list, unique)
$a
[1]  2 13  1  3  8

$b
[1] 1 2 0

$c
[1] 0 1

laply 則代表輸入變數為列表,而輸出為向量:

laply(x.list, length)
[1] 6 6 6

raplyreplicate 的一個替代函數,傳回值是一般的向量,而 rlplyrdply 函數則可以將重複的結果以列表或 data frame 的形式傳回,另外 r_ply 會直接將結果丟棄,不傳回任何東西(適用於繪圖等情況)。

raply(3, rnorm(1))
[1]  0.08807394 -1.17962037  2.76096399
rlply(3, rnorm(1))
[[1]]
[1] -1.242925

[[2]]
[1] 0.3688085

[[3]]
[1] 0.6099664
rdply(3, rnorm(1))
  .n         V1
1  1  0.6272091
2  2 -0.3050538
3  3 -0.6814648
r_ply(3, rnorm(1))

ddply 的輸入與輸出都是 data frame 變數,而且可以同時處理多個欄位的資料,可以用來替代 tapply 函數。假設我們要將 iris 這個 data frame 的所有資料都依照 Species 分組後計算平均值,可以這樣做:

ddply(
  iris, # 輸入的 data frame
  .(Species), # 依據 Species 分組
  colwise(mean) # 對每個行(column)執行 mean 計算平均值
)
     Species Sepal.Length Sepal.Width Petal.Length Petal.Width
1     setosa        5.006       3.428        1.462       0.246
2 versicolor        5.936       2.770        4.260       1.326
3  virginica        6.588       2.974        5.552       2.026

這裡使用 colwise 會自動對每一個欄位(除了第二個參數有指定的欄位之外)做指定的運算,不過它的限制就是每個欄位的計算方式都相同。

第二個參數中所使用的句點函數(.)是 plyr 套件中所定義的一個特殊函數,其作用類似 ~,是為了取得變數名稱而設計的,沒有特別的含義,以下幾種寫法的作用都是相同的:

ddply(iris, .(Species), colwise(mean))
ddply(iris, "Species", colwise(mean))
ddply(iris, ~ Species, colwise(mean))

我們也可以使用 summarize 的方式,自行指定要計算的欄位以及各欄位的計算方式:

ddply(
  iris, # 輸入的 data frame
  .(Species), # 依據 Species 分組
  summarize, # 自行指定每個欄位的計算方式
  SL.Mean = mean(Sepal.Length), # Sepal.Length 的平均值
  PL.Max = max(Petal.Length) # Petal.Length 的最大值
)
     Species SL.Mean PL.Max
1     setosa   5.006    1.9
2 versicolor   5.936    5.1
3  virginica   6.588    6.9