撰寫 R 語言函數:Learn from the Wickhams

To understand computations in R: everything that happens is a function call.
John Chambers

R 語言是根基於 S 語言的開源軟體計畫,John Chambers 身為 S 語言之父以及 R 語言的核心開發成員,利用他描述 R 語言函數編程的這項特質作為我們的第一個印象再適切不過了。

R, at its heart, is a functional programming (FP) language.
Hadley Wickham

又或者引用將 R 語言推向資料科學高峰的 Hadley Wickham,在其知名著作 The Advanced R 一書中提到 R 語言本質就是一個函數編程程式語言,相信撰寫函數必定是在 R 語言的學習之旅中不能略過的一站。

Photo by DocChewbacca on Visual Hunt


Writing Functions in R

DataCamp 邀請到 Hadley 與 Charlotte 姊弟共同來教授 Writing Functions in R 課程,這也是 DataCamp 課程中唯一有 Hadley 親自來指導教學的內容,是粉絲朝聖的首選;課程時數約四小時,完成這個課程使用者將會對 R 語言的自訂函數有更深一層的理解,這門課程並不適合初學者修習,適合已經暸解變數類型、資料結構、流程控制與迴圈等 Base R 觀念的使用者修習。

既然函數這麼重要,究竟撰寫函數能夠在什麼時候派上用場?Hadley 在課程中有一個很清晰的指引:

If you have copy-and-pasted twice, it’s time to write a function.

假如使用者發現自己開始複製貼上自己的程式碼兩次,就應該要停下來並改寫為函數(這個定義太美,粉絲模式不自覺地開啟!)

課程所舉的範例是一個將資料數值標準化的情境,這是很常見當數值單位不同時所採取的資料預處理。假如我們要將內建資料 iris 的四個數值變數標準化(Min Max Scale 方法):

iris_std <- iris
iris_std$Sepal.Length <- (iris_std$Sepal.Length - min(iris_std$Sepal.Length, na.rm = TRUE)) / (max(iris_std$Sepal.Length, na.rm = TRUE) - min(iris_std$Sepal.Length, na.rm = TRUE))
# 複製貼上第一次
iris_std$Sepal.Width <- (iris_std$Sepal.Width - min(iris_std$Sepal.Width, na.rm = TRUE)) / (max(iris_std$Sepal.Width, na.rm = TRUE) - min(iris_std$Sepal.Width, na.rm = TRUE))
# 複製貼上第二次
iris_std$Petal.Length <- (iris_std$Petal.Length - min(iris_std$Petal.Length, na.rm = TRUE)) / (max(iris_std$Petal.Length, na.rm = TRUE) - min(iris_std$Petal.Length, na.rm = TRUE))
# 該停下來囉...

複製貼上修改變數名稱兩次之後,就意識到必須停下來改寫為函數,首先撰寫可以將輸入向量標準化的函數 min_max_scale()

# 宣告函數
min_max_scale <- function(x) {
x_min <- min(x, na.rm = TRUE)
x_max <- max(x, na.rm = TRUE)
output <- (x - x_min) / (x_max - x_min)
return(output)
}

接著呼叫函數將前兩個數值變數 Sepal.Length 與 Sepal.Width 標準化:

# 宣告函數
min_max_scale <- function(x) {
x_min <- min(x, na.rm = TRUE)
x_max <- max(x, na.rm = TRUE)
output <- (x - x_min) / (x_max - x_min)
return(output)
}
# 呼叫函數
iris_std <- iris
iris_std$Sepal.Length <- min_max_scale(iris_std$Sepal.Length)
iris_std$Sepal.Width <- min_max_scale(iris_std$Sepal.Width)
View(iris_std)
將前兩個數值變數標準化

有四個數值變數要標準化,勢必也不能複製貼上三次,因此改寫成一個迴圈:

# 宣告函數
min_max_scale <- function(x) {
x_min <- min(x, na.rm = TRUE)
x_max <- max(x, na.rm = TRUE)
output <- (x - x_min) / (x_max - x_min)
return(output)
}
# 呼叫函數
iris_std <- iris
for (i in 1:4) {
iris_std[, i] <- min_max_scale(iris_std[, i])
}

除了使用迴圈將 min_max_scale() 函數應用到 iris_std 以外,是否還有其他的方法能夠完成呢?我們可以透過 lapply() 或者 purrr 套件中的 map() 這兩個函數都能夠將完成標準化的向量儲存在 list 中回傳:

library(purrr)
# 宣告函數
min_max_scale <- function(x) {
x_min <- min(x, na.rm = TRUE)
x_max <- max(x, na.rm = TRUE)
output <- (x - x_min) / (x_max - x_min)
return(output)
}
# 呼叫函數
lapply(iris[, 1:4], FUN = min_max_scale)
map(iris[, 1:4], .f = min_max_scale)
將完成標準化的向量儲存在 list 中回傳

