如何在for循环中基于现有data.table创建新对象并更新列?
问题:用for循环模拟data.table逐列赋值逻辑失败
我想通过for循环模拟以下代码的逻辑:
compile_1 <- compile_0[, final_1 := ifelse((!is.na(final_0) & is.na(input_1)), final_0, final_1)] compile_2 <- compile_1[, final_2 := ifelse((!is.na(final_1) & is.na(input_2)), final_1, final_2)] compile_3 <- compile_2[, final_3 := ifelse((!is.na(final_2) & is.na(input_3)), final_2, final_3)]
但尝试后失败了,我猜测可能是i没有被正确封装在assign函数的私有环境中。我的初始循环代码如下:
for(i in 1:3){ assign(paste0("compile_", get("i")-1), get(paste0("compile_", get("i")-1))[, paste0("final_", get("i")) := ifelse((!is.na(get(paste0("final_", get("i")-1))) & is.na(get(paste0("input_",get("i"))))), get(paste0("final_", get("i")-1)),get(paste0("final_", get("i"))))]) }
之后我尝试把i复制到本地环境,但还是无效,推测是因为代码找不到不在函数本地环境中的compile_0。
数据示例:
# 已移除不必要列 structure(list(id = c("100", "10000", "10002", "10006", "1000794", "10010", "1002162", "1002820", "1003005", "1003330", "1003458", "1003617", "1004034", "1004235", "1004649", "1004731", "1004795", "1004796", "1004798", "1004799"), final_0 = c(445L, 960L, 655L, 590L, 550L, 780L, 570L, 200L, 272L, 540L, 550L, 470L, 588L, 320L, 750L, 500L, 418L, 418L, 438L, 648L), final_1 = c(445L, 960L, 655L, 590L, 550L, 780L, 570L, 200L, 272L, 540L, 550L, 470L, 588L, 320L, 750L, 500L, 418L, 418L, 438L, 648L), final_2 = c(460L, 960L, NA, NA, NA, 780L, 570L, 200L, 200L, 540L, 550L, NA, 568L, 320L, 705L, 400L, 449L, 449L, 469L, 648L), final_3 = c(460L, 960L, NA, NA, NA, 780L, 570L, 200L, 200L, 540L, 550L, NA, 568L, 320L, 705L, 400L, 449L, 449L, 469L, 648L), input_0 = c(445L, 960L, 655L, 590L, 550L, 780L, 570L, 200L, 272L, 540L, 550L, 470L, 588L, 320L, 750L, 500L, 418L, 418L, 438L, 648L), input_1 = c(445L, 960L, 655L, 590L, 550L, 780L, 570L, 200L, 272L, 540L, 550L, 470L, 588L, 320L, 750L, 500L, 418L, 418L, 438L, 648L), input_2 = c(460L, 960L, NA, NA, NA, 780L, 570L, 200L, 200L, 540L, 550L, NA, 568L, 320L, 705L, 400L, 449L, 449L, 469L, 648L), input_3 = c(460L, 960L, NA, NA, NA, 780L, 570L, 200L, 200L, 540L, 550L, NA, 568L, 320L, 705L, 400L, 449L, 449L, 469L, 648L)), row.names = c(NA, -20L), class = c("data.table", "data.frame"), .internal.selfref = <pointer: 0x00000176df6d1ef0>, sorted = "id")
我希望for循环能得到和单独执行每行代码完全相同的输出。
解决方案
你的问题主要出在两个地方:一是get()在data.table的[]内的作用域问题,二是没必要反复创建新的data.table对象(data.table是按引用修改的,不需要每次赋值新对象)。
优化方案1:避免assign,直接在原对象上迭代(推荐)
既然data.table支持按引用修改,完全没必要每次生成compile_1、compile_2这类新对象,直接在复制后的对象上逐列处理即可:
# 先复制原数据避免修改原始对象 compile <- copy(compile_0) for(i in 1:3){ # 定义列名变量,让代码更清晰 prev_final_col <- paste0("final_", i-1) curr_final_col <- paste0("final_", i) curr_input_col <- paste0("input_", i) # 使用get()引用列名,完成赋值逻辑 compile[, (curr_final_col) := ifelse(!is.na(get(prev_final_col)) & is.na(get(curr_input_col)), get(prev_final_col), get(curr_final_col))] }
优化方案2:如果必须保留多个compile对象
如果你确实需要保留compile_0、compile_1等多个对象,可以在循环内先获取当前对象,修改后再赋值回去,避免嵌套的get()和assign()导致的作用域混乱:
for(i in 1:3){ prev_compile_name <- paste0("compile_", i-1) curr_compile_name <- paste0("compile_", i) # 获取上一个版本的data.table curr_dt <- get(prev_compile_name) # 定义列名 prev_final_col <- paste0("final_", i-1) curr_final_col <- paste0("final_", i) curr_input_col <- paste0("input_", i) # 修改当前dt curr_dt[, (curr_final_col) := ifelse(!is.na(get(prev_final_col)) & is.na(get(curr_input_col)), get(prev_final_col), get(curr_final_col))] # 赋值为新的对象 assign(curr_compile_name, curr_dt) }
原始代码失败原因
- 作用域问题:在data.table的
[]内嵌套使用get("i")时,内层的get("i")无法正确捕获循环变量i,因为data.table的表达式会在自身专属作用域内求值。 - 冗余操作:你尝试修改
compile_{i-1}并重新赋值给它,但data.table是按引用修改的,这种操作会导致不必要的对象复制,同时增加作用域复杂度。
执行上述方案后,compile(方案1)或compile_3(方案2)的结果会和手动执行三行代码的结果完全一致。
内容的提问来源于stack exchange,提问作者smackington
相关产品推荐
相关产品推荐

