陣列的迭代

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