在for循环中创建的data.table新列不显示的问题排查
解决data.table在for循环中创建新列不生效的问题
我来帮你分析这个问题,并且给出可行的解决办法:
首先先复现你的场景,初始的data.table是这样的:
dt = structure(list(id = c("a", "b", "c"), val = c(1, 4, 6)), .Names = c("id", "val"), row.names = c(NA, -3L), class = c("data.table", "data.frame" ))
你尝试的for循环代码:
for(i in seq(1:1)) { # 1:1不是bug varname = 'flag1' cname = 'val' threshold = 4 expr = parse(text=paste0(varname, ' := (', cname, ' > ', threshold, ')')) dt[, eval(expr)] }
问题原因
核心问题出在**eval()的执行环境**上:
- 当你直接执行
dt[, eval(expr)](不在循环里)时,eval是在全局环境中运行的,:=能正确找到并修改全局的dt对象。 - 但在for循环内部,
eval默认会在循环的局部临时环境中执行,这时候:=的赋值操作没有正确关联到你全局的dt上——实际可能是修改了局部环境中的临时副本,而你最后查看的是全局的dt,所以看起来修改没生效。
解决建议
推荐两种方案,优先选第一种(更符合data.table的最佳实践):
方案1:避免使用parse/eval,用data.table原生的动态列名语法
data.table支持直接用括号包裹变量来指定新列名,用get()来引用动态的列名,完全不需要parse和eval,代码更简洁安全:
for(i in seq(1:1)) { varname = 'flag1' cname = 'val' threshold = 4 # 用(varname)指定新列名,get(cname)获取要判断的列 dt[, (varname) := get(cname) > threshold] }
如果你的实际场景是批量处理多个列,可以把列名、新列名和阈值放在列表里循环,比如:
# 批量处理示例 col_configs = list( list(new_col = "flag1", target_col = "val", threshold = 4), list(new_col = "flag2", target_col = "val", threshold = 5) ) for(config in col_configs) { dt[, (config$new_col) := get(config$target_col) > config$threshold] }
方案2:如果一定要用parse/eval,指定eval的执行环境
如果你因为某些原因必须保留parse/eval的写法,只需要给eval()指定envir = parent.frame(),让它在循环的父环境(也就是全局环境)中执行,这样就能正确修改全局的dt了:
for(i in seq(1:1)) { # 1:1不是bug varname = 'flag1' cname = 'val' threshold = 4 expr = parse(text=paste0(varname, ' := (', cname, ' > ', threshold, ')')) dt[, eval(expr, envir = parent.frame())] }
内容的提问来源于stack exchange,提问作者broccoli
相关产品推荐
相关产品推荐

