《R数据科学》学习笔记|Note13:函数

简介: 《R数据科学》学习笔记|Note13:函数

13.1 什么时候该用函数


先看一个例子:

df <- tibble::tibble(
  a = rnorm(10),#产生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$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))

显然,上面这一大段代码是数据标准化(将每列的值调整到 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。使用具有通用名称的临时变量来重写代码。 以上代码只需要一个数值向量,我们可以称其为 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])

接下来就可以将其转换为函数了:

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

要想创建一个新函数,需要 3 个关键步骤。


为函数选择一个名称。在以上示例中,我们使用 rescale01 作为函数名称,因为这个函数的功能是将一个向量调整到 0 到 1 之间。

列举出 function 中所用的输入,即参数。这个示例中只有一个参数,如果有更多参数, 那么函数调用形式就类似于 function(x, y, z)。

将已经编写好的代码放在函数体中。在 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)

相对于原来的代码,这段代码更清楚易懂,而且还消除了复制粘贴可能带来的错误。但这段代码中仍然有一些重复,因为我们对多个数据列进行了同样的操作。(如何消除这种重复后面的章节会有)


函数的另一个优点是,如果需求发生变化,我们只需要在一处进行修改。


13.2 人与计算机的函数


简单来说,不止得让计算机运行你的函数,还得让别人能读懂。


函数名是非常重要的。理想的函数名应该既简短,又能清楚地说明函数的作用。

# 名称太短
f()
# 名称不是动词,或者没有描述力
my_awesome_function()
# 名称虽然长,但是表达得很清楚
impute_missing()
collapse_years()
如果你的函数名由多个

如果你的函数名由多个单词组成,建议使用“snake_case”命名法,即使用小写单词,单词之间用下划线隔开。

# 千万别这样!
col_mins <- function(x, y) {}
rowMaxes <- function(y, x) {}
# 良好的命名方式
input_select()
input_checkbox()
input_text()
# 不太好的命名方式
select_input()
checkbox_input()
text_input()

尽可能避免覆盖现有的函数和变量。总体来说,完全不覆盖是不可能的,因为太多好名称 已经被其他 R 包占用了,但完全可以不覆盖 R 基础包中最常用的名称,这样可以避免混淆。


13.3 条件执行


if 语句可以使得你有条件地执行代码。其形式如下所示:

if (condition) {
 # 条件为真时执行的代码
} else {
 # 条件为假时执行的代码
}

13.3.1 条件


condition 的值要么是 TRUE,要么是 FALSE。如果它是一个向量,那么你会收到一条警告; 如果它是 NA,那么程序就会出错。


可以使用 ||(或)和 &&(与)操作符来组合多个逻辑表达式。


不能在 if 语句中使用 | 或 &,它们是向量化的操作符,只可以用于多个值(这就是我们在 filter() 函数中使用它们的原因)。


你还需要提防浮点数的问题:

x <- sqrt(2) ^ 2
x
#> [1] 2
x == 2
#> [1] FALSE
x - 2
#> [1] 4.44e-16

解决方式是使用 dplyr::near() 函数进行比较,详见 。


13.3.2 多重条件


你可以将多个 if 语句串联起来:

