
寫在前面
本系列為《R數(shù)據(jù)科學(xué)》(R for Data Science)的學(xué)習(xí)筆記。相較于其他R語言教程來說,本書一個很大的優(yōu)勢就是直接從實用的R包出發(fā),來熟悉R及數(shù)據(jù)科學(xué)。更新過程中,讀者朋友如發(fā)現(xiàn)錯誤,歡迎指正。如果有疑問,也可以后臺私信。希望各位讀者朋友能學(xué)有所得!
函數(shù)
[TOC]
13.1 什么時候該用函數(shù)
先看一個例子:
df <- tibble::tibble(
a = rnorm(10),#產(chǎn)生10個服從正態(tài)分布的隨機數(shù)
b = rnorm(10),
c = rnorm(10),
d = rnorm(10)
)
df$a <- (df$a - min(df$a, na.rm = TRUE)) /
(max(df$a, na.rm = TRUE) - min(df$a, na.rm = TRUE))
df$b <- (df$b - min(df$b, na.rm = TRUE)) /
(max(df$b, na.rm = TRUE) - min(df$b, na.rm = TRUE))
df$c <- (df$c - min(df$c, na.rm = TRUE)) /
(max(df$c, na.rm = TRUE) - min(df$c, na.rm = TRUE))
df$d <- (df$d - min(df$d, na.rm = TRUE)) /
(max(df$d, na.rm = TRUE) - min(df$d, na.rm = TRUE))
顯然,上面這一大段代碼是數(shù)據(jù)標準化(將每列的值調(diào)整到 0 到 1 之間)常用的一個方法Max-Min。
先分析一下代碼。
df$a - min(df$a, na.rm = TRUE)) /
(max(df$a, na.rm = TRUE) - min(df$a, na.rm = TRUE))
這段代碼只有一個輸入:df$a。使用具有通用名稱的臨時變量來重寫代碼。 以上代碼只需要一個數(shù)值向量,我們可以稱其為 x:
x <- df$a
(x - min(x, na.rm = TRUE)) /
(max(x, na.rm = TRUE) - min(x, na.rm = TRUE))
這段代碼中還有一些重復(fù),計算了 3 次數(shù)據(jù)最大值和最小值,可以簡化:
rng <- range(x, na.rm = TRUE) #該向量包含給定參數(shù)的最大值和最小值。
(x - rng[1]) / (rng[2] - rng[1])
接下來就可以將其轉(zhuǎn)換為函數(shù)了:
rescale01 <- function(x) {
rng <- range(x, na.rm = TRUE)
(x - rng[1]) / (rng[2] - rng[1])
}
rescale01(c(0, 5, 10)) #測試
#> [1] 0.0 0.5 1.0
要想創(chuàng)建一個新函數(shù),需要 3 個關(guān)鍵步驟。
- 為函數(shù)選擇一個名稱。在以上示例中,我們使用
rescale01作為函數(shù)名稱,因為這個函數(shù)的功能是將一個向量調(diào)整到 0 到 1 之間。 - 列舉出
function中所用的輸入,即參數(shù)。這個示例中只有一個參數(shù),如果有更多參數(shù), 那么函數(shù)調(diào)用形式就類似于function(x, y, z)。 - 將已經(jīng)編寫好的代碼放在函數(shù)體中。在
function(...)后面要緊跟一個用{}括起來的 代碼塊。
此時我們應(yīng)該使用其他輸入來測試函數(shù)是否正確:
rescale01(c(-10, 0, 10))
#> [1] 0.0 0.5 1.0
rescale01(c(1, 2, 3, NA, 5))
#> [1] 0.00 0.25 0.50 NA 1.00
既然已經(jīng)有了函數(shù),那么我們就可以利用它來簡化原來的示例了:
df$a <- rescale01(df$a)
df$b <- rescale01(df$b)
df$c <- rescale01(df$c)
df$d <- rescale01(df$d)
相對于原來的代碼,這段代碼更清楚易懂,而且還消除了復(fù)制粘貼可能帶來的錯誤。但這段代碼中仍然有一些重復(fù),因為我們對多個數(shù)據(jù)列進行了同樣的操作。(如何消除這種重復(fù)后面的章節(jié)會有)
函數(shù)的另一個優(yōu)點是,如果需求發(fā)生變化,我們只需要在一處進行修改。
13.2 人與計算機的函數(shù)
簡單來說,不止得讓計算機運行你的函數(shù),還得讓別人能讀懂。
函數(shù)名是非常重要的。理想的函數(shù)名應(yīng)該既簡短,又能清楚地說明函數(shù)的作用。
# 名稱太短
f()
# 名稱不是動詞,或者沒有描述力
my_awesome_function()
# 名稱雖然長,但是表達得很清楚
impute_missing()
collapse_years()
如果你的函數(shù)名由多個
如果你的函數(shù)名由多個單詞組成,建議使用“snake_case”命名法,即使用小寫單詞,單詞之間用下劃線隔開。
# 千萬別這樣!
col_mins <- function(x, y) {}
rowMaxes <- function(y, x) {}
# 良好的命名方式
input_select()
input_checkbox()
input_text()
# 不太好的命名方式
select_input()
checkbox_input()
text_input()
盡可能避免覆蓋現(xiàn)有的函數(shù)和變量??傮w來說,完全不覆蓋是不可能的,因為太多好名稱 已經(jīng)被其他 R 包占用了,但完全可以不覆蓋 R 基礎(chǔ)包中最常用的名稱,這樣可以避免混淆。
13.3 條件執(zhí)行
if 語句可以使得你有條件地執(zhí)行代碼。其形式如下所示:
if (condition) {
# 條件為真時執(zhí)行的代碼
} else {
# 條件為假時執(zhí)行的代碼
}
13.3.1 條件
condition 的值要么是 TRUE,要么是 FALSE。如果它是一個向量,那么你會收到一條警告; 如果它是 NA,那么程序就會出錯。
可以使用 ||(或)和 &&(與)操作符來組合多個邏輯表達式。
不能在 if 語句中使用 | 或 &,它們是向量化的操作符,只可以用于多個值(這就是我們在 filter() 函數(shù)中使用它們的原因)。
你還需要提防浮點數(shù)的問題:
x <- sqrt(2) ^ 2
x
#> [1] 2
x == 2
#> [1] FALSE
x - 2
#> [1] 4.44e-16
解決方式是使用 dplyr::near() 函數(shù)進行比較,詳見 。
13.3.2 多重條件
你可以將多個 if 語句串聯(lián)起來:
if (this) {
# 做一些操作
} else if (that) {
# 做另外一些操作
} else {
#
}
但如果你有一長串 if 語句,那么就要考慮重寫了。重寫的一種方法是使用 switch() 函數(shù), 它先對第一個參數(shù)求值,然后按照名稱或位置在后面的參數(shù)列表中匹配返回結(jié)果:
#> function(x, y, op) {
#> switch(op,
#> plus = x + y,
#> minus = x - y,
#> times = x * y,
#> divide = x / y,
#> stop("Unknown op!")
#> )
#> }
13.3.3 代碼風格
if 和 function 后面總是要跟著一對大括號({}),其中的內(nèi)容應(yīng)該縮進兩個空格。這樣通過左側(cè)空白就可以很容易地知道代碼層次。
左大括號不應(yīng)該自己占一行,而且后面要換行。右大括號應(yīng)該自己占一行,除非后面跟著 else。大括號中的代碼一定要縮進:
# 好
if (y < 0 && debug) {
message("Y is negative")
}
if (y == 0) {
log(x)
} else {
y ^ x
}
# 不好
if (y < 0 && debug)
message("Y is negative")
if (y == 0) {
log(x)
}
else {
y ^ x
}
如果 if 語句非常短,可以在一行內(nèi)寫下,那么可以不用大括號:
y <- 10
x <- if (y < 20) "Too low" else "Too high"
我們建議只對特別短的 if 語句采用這種形式,其他情況下還是完整形式更易于閱讀:
if (y < 20) {
x <- "Too low"
} else {
x <- "Too high"
}
13.4 函數(shù)參數(shù)
函數(shù)的參數(shù)通常分為兩大類:一類提供需要進行計算的數(shù)據(jù),另一類控制計算過程的細節(jié)。舉例如下。
- 在
log()函數(shù)中,數(shù)據(jù)是x,細節(jié)則是對數(shù)的底,即base。 - 在
mean()函數(shù)中,數(shù)據(jù)是x,細節(jié)則是從x前后兩端(trim)移除多大比例的數(shù)據(jù),以 及如何處理缺失值(na.rm)。 - 在
t.test()函數(shù)中,數(shù)據(jù)是x和y,檢驗的細節(jié)則是alternative、mu、paired、var. equal以及conf.level等設(shè)置。 - 在
str_c()函數(shù)中,你可以向 ... 參數(shù)提供任意數(shù)量的字符串作為數(shù)據(jù),連接的細節(jié)則由sep和collapse參數(shù)控制。
通常情況下,數(shù)據(jù)參數(shù)應(yīng)該放在最前面,細節(jié)參數(shù)則放在后面,而且一般都有默認值。設(shè)置默認值的方式與使用命名參數(shù)調(diào)用函數(shù)的方式是一樣的:
# 使用近似正態(tài)分布計算均值兩端的置信區(qū)間
mean_ci <- function(x, conf = 0.95) {
se <- sd(x) / sqrt(length(x))
alpha <- 1 - conf
mean(x) + se * qnorm(c(alpha / 2, 1 - alpha / 2))
}
x <- runif(100)
mean_ci(x)
> [1] 0.498 0.610
mean_ci(x, conf = 0.99)
> [1] 0.480 0.628
默認值應(yīng)該幾乎總是最常用的值。這種原則的例外情況非常少,除非出于安全考慮。例如,將 na.rm 的默認值設(shè)為 FALSE 是情有可原的,因為缺失值有時是非常重要的。雖然代碼中經(jīng)常使用的是 na.rm = TRUE,但是通過默認設(shè)置不聲不響地忽略缺失值并不是一種良好的做法。
在調(diào)用函數(shù)時,應(yīng)該在其中 = 的兩端都加一個空格。逗號后面應(yīng)該總是加一個空格, 逗號前面則不要加空格(與英文寫法相同)。使用空格可以使得函數(shù)的重要部分更易讀:
# 好
average <- mean(feet / 12 + inches, na.rm = TRUE)
# 不好
average<-mean(feet/12+inches,na.rm=TRUE)
13.4.1 選擇參數(shù)名稱
參數(shù)名稱也很重要。通常應(yīng)該選擇那些較長的、更具描述性的名稱,但 R 中有一些非常短的通用名稱,你應(yīng)該記住它們。
-
x,y,z:向量。 -
w:權(quán)重向量。 -
df:數(shù)據(jù)框。 -
i,j:數(shù)值索引(通常用于表示行和列)。 -
n:長度或行的數(shù)量。 -
p:列的數(shù)量。
除此之外,你還可以考慮使用現(xiàn)有 R 函數(shù)中的參數(shù)名稱。例如,使用 na.rm 來確定是否需要除去缺失值。
13.4.2 檢查參數(shù)值
當編寫的函數(shù)越來越多時,你有時會記不清某個函數(shù)到底是用來做什么的。這時就很容易 使用無效的參數(shù)來調(diào)用函數(shù)。為了解決這種問題,應(yīng)該對函數(shù)參數(shù)進行明確的限制。
13.4.3 點點點(...)
R 中的很多函數(shù)可以接受任意數(shù)量的輸入。它們需要一個特殊參數(shù):...(讀作點點點)。這個特殊參數(shù)會捕獲任意數(shù)量的未匹配參數(shù)。
這個參數(shù)的作用非常大,因為你可以將它捕獲的值傳給另一個函數(shù)。如果你的函數(shù)是另一 個函數(shù)的包裝器,那么這種一網(wǎng)打盡的方式就非常有用了。例如,我們經(jīng)常用以下方式創(chuàng)建輔助函數(shù)來包裝 str_c() 函數(shù):
commas <- function(...) stringr::str_c(..., collapse = ", ")
commas(letters[1:10])
#> [1] "a, b, c, d, e, f, g, h, i, j"
rule <- function(..., pad = "-") {
title <- paste0(...)
width <- getOption("width") - nchar(title) - 5
cat(title, " ", stringr::str_dup(pad, width), "\n", sep = "")
}
rule("Important output")
#> Important output ----------------------------------------
這里 ... 可以將我們不想處理的所有參數(shù)傳遞給 str_c()。雖然非常方便,但這種技術(shù)是有代價的:所有拼寫錯誤的參數(shù)都不會引發(fā)錯誤消息。這使得我們很難發(fā)現(xiàn)輸入錯誤:
x <- c(1, 2)
sum(x, na.mr = TRUE)
> [1] 4
如果想要檢查 ... 中的值,那么你可以使用 list(...)。
13.4.4 惰性求值
R 中的參數(shù)求值的方式是惰性的,即直到需要參數(shù)時才會進行求值。這意味著,如果沒有 使用參數(shù),那么它就一直沒有實際值。
13.5 返回值
13.5.1 顯式返回語句
函數(shù)的返回值通常是最后一個語句的值,但你可以通過 return() 語句提前返回一個值。我 們認為最好有節(jié)制地使用 return() 語句,因為提前返回的一般都是比較簡單的情況。常見 的提前返回原因就是輸入為空:
complicated_function <- function(x, y, z) {
if (length(x) == 0 || length(y) == 0) {
return(0)
}
# 這里是復(fù)雜的代碼
}
需要提前返回的另一個原因是,if 語句的一個分支非常復(fù)雜,而另一個分支則特別簡單。 例如,你可能寫出如下的 if 語句:
f <- function() {
if (x) {
# 需要
# 多行
# 代碼
# 才能
# 完成
# 的
# 操作
# express
} else {
# 返回一個非常簡單的值
}
}
但如果第一個分支中的代碼非常長,到達 else 語句前,你可能就已經(jīng)記不清 condition 了。解決這個問題的一種方法是將簡單情形提前返回:
f <- function() {
if (!x) {
return(something_short)
}
# 需要
# 多行
# 代碼
# 才能
# 完成
# 的
# 操作
# express
}
這樣應(yīng)該會使得代碼更容易理解,因為不需要太多的上下文。
13.5.2 使得函數(shù)支持管道
如果想要讓自己的函數(shù)支持管道操作,那么你應(yīng)該仔細思考一下返回值??梢灾С止艿啦?作的函數(shù)有兩種主要類型:轉(zhuǎn)換函數(shù)與副作用函數(shù)。
轉(zhuǎn)換函數(shù)會傳入一個明確的“基本”對象作為第一個參數(shù),對這個對象進行處理后,再將其返回。例如,在 dplyr 中,這個關(guān)鍵的對象就是數(shù)據(jù)框。如果能夠確定在自己的領(lǐng)域內(nèi)應(yīng)該使用哪種數(shù)據(jù)類型,那么你就可以讓自己的函數(shù)支持管道操作了。
副作用函數(shù)經(jīng)常用來執(zhí)行某種行為,比如繪圖或保存文件,而不是轉(zhuǎn)換對象。這些函數(shù)會 “悄悄地”返回第一個參數(shù),因此,默認情況下,第一個參數(shù)不顯示在輸出中,但仍然可 以由管道操作使用。
13.6 環(huán)境
剛開始編寫函數(shù)時,不需要對環(huán)境有多深入的理解。但我們還是應(yīng)該了解一些關(guān)于環(huán)境的知識,因為這些知識對于理解函數(shù)如何運行非常重要。 函數(shù)的環(huán)境決定了 R 如何尋找對象的值。例如,查看以下函數(shù):
f <- function(x) {
x + y
}
在很多編程語言中,這段代碼會引發(fā)一個錯誤,因為函數(shù)沒有定義 y。這種代碼在 R 中是有效的,因為 R 使用稱為詞法定界的一種規(guī)則來搜索對象的值。因為 y 沒有在函數(shù)中進行 定義,所以 R 會在定義函數(shù)的環(huán)境中尋找 y:
y <- 100
f(10)
#> [1] 110
y <- 1000
f(10)
#> [1] 1010
往期:
《R數(shù)據(jù)科學(xué)》學(xué)習(xí)筆記|Note12:使用magrittr進行管道操作
《R數(shù)據(jù)科學(xué)》學(xué)習(xí)筆記|Note11:使用forcats處理因子