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

13.jpg

写在前面

本系列为《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 代码风格

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

左大括号不应该自己占一行,而且后面要换行。右大括号应该自己占一行,除非后面跟着 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() 函数中,数据是 xy,检验的细节则是 alternativemupairedvar. equal 以及 conf.level 等设置。
  • str_c() 函数中,你可以向 ... 参数提供任意数量的字符串作为数据,连接的细节则由 sepcollapse 参数控制。

通常情况下,数据参数应该放在最前面,细节参数则放在后面,而且一般都有默认值。设置默认值的方式与使用命名参数调用函数的方式是一样的:

# 使用近似正态分布计算均值两端的置信区间
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处理因子

《R数据科学》学习笔记|Note10:使用stringr处理字符串(下)

《R数据科学》学习笔记|Note9:使用stringr处理字符串(上)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容