purrr 套件

purrr 套件是 Hadley 開發的套件,目的是盡可能地減少 for 迴圈並提供函數編程更完善的支援,同時亦提供了 apply 函數家族以外的另一個選擇,套件的入門函數除了我們剛才使用過的 map() 外尚有特別用來處理圖形的 walk()。像是我們想將內建資料 iris 的四個數值變數繪畫出直方圖觀察分布的情況,以迴圈可以這樣實作:

par(mfrow = c(2, 2))
plot_title <- "Hist of"
iris_vars <- names(iris)[-5]
plot_titles <- paste(plot_title, iris_vars)
for (i in 1:4) {
hist(iris[, i], xlab = "", col = rgb(1, 0, 0, 0.4), main = plot_titles[i])
}
用迴圈繪畫出直方圖觀察四個數值變數分布

walk() 函數實作則是這樣撰寫:

library(purrr)
walk(iris[, 1:4], .f = hist, col = rgb(1, 0, 0, 0.4), xlab = "")
用 walk() 繪畫出直方圖觀察四個數值變數分布

接著我們用 pwalk() 來處理難看的標題並且依照變數上不同的顏色,注意要將資料、標題參數以及顏色參數用一個 list 包括起來:

plot_title <- "Hist of"
iris_vars <- names(iris)[-5]
plot_titles <- paste(plot_title, iris_vars)
hist_colors <- c(
rgb(1, 0, 0, 0.4),
rgb(0, 1, 0, 0.4),
rgb(0, 0, 1, 0.4),
rgb(1, 0.5, 0, 0.4)
)
pwalk(list(iris[, 1:4], main = plot_titles, col = hist_colors), .f = hist, xlab = "")
用 pwalk() 處理標題與上色

例外處理

min_max_scale() 函數只能用來處理數值向量的輸入,假如在前述應用時候沒有將內建資料 irisSpecies 變數排除,會出現 min not meaningful for factors 的錯誤,因為對於因素向量(factors)來說計算最小值是沒有意義的:

# 宣告函數
min_max_scale <- function(x) {
x_min <- min(x, na.rm = TRUE)
x_max <- max(x, na.rm = TRUE)
output <- (x - x_min) / (x_max - x_min)
return(output)
}
# 呼叫函數
lapply(iris, FUN = min_max_scale)
map(iris, .f = min_max_scale)
對因素向量計算最小值沒有意義

面對這種情境我們可以採用兩種方法來處理,一是在函數宣告的主體中加入 tryCatch() 函數:

# 宣告函數
min_max_scale <- function(x) {
tryCatch({
x_min <- min(x, na.rm = TRUE)
x_max <- max(x, na.rm = TRUE)
output <- (x - x_min) / (x_max - x_min)
return(output)
}, error = function(e){
return("x 必須為數值型變數")
})
}
# 呼叫函數
lapply(iris, FUN = min_max_scale)
map(iris, .f = min_max_scale)
使用 tryCatch() 針對錯誤發生時回傳客製訊息

另一種處理方法是透過 purrr 套件提供的 safely() 函數,它會為我們原本的自訂函數做好例外處理:

library(purrr)
# 宣告函數
min_max_scale <- function(x) {
x_min <- min(x, na.rm = TRUE)
x_max <- max(x, na.rm = TRUE)
output <- (x - x_min) / (x_max - x_min)
return(output)
}
# 呼叫 safely() 為函數加上例外處理
safe_min_max_scale <- safely(min_max_scale)
# 呼叫函數
lapply(iris, FUN = safe_min_max_scale)
map(iris, .f = safe_min_max_scale)
Species 變數類型導致的錯誤並沒有中斷函數呼叫

幾個注意事項

課程中 Hadley 與 Charlotte 還特別叮嚀了幾個撰寫函數時的注意事項:

  • 先用慣常的方式解決問題,接著改為撰寫函數再解決一次
  • 函數命名使用動詞並以底線分隔多個單字,務必讓使用者容易理解
  • 參數命名使用名詞,先擺放資料參數,再放置細節參數,細節參數記得要給定預設值
  • 暸解 R 語言的編程風格
  • 撰寫函數的首要目的是解決我們的問題,而不是漂亮簡潔的程式碼,不要因為使用 for 迴圈而感到不開心
  • 短期試著先將問題中較簡單的 80% 用函數解決,這時會顯得吃力費時
  • 長期就能夠將問題中的 99% 用函數解決,這時會顯得輕鬆快捷

延伸閱讀

Like what you read? Give Yao-Jen Kuo a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.