自訂函數

When you’ve written the same code 3 times, write a function.

David Robinson

我們在常用內建函數學習了很多 R 語言針對數值向量、文字向量與完成描述性統計所內建的常用函數,既然函數在 R 語言中的重要性如此之高,您有沒有想過自己撰寫函數來滿足特殊的需求呢?比如說,針對手邊雜亂無章的資料,撰寫一個 clean_data() 函數,只要將雜亂無章資料輸入,就能夠回傳您心中理想的乾淨資料樣式;聽起來真是令人心動,我們趕快來瞭解一下如何自訂函數!

自訂函數的架構

自訂一個函數我們需要考慮的元件有五個:

  1. 函數名稱(function name)
  2. 輸入與參數(inputs and parameters)
  3. 主體(body)
  4. 輸出(outputs)
  5. 保留字( functionreturn()

首先要給函數取個名字(function name),利用保留字 function 告訴 R 語言這是一個函數,接著在小括號中放入想好的輸入(inputs)與參數(parameters),然後在大括號內縮排撰寫我們主要的程式(body),最後是將輸出(outputs)放在保留字 return() 裡頭。

# How to define a function
FUNCTION_NAME <- function(INPUT1, INPUT2, ..., PARAM1, PARAM2, ...) {
  # BODY
  return(OUTPUT)
}
1
2
3
4
5

熟能生巧,透過練習自訂幾個功能單純的函數,上手如何在 R 語言中自訂函數,在這幾個練習中,我們將函數功能由簡入繁,從單純的情況:一個輸入一個輸出漸漸發展為多個輸入多個輸出、暸解物件的作用範圍以及例外處理。

一個輸入的自訂函數

custom_squared() 函數的作用是將輸入的數字平方之後回傳;函數的名字取為 custom_squared ,輸入為 x ,主要程式為 x**2 ,輸出為 squared_x

custom_squared <- function(x) {
  squared_x <- x**2
  return(squared_x)
}
custom_squared(-3)
custom_squared(-3:3)
1
2
3
4
5
6
## > custom_squared <- function(x) {
## +   squared_x <- x**2
## +   return(squared_x)
## + }
## > custom_squared(-3)
## [1] 9
## > custom_squared(-3:3)
## [1] 9 4 1 0 1 4 9
1
2
3
4
5
6
7
8

兩個輸入的函數

get_bmi() 函數的作用是將輸入的身高與體重換算為身體質量指數(Body Mass Index,BMI);函數的名字取為 get_bmi ,輸入為 heightweight ,主要程式為 weight/height**2 ,輸出為 bmi 。第二個自訂函數 get_bmi() 與前一個自訂函數差異之處在於計算 BMI 需要兩個數值向量作為輸入,但計算數值的平方只需要一個數值向量。

get_bmi <- function(height, weight) {
  height <- height/100
  bmi <- weight / height**2
  return(bmi)
}
get_bmi(216, 147)
get_bmi(c(216, 198), c(147, 98))
1
2
3
4
5
6
7
## > get_bmi <- function(height, weight) {
## +   height <- height/100
## +   bmi <- weight / height**2
## +   return(bmi)
## + }
## > get_bmi(216, 147)
## [1] 31.5072
## > get_bmi(c(216, 198), c(147, 98))
## [1] 31.50720 24.99745
1
2
3
4
5
6
7
8
9

搭配預設參數的函數

circle_calculator() 函數的作用是將輸入的圓半徑換算為圓形面積或者圓形周長;函數的名字取為 circle_calculator ,輸入為 ris_area ,輸出為 areaperimeter ,函數預設回傳圓形面積,若是使用時輸入 is_area = FALSE 則回傳圓形周長。第三個自訂函數 circle_calculator() 與前一個自訂函數差異之處在於有一個預設參數,當使用者沒有輸入該參數時會自動取用預設值。

circle_calculator <- function(r, is_area = TRUE) {
  # R 語言有內建圓周率 pi
  area <- pi * r**2
  perimeter <- 2 * pi * r
  if (is_area) {
    return(area)
  } else {
    return(perimeter)
  }
}
circle_calculator(3) # 預設回傳圓形面積
circle_calculator(3, is_area = FALSE) # 回傳圓形周長
1
2
3
4
5
6
7
8
9
10
11
12
## > circle_calculator <- function(r, is_area = TRUE) {
## +   # R 語言有內建圓周率 pi
## +   area <- pi * r**2
## +   perimeter <- 2 * pi * r
## +   if (is_area) {
## +     return(area)
## +   } else {
## +     return(perimeter)
## +   }
## + }
## > circle_calculator(3) # 預設回傳圓形面積
## [1] 28.27433
## > circle_calculator(3, is_area = FALSE) # 回傳圓形周長
## [1] 18.84956
1
2
3
4
5
6
7
8
9
10
11
12
13
14

多個輸出的自訂函數

bmi_calculator() 函數的作用是將輸入的身高與體重換算為身體質量指數(Body Mass Index,BMI),並且依據 BMI Chart 回傳對應的區間標籤;函數的名字取為 bmi_calculator ,輸入為 heightweight ,輸出為 bmibmi_label。第四個自訂函數 bmi_calculator() 與第二個自訂函數 get_bmi() 函數差異之處在於這次除了輸出(回傳)BMI 值,還會包含區間標籤,指出這樣的 BMI 值會被歸類為過輕、正常、過重或肥胖。面對多個輸出的需求時,通常我們選擇以有命名的清單(named list)儲存,因為這樣能夠確認輸出保留各自的資料結構,同時在取用個別輸出時能夠以 [["KEY"]] 或者 $KEY 快速獲取。

bmi_calculator <- function(height, weight) {
  height <- height/100
  bmi <- weight / height**2
  if (bmi < 18.5) {
    bmi_label <- "過輕"
  } else if (bmi > 30) {
    bmi_label <- "肥胖"
  } else if (bmi >= 18.5 & bmi < 25) {
    bmi_label <- "正常"
  } else {
    bmi_label <- "過重"
  }
  bmi_list <- list(
    bmi = bmi,
    bmiLabel = bmi_label
  )
  return(bmi_list)
}
shaq <- bmi_calculator(216, 147)
shaq$bmi
shaq$bmiLabel
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
## > bmi_calculator <- function(height, weight) {
## +   height <- height/100
## +   bmi <- weight / height**2
## +   if (bmi < 18.5) {
## +     bmi_label <- "過輕"
## +   } else if (bmi > 30) {
## +     bmi_label <- "肥胖"
## +   } else if (bmi >= 18.5 & bmi < 25) {
## +     bmi_label <- "正常"
## +   } else {
## +     bmi_label <- "過重"
## +   }
## +   bmi_list <- list(
## +     bmi = bmi,
## +     bmiLabel = bmi_label
## +   )
## +   return(bmi_list)
## + }
## > shaq <- bmi_calculator(216, 147)
## > shaq$bmi
## [1] 31.5072
## > shaq$bmiLabel
## [1] "肥胖"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

輸入 Shaquille O’Neal 湖人隊時期的身高與體重,會顯示肥胖的標籤(由此得知這是很不可靠的指標。)

處理雜亂無章資料的函數

回應這個小節一開始提到的概念,手邊有一個雜亂無章的資料,如果能寫一個 clean_data() 函數把它依照自己理想的方法處理,是多麽輕鬆方便的一件事呢?假設雜亂無章的資料長得像這樣。

# 雜亂無章的資料
messy_data <- data.frame(c(1, 2, 3, 4, NA), c(1, 2, 3, NA, 5), c(1, 2, NA, 4, 5))
names(messy_data) <- c("var_1", "var_2", "var_3")
messy_data
1
2
3
4
## > # 雜亂無章的資料
## > messy_data <- data.frame(c(1, 2, 3, 4, NA), c(1, 2, 3, NA, 5), c(1, 2, NA, 4, 5))
## > names(messy_data) <- c("var_1", "var_2", "var_3")
## > messy_data
##   var_1 var_2 var_3
## 1     1     1     1
## 2     2     2     2
## 3     3     3    NA
## 4     4    NA     4
## 5    NA     5     5
1
2
3
4
5
6
7
8
9
10

我們理想中的 clean_data() 函數會提供兩種輸出:

  1. 把有出現 NA 的觀測值都刪除
  2. NA 用指定數值取代(imputation)
# 自訂函數 clean_data
clean_data <- function(df, impute_value){
  n_rows <- nrow(df)
  na_sum <- rep(NA, times = n_rows)
  for (i in 1:n_rows) {
    na_sum[i] <- sum(is.na(df[i, ])) # 計算每個觀測值有幾個 NA
    df[i, ][is.na(df[i, ])] <- impute_value # 把 NA 用某個數值取代
  }
  complete_cases <- df[as.logical(!na_sum), ] # 把沒有出現 NA 的觀測值保留下來
  imputed_data <- df
  df_list <- list(
    complete_cases = complete_cases,
    imputed_data = imputed_data
  )
  return(df_list)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

接著呼叫 clean_data() 函數,輸入我們的 messy_data,並指定用 999 取代 NA

# 自訂函數 clean_data
clean_data <- function(df, impute_value){
  n_rows <- nrow(df)
  na_sum <- rep(NA, times = n_rows)
  for (i in 1:n_rows) {
    na_sum[i] <- sum(is.na(df[i, ])) # 計算每個觀測值有幾個 NA
    df[i, ][is.na(df[i, ])] <- impute_value # 把 NA 用某個數值取代
  }
  complete_cases <- df[as.logical(!na_sum), ] # 把沒有出現 NA 的觀測值保留下來
  imputed_data <- df
  df_list <- list(
    complete_cases = complete_cases,
    imputed_data = imputed_data
  )
  return(df_list)
}

# 雜亂無章的資料
messy_data <- data.frame(c(1, 2, 3, 4, NA), c(1, 2, 3, NA, 5), c(1, 2, NA, 4, 5))
names(messy_data) <- c("var_1", "var_2", "var_3")
messy_data
cleaned_data <- clean_data(messy_data, impute_value = 999)
cleaned_data$complete_cases # 保留完整觀測值的資料
cleaned_data$imputed_data   # 取代 NA 為 999 的資料
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
## > # 自訂函數 clean_data
## > clean_data <- function(df, impute_value){
## +   n_rows <- nrow(df)
## +   na_sum <- rep(NA, times = n_rows)
## +   for (i in 1:n_rows) {
## +     na_sum[i] <- sum(is.na(df[i, ])) # 計算每個觀測值有幾個 NA
## +     df[i, ][is.na(df[i, ])] <- impute_value # 把 NA 用某個數值取代
## +   }
## +   complete_cases <- df[as.logical(!na_sum), ] # 把沒有出現 NA 的觀測值保留下來
## +   imputed_data <- df
## +   df_list <- list(
## +     complete_cases = complete_cases,
## +     imputed_data = imputed_data
## +   )
## +   return(df_list)
## + }
## > 
## > # 雜亂無章的資料
## > messy_data <- data.frame(c(1, 2, 3, 4, NA), c(1, 2, 3, NA, 5), c(1, 2, NA, 4, 5))
## > names(messy_data) <- c("var_1", "var_2", "var_3")
## > messy_data
##   var_1 var_2 var_3
## 1     1     1     1
## 2     2     2     2
## 3     3     3    NA
## 4     4    NA     4
## 5    NA     5     5
## > cleaned_data <- clean_data(messy_data, impute_value = 999)
## > cleaned_data$complete_cases # 保留完整觀測值的資料
##   var_1 var_2 var_3
## 1     1     1     1
## 2     2     2     2
## > cleaned_data$imputed_data   # 取代 NA 為 999 的資料
##   var_1 var_2 var_3
## 1     1     1     1
## 2     2     2     2
## 3     3     3   999
## 4     4   999     4
## 5   999     5     5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

太完美了,不論是將有遺漏值的觀測值全部刪去或是取代成指定數字 999 都沒有問題!

物件的作用範圍

開始自行寫作函數之後我們必須要意識到物件的作用範圍(scope),當程式中沒有函數區塊(function block,這指的是自訂函數大括號所間隔出來的縮排區域),所有的物件作用範圍都被視為全域變數(global variables),在物件成功宣告後的任何一段程式碼中我們都能去使用它。

當程式中開始出現函數區塊後,物件作用範圍就被分為區域變數(local variables)全域變數;在函數區塊內的程式碼我們能夠使用兩種被成功宣告的物件,但是在函數區塊以外的程式碼就僅能使用全域變數。用文字敘述還是相當抽象,我們應該動手自訂函數來釐清觀念。在以下程式範例中,bmi 這個數值向量的作用範圍是區域變數,不能夠在函數區塊以外使用。

get_bmi <- function(height, weight) {
  height <- height/100 # local numeric
  bmi <- weight / height**2 # local numeric
  return(bmi)
}
shaq_height <- 216 # globel numeric
shaq_weight <- 147 # global numeric
shaq_bmi <- get_bmi(shaq_height, shaq_weight) # global numeric
shaq_bmi # global numeric
bmi # local numeric
1
2
3
4
5
6
7
8
9
10
## > get_bmi <- function(height, weight) {
## +   height <- height/100 # local numeric
## +   bmi <- weight / height**2 # local numeric
## +   return(bmi)
## + }
## > shaq_height <- 216 # globel numeric
## > shaq_weight <- 147 # global numeric
## > shaq_bmi <- get_bmi(shaq_height, shaq_weight) # global numeric
## > shaq_bmi # global numeric
## [1] 31.5072
## > bmi # local numeric
## Error: object 'bmi' not found
1
2
3
4
5
6
7
8
9
10
11
12

在以下程式範例中,heightweight 這兩個數值向量的作用範圍是全域變數,能夠在函數區塊以內使用,也同樣可以在函數區塊以外使用,值得注意的是, height 在函數區塊中即便被換算單位成為公尺,但由於換算過程在函數區塊以內,並不會影響到區塊以外的程式碼,因此在成功呼叫 get_bmi() 函數作用之後, height 依然保持原本的單位公分。

height <- 216 # global numeric
weight <- 147 # global numeric
get_bmi <- function() {
  height <- height/100 # local numeric
  bmi <- weight / height**2 # local numeric
  return(bmi)
}
shaq_bmi <- get_bmi() # global numeric
shaq_bmi # global numeric
height   # global numeric
1
2
3
4
5
6
7
8
9
10
## > height <- 216 # global numeric
## > weight <- 147 # global numeric
## > get_bmi <- function() {
## +   height <- height/100 # local numeric
## +   bmi <- weight / height**2 # local numeric
## +   return(bmi)
## + }
## > shaq_bmi <- get_bmi() # global numeric
## > shaq_bmi # global numeric
## [1] 31.5072
## > height   # global numeric
## [1] 216
1
2
3
4
5
6
7
8
9
10
11
12

例外處理

在使用內建的 R 語言函數時候常有各種原因會導致錯誤或者警示,這時收到的回傳訊息可以幫助我們修改程式。

as.numeric(TRUE)
as.numeric("TRUE") # 產生警示
sum(1:5)
sum(as.character(1:5)) # 產生錯誤
1
2
3
4
## > as.numeric(TRUE)
## [1] 1
## > as.numeric("TRUE") # 產生警示
## [1] NA
## Warning message:
## NAs introduced by coercion 
## > sum(1:5)
## [1] 15
## > sum(as.character(1:5)) # 產生錯誤
## Error in sum(as.character(1:5)) : invalid 'type' (character) of argument
1
2
3
4
5
6
7
8
9
10

自訂函數時如果能夠預先掌握某些可能的警示或錯誤,撰寫客製的訊息,可以讓使用這些函數的使用者更快完成偵錯與修正,亦可以減少在排程 R 程式碼時因為產生錯誤而導致;在 R 語言中我們使用 tryCatch() 函數進行例外處理,其外觀架構比較複雜。

# tryCatch() function
tryCatch(
  {
    # 程式碼
  },
  warning = function(w) {
    # 程式碼若產生警示,該做些什麼
  },
  error = function(e) {
    # 程式碼若產生錯誤,該做些什麼
  }
)
1
2
3
4
5
6
7
8
9
10
11
12

讓我們試著將 as.numeric() 函數的警示掌握起來,使用者輸入文字向量,導致警示產生時會回傳訊息:「請不要輸入文字向量。」

# 沒有例外處理
custom_as_numeric <- function(x) {
  return(as.numeric(x))
}
custom_as_numeric(TRUE)
custom_as_numeric("TRUE")

# 有例外處理
custom_as_numeric <- function(x) {
  tryCatch({
    return(as.numeric(x))
  },
  warning = function(w) {
    return("請不要輸入文字向量。")
  })
}
custom_as_numeric(TRUE)
custom_as_numeric("TRUE")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
## > # 沒有例外處理
## > custom_as_numeric <- function(x) {
## +     return(as.numeric(x))
## + }
## > custom_as_numeric(TRUE)
## [1] 1
## > custom_as_numeric("TRUE")
## [1] NA
## Warning message:
## In custom_as_numeric("TRUE") : NAs introduced by coercion
## > 
## > # 有例外處理
## > custom_as_numeric <- function(x) {
## +     tryCatch({
## +         return(as.numeric(x))
## +     },
## +     warning = function(w) {
## +         return("請不要輸入文字向量。")
## +     })
## + }
## > custom_as_numeric(TRUE)
## [1] 1
## > custom_as_numeric("TRUE")
## [1] "請不要輸入文字向量。"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

再來讓我們試著將輸入數值向量平方的 custom_squared() 函數的錯誤掌握起來,當使用者輸入文字向量,導致錯誤產生時會回傳訊息:「請輸入數值向量。」

# 沒有例外處理
custom_squared <- function(x) {
  return(x**2)
}
custom_squared(-3)
custom_squared("-3") # 產生錯誤
# 有例外處理
custom_squared <- function(x) {
  tryCatch({
    return(x**2)
  },
  error = function(e) {
    return("請輸入數值向量。")
  })
}
custom_squared(-3)
custom_squared("-3") # 觸發例外處理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
## > # 沒有例外處理
## > custom_squared <- function(x) {
## +   return(x**2)
## + }
## > custom_squared(-3)
## [1] 9
## > custom_squared("-3") # 產生錯誤
## Error in x^2 : non-numeric argument to binary operator
## > # 有例外處理
## > custom_squared <- function(x) {
## +   tryCatch({
## +     return(x**2)
## +   },
## +   error = function(e) {
## +     return("請輸入數值向量。")
## +   })
## + }
## > custom_squared(-3)
## [1] 9
## > custom_squared("-3") # 觸發例外處理
## [1] "請輸入數值向量。"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

讓我們試著將 as.numeric() 函數的警示、錯誤都掌握起來,使用者輸入文字向量,導致警示產生時會回傳訊息:「請不要輸入文字向量。」使用者若輸入未宣告的物件名稱,導致錯誤產生時會回傳訊息:「找不到物件。」

# 有例外處理
custom_as_numeric <- function(x) {
  tryCatch(
    {
      return(as.numeric(x))
    },
    warning = function(w) {
      return("請不要輸入文字向量。")
    },
    error = function(e) {
      return("找不到物件。")
    }
  )
}
custom_as_numeric(TRUE)
custom_as_numeric("TRUE")
custom_as_numeric(True)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
## > # 有例外處理
## > custom_as_numeric <- function(x) {
## +   tryCatch(
## +     {
## +       return(as.numeric(x))
## +     },
## +     warning = function(w) {
## +       return("請不要輸入文字向量。")
## +     },
## +     error = function(e) {
## +       return("找不到物件。")
## +     }
## +   )
## + }
## > custom_as_numeric(TRUE)
## [1] 1
## > custom_as_numeric("TRUE")
## [1] "請不要輸入文字向量。"
## > custom_as_numeric(True)
## [1] "找不到物件。"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

最後讓 custom_as_numeric() 函數除了正確的 TRUEFALSE 都能夠輸出正確的 1 與 0,我們也試著利用例外處理,讓不論是 "TRUE""FALSE""True""False""true""false" 都可以正確輸出。

# 有例外處理
custom_as_numeric <- function(x) {
  tryCatch(
    {
      return(as.numeric(x))
    },
    warning = function(w) {
      x <- toupper(x)
      x <- as.logical(x)
      return(as.numeric(x))
    },
    error = function(e) {
      return("找不到物件。")
    }
  )
}
custom_as_numeric(TRUE)
custom_as_numeric(FALSE)
custom_as_numeric("TRUE")
custom_as_numeric("True")
custom_as_numeric("true")
custom_as_numeric("FALSE")
custom_as_numeric("False")
custom_as_numeric("TRUE")
custom_as_numeric(True)
custom_as_numeric(False)
custom_as_numeric(true)
custom_as_numeric(false)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
## > # 有例外處理
## > custom_as_numeric <- function(x) {
## +   tryCatch(
## +     {
## +       return(as.numeric(x))
## +     },
## +     warning = function(w) {
## +       x <- toupper(x)
## +       x <- as.logical(x)
## +       return(as.numeric(x))
## +     },
## +     error = function(e) {
## +       return("找不到物件。")
## +     }
## +   )
## + }
## > custom_as_numeric(TRUE)
## [1] 1
## > custom_as_numeric(FALSE)
## [1] 0
## > custom_as_numeric("TRUE")
## [1] 1
## > custom_as_numeric("True")
## [1] 1
## > custom_as_numeric("true")
## [1] 1
## > custom_as_numeric("FALSE")
## [1] 0
## > custom_as_numeric("False")
## [1] 0
## > custom_as_numeric("TRUE")
## [1] 1
## > custom_as_numeric(True)
## [1] "找不到物件。"
## > custom_as_numeric(False)
## [1] "找不到物件。"
## > custom_as_numeric(true)
## [1] "找不到物件。"
## > custom_as_numeric(false)
## [1] "找不到物件。"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

小結

在這個小節中我們簡介自訂函數的架構,透過多個練習範例熟練設計函數的輸入、參數與輸出,設計了一個 clean_data() 函數針對資料框中的遺漏值做兩種不同處理,討論物件的作用範圍以及如何做例外處理。

延伸閱讀