4 函數

4.1 為什麼需要函數

R for Data Science中提到,要變成傑出的資料科學家,學習如何撰寫函數(function)是非常好的方法,函數的優點包括:

  • 使程式碼更好理解
  • 若需修改程式碼,若有搭配function的使用,只需要修改函數中的程式碼,其他部分則維持原有叫用函數方式即可
  • 程式碼可以重複使用

也因此,如果同一段程式碼已經被剪下貼上兩次,那就是寫一個函式(Function)的時機。

舉例來說,若我們要計算iris資料中Sepal.LengthSepal.Width的比例,以及Petal.LengthPetal.Width的比例,並且四捨五入到小數點第一位,沒有使用函數的程式碼如下:

## [1] 1.5 1.6 1.5 1.5 1.4
## [1] 7.0 7.0 6.5 7.5 7.0

每次計算都需指定 ,1 ,以完成四捨五入到小數點第一位的任務。

4.2 函數組成

要學習如何撰寫函數,首先須了解函數包含四個重要的部分:

  • 名字 Function Name function_name
  • 參數 Arguments, optional arg_1, arg_2, ...
  • 程式碼本體 Function Body
  • 回傳值 Return Value, optional return_value

而撰寫函數有四個重要步驟:

  • 先寫出可以用的程式碼
  • 將程式碼內可以重複使用的部分指派給臨時變數
  • 其餘程式碼用臨時變數表示,讓程式碼變簡潔
  • 把程式碼變成函數function

以常見的函數mean()為例,其實就是將輸入向量加總後,除以個數,完成平均值的計算

## [1] 3.5
## [1] 3.5
## [1] 3.5

也因此,若我們想要自己撰寫一個計算平均值的函數myMean(),只要將上述邏輯寫入function內即可

## [1] 3.5
## [1] 6

又以計算三次方為例,其實就是將輸入數字x自乘三次,程式碼如下:

## [1] 8
## [1] 64
## [1] 216

4.3 函數命名原則

根據上述兩個簡單的函數,可以發現寫好的函數的方法是先確保程式的正確性,再確保函數是否容易閱讀與理解,為了達成函數的易理解性,函數有以下建議的命名原則:

  • 長的函數名稱要遵循一樣的命名樣式
  • 不要用原本就存在R中的函數名稱
  • 可以被理解的名稱,通常是動詞

而參數也有對應的命名原則:

  • 可以被理解的名稱
  • 通常是名詞
  • 輸入資料參數通常放在第一個
  • 其他設定值通常會設定預設值

4.4 函數範例

接下來的範例使用兩個參數,heightweight,計算BMI值,並且考慮到輸入身高的單位可能會是公分的情況,邏輯為假設輸入的身高>5,因正常人身高不可能大於5公尺,因此假設輸入之單位為公分,需要在函數內將公分換算為公尺:

## [1] 20
## [1] 20

4.5 函式編程Functional programming

函式編程(Functional programming)的技巧可以用在所有支援函數為一等公民(First Class)的程式語言,其中包括:

  • 可以將函數指定為一個變數 Assign a function to a variable
  • 可以將函數當作參數傳遞 Pass a function as an argument
  • 可以回傳一個函數 Return a function

其中,可以將函數指定為一個變數在前述程式碼中其實已經出現過,在cal_bmi 函數中,我們就是將cal_bmi 變數設定為cal_bmi函數:

## [1] 20
## [1] 20

而將函數當作參數並且傳遞的功能,在R中是非常實用且必須要學習的功能,在purrr(Henry and Wickham 2020)套件中,此功能被大量應用。最後則是將函數回傳的功能,以下以hello()以及sayHello()函數為例,在sayHello()函數中,回傳值即為hello()函數。

## [1] "Hello"

4.6 purrr

而上述支援函數為一等公民(First Class)的條件,最實用的應該為在purrr套件中,將函數當作參數傳遞,purrr套件提供一系列功能,將向量與函數的功能搭配,解決for迴圈速度很慢且程式碼很長的問題,在purrr套件中最重要的功能是map家族,包括map(), map_chr(), map_int(), map_dbl(), map_df(), walk()等,概念如下:

圖片來源

以下為將學生成績開根號乘以10的函數,可以發現若輸入的原始成績是30分,輸出則是加分後的成績,若原始成績是50分,輸出成績就會及格,如果只是要算兩位同學的成績,一筆一筆輸入不是什麼大問題,但若要計算30名學生的成績,要打30次程式碼,還是非常麻煩的。

## [1] 55
## [1] 71

以下為計算30個學生的成績時的程式碼,可以看到每筆成績都需要耗費一行程式碼來處理,十分麻煩。

## [1] 87 60  9
## [1] 93
## [1] 77

此時若能妥善利用purrr套件的map_dbl功能,就能將程式碼縮短,並且提升易讀性。map()函數家族的使用概念為map(需逐一計算的向量,計算所需的函數),如下所示,我們需要計算的學生原始成績存在ori_score_list向量中,而加分的函數為good_teacher_score() 圖片來源

學生原始成績存在ori_score_list變數內:

##  [1] 87 60  9  5 84 77 64 54 81 70 95 58 59  7 66 20 28 25 94  3 35 78 12 24 39
## [26] 53 98  4 16 10

使用map_dbl將每位學生的成績加分:

ori_score_list new_score_list
87 93
60 77
9 30
5 22
84 92
77 88
64 80
54 73
81 90
70 84
95 97
58 76
59 77
7 26
66 81
20 45
28 53
25 50
94 97
3 17
35 59
78 88
12 35
24 49
39 62
53 73
98 99
4 20
16 40
10 32

使用map函數即可完成逐一計算向量中原始成績的任務。

4.7 map2 family

map函數的設計是輸入一組需逐一計算的向量,但有時我們需要兩組成對且需逐一計算的向量,這時就可以使用map2家族函數,概念為輸入兩組成對且需逐一計算的向量,成對帶入後置函數內,完成計算。

圖片來源

以計算加權成績為例,我們有一組學生的國文成績chi_score以及英文成績eng_score,並且希望將這些成績以(國文成績+2*英文成績)的方式做加權計算,首先,我們須先將加權的程式碼撰寫成weight_score()函數,並使用map2家族函數,完成逐一計算的任務。map2家族函數的使用方法為map2_dbl(需逐一計算的向量1,需逐一計算的向量2,計算所需的函數)

首先建立國文成績chi_score以及英文成績eng_score向量:

完成向量值的指定後,撰寫(國文成績+2*英文成績)加權計算函數weight_score()

使用map2家族函數,完成逐一計算的任務:

chi_score eng_score weight_score_list
60 60 180
50 50 150
40 40 120

References

Henry, Lionel, and Hadley Wickham. 2020. Purrr: Functional Programming Tools. https://CRAN.R-project.org/package=purrr.