4 函數
4.1 為什麼需要函數
在R for Data Science中提到,要變成傑出的資料科學家,學習如何撰寫函數(function)是非常好的方法,函數的優點包括:
- 使程式碼更好理解
- 若需修改程式碼,若有搭配function的使用,只需要修改函數中的程式碼,其他部分則維持原有叫用函數方式即可
- 程式碼可以重複使用
也因此,如果同一段程式碼已經被剪下貼上兩次,那就是寫一個函式(Function)的時機。
舉例來說,若我們要計算iris資料中Sepal.Length和Sepal.Width的比例,以及Petal.Length和Petal.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 函數範例
接下來的範例使用兩個參數,height與weight,計算BMI值,並且考慮到輸入身高的單位可能會是公分的情況,邏輯為假設輸入的身高>5,因正常人身高不可能大於5公尺,因此假設輸入之單位為公分,需要在函數內將公分換算為公尺:
cal_bmi<-function(height,weight){
if(height>5){
height <- height/100
}
bmi <- weight / height**2
return(bmi)
}
cal_bmi(160,50)## [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函數:
cal_bmi<-function(height,weight){
if(height>5){
height <- height/100
}
bmi <- weight / height**2
return(bmi)
}
cal_bmi(160,50)## [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次程式碼,還是非常麻煩的。
good_teacher_score<-function(ori_score){
better_score<-sqrt(ori_score)*10
return(better_score)
}
good_teacher_score(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 |
4.8 參考資料
References
Henry, Lionel, and Hadley Wickham. 2020. Purrr: Functional Programming Tools. https://CRAN.R-project.org/package=purrr.