如何优化基于mapply和向量化的R数据匹配代码性能?
企业绩效数据关联的性能优化问题
我有一张包含企业绩效数据的DATASET表,表中有多行数据。需要将每行数据与另一张LOOKUPTABLE表中各部门的数据进行关联——LOOKUPTABLE包含一个字符串列和两个表示数值范围的数值列。关联的难点在于:DATASET中用于关联的列因部门而异,部分部门甚至不使用某些列,且部门的关联列可能随时间变化。因此我创建了COLUMN_MAP映射表,用于定义各部门的数据关联列。
我已经编写了两个可运行但速度较慢的代码:一个用mapply,另一个基于outer()和向量化,但两者的性能都没有达到预期。请帮忙优化这两段代码的性能。
方案1(使用mapply)
LOOKUPTABLE <- data.frame ( DEPARTMENT_NAME = c("LOGISTICS", "LOGISTICS", "LOGISTICS", "LOGISTICS", "VEHICLES", "VEHICLES", "ACCOUNTING", "ACCOUNTING"), RANGEFROM = c(1, 1, 4, 4, 1, 4, NA, NA), RANGETILL = c(4, 4, 9, 9, 4, 9, NA, NA), STRINGVALUE = c("A", "B", "A", "B", "", "", "A", "B"), RESULT = c(11, 12, 13, 14, 15, 16, 17, 18) ) COLUMN_MAP <- data.frame ( DEPARTMENT_NAME = c("LOGISTICS", "VEHICLES", "ACCOUNTING"), COLUMNNAME_NUMERIC = c("INTERNAL_num", "INTERNAL_num", NA), COLUMNNAME_STRING = c("INTERNAL_string", NA, "INTERNAL_string") ) DATASET <- data.frame ( INTERNAL_num = rep(1:8, 100), INTERNAL_string = rep(c("B", "A", "B", "B"), 200) ) tic() Function_result_join <- function(LOOKUPTABLE_rows, COLUMN_MAP_rows, DATASET) { DATASET2<<-DATASET RESULT<-list() for( i in 1:nrow(DATASET2)) { a <<- LOOKUPTABLE_rows a <<- if(!is.na(COLUMN_MAP_rows[1,"COLUMNNAME_NUMERIC"])) a[!is.na(a$RANGEFROM) & a$RANGEFROM<=as.numeric(DATASET2[i,which( colnames(DATASET2) == COLUMN_MAP_rows[1,"COLUMNNAME_NUMERIC"])]) & a$RANGETILL>as.numeric(DATASET2[i,which( colnames(DATASET2) == COLUMN_MAP_rows[1,"COLUMNNAME_NUMERIC"])])],] else a a <<- if(!is.na(COLUMN_MAP_rows[1,"COLUMNNAME_STRING"])) a[!is.na(a$STRINGVALUE) & a$STRINGVALUE==DATASET2[i,which( colnames(DATASET2) == COLUMN_MAP_rows[1,"COLUMNNAME_STRING"])],] else a RESULT[i] <- ifelse((nrow(a)==0 || is.na(sum(a$RESULT))),1,a$RESULT) } return(as.numeric(RESULT)) } LOOKUPTABLE_LIST <- split(LOOKUPTABLE,LOOKUPTABLE$DEPARTMENT_NAME) COLUMN_MAP_LIST <- split(COLUMN_MAP,COLUMN_MAP$DEPARTMENT_NAME) match_result <- as.data.frame(mapply(Function_result_join, LOOKUPTABLE_LIST, COLUMN_MAP_LIST, MoreArgs = list(DATASET=DATASET))) final_result_multiplied <- apply(match_result,1,prod) cbind(DATASET,final_result_multiplied) toc()
方案2(使用outer()和向量化)
LOOKUPTABLE <- data.frame ( DEPARTMENT_NAME = c("LOGISTICS", "LOGISTICS", "LOGISTICS", "LOGISTICS", "VEHICLES", "VEHICLES", "ACCOUNTING", "ACCOUNTING"), RANGEFROM = c(1, 1, 4, 4, 1, 4, NA, NA), RANGETILL = c(4, 4, 9, 9, 4, 9, NA, NA), STRINGVALUE = c("A", "B", "A", "B", "", "", "A", "B"), RESULT = c(11, 12, 13, 14, 15, 16, 17, 18) ) COLUMN_MAP <- data.frame ( DEPARTMENT_NAME = c("LOGISTICS", "VEHICLES", "ACCOUNTING"), COLUMNNAME_NUMERIC = c("INTERNAL_num", "INTERNAL_num", NA), COLUMNNAME_STRING = c("INTERNAL_string", NA, "INTERNAL_string") ) DATASET <- data.frame ( INTERNAL_num = rep(1:8, 100), INTERNAL_string = rep(c("B", "A", "B", "B"), 200) ) tic() DATASET_stack <- stack(DATASET) DATASET_stack$rowid <- rep(1:nrow(DATASET)) indextable <- data.frame(rowid=rep(1:nrow(DATASET)), DEPARTMENT_NAME=rep(COLUMN_MAP$DEPARTMENT_NAME,each=nrow(DATASET)), index=1:(nrow(COLUMN_MAP)*nrow(DATASET))) NUMERIC_columnnames <- data.frame(ind = rep(COLUMN_MAP$COLUMNNAME_NUMERIC,each=nrow(DATASET)), rowid=rep(1:nrow(DATASET)), DEPARTMENT_NAME=rep(COLUMN_MAP$DEPARTMENT_NAME,each=nrow(DATASET)), index=1:(nrow(COLUMN_MAP)*nrow(DATASET))) STRING_columnnames <- data.frame(ind = rep(COLUMN_MAP$COLUMNNAME_STRING,each=nrow(DATASET)), rowid=rep(1:nrow(DATASET)), DEPARTMENT_NAME=rep(COLUMN_MAP$DEPARTMENT_NAME,each=nrow(DATASET)), index=1:(nrow(COLUMN_MAP)*nrow(DATASET))) NUMERIC_values <- merge(x = NUMERIC_columnnames, y = DATASET_stack, by = c("ind","rowid"), all.x = TRUE, sort=FALSE) STRING_values <- merge(x = STRING_columnnames, y = DATASET_stack, by = c("ind","rowid"), all.x = TRUE, sort=FALSE) NUMERIC_values<-NUMERIC_values[order(NUMERIC_values$index), ] STRING_values<-STRING_values[order(STRING_values$index), ] NUMERIC_values$required <- (!is.na(NUMERIC_values$ind)) STRING_values$required <- (!is.na(STRING_values$ind)) FunEqual <- function(lookuptable,lookupvalue) (ifelse(is.na(lookuptable) || lookuptable=="", TRUE, (lookuptable==lookupvalue))) FunSmaller <- function(lookuptable,lookupvalue) (ifelse(is.na(lookuptable) || lookuptable=="", TRUE, (lookuptable<=lookupvalue))) FunLarger <- function(lookuptable,lookupvalue) (ifelse(is.na(lookuptable) || lookuptable=="", TRUE, (lookuptable>lookupvalue))) FunRequired <- function(lookuptable,lookupvalue) (lookupvalue) FunResult <- function(lookuptable,lookupvalue) (lookuptable) VecFunEqual <- Vectorize( FunEqual ) VecFunLarger <- Vectorize( FunLarger ) VecFunSmaller <- Vectorize( FunSmaller ) VecFunRequired <- Vectorize( FunRequired ) VecFunResult <- Vectorize( FunResult ) match_STRING_ind_required <- outer(LOOKUPTABLE$DEPARTMENT_NAME, STRING_values$required, VecFunRequired ) match_STRING_DEPARTMENT_NAME <- outer(LOOKUPTABLE$DEPARTMENT_NAME, STRING_values$DEPARTMENT_NAME, VecFunEqual ) match_STRING_value <- outer(LOOKUPTABLE$STRINGVALUE, STRING_values$values, VecFunEqual ) match_STRING_filter <- ifelse(!match_STRING_ind_required,TRUE,match_STRING_DEPARTMENT_NAME*match_STRING_value) match_NUMERIC_ind_required <- outer(LOOKUPTABLE$DEPARTMENT_NAME, NUMERIC_values$required, VecFunRequired ) match_NUMERIC_DEPARTMENT_NAME <- outer(LOOKUPTABLE$DEPARTMENT_NAME, NUMERIC_values$DEPARTMENT_NAME, VecFunEqual ) match_NUMERIC_valuefrom <- outer(LOOKUPTABLE$RANGEFROM, NUMERIC_values$values, VecFunSmaller ) match_NUMERIC_valuetill <- outer(LOOKUPTABLE$RANGETILL, NUMERIC_values$values, VecFunLarger ) match_NUMERIC_filter <- ifelse(!match_NUMERIC_ind_required,TRUE,match_NUMERIC_DEPARTMENT_NAME*match_NUMERIC_valuefrom*match_NUMERIC_valuetill) match_result <- outer(LOOKUPTABLE$RESULT, NUMERIC_values$rowid, VecFunResult) * match_STRING_filter * match_NUMERIC_filter match_result_table <- data.frame(index = 1:(nrow(COLUMN_MAP)*nrow(DATASET)), RESULT = colSums(match_result)) final_result_long <- merge(x = indextable, y = match_result_table, by = c("index"), all.x = TRUE) final_result_wide <- unstack(final_result_long, RESULT ~ DEPARTMENT_NAME) final_result_multiplied <- apply(final_result_wide,1,prod) cbind(DATASET,final_result_multiplied) toc()
优化思路与实现
核心问题分析
原代码的性能瓶颈主要在于:
- 方案1使用逐行循环和全局赋值(
<<-),这在R中是低效操作,会频繁触发内存读写; - 方案2中
outer()生成了巨大的中间矩阵,不仅占用大量内存,还存在大量冗余计算; - 两者都没有利用R中专门针对大数据处理优化的工具包。
基于data.table的优化实现
data.table以高效的分组、非等值连接和向量化操作著称,非常适合这类多条件关联场景。以下是优化后的代码:
library(data.table) # 转换为data.table格式,提升操作效率 LOOKUPTABLE <- as.data.table(LOOKUPTABLE) COLUMN_MAP <- as.data.table(COLUMN_MAP) DATASET <- as.data.table(DATASET) DATASET[, rowid := .I] # 添加行号,用于后续按行聚合结果 # 预合并部门映射表与LOOKUPTABLE,避免重复匹配部门信息 lookup_with_map <- merge(LOOKUPTABLE, COLUMN_MAP, by = "DEPARTMENT_NAME", all.x = TRUE) # 分部门处理关联逻辑 result_list <- lapply(unique(lookup_with_map$DEPARTMENT_NAME), function(dept) { dept_lookup <- lookup_with_map[DEPARTMENT_NAME == dept] dept_map <- COLUMN_MAP[DEPARTMENT_NAME == dept] # 初始化数据集,准备匹配 dt <- DATASET # 处理数值范围匹配(如果当前部门需要) if (!is.na(dept_map$COLUMNNAME_NUMERIC)) { num_col <- dept_map$COLUMNNAME_NUMERIC # 非等值连接直接匹配数值范围,替代逐行判断 dt <- dt[dept_lookup, on = .(get(num_col) >= RANGEFROM, get(num_col) < RANGETILL), allow.cartesian = TRUE] } else { # 不需要数值匹配时,直接全量关联部门lookup数据 dt <- dt[dept_lookup, on = .(), allow.cartesian = TRUE] } # 处理字符串匹配(如果当前部门需要) if (!is.na(dept_map$COLUMNNAME_STRING)) { str_col <- dept_map$COLUMNNAME_STRING dt <- dt[get(str_col) == STRINGVALUE | is.na(STRINGVALUE)] } # 按行聚合结果,无匹配时返回1 dt[, .(RESULT = if (.N == 0) 1 else unique(RESULT)), by = rowid] }) # 合并各部门结果,转换为宽格式后计算乘积 final_result <- rbindlist(result_list, idcol = "DEPARTMENT_NAME") final_result_wide <- dcast(final_result, rowid ~ DEPARTMENT_NAME, value.var = "RESULT") final_result_wide[, final_result_multiplied := LOGISTICS * VEHICLES * ACCOUNTING] # 合并回原始数据集,移除临时行号 DATASET[final_result_wide, on = "rowid"][, rowid := NULL]
关键优化点
- 移除逐行循环:用
data.table的非等值连接替代逐行判断,实现全量向量化处理,速度提升显著; - 取消全局赋值:所有操作在局部环境完成,避免不必要的内存开销和副作用;
- 减少中间数据:预合并映射表与lookup表,避免重复处理部门信息;
- 高效聚合:利用
data.table的by=分组操作快速按行聚合结果,替代低效的apply或列表循环。
内容的提问来源于stack exchange,提问作者user3516915
相关产品推荐
相关产品推荐

