20  函数

20.1 向量函数

20.1.1 自定义函数把百分制分数转化为五分制分数的功能

第一步,分析输入和输出,设计函数外形

  • 输入有几个,分别是什么,适合用什么数据类型存放?

  • 输出有几个,分别是什么,适合用什么数据类型存放?

这个问题的输入有1个:百分制分数,数值型;输出有1个:五分制分数,字符串。在此基础设计自定义函数的外形。

Score_Conv <- function(score) {
  # 实现将一个百分制分数转化为五级分数
  # 输入参数:score为数值型,百分制分数
  # 返回值:res为字符串型,五分制分数
  ...
}

第二步,梳理功能实现的过程

  • 分解问题+实例梳理+翻译及调试

  • 拿一组本例的具体形参的值作为输入,如何将76分转换为五分制分数”良”,这依赖于对五级分数界限的选取,选定后可以使用分支判断实现。逐步调试,得到正确的返回值结果,这步很关键,是实现下一步复杂功能的基础。

library(tidyverse)
score <- 76
if (score >= 90) {
  res <- "优"
} else if (score >= 80) {
  res <- "良"
} else if (score >= 70) {
  res <- "中"
} else if (score >= 60) {
  res <- "及格"
} else {
  res <- "不及格"
}
res
[1] "中"

第三步,将第二部的代码封装成为函数体

Score_Conv <- function(score) {
  if (score >= 90) {
    res <- "优"
  } else if (score >= 80) {
    res <- "良"
  } else if (score >= 70) {
    res <- "中"
  } else if (score >= 60) {
    res <- "及格"
  } else {
    res <- "不及格"
  }
  res
}

在函数编制好后,我们可以尝试调用它

Score_Conv(76)
[1] "中"

20.1.2 改进函数-函数向量化

目前的函数仅能输入一个参数,在实际下通常需要输入多个参数。我们有两种方法实现

方法1:直接修改自定义函数

输入的参数为一个数值向量,对函数体进行修改。

Score_Conv2 <- function(score) {
  n <- length(score)
  res <- vector("character", n)
  for (i in 1:n) {
    if (score[i] >= 90) {
      res[i] <- "优"
    } else if (score[i] >= 80) {
      res[i] <- "良"
    } else if (score[i] >= 70) {
      res[i] <- "中"
    } else if (score[i] >= 60) {
      res[i] <- "及格"
    } else {
      res[i] <- "不及格"
    }
  }
  res
}

# 测试函数
Score_Conv2(c(35, 67, 100))
[1] "不及格" "及格"   "优"    

方法2:直接使用map系列函数

得益于purrr中的map系列函数,我们可以将一个函数批量用在一些列元素中,从而达到不修改原函数而实现向量化操作。

scores <- c(35, 67, 100)
map_chr(scores, Score_Conv)
[1] "不及格" "及格"   "优"    

20.1.3 返回多个处理值

如果需要返回多个处理值,则需要将多个值大包成一个列表(或数据框)再返回。

# 自定义函数,实现计算一个数值型向量的均值和标准差。
MeanStd <- function(x) {
  mu <- mean(x)
  std <- sqrt(sum((x - mu)^2) / (length(x) - 1))
  paste(paste("均值为", mu)," ", paste("标准差为", std))
}

MeanStd(c(2, 4, 6, 9, 12))
[1] "均值为 6.6   标准差为 3.97492138287036"

20.1.4 ...参数

一般函数参数只接受一个对象,比如对两个数加和的函数,给它 3 个数加和就会报错

...参数可以接受多个对象,并将其打包为一个列表传递给函数体

my_sum2 <- function(...){
  sum(...)
}
my_sum2(1, 2, 3, 4, 5, 6)
[1] 21
注意

r中常用的概率函数中,不同前缀的含义分别如下:

  • d:密度函数(density)

  • p:分布函数(distribution)

  • q:分位数函数(quantile)

  • r:随机数函数(random)

上述四个字母+分布缩写,就构成通常使用的概率函数

dnorm(3, 0, 2)  # 正态分布N(0, 4)在3处的密度值
[1] 0.0647588
rnorm(5, 0, 1)  # 生成5个服从N(0, 1)分布的随机数
[1] -0.8483817 -0.2601649 -0.4752770  1.5847744  1.6659338

