最简R语言教程(二)

基础语法篇-函数

R语言
基础语法
Author

Lee

Published

May 8, 2026

1 自定义函数

R 中,函数是执行特定任务的代码块,可以接受输入参数并返回输出结果。自定义函数可以帮助我们封装重复使用的代码,提高代码的可读性和维护性。

函数

  • 实际上是一段代码的封装,就可以复用,
  • 提供输入,执行函数体,返回输出,
  • 输入,由功能决定,必须都有才能往下计算,
  • 输出,你想要几个返回结果都行。

1.1 编写自定义函数

加入我们想设计一个函数,将百分制分数转化为三个级别(例如大于80分则评级为”优”),我们可以按照以下步骤来定义这个函数:

  1. 先写一个具体的输入,测试单个输入的结果,确保函数逻辑正确。
score <- 85

if (score > 80) {
  grade <- "优"
} else if (score > 60) {
  grade <- "良"
} else {
  grade <- "差"
}
grade
[1] "优"
  1. 定义函数主体:函数名 <- function(参数列表) { 函数体 },注意要写好注释,说明函数的功能、输入参数和输出结果。
convert_score_to_grade <- function(score){
  # 将百分制转化为三级制分数
  # 输入:1个百分制分数(double)
  # 输出:1个三级制分数(character)
}
  1. 将第1步调试好的代码体放入函数体
convert_score_to_grade <- function(score){
  # 将百分制转化为三级制分数
  # 输入:1个百分制分数(double)
  # 输出:1个三级制分数(character)
  
  if (score > 80) {
    grade <- "优"
  } else if (score > 60) {
    grade <- "良"
  } else {
    grade <- "差"
  }
  
  return(grade)
}

1.2 调用函数

convert_score_to_grade(score = 57)
[1] "差"

值得注意的是,在调用函数时,参数可以通过位置传递(positional argument)或命名传递(named argument):

  • 函数的输入参数可以有默认值,例如 function(score = 0),如果调用函数时没有提供参数值,则使用默认值。
  • 如果参数写为了参数名 =,则调用函数时可以通过命名传递参数值,例如 convert_score_to_grade(score = 85),这样可以提高代码的可读性。
  • 建议在定义函数时,尽量使用命名参数,并为参数提供默认值,这样可以使函数更灵活和易于使用。

1.3 函数的向量化改写

可以使用for循环来实现函数的向量化改写,但在R中,向量化操作通常更高效且更简洁。我们可以使用case_when()函数来实现向量化改写:

convert_score_to_grade_vectorized <- function(scores) {
  # 将百分制转化为三级制分数
  # 输入:一个百分制分数的向量(double)
  # 输出:一个三级制分数的向量(character)

  grades <- case_when(
    scores > 80 ~ "优",
    scores > 60 ~ "良",
    TRUE ~ "差"
  )

  return(grades)
}

# 测试向量化函数
convert_score_to_grade_vectorized(c(85, 75, 55))
[1] "优" "良" "差"

1.4 返回多个值

R 不支持直接返回多个值,只能将多个返回值打包成 1 个列表(如果可以,打包成 1 个数据框也行)。

# 计算均值和标准差
mean_std <- function(x) {
  n = length(x)
  mu = mean(x)
  std = sqrt(sum((x - mu)^2) / (n - 1))
  list(mean = mu, std = std)
}

x <- c(100, 62, 65, 34)
mean_std(x)
$mean
[1] 65.25

$std
[1] 27.0478

1.5 默认参数值

调用函数时,若不提供其实参,则按默认值。

mean_std_1 <- function(x, type = 1) {
  n = length(x)
  mu = mean(x)

  if (!type %in% c(1, 2)) {
    stop("Invalid type. Use 1 for population std and 2 for sample std.")
  }

  std = case_when(
    type == 1 ~ sqrt(sum((x - mu)^2) / n), # 总体标准差
    type == 2 ~ sqrt(sum((x - mu)^2) / (n - 1)), # 样本标准差
  )
  list(mean = mu, std = std)
}

# 测试默认参数值
mean_std_1(x) # 使用默认参数type = 1
$mean
[1] 65.25

$std
[1] 23.42408
mean_std_1(x, type = 2) # 指定参数type = 2
$mean
[1] 65.25

$std
[1] 27.0478
# mean_std_1(x, type = 3) # 无效参数值,触发错误

1.6 ...参数

