美文网首页R数据科学
《R数据科学》学习笔记|Note13:函数

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

作者: 木舟笔记 | 来源:发表于2021-03-14 19:46 被阅读0次
    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处理字符串(上)

    相关文章

      网友评论

        本文标题:《R数据科学》学习笔记|Note13:函数

        本文链接:https://www.haomeiwen.com/subject/xolccltx.html