基于主键分组,仅当指定列未变更时执行na.locf向前填充需求
嘿,这个带条件的分组填充需求确实比普通的zoo::na.locf要讲究,我来给你一步步实现它!
首先我注意到你提供的示例数据里缺少了那个用来判断“是否允许填充”的关键列——咱们先补上它,假设这个列叫Status,用来模拟同Id内会发生变化的状态,我们的规则是:只有在Status连续不变的区间内,才对FillCol做向前填充,跨状态变化的NA则保持原样。
步骤1:补全示例数据
先构造带Status列的测试数据,方便验证效果:
library(dplyr) library(zoo) set.seed(20180409) data <- data.frame( Id = rep(1:10, each = 24), Date = rep(seq.Date(as.Date("2016-01-01"), as.Date("2017-12-01"), by = "month"), 10), FillCol = replace(runif(240), runif(240) < 0.9, NA), # 新增Status列:同Id内每6个月切换一次状态,模拟变化场景 Status = rep(rep(c("A", "B", "C", "A"), each = 6), 10) )
步骤2:实现核心逻辑
核心思路是先按Id分组,再在每个Id内部,把连续相同Status的行划分为子区间——只有在同一个子区间内,才用na.locf填充NA。代码如下:
filled_data <- data %>% # 先按Id分组 group_by(Id) %>% # 生成子区间标记:当Status和上一行不同时,标记为新的子区间 mutate(status_subgroup = cumsum(Status != lag(Status, default = first(Status)))) %>% # 同时按Id和子区间分组,在每个子区间内做向前填充 group_by(Id, status_subgroup) %>% mutate(FillCol_filled = na.locf(FillCol, na.rm = FALSE)) %>% # 清理临时分组列,回到原始结构 ungroup() %>% select(-status_subgroup)
关键细节说明
cumsum(Status != lag(Status, default = first(Status))):这个小技巧会为每个连续相同的Status段生成唯一的编号,一旦Status变化,编号就加1,完美把每个Id拆分成了“状态不变”的子区间。na.locf(FillCol, na.rm = FALSE):这里一定要设置na.rm = FALSE,这样如果某个子区间的第一个值就是NA,它会保持NA,不会被其他区间的值填充,完全符合“仅用当前区间最后观测值填充”的要求。
验证填充效果
你可以挑一个Id来查看对比结果,确认逻辑是否符合预期:
filled_data %>% filter(Id == 1) %>% select(Date, Status, FillCol, FillCol_filled) %>% print(n = 24)
你会清晰看到:只有在Status连续相同的区间里,FillCol的NA才会被前一个非NA值填充;一旦Status切换,新区间的NA不会被之前区间的值“跨区”填充。
内容的提问来源于stack exchange,提问作者Jordo82
相关产品推荐
相关产品推荐

