如何在data.table中无循环替换特定行的changed_time值?
高效替换data.table中指定行的changed_time值
问题背景
我有一个大型data.table,部分数据如下(实际包含更多列和数千行):
stop_id path changed_event_status changed_time <i64> <char> <char> <i64> 1: 4398037956893976209 S <NA> 2405071040 2: 1500925206899141237 RT <NA> 2405071041 3: 2333532852925690131 S <NA> 2405071105 4: 4636036529075799544 TÜ <NA> 2405071044 5: 4680830034956468939 S <NA> 2405071046 6: 7584560746915960683 S c 2405071049 <- 1a: 替换2405071049 7: 2333532852925690131 RT <NA> 2405071116 8: 4747322524233582527 S <NA> 2405071100 <- 1b: 替换为2405071100 9: 285273127640529713 S <NA> 2405071103 10: 6134967434625106066 S <NA> 2405071101 11: 3684003552999415659 RT <NA> 2405071103 <- 2b: 替换为2405071103 12: 7584560746915960683 RT c 2405071058 <- 2a: 替换2405071058 13: 4680830034956468939 TÜ <NA> 2405071103 14: 8123621717351038368 S <NA> 2405071113 15: 8702942397103782624 TÜ <NA> 2405071114 16: 6134967434625106066 TÜ <NA> 2405071114 17: 4138386908727054325 S <NA> 2405071115 18: 285273127640529713 RT <NA> 2405071123 19: 2445758245483744446 S <NA> 2405071119 20: 8153934371487726263 TÜ <NA> 2405071132 21: 4138386908727054325 RT <NA> 2405071126 22: 310332233182112225 S <NA> 2405071127 stop_id path changed_event_status changed_time
需求说明
对于所有changed_event_status == "c"的行,需要在相同path的所有行中,找到changed_time列里大于等于当前行changed_time的最小值,并用该值替换当前行的changed_time。比如:
- 第6行的2405071049要替换为第8行的2405071100
- 第12行的2405071058要替换为第11行的2405071103
我已经用循环实现了需求,但循环效率太低,希望找到无需循环的高效解决方案。
示例数据
library(data.table) bahn <- fread("\nstop_id path changed_event_status changed_time\n4398037956893976209 S NA 2405071040\n1500925206899141237 RT NA 2405071041\n2333532852925690131 S NA 2405071105\n4636036529075799544 TÜ NA 2405071044\n4680830034956468939 S NA 2405071046\n7584560746915960683 S c 2405071049\n2333532852925690131 RT NA 2405071116\n4747322524233582527 S NA 2405071100\n285273127640529713 S NA 2405071103\n6134967434625106066 S NA 2405071101\n3684003552999415659 RT NA 2405071103\n7584560746915960683 RT c 2405071058\n4680830034956468939 TÜ NA 2405071103\n8123621717351038368 S NA 2405071113\n8702942397103782624 TÜ NA 2405071114\n6134967434625106066 TÜ NA 2405071114\n4138386908727054325 S NA 2405071115\n285273127640529713 RT NA 2405071123\n2445758245483744446 S NA 2405071119\n8153934371487726263 TÜ NA 2405071132\n4138386908727054325 RT NA 2405071126\n310332233182112225 S NA 2405071127\n")
循环解决方案(低效)
for(i in 1:nrow(bahn)) { if(!is.na(bahn[i, changed_event_status]) & bahn[i, changed_event_status] == "c") { bahn[i, ]$changed_time <- sort( bahn[ is.na(changed_event_status) & changed_time >= bahn[i, changed_time] & path == bahn[i, path] ]$changed_time )[1] } }
高效的data.table解决方案
利用data.table的分组和非等值连接特性,可以高效完成需求,无需循环,以下提供两种实用写法:
写法1:非等值连接+.EACHI分组
这种写法直接通过连接匹配条件,对每一行目标行单独计算最小值,代码简洁高效:
# 先提取所有有效行(changed_event_status为NA) valid_rows <- bahn[is.na(changed_event_status), .(path, valid_time = changed_time)] # 对目标行执行非等值连接,替换changed_time bahn[changed_event_status == "c", changed_time := valid_rows[.SD, on = .(path, valid_time >= changed_time), min(valid_time), by = .EACHI]$V1]
写法2:预排序+findInterval定位
对于超大型数据集,先按path预存排序后的有效时间,再用findInterval快速定位最小值,性能更优:
# 按path分组,预存排序后的有效changed_time bahn[, sorted_valid := list(sort(changed_time[is.na(changed_event_status)])), by = path] # 对目标行替换:找到第一个大于等于当前时间的最小值 bahn[changed_event_status == "c", changed_time := sorted_valid[[1]][findInterval(changed_time, sorted_valid[[1]]) + 1], by = path] # 清理临时列 bahn[, sorted_valid := NULL]
方案解释
- 写法1利用
data.table的非等值连接能力,通过on = .(path, valid_time >= changed_time)匹配同path下时间不小于当前值的行,再用by = .EACHI对每一行目标行取最小值。 - 写法2先对每个path的有效时间做一次排序,后续用
findInterval快速定位当前时间在排序数组中的位置,直接取下一个元素就是符合要求的最小值,排序仅执行一次,适合数据量极大的场景。
验证结果
运行上述任意一种高效代码后,目标行的changed_time会按需求替换:
- 第6行的
changed_time变为2405071100 - 第12行的
changed_time变为2405071103
内容的提问来源于stack exchange,提问作者user24031531
相关产品推荐
相关产品推荐