...参数允许函数接受任意数量的参数,这些参数可以在函数体内通过list(...)来访问。这对于编写灵活的函数非常有用,特别是当你不确定需要多少参数时。例如,可以使用...参数来实现一个能够处理任意数量输入的函数。

my_sum <- function(...) {
  sum(...)
}
my_sum(1, 2, 3, 4)
[1] 10
Important
  • R 自带函数,直接使用;
  • R 包函数,加载 R 包或包名::函数()使用;
  • 多查阅函数帮助:?函数名

2 泛函式循环迭代

purrr包是一个非常强大的包,它提供了一种更简洁的方式来处理循环和迭代。它的核心思想是使用函数式编程的方式来处理数据,而不是使用传统的for循环。

循环迭代,本质上是将一个函数依次应用到序列1的每一个元素上,在purrr中,这个函数就是map()map()函数的基本语法为purrr::map(.x, .f, ...)

在最新的purrr中,为使逻辑更清晰一致,已经建议弃用map_df/map_dfr/map_dfc这些一步到位的操作,改用map+list_bind/list_cbind

2.1 修改多列

2.1.1 选择多列并调用单个函数

library(tidyverse)
df <- tibble(
  a = rnorm(10),
  b = rnorm(10),
  c = rnorm(10),
  d = rnorm(10)
)

df |>
  summarise(
    n = n(),
    a = median(a),
    b = median(b),
    c = median(c),
    d = median(d),
  )
# A tibble: 1 × 5
      n      a      b     c     d
  <int>  <dbl>  <dbl> <dbl> <dbl>
1    10 -0.132 -0.537 0.139 0.266

上述代码在计算df各列时,median()函数被重复执行了四次,意味着我们进行了多次的复制粘贴操作,这与“在代码中永远不要复制粘贴超过2次”的原则相违背。试想如果数据集中有10个以上的列,多次进行复制粘贴不仅繁琐,而且容易产生错误。幸好,在tidyverse中的acorss()函数就排上了用场。

across()是一个功能异常强大的函数,它有3个关键的参数:

  1. .cols,操作的目标列,可以是多列,where()which()everything()starts_with()等在此处同样适用。
  2. .fns,对目标列进行操作的函数,同样可以是多个函数,如果是多个函数,则需要以列表的形式传入。
  3. .names,控制输出列的名称,在与mutate()连用时非常有用。

那么上述代码可以使用across()进行改写。

df |>
  summarise(
    across(a:d, median)
  )
# A tibble: 1 × 4
       a      b     c     d
   <dbl>  <dbl> <dbl> <dbl>
1 -0.132 -0.537 0.139 0.266
Important

across()函数有两个使用注意点一定要牢记:

  1. across()不能单独使用,它必须在summarise()mutate()等函数中使用。
  2. across().fns传入函数时,函数后不需要加括号。如果函数中需要传入参数,则需要使用匿名函数。有关匿名函数,在下节中会有描述。

2.1.2 选择多列并调用多个函数

# 生成一个可操作的数据集
rnorm_na <- function(n, n_na, mean = 0, sd = 1) {
  sample(c(rnorm(n - n_na, mean = mean, sd = sd), rep(NA, n_na)))
} # 生成包含缺失值的正态分布数列,项数为n-n_na,其中有n-n_na项缺失值
df_miss <- tibble(
  a = rnorm_na(5, 1),
  b = rnorm_na(5, 1),
  c = rnorm_na(5, 2),
  d = rnorm(5)
)

# 计算df_miss各列的中数,计算时需要去掉缺失值
df_miss |>
  summarise(
    across(a:d, \(x) median(x, na.rm = TRUE))
  )
# A tibble: 1 × 4
       a      b     c      d
   <dbl>  <dbl> <dbl>  <dbl>
1 -0.199 -0.400 0.277 -0.580
Warning

上述代码的匿名函数写作了\(x) median(x, na.rm = TRUE),而我们更熟悉的写法是~median(.x, na.rm = TRUE)。Hadley之所以修改写法基于以下两个方面:

  1. .x的写法只适用于tidyverse函数内。
  2. .x有时指代比较抽象,不便理解。

基于此,Hadley建议使用\(x)的写法,例如~.x + 1建议写为\(x) x + 1

如果我们系统同时计算数据集中每列的中数并求和呢?这就需要调用两个函数。