常用概率分布-1常用概率分布-2

20.2 数据框函数

当我们需要重复使用dplyr函数时,就可以考虑编写一个数据框函数。它们以数据框作为第一个参数,后面跟着一些额外的参数用于说明如何处理,并输出一个数据框或向量。

20.2.1 简洁引用和整洁计算

dplyr 默认直接捕获函数参数中传入的变量名,而不是评估新传入的参数名,简单来讲就是将函数的参数作为变量引用了,这就是“间接引用”。它产生的原因是 dplyr 采取“整洁求值”(tidy evaluation)的规则,本意是方便我们在数据框中直接引用变量名而无需特别处理,但在封装成函数时却成了绊脚石。

针对这个问题,dplyr提供了解决方案,称为 embracingembracing 将变量包裹在双层大括号中,例如 var 写成 {var},意为使用参数中的值,而不是把参数本身当作变量名。在使用以下几类函数时需要使用 embracing

# embracing
df <- tibble(
  mean_var = 1,
  group_var = "g",
  group = 1,
  x = 10,
  y = 100
)

group_mean <- function(df, group_var, mean_var) {
  df |>
    group_by(pick({{ group_var }})) |> # group_by作为参数而不是变量名
    summarise(
      mean_value = mean({{ mean_var }}, na.rm = TRUE)
    )
}

df |>
  group_mean(group, x)
# A tibble: 1 × 2
  group mean_value
  <dbl>      <dbl>
1     1         10
# 整洁应用和数据掩码
count_missing <- function(df, group_vars, x_var) {
  df |>
    group_by(pick({{ group_vars }})) |>
    summarise(
      n_miss = sum(is.na({{ x_var }})),
      .groups = "drop"
    )
}
flights |>
  count_missing(c(year, month, day), dep_time)
# A tibble: 365 × 4
    year month   day n_miss
   <int> <int> <int>  <int>
 1  2013     1     1      4
 2  2013     1     2      8
 3  2013     1     3     10
 4  2013     1     4      6
 5  2013     1     5      3
 6  2013     1     6      1
 7  2013     1     7      3
 8  2013     1     8      4
 9  2013     1     9      5
10  2013     1    10      3
# ℹ 355 more rows

20.3 绘图函数

ggplot()函数中的aes() 同样是一个数据掩码函数(data-masking function),所以技巧大差不差。

20.3.1 与 tidyverse 结合

高效的绘图函数一般都将数据处理和 ggplot2 相结合。

sort_bars <- function(df, var){
  df |> 
    # :=替代 =,用于指定动态列名
    mutate({{var}} := fct_rev(fct_infreq({{var}}))) |> 
    ggplot(aes(y = {{var}}))+
    geom_bar()
}
diamonds |> 
  sort_bars(clarity)

提示

在 R 语言中,:= 是 动态列名赋值运算符,核心用途是「在不提前明确列名(如用变量、表达式指定列名)的场景下,创建或修改数据框的列」。

20.3.2 绘图函数中的图表

如果在直方图中自动标注变量名和bin的宽度岂不美哉?要实现这个功能,我们需要使用rlang::englue(),此函数:

  • {}包裹的值插入字符串。
  • 使用{{}}识别并插入变量名。
histogram <- function(df, var, binwidth = NULL) {
  label <- rlang::englue("变量{{var}}的直方图,bin宽度为{binwidth}")

  df |>
    ggplot(aes(x = {{ var }})) +
    geom_histogram(binwidth = binwidth) +
    labs(title = label)
}


diamonds |>
  histogram(carat, 0.1)

20.4 代码风格规范

虽然函数或参数命名的规范性不会影响R对其的执行,但恰当的命名对代码的可读性至关重要。理想的函数名应当简洁明了,能准确传达函数的功能。

通常而言,函数名宜采用动词,参数名宜采用名词。当然也有例外,比如某些约定成俗的名词,如均值函数mean()就比compute_mean()更合适。开发者应当灵活判断,大胆命名。此外,还有两点需格外注意:

  • function()后必须紧跟花括号{},且函数体需缩进两个空格。

  • 建议在{ }内部添加额外空格(如{ color }),能显著提醒读者此处存在特殊语法操作。