迴圈是程式碼中一種常見的控制流程。指的是一段在程式中只出現一次,但隨著條件的設置,會連續執行特定次數、針對某一集合中的所有項目執行一次,或是在達到特定條件時結束運算的一段程式碼。常見的有 for 迴圈 (for loop)、while 迴圈 (while loop)。這一篇介紹的是 for 迴圈,還有將 for 迴圈改寫成 apply 系列函數中的 lapply。
假設今天想要分別印出 1 到 10 的平方,在 R 裡面要計算平方的程式碼為 x^2
,最土法煉鋼的寫法是像下面這樣,將要重複執行的程式碼複製貼上 10 次,然後將 1 到 10 分別帶入 x 的位置。
print(c(1^2,
2^2,
3^2,
4^2,
5^2,
6^2,
7^2,
8^2,
9^2,
10^2))
## [1] 1 4 9 16 25 36 49 64 81 100
但如果今天是要計算 1 到 1000,甚至是到更大的數字的平方呢?像上面那樣複製程式碼的做法就顯的有些不切實際。當我們要重複做的事情有一定的規則可循,像是要針對一個變數中的每一項目去操作,或是同樣的事情要重複執行一定次數,就可透過 for 迴圈來簡化程式碼。
for 迴圈的架構像這樣:
for (i in x){
# 每次迭代要執行的程式
}
其中 i 告訴 R 在每一次迭代(iteration)中要改變的地方在哪裡,x 是每一次的迭代在執行時的實際項目,在第一次的迭代中的 i 是 x 的第一項 (x[1]);第二次的迭代中的 i 是 x 的第二項 (x[2]);以此類推,然後在每一次迭代時,都會執行大括號 {} 裡面的程式,x 有多少個項目這段迴圈就會執行多少次。
像上面平方的例子用 for 迴圈可以改寫成這樣…
for (i in 1:10){
print(i^2)
}
## [1] 1
## [1] 4
## [1] 9
## [1] 16
## [1] 25
## [1] 36
## [1] 49
## [1] 64
## [1] 81
## [1] 100
x 是 1 到 10 的字串,然後在後面的程式碼中,透過 i 來指定每一次迭代要改變的是計算平方的數字。
但是在 R 語言中,其實要盡量避免使用 for 迴圈,理由是 for 迴圈的效率差,當處理的資料量或次數達到一定程度時,很容易拖慢整體的運算速度。為了提升效率,在 R 語言的撰寫上會盡可能使用向量化 (vectorize) 運算的方式來處理各種重複性的運算,除了可讓程式碼更容易閱讀之外 (不過理解又是另外一回事XD),執行速度也會比一般性的迴圈高出許多。
簡單的向量化運算概念是像下面這樣…
今天我們有 100 萬個人的身高跟體重,想計算這些人的 BMI 指數
我們先產生身高體重的資料
# 用 runif 隨機產生 100 萬人的身高 (單位:公分)
height <- runif(1000000)*40 + 165
# 用 runif 隨機產生 100 萬人的體重 (單位:公斤)
weight <- runif(1000000)*30 + 60
# 合併成data.frame
data <- data.frame(height = height,
weight = weight)
# 先指定一個list來存放等一下計算出來的BMI
BMI <- list()
# 對資料表當中的每一列(每一個人)執行BMI的運算
for (i in 1:nrow(data)){
BMI[i] <- data$weight[i] / ((data$height[i]/100)^2)
}
只要一行程式碼就可以搞定啦!
BMI <- weight / ((height/100)^2)
然而不是所有的程式邏輯都可以很直接的使用向量化運算來處理,當遇到無法修改成向量化程式碼的情況時,則會建議用 apply 家族來處理,apply 家族在本質上仍然是迴圈,不過畢竟是 R 本身內建的函數,所以使用上效能還是會比 for 迴圈來的好,而且 lapply 還有別人改寫好的平行運算套件,真正遇到一次會跑很久然後也需要進行多次的運算,就可以開啟多核心,開外掛來處理啦!
上面的範例如果用 apply 家族中的 lapply()
可以改寫成這樣
BMI <-
lapply(1:nrow(data), function(i)
data$weight[i] / ((data$height[i]/100)^2))
在 lapply()
中,1:nrow(data)
指的是針對 1:row(data) 中的每一項都分別進行運算,而在執行的運算中,跟 for 迴圈一樣用 i 來指定要改變的項目位置在哪裡 (i 可以改成其他字母)。詳細的 lapply 介紹請見 取代迴圈的 apply 家族。
system.time()
來比較三種方法的運算效能。# 1. 使用 for 迴圈
system.time({
BMI <- list()
for (i in 1:nrow(data)){
BMI[i] <- data$weight[i] / ((data$height[i]/100)^2)
}
})
## user system elapsed
## 17.31 0.05 17.55
# 2. 使用向量化運算
system.time(
bmi <- weight / ((height/100)^2)
)
## user system elapsed
## 0 0 0
# 3. 使用 lapply
system.time(
bmi.L <- lapply(1:nrow(data), function(i)
data$weight[i] / ((data$height[i]/100)^2))
)
## user system elapsed
## 14.60 0.00 14.63
從總耗時 (elapsed) 來看可以發現在效能上,向量化運算 >> lapply > for 迴圈。
所以以後要寫重複執行程式的時候,不能改寫成向量運算的話建議至少也要用 lapply 來寫,只要能寫成迴圈的程式碼就一定可以改寫成 lapply!寫成 lapply 的好處是,如果覺得效率不夠高還可以改寫成平行運算來加速~~
# 先指定一個list來存放等一下讀出來的檔案
data <- list()
# 用list.file列出D:/filepath資料夾內csv檔的檔案路徑
file.name <- list.files("D:/filepath",
pattern = ".csv",
full.names = TRUE)
# 用for迴圈來讀檔案
for (i in 1:length(file.name){
data[[i]] <- fread(file.name[i])
})
# 用list.file列出D:/filepath資料夾內csv檔的檔案路徑
file.name <- list.files("D:/filepath",
pattern = ".csv",
full.names = TRUE)
# 用lapply來讀檔案
data <- lapply(1:length(file.name), function(i)
fread(file.name[i]))
# 也可以改寫成這樣
data <- lapply(file.name, function(i)
fread(i))
lapply 也可以處理多個步驟的 function~
以上步驟重複執行1000次。
b.coef <- list()
for (i in 1:1000){ # 1:1000 = 執行1000次
# 隨機取樣
boot.data <-
dataset[, .SD[sample(.N, min(.N,200), replace = FALSE)],
by = list(ZONE8, Year)]
# 用boot.data來跑GLMM
boot.logit <-
glmer(Count.F ~ Year.s + (0 + Year.s|ZONE8),
data = boot.data,
family = poisson)
# 輸出斜率與截距,輸出到 b.coef 裡面
b.coef[[i]] <- fixef(boot.logit)
}
b.coef <- do.call(rbind, b.coef) %>% data.table
b.coef <- lapply(1:1000, function(i){ # 1:1000 = 執行1000次
# 隨機取樣
boot.data <-
dataset[, .SD[sample(.N, min(.N,200), replace = FALSE)],
by = list(ZONE8, Year)]
# 用boot.data來跑GLMM
boot.logit <-
glmer(Count.F ~ Year.s + (0 + Year.s|ZONE8),
data = boot.data,
family = poisson)
# 輸出斜率與截距,在lapply中要用 return() 來指定要回傳的變數
return(fixef(boot.logit))
}) %>%
do.call(rbind, .) %>%
data.table