R中为dataframe现有列应用自定义函数生成新列的最优方法
问题需求
现有一个dataframe,其中字符类型列存储逗号分隔的数字字符串,例如1, 2, 3, 4,需要按行对该列的每个值应用自定义函数,将得到的结果作为新列存入df。
初始示例数据
A B str 1 1 1, 2, 5 1 2 NA 2 1 NA 2 2 1, 3
期望输出结果
A B str res 1 1 1, 2, 5 2 1 2 NA 0 2 1 NA 0 2 2 1, 3 1
自定义函数定义
getCounts <- function(str, x, y){ if (is.na(str)){ return(as.integer(0)) } vec <- as.integer(unlist(strsplit(str, ','))) count <- 0 for (i in vec) { if (i >= x & i <= y){ count <- count + 1 } } return(as.integer(count)) }
报错情况
最初尝试使用lapply实现行级计算,参考其他技术帖子认为该函数适配当前场景,但执行持续报错,对应代码如下:
df <- df %>% mutate(res = lapply(df$str, getCounts(df$str, 0, 2)))
报错信息
Error: Problem with `mutate()` input `res`. x missing value where TRUE/FALSE needed i Input `res` is `lapply(df$str, getCounts(df$str, 0, 2))`
当前方案与疑问
目前仅使用mapply的方式可正常运行,但不理解其工作原理,也想知道是否存在更优的实现方法,可运行的代码如下:
df <- df %>% mutate(res = mapply(getCounts, df$str, 0, 2))
问题解答
1. 原lapply写法报错的原因
原写法存在两个核心问题:
- 调用顺序错误:
lapply(X, FUN)的语法是第一个参数为待循环的向量,第二个参数为待调用的函数本身,而非函数执行后的结果。原代码相当于先一次性把整个df$str向量传给getCounts执行,而非逐行传入单个字符串值。 - 参数传递与判断逻辑不匹配:当整个
df$str向量传入getCounts时,is.na(str)会返回一个长度为4的逻辑向量,if判断要求输入单个布尔值,因此直接触发missing value where TRUE/FALSE needed的报错。
2. mapply的工作原理
mapply是多参数版本的sapply,核心作用是按位置同时遍历多个输入向量的元素,逐个传入指定函数执行:
- 代码中
mapply(getCounts, df$str, 0, 2)的逻辑为:df$str是长度为4的字符串向量,0和2是长度为1的常量向量,R会自动把短向量循环到和最长向量等长,等价于逐行执行getCounts(df$str[1], 0, 2)、getCounts(df$str[2], 0, 2)……直到遍历完所有行。 - 最终返回值为整型向量,刚好适配
mutate的列赋值要求,因此可正常运行。
3. 更优实现方案
方案1:修正后的lapply写法
如果一定要用lapply,需要把函数封装为只接收单条字符串值的形式,显式把x和y作为常量传入,同时注意lapply返回列表,需要转成向量才能赋值给列:
df <- df %>% mutate(res = lapply(df$str, getCounts, x = 0, y = 2) %>% unlist())
方案2:向量化改写函数(性能最优)
原getCounts用循环逐元素计数,效率较低,可以改成完全向量化的写法,避免逐行函数调用开销:
getCounts_vec <- function(str_vec, x, y) { res <- integer(length(str_vec)) non_na_idx <- which(!is.na(str_vec)) count_res <- str_vec[non_na_idx] %>% strsplit(',') %>% lapply(\(s) { vec <- as.integer(trimws(s)) # 移除字符串前后的空格,避免转整数异常 sum(vec >= x & vec <= y) }) %>% unlist() res[non_na_idx] <- count_res return(res) }
直接调用即可,数据量越大性能优势越明显:
df <- df %>% mutate(res = getCounts_vec(str, 0, 2))
方案3:purrr适配写法(和tidyverse生态兼容更好)
如果已经在使用dplyr管道语法,可以用purrr::map_int直接返回整型向量,不需要额外转格式:
library(purrr) df <- df %>% mutate(res = map_int(str, ~getCounts(.x, x = 0, y = 2)))
内容的提问来源于stack exchange,提问作者fastlanes
相关产品推荐
相关产品推荐

