R基础回顾(二):函数、条件语句、循环、apply家族
好,我们继续往下走。上一章我们把R的数据结构和基本操作捋了一遍。说实话,那些是砖块。这一章咱们讲的,就是水泥和钢筋——怎么把这些砖块组合起来,建成真正能用的东西。
我个人一直觉得,写R代码的核心就两件事:一是控制流程,二是重复劳动自动化。条件语句控制怎么走,循环和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)
})
- 先看有没有纯向量化函数(如
rowMeans) - 没有的话用
apply/lapply - 实在搞不定复杂的条件逻辑,才考虑for循环
为什么我偏爱apply?你想想看,for循环的每一步都要手动管理迭代变量、处理边界条件。而apply家族的代码更短、更清晰。读起来就像是在说:「对这个矩阵的每一行,计算t检验的p值。」——干净利落。
还有一个细节:vapply 是 sapply 的「安全版」。它要求你指定返回值类型,运行时就会检查。我用过很多次 sapply 偷懒,结果某天数据变了,返回值类型变了,脚本就报错了。从那以后,在正式的pipeline里我尽量用 vapply。
# sapply - 自动简化,有时候会"猜错"
sapply(expr_df, mean) # 如果全是数值,返回向量
# vapply - 明确指定返回类型
vapply(expr_df, mean, numeric(1)) # 强制返回数值向量