if (this) {
 # 做一些操作
} else if (that) {
 # 做另外一些操作
} else {
 #

但如果你有一长串 if 语句,那么就要考虑重写了。重写的一种方法是使用 switch() 函数, 它先对第一个参数求值,然后按照名称或位置在后面的参数列表中匹配返回结果:

#> 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 后面总是要跟着一对大括号({}),其中的内容应该缩进两个空格。这样通过左侧空白就可以很容易地知道代码层次。


左大括号不应该自己占一行,而且后面要换行。右大括号应该自己占一行,除非后面跟着 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 语句非常短,可以在一行内写下,那么可以不用大括号:

y <- 10
x <- if (y < 20) "Too low" else "Too high"

我们建议只对特别短的 if 语句采用这种形式,其他情况下还是完整形式更易于阅读:

if (y < 20) {
 x <- "Too low"
} else {
 x <- "Too high"
}

13.4 函数参数


函数的参数通常分为两大类:一类提供需要进行计算的数据,另一类控制计算过程的细节。举例如下。


在 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,但是通过默认设置不声不响地忽略缺失值并不是一种良好的做法。


在调用函数时,应该在其中 = 的两端都加一个空格。逗号后面应该总是加一个空格, 逗号前面则不要加空格(与英文写法相同)。使用空格可以使得函数的重要部分更易读:

# 好
average <- mean(feet / 12 + inches, na.rm = TRUE)
# 不好
average<-mean(feet/12+inches,na.rm=TRUE)

13.4.1 选择参数名称


参数名称也很重要。通常应该选择那些较长的、更具描述性的名称,但 R 中有一些非常短的通用名称,你应该记住它们。


x, y, z:向量。

w:权重向量。

df:数据框。

i, j:数值索引(通常用于表示行和列)。

n:长度或行的数量。

p:列的数量。

除此之外,你还可以考虑使用现有 R 函数中的参数名称。例如,使用 na.rm 来确定是否需要除去缺失值。


13.4.2 检查参数值

当编写的函数越来越多时,你有时会记不清某个函数到底是用来做什么的。这时就很容易 使用无效的参数来调用函数。为了解决这种问题,应该对函数参数进行明确的限制。


13.4.3 点点点(…)

R 中的很多函数可以接受任意数量的输入。它们需要一个特殊参数:...(读作点点点)。这个特殊参数会捕获任意数量的未匹配参数。


这个参数的作用非常大,因为你可以将它捕获的值传给另一个函数。如果你的函数是另一 个函数的包装器,那么这种一网打尽的方式就非常有用了。例如,我们经常用以下方式创建辅助函数来包装 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")
#> Important output ----------------------------------------

这里 ... 可以将我们不想处理的所有参数传递给 str_c()。虽然非常方便,但这种技术是有代价的:所有拼写错误的参数都不会引发错误消息。这使得我们很难发现输入错误:

x <- c(1, 2)
sum(x, na.mr = TRUE)
> [1] 4

如果想要检查 ... 中的值,那么你可以使用 list(...)。


13.4.4 惰性求值

R 中的参数求值的方式是惰性的,即直到需要参数时才会进行求值。这意味着,如果没有 使用参数,那么它就一直没有实际值。


13.5 返回值


13.5.1 显式返回语句

函数的返回值通常是最后一个语句的值,但你可以通过 return() 语句提前返回一个值。我 们认为最好有节制地使用 return() 语句,因为提前返回的一般都是比较简单的情况。常见 的提前返回原因就是输入为空:

complicated_function <- function(x, y, z) {
 if (length(x) == 0 || length(y) == 0) {
 return(0)
 }
 # 这里是复杂的代码

需要提前返回的另一个原因是,if 语句的一个分支非常复杂,而另一个分支则特别简单。 例如,你可能写出如下的 if 语句:

f <- function() {
 if (x) {
 # 需要
 # 多行
 # 代码
 # 才能
 # 完成
 # 的
 # 操作
 # express
 } else {
 # 返回一个非常简单的值

但如果第一个分支中的代码非常长,到达 else 语句前,你可能就已经记不清 condition 了。解决这个问题的一种方法是将简单情形提前返回:

f <- function() {
 if (!x) {
 return(something_short)
 }
 # 需要
 # 多行
 # 代码
 # 才能
 # 完成
 # 的
 # 操作
 # express

这样应该会使得代码更容易理解,因为不需要太多的上下文。


13.5.2 使得函数支持管道


如果想要让自己的函数支持管道操作,那么你应该仔细思考一下返回值。可以支持管道操 作的函数有两种主要类型:转换函数与副作用函数。


转换函数会传入一个明确的“基本”对象作为第一个参数,对这个对象进行处理后,再将其返回。例如,在 dplyr 中,这个关键的对象就是数据框。如果能够确定在自己的领域内应该使用哪种数据类型,那么你就可以让自己的函数支持管道操作了。


副作用函数经常用来执行某种行为,比如绘图或保存文件,而不是转换对象。这些函数会 “悄悄地”返回第一个参数,因此,默认情况下,第一个参数不显示在输出中,但仍然可 以由管道操作使用。


13.6 环境


刚开始编写函数时,不需要对环境有多深入的理解。但我们还是应该了解一些关于环境的知识,因为这些知识对于理解函数如何运行非常重要。 函数的环境决定了 R 如何寻找对象的值。例如,查看以下函数:

f <- function(x) {
 x + y
}

在很多编程语言中,这段代码会引发一个错误,因为函数没有定义 y。这种代码在 R 中是有效的,因为 R 使用称为词法定界的一种规则来搜索对象的值。因为 y 没有在函数中进行 定义,所以 R 会在定义函数的环境中寻找 y:

y <- 100
f(10)
#> [1] 110
y <- 1000
f(10)
#> [1] 1010


相关文章
|
6月前
|
存储 JSON 数据格式
UCB Data100:数据科学的原理和技巧:第一章到第五章
UCB Data100:数据科学的原理和技巧:第一章到第五章
314 0
UCB Data100:数据科学的原理和技巧:第一章到第五章
|
人工智能 测试技术 Python
书籍:python数据科学傻瓜书 Python for Data Science For Dummies 2nd Edition - 2019
简介 学习Python编程和统计数据的快捷方法 Python是一种通用编程语言,创建于20世纪80年代后期 - 以Monty Python命名 - 成千上万的人用它来测试英特尔的微芯片,为Instagram提供动力,以及使用PyGame库构建视频游戏。
《R数据科学》学习笔记|Note10:使用stringr处理字符串(下)
《R数据科学》学习笔记|Note10:使用stringr处理字符串(下)
231 0
《R数据科学》学习笔记|Note10:使用stringr处理字符串(下)
《R数据科学》学习笔记|Note9:使用stringr处理字符串(上)
本周内容较多且杂,故分成上下两篇。正则表达式是从左到右来匹配一个字符串的。“Regular Expression”这个词太长了,我们通常使用它的缩写“regex”或者“regexp”。 正则表达式可以被用来替换字符串中的文本、验证表单、基于模式匹配从一个字符串中提取字符串等等。
173 0
《R数据科学》学习笔记|Note9:使用stringr处理字符串(上)
|
资源调度 索引
《R数据科学》学习笔记|Note16:使用purrr实现迭代(下)
《R数据科学》学习笔记|Note16:使用purrr实现迭代(下)
124 0
《R数据科学》学习笔记|Note16:使用purrr实现迭代(下)
|
大数据 数据处理
《R数据科学》学习笔记|Note6:使用tibble实现简单数据框
本系列为《R数据科学》(R for Data Science)的学习笔记。相较于其他R语言教程来说,本书一个很大的优势就是直接从实用的R包出发,来熟悉R及数据科学。更新过程中,读者朋友如发现错误,欢迎指正。如果有疑问,也可以在评论区留言或后台私信。希望各位读者朋友能学有所得!
548 0
《R数据科学》学习笔记|Note6:使用tibble实现简单数据框
|
数据可视化
《R数据科学》学习笔记|Note14:向量
《R数据科学》学习笔记|Note14:向量
273 0
《R数据科学》学习笔记|Note14:向量
《R数据科学》学习笔记|Note12:使用magrittr进行管道操作
《R数据科学》学习笔记|Note12:使用magrittr进行管道操作
168 0
《R数据科学》学习笔记|Note12:使用magrittr进行管道操作
《R数据科学》学习笔记|Note11:使用forcats处理因子
《R数据科学》学习笔记|Note11:使用forcats处理因子
150 0
《R数据科学》学习笔记|Note11:使用forcats处理因子
|
SQL 数据挖掘 定位技术
《R数据科学》学习笔记|Note8:使用dplyr处理关系数据
《R数据科学》学习笔记|Note8:使用dplyr处理关系数据
194 0
《R数据科学》学习笔记|Note8:使用dplyr处理关系数据