写在前面
本系列为《R数据科学》(R for Data Science)的学习笔记。相较于其他R语言教程来说,本书一个很大的优势就是直接从实用的R包出发,来熟悉R及数据科学。更新过程中,读者朋友如发现错误,欢迎指正。如果有疑问,也可以后台私信。希望各位读者朋友能学有所得!
函数
[TOC]
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
往期:
《R数据科学》学习笔记|Note12:使用magrittr进行管道操作
《R数据科学》学习笔记|Note11:使用forcats处理因子
网友评论