R学习:R for Data Science(七)函数
R学习往期回顾:
R学习:R for Data Science(六)使用forcats处理因子
R学习 从Tidyverse学起,入门R语言 dplyr合并数据
R学习 从Tidyverse学起,入门R语言(tidyr和stringr)
R学习 从Tidyverse学起,入门R语言(tibble,readr和dplyr)
与复制粘贴相比,函数可以通过更强大、更通用的方式来自动执行常用任务。相比于复制粘贴,函数具有以下3 个主要优点。· 你可以给函数起一个意味深长的名字,从而让代码更容易理解。· 如果需求发生了变化,只需要修改一处代码即可,无须修改多处。· 消除了复制粘贴时可能出现的无心之失(比如,修改了一处的变量名称,但却没有修改另一处)。
什么时候应该使用函数
只要一段代码需要复制粘贴的次数超过两次(也就是说,同一段代码至少有 3 个副本),那么就应该考虑编写一个函数。
例如
df <- tibble::tibble(
a = rnorm(10),
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$a, 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))
这段代码的作用是将每列的值调整到 0 到 1 之间。
这里犯了一个错误:忘记将其中的一个a 改成 b 了。提取重复代码,将其转换为函数是一种非常好的做法,因为这样可以防止此类错误的发生。
要想编写一个函数,首先需要分析代码,比如,函数需要多少个输入?
(df$a - min(df$a, na.rm = TRUE)) /
(max(df$a, na.rm = TRUE) - min(df$a, na.rm = TRUE))
这段代码只有一个输入:df$a
为了让输入更加清晰,应该使用具有通用名称的临时变量来重写代码。
以上代码只需要一个数值向量,我们可以称其为 x:
x <- df$a
(x - min(x, na.rm = TRUE)) /
(max(x, na.rm = TRUE) - min(x, na.rm = TRUE))
这段代码中还有一些重复,我们计算了 3 次数据最大值和最小值,但这可以一步完成
rng <- range(x, na.rm = TRUE)
(x - rng[1]) / (rng[2] - rng[1])
将中间计算结果保存为命名变量是一种非常好的做法,因为这样可以让代码的意义更加清楚。既然我们已经简化了代码,并检验了代码可以正常运行,接下来就可以将其转换为函数了:(将0,5,10转换为0-1)
rescale01 <- function(x) {
rng <- range(x, na.rm = TRUE)
(x - rng[1]) / (rng[2] - rng[1])
}
rescale01(c(0, 5, 10))
要想创建一个新函数,需要以下 3 个关键步骤 (1) 为函数选择一个名称。在以上示例中,我们使用 rescale01 作为函数名称,因为这个函数的功能是将一个向量调整到 0 到 1 之间。 (2) 列举出 function 中所用的输入,即参数。这个示例中只有一个参数,如果有更多参数,那么函数调用形式就类似于 function(x, y, z)。 (3) 将已经编写好的代码放在函数体中。在 function(...) 后面要紧跟一个用 {} 括起来的代码块。
注意以上创建函数的整体过程。确定函数如何使用简单输入来运行后,我们才开始编写函数。从工作代码开始,再将其转换为函数是相对容易的;先创建函数,再让其正确运行则是比较困难的。
此时我们应该使用其他输入来测试函数是否正确
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
既然已经有了函数,那么我们就可以利用它来简化原来的示例了
df$a <- rescale01(df$a)
df$b <- rescale01(df$b)
df$c <- rescale01(df$c)
df$d <- rescale01(df$d)
相对于原来的代码,这段代码更清楚易懂,而且还消除了复制粘贴可能带来的错误。
函数的另一个优点是,如果需求发生变化,我们只需要在一处进行修改。例如,我们发现,如果有些变量中包括无限值,那么 rescale01() 函数就会出错
x <- c(1:10, Inf)
rescale01(x)
#> [1] 0 0 0 0 0 0 0 0 0 0 NaN
因为已经将代码放在函数中了,所以我们只需要在函数中进行修改即可
rescale01 <- function(x) {
rng <- range(x, na.rm = TRUE, finite = TRUE)
(x - rng[1]) / (rng[2] - rng[1])
}
rescale01(x)
#> [1] 0.000 0.111 0.222 0.333 0.444 0.556 0.667 0.778 0.889
#> [10] 1.000 Inf
函数参数
函数的参数通常分为两大类:一类提供需要进行计算的数据,另一类控制计算过程的细节。举例如下。 · 在 log() 函数中,数据是 x,细节则是对数的底,即 base。 · 在 mean() 函数中,数据是 x,细节则是从 x 前后两端(trim)移除多大比例的数据,以及如何处理缺失值(na.rm)。 · 在 t.test() 函数中,数据是 x 和 y,检验的细节则是 alternative、 mu、 paired、 var.equal 以及 conf.level 等设置。 · 在 str_c() 函数中,你可以向 ... 参数提供任意数量的字符串作为数据,连接的细节则由 sep 和 collapse 参数控制。
通常情况下,数据参数应该放在最前面,细节参数则放在后面,而且一般都有默认值。设置默认值的方式与使用命名参数调用函数的方式是一样的
使用近似正态分布计算均值两端的置信区间
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
默认值应该几乎总是最常用的值。
将 na.rm 的默认值设为 FALSE 是情有可原的,因为缺失值有时是非常重要的。虽然代码中经常使用的是 na.rm = TRUE,但是通过默认设置不声不响地忽略缺失值并不是一种良好的做法。
在调用函数时,我们经常省略数据参数的名称,因为其使用太普遍了。如果不想使用细节参数的默认值,那么你应该使用细节参数的完整名称
# 好
mean(1:10, na.rm = TRUE)
# 不好
mean(x = 1:10, , FALSE)
mean(, TRUE, x = c(1:10, NA))
如果一个参数名称的前几个字母可以唯一标识这个参数,那么你可以通过这些字母来引用这个参数,如 mean(x, n = TRUE)。但是,通常要在不会引起混淆的情况下才能这样做。注意,在调用函数时,应该在其中 = 的两端都加一个空格。逗号后面应该总是加一个空格,逗号前面则不要加空格(与英文写法相同)。使用空格可以使得函数的重要部分更易读
# 好
average <- mean(feet / 12 + inches, na.rm = TRUE)
# 不好
average<-mean(feet/12+inches,na.rm=TRUE)
点点点(...)
R 中的很多函数可以接受任意数量的输入
sum(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
#> [1] 55
stringr::str_c("a", "b", "c", "d", "e", "f")
#> [1] "abcdef
这种函数是如何运行的呢?它们需要一个特殊参数:...(读作点点点)。这个特殊参数会捕获任意数量的未匹配参数。
这个参数的作用非常大,因为你可以将它捕获的值传给另一个函数。如果你的函数是另一个函数的包装器,那么这种一网打尽的方式就非常有用了。例如,我们经常用以下方式创建辅助函数来包装 str_c() 函数:
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")
这里 ... 可以将我们不想处理的所有参数传递给 str_c()。
公众号“生信小课堂”