本节摘要:当同一套勘验动作要重复几十次,就该把它沉淀成函数。本节讲 if 与 for 的基本控制流、函数的定义与返回,更重要的主张是:R 里优先用向量化与分组汇总代替循环,循环只留给真正的逐案场景。
假设要对多个国家分别计算寿命年均增速。逐国复制粘贴代码是新手写法;正确做法是把"算增速"封装成函数:
# 定义:输入数值向量,输出年均增速 growth_rate <- function(values, years) { first <- values[1] last <- values[length(values)] span <- years[length(years)] - years[1] (last / first)^(1 / span) - 1 } # 调用:挪威半个多世纪的寿命年均增速 growth_rate(norway$lifeExp, norway$year)
函数定义的三要素:参数(输入口)、函数体(勘验动作)、返回值(默认最后一行表达式的结果,return 一般省略)。
# 分支:按增速快慢给结论 rate <- growth_rate(norway$lifeExp, norway$year) if (rate > 0.01) { verdict <- "快速增长" } else if (rate > 0.003) { verdict <- "平稳改善" } else { verdict <- "基本停滞" } # 循环:对一批国家逐一结案 countries <- c("Norway", "China", "Nigeria") for (cty in countries) { d <- gapminder_long[gapminder_long$country == cty, ] cat(cty, "年均增速:", round(growth_rate(d$lifeExp, d$year), 4), "\n") }
上面那个 for 循环,在 R 里通常被一次分组汇总替代:
library(dplyr) gapminder_long %>% group_by(country) %>% summarize( rate = (last(lifeExp) / first(lifeExp))^(1 / (last(year) - first(year))) - 1, .groups = "drop" ) %>% arrange(desc(rate))
一行链式代码完成 142 个国家的批量结案,速度与可读性双赢。同理,apply 家族(sapply、lapply、vapply)是"对每个元素套同一个函数"的循环替代品:
# 对每个国家列各求均值——不需要写循环 sapply(gapminder_wide[, 2:5], mean, na.rm = TRUE)
| 手法 | 适用 | 注意 |
|---|---|---|
| 向量化运算 | 对整列做同一变换 | R 的第一选择 |
| 分组汇总 | 按组各自计算统计量 | 长表最顺 |
| for / apply | 逐案处理、前后依赖、函数本身不向量化 | 结果先建容器再填充 |
# 循环填容器的正确姿势:先开好房间再住人 results <- numeric(length(countries)) for (i in seq_along(countries)) { results[i] <- growth_rate(...) }
用 seq_along 而不是 1:length(x):当 x 长度为 0 时,后者会产生 1 到 0 的倒序序列,循环反着跑一遍——经典隐性事故。
💡 关键直觉:写循环前先问一句"这个函数能不能整表喂进去"。能向量化就向量化,不能的再写循环;for 循环在 R 里是最后手段,不是默认手段。
把 growth_rate 升级成"能挡脏输入"的生产版本,三个升级点逐一说明:
growth_rate2 <- function(values, years, min_span = 5) { # 升级一:输入校验——错误越早暴露,代价越小 if (length(values) != length(years)) stop("values 与 years 长度不一致:", length(values), " 对 ", length(years)) if (anyNA(values) || anyNA(years)) stop("输入含缺失值,请先清洗") # 升级二:默认参数——常用口径写成缺省,特殊案情再覆盖 span <- years[length(years)] - years[1] if (span < min_span) warning("时间跨度仅 ", span, " 年,增速估计不稳定") # 升级三:边界防御——首值为 0 或负数时幂运算无意义 if (values[1] <= 0) return(NA_real_) (values[length(values)] / values[1])^(1 / span) - 1 } # 试跑三条:正常、脏输入、短跨度 growth_rate2(c(44, 55, 65), c(1952, 1972, 1982)) growth_rate2(c(44, NA, 65), c(1952, 1972, 1982)) # 报错:输入含缺失值 growth_rate2(c(44, 46), c(1980, 1982)) # 警告:跨度不足
stop 与 warning 的分工要记牢:不可恢复的问题用 stop 当场拦截,可疑但能继续的用 warning 记录在案。作用域方面,函数内部创建的变量出了函数就消失,函数只能通过返回值向外传证据——这个"只出不进"的纪律保证了函数可以放心复用。
口说无凭,实测一次向量化与循环的差距。任务:对十万个测量值同乘系数再取对数:
x <- runif(1e5, 1, 100) # 循环版 system.time({ out1 <- numeric(length(x)) for (i in seq_along(x)) out1[i] <- log(x[i] * 2) }) # 向量化版 system.time({ out2 <- log(x * 2) }) identical(out1, out2) # TRUE,结果完全一致
典型结果:循环版耗时约为向量化版的几十倍。差距的来源是 R 的每次循环迭代都有解释开销,而向量化运算下沉到编译好的 C 层整块执行。日常数据量小时差距无感,但数据上了百万行,"改写法"就是最便宜的性能优化。
⚠️ 排错实录:新手常在函数里写
df$rate <- ...想把结果写回外部数据框——函数内的 df 是副本,改的是副本,外面的原表纹丝不动。R 的赋值是"改值先复制",跨层传结果只有返回值一条正路。