df_miss |>
  summarise(
    across(
      a:d, # 一定要指定列
      list(
        median = \(x) median(x, na.rm = TRUE),
        sum = \(x) sum(x, na.rm = TRUE)
      )
    )
  )
# A tibble: 1 × 8
  a_median  a_sum b_median  b_sum c_median c_sum d_median  d_sum
     <dbl>  <dbl>    <dbl>  <dbl>    <dbl> <dbl>    <dbl>  <dbl>
1   -0.199 -0.991   -0.400 -0.923    0.277 0.327   -0.580 -0.388

上述代码输出的结果列的名称是使用一个类似于 {.col}_{.fn}glue 规范命名的,其中 .col 是原始列的名称,.fn 是函数的名称。我们可以使用 .names 参数来提供您自己的 glue 规范

df_miss |>
  summarise(
    across(
      a:d, # 一定要指定列
      list(
        median = \(x) median(x, na.rm = TRUE),
        sum = \(x) sum(x, na.rm = TRUE)
      ),
      .names = "{.fn}-{.col}"
    )
  )
# A tibble: 1 × 8
  `median-a` `sum-a` `median-b` `sum-b` `median-c` `sum-c` `median-d` `sum-d`
       <dbl>   <dbl>      <dbl>   <dbl>      <dbl>   <dbl>      <dbl>   <dbl>
1     -0.199  -0.991     -0.400  -0.923      0.277   0.327     -0.580  -0.388

.names参数在与mutate()连用时非常有用,通常用来对比新生成的列与原有列的不同。

# 求数据集的绝对值
df_miss |>
  mutate(
    across(a:d, \(x) abs(x), .names = "{.col}_abs")
  )
# A tibble: 5 × 8
        a       b      c      d   a_abs   b_abs  c_abs d_abs
    <dbl>   <dbl>  <dbl>  <dbl>   <dbl>   <dbl>  <dbl> <dbl>
1 -0.0292 -0.738  NA      1.06   0.0292  0.738  NA     1.06 
2  0.485  -1.38    0.434  0.997  0.485   1.38    0.434 0.997
3 -0.369   1.26   -0.384 -0.580  0.369   1.26    0.384 0.580
4 -1.08   NA       0.277 -0.726  1.08   NA       0.277 0.726
5 NA      -0.0612 NA     -1.14  NA       0.0612 NA     1.14 

2.2 行操作

across()summarize()mutate()匹配度很好,但都是针对列的操作,有没有针对filter()的行的操作呢?

dplyrif_any()if_all()两个函数功能就是across()针对行操作的变种。我们具体来看:

# 选择包含缺失值的所有行
x <- df_miss |>
  filter(
    if_any(a:d, is.na)
  )

# 选择全是缺失值的所有行
df_miss |>
  filter(
    if_all(a:d, is.na)
  )
# A tibble: 0 × 4
# ℹ 4 variables: a <dbl>, b <dbl>, c <dbl>, d <dbl>

这部分更详细的说明可参看:https://bookdown.org/wangminjie/R4DS/tidyverse-beauty-of-across2.html#dplyr-1.0.4-if_any-and-if_all。

2.3 在自定义函数中使用across()

expand_dates <- function(df) {
  df |>
    mutate(across(where(is.Date), list(year = year, month = month, day = mday)))
}

上述函数的作用为:将所有日期列扩展为年、月和日列:

df_date <- tibble(
  name = c("Amy", "Bob"),
  date = ymd(c("2009-08-03", "2010-01-16"))
)
df_date
# A tibble: 2 × 2
  name  date      
  <chr> <date>    
1 Amy   2009-08-03
2 Bob   2010-01-16
df_date |>
  expand_dates()
# A tibble: 2 × 5
  name  date       date_year date_month date_day
  <chr> <date>         <dbl>      <dbl>    <int>
1 Amy   2009-08-03      2009          8        3
2 Bob   2010-01-16      2010          1       16

across()还可以与非标准性评估(tidy eval)结合,实现给一个参数传入多个列的操作,只要注意将这些传入多个列的参数用两个大括号括起来。关于非标准性评估(tidy eval),可参看这里这里。还有我自己的blogTidyverse包中的across()函数

2.4 与pivot_longer()连用

pivot_longer()across()间有许多有趣的联系,它们可以实现相同的操作并得到一致的结果。我们看一个例子,例如我们需要计算df数据集每列的中数和众数。

  • 使用across():我们可以看到结果是一个宽表格,每列的名称的形式为”列名_函数名”。
