3.2 控制流与自编函数:勘验流程自动化


3.2 控制流与自编函数:勘验流程自动化

本节摘要:当同一套勘验动作要重复几十次,就该把它沉淀成函数。本节讲 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 的赋值是"改值先复制",跨层传结果只有返回值一条正路。

本节要点回顾

  • 函数三要素:参数、函数体、末行即返回值
  • if 分支配 for 循环:传统的逐案勘验骨架
  • 向量化与分组汇总优先:142 次重复一行搞定
  • apply 家族:对每元素套函数的循环替代品
  • seq_along 防倒序:空向量下 1:length 会反向遍历

作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U