R基础回顾(二):函数、条件语句、循环、apply家族

好,我们继续往下走。上一章我们把R的数据结构和基本操作捋了一遍。说实话,那些是砖块。这一章咱们讲的,就是水泥和钢筋——怎么把这些砖块组合起来,建成真正能用的东西。

我个人一直觉得,写R代码的核心就两件事:一是控制流程,二是重复劳动自动化。条件语句控制怎么走,循环和apply帮你甩掉重复的体力活。这一章过后,你写的代码就能从「脚本」升级成「程序」了。

R基础回顾(二):核心知识体系 程序控制与自动化 自定义函数 条件逻辑 循环结构 参数传递与返回值 作用域与词法闭包 if / else / ifelse 向量化 for / while / repeat 高效迭代:apply家族 🎯 核心原则:能用apply 就不用显式循环 向量化优先于遍历

自定义函数:把你的逻辑装进盒子里

函数是什么?说白了,就是把一串操作打包成一个名字。以后你只要叫这个名字,就能重复执行这串操作。我在做差异表达分析的时候,经常需要反复对不同的基因子集做同样的统计检验。写个函数,一次定义,到处调用,省心得多。

R里定义一个函数特别简单:

# 最基本的形式
my_function <- function(参数1, 参数2 = 默认值) {
  # 函数体
  计算结果
  return(结果)
}

# 举个例子:计算基因表达量的效应大小(Cohen's d)
cohens_d <- function(group1, group2) {
  mean_diff <- mean(group1) - mean(group2)
  pooled_sd <- sqrt((sd(group1)^2 + sd(group2)^2) / 2)
  d <- mean_diff / pooled_sd
  return(d)
}
小提示:我个人习惯在函数开头多做一步输入检查。比如用 stopifnot(is.numeric(group1)) 确保传进来的参数是数值。这个习惯救过我很多次——特别是当你把函数丢给合作者用的时候。

条件语句:让代码学会做选择

代码不应该是死板的流水线。你需要它根据不同的数据做出不同的反应。这就是条件语句的活儿。

基本的 if / else 结构:

# 单分支
if (p_value < 0.05) {
  cat("差异显著")
}

# 双分支
if (log2FC > 1) {
  status <- "上调"
} else if (log2FC < -1) {
  status <- "下调"
} else {
  status <- "无变化"
}

但这里有个坑,我必须要说。R的 if 语句只接受单个逻辑值。如果你的判断对象是个向量——比如一个包含了1000个基因的p值向量——直接用 if (p_values < 0.05) 就会报错。

⚠ 不要这样写:
if (p_values < 0.05) {  # 报错:条件长度 > 1
  print("显著")
}

怎么办?用 ifelse()——它是向量化的条件判断:

# 向量化条件判断
results <- ifelse(p_values < 0.05, "显著", "不显著")

# 更复杂的嵌套
status <- ifelse(log2FC > 1 & p_values < 0.05, "上调显著",
           ifelse(log2FC < -1 & p_values < 0.05, "下调显著",
                  "无显著变化"))

你看,一行代码处理了上万个基因。性能比用for循环一个个判断好太多了。

循环结构:批量操作的经典方案

R语言里有三种主要的循环方式:for、while 和 repeat。说实话,我90%的情况下只用 for。

# for循环 - 最常用
for (i in 1:10) {
  print(i^2)
}

# 在数据框上循环
for (col_name in colnames(expr_matrix)) {
  mean_expr <- mean(expr_matrix[[col_name]], na.rm = TRUE)
  cat(col_name, "的平均表达量:", mean_expr, "\n")
}

不过我得提醒你一句:R的for循环效率不高。特别是当循环次数上万时,速度会明显变慢。这是R语言的一个特点——它本质上是向量化语言。

核心原则:能用向量化操作就别用循环。先想一下有没有现成的函数(rowMeans、colSums等),再考虑写for循环。

如果实在要用循环,我建议你这样做:

# 1. 预分配结果空间,不要动态扩充
results <- vector("list", length = nrow(gene_data))

# 2. 循环体内只做必要操作
for (i in seq_len(nrow(gene_data))) {
  results[[i]] <- some_analysis(gene_data[i, ])
}

# 3. 循环外合并结果
final_df <- do.call(rbind, results)

我曾经犯过一个错误:在循环里用 rbind() 一点点拼接结果。数据量小还感觉不出来,等处理全基因组数据的时候……嗯,我泡了杯咖啡回来它还在跑。

apply家族:告别显式循环

这才是R语言循环的真功夫。apply家族的核心理念是把「循环哪个维度」和「每次做什么」分开。你只需要告诉它:沿着哪个轴,用哪个函数。

函数 作用对象 返回结果 典型场景
apply() 矩阵/数组 向量/矩阵/列表 逐行或逐列计算
lapply() 列表/数据框 列表 对每列做相同处理
sapply() 列表/数据框 简化后的向量/矩阵 lapply的"自动简化"版
vapply() 列表/数据框 指定类型的向量 比sapply更安全
tapply() 向量 + 分组因子 数组 分组统计(堪比SQL的group by)
mapply() 多个向量/列表 向量/列表 多参数版本的lapply

来看看实战场景。假设我们有一个表达矩阵,想要对每个基因(每行)做t检验:

# 模拟数据:100个基因,每组6个样本
expr_matrix <- matrix(rnorm(1200), nrow = 100, ncol = 12)
group <- factor(rep(c("control", "treatment"), each = 6))

# 用apply逐行做t检验 —— 不用写任何循环
t_results <- apply(expr_matrix, 1, function(row) {
  t.test(row[1:6], row[7:12])$p.value
})

# 用lapply对数据框的每一列做归一化
normalized <- lapply(expr_df, function(col) {
  (col - mean(col, na.rm = TRUE)) / sd(col, na.rm = TRUE)
})
我的选择优先级:
  1. 先看有没有纯向量化函数(如 rowMeans)
  2. 没有的话用 apply / lapply
  3. 实在搞不定复杂的条件逻辑,才考虑for循环

为什么我偏爱apply?你想想看,for循环的每一步都要手动管理迭代变量、处理边界条件。而apply家族的代码更短、更清晰。读起来就像是在说:「对这个矩阵的每一行,计算t检验的p值。」——干净利落。

还有一个细节:vapply 是 sapply 的「安全版」。它要求你指定返回值类型,运行时就会检查。我用过很多次 sapply 偷懒,结果某天数据变了,返回值类型变了,脚本就报错了。从那以后,在正式的pipeline里我尽量用 vapply。

# sapply - 自动简化,有时候会"猜错"
sapply(expr_df, mean)  # 如果全是数值,返回向量

# vapply - 明确指定返回类型
vapply(expr_df, mean, numeric(1))  # 强制返回数值向量