df |>
  summarise(
    across(a:d, list(median = median, mean = mean))
  )
# A tibble: 1 × 8
  a_median  a_mean b_median b_mean c_median c_mean d_median d_mean
     <dbl>   <dbl>    <dbl>  <dbl>    <dbl>  <dbl>    <dbl>  <dbl>
1   -0.132 -0.0953   -0.537 -0.506    0.139 -0.116    0.266  0.267
long <- df |>
  pivot_longer(a:d) |>
  group_by(name) |>
  summarise(
    median = median(value),
    mean = mean(value)
  )
long
# A tibble: 4 × 3
  name  median    mean
  <chr>  <dbl>   <dbl>
1 a     -0.132 -0.0953
2 b     -0.537 -0.506 
3 c      0.139 -0.116 
4 d      0.266  0.267 
long |>
  pivot_wider(
    names_from = name,
    values_from = c(median, mean),
    names_vary = "slowest",
    names_glue = "{name}_{.value}"
  )
# A tibble: 1 × 8
  a_median  a_mean b_median b_mean c_median c_mean d_median d_mean
     <dbl>   <dbl>    <dbl>  <dbl>    <dbl>  <dbl>    <dbl>  <dbl>
1   -0.132 -0.0953   -0.537 -0.506    0.139 -0.116    0.266  0.267

这个操作在我们无法使用across()的时候非常有用:即当我们需要同时计算一组多列的时候。例如,数据框同时包含了数值列和权重列,我们需要计算加权平均值时(我理解其实就是数据不整洁的时候):

df_paired <- tibble(
  a_val = rnorm(10),
  a_wts = runif(10),
  b_val = rnorm(10),
  b_wts = runif(10),
  c_val = rnorm(10),
  c_wts = runif(10),
  d_val = rnorm(10),
  d_wts = runif(10)
)
df_paired
# A tibble: 10 × 8
     a_val a_wts   b_val  b_wts  c_val c_wts   d_val d_wts
     <dbl> <dbl>   <dbl>  <dbl>  <dbl> <dbl>   <dbl> <dbl>
 1  0.259  0.305 -0.0603 0.202  -0.652 0.771  0.493  0.883
 2 -0.218  0.482 -1.17   0.371   1.31  0.241 -0.0941 0.815
 3 -0.0594 0.662  0.747  0.200  -1.06  0.815  0.279  0.949
 4 -0.806  0.246 -0.0612 0.153   1.16  0.132  0.375  0.155
 5 -0.637  0.349  0.805  0.857  -0.253 0.679 -1.98   0.607
 6  0.0821 0.808 -0.371  0.826  -0.500 0.817 -0.327  0.530
 7 -0.248  0.867  1.66   0.0765  2.34  0.207  0.936  0.898
 8 -1.14   0.281  0.816  0.640   2.40  0.276  1.05   0.699
 9  1.48   0.311  2.55   0.280   1.00  0.460  0.108  0.957
10  0.327  0.988  0.547  0.426   0.873 0.527  0.645  0.365

df_paired数据框是不整洁的,我们无法使用across()函数,此时我们需要进行转换:

df_long <- df_paired |>
  pivot_longer(
    everything(),
    names_to = c("group", ".value"),
    names_sep = "_"
  )
df_long
# A tibble: 40 × 3
   group     val   wts
   <chr>   <dbl> <dbl>
 1 a      0.259  0.305
 2 b     -0.0603 0.202
 3 c     -0.652  0.771
 4 d      0.493  0.883
 5 a     -0.218  0.482
 6 b     -1.17   0.371
 7 c      1.31   0.241
 8 d     -0.0941 0.815
 9 a     -0.0594 0.662
10 b      0.747  0.200
# ℹ 30 more rows
df_long |>
  group_by(group) |>
  summarise(mean = weighted.mean(val, wts)) |>
  pivot_wider(
    names_from = group,
    values_from = mean,
    names_glue = "{group}_{.value}"
  )
# A tibble: 1 × 4
   a_mean b_mean c_mean d_mean
    <dbl>  <dbl>  <dbl>  <dbl>
1 -0.0323  0.415  0.119  0.178

Footnotes

  1. 序列由一系列可以根据位置索引的元素构成,向量、列表、数据框都是经常用作序列的数据结构。↩︎