《R數(shù)據(jù)科學(xué)》學(xué)習(xí)筆記|Note13:函數(shù)

13.jpg

寫在前面

本系列為《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 代碼風格

iffunction 后面總是要跟著一對大括號({}),其中的內(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ù)是 xy,檢驗的細節(jié)則是 alternative、mu、paired、var. equal 以及 conf.level 等設(shè)置。
  • str_c() 函數(shù)中,你可以向 ... 參數(shù)提供任意數(shù)量的字符串作為數(shù)據(jù),連接的細節(jié)則由 sepcollapse 參數(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處理因子

《R數(shù)據(jù)科學(xué)》學(xué)習(xí)筆記|Note10:使用stringr處理字符串(下)

《R數(shù)據(jù)科學(xué)》學(xué)習(xí)筆記|Note9:使用stringr處理字符串(上)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容