根据前面的值,将一系列值替换为相应组的值

10

我有一个数据表,格式如下(2000000+行,1000+组):

set.seed(1)    
dt <- data.table(id = rep(1:3, each = 5), values = sample(c("a", "b","c"), 15, TRUE))

> dt
    id values
 1:  1      a
 2:  1      c
 3:  1      a
 4:  1      b
 5:  1      a
 6:  2      c
 7:  2      c
 8:  2      b
 9:  2      b
10:  2      c
11:  3      c
12:  3      a
13:  3      a
14:  3      a
15:  3      b

我希望在每个ID组内,将出现在字符 "b" 之前的所有字符 "a" 序列都替换为字符 "b"。所以条件是:如果在 "b" 之前出现字符 "a" 或一系列字符 "a"替换所有字符 "a"。(实际上,在我的真实表格中,当 "b" 被 "a"、"x" 或 "y" 前缀时,前一个字符应该被替换,但我应该能够推广)

在上面的例子中,第 3 行中的 "a" 值应该被替换(使用 data.table 中的 shift 很容易实现),以及第 12-14 行中的所有字符 "a" 应该被替换(不确定如何操作)。因此,期望的输出结果如下:

> dt
    id values
 1:  1      a
 2:  1      c
 3:  1      b
 4:  1      b
 5:  1      a
 6:  2      c
 7:  2      c
 8:  2      b
 9:  2      b
10:  2      c
11:  3      c
12:  3      b
13:  3      b
14:  3      b
15:  3      b

我首先想到的是从最后一个索引开始循环,但如果有多个分组(比如ID和DATE)我不确定该如何操作,而且这似乎并不是最快的dt解决方案。

4个回答

5

这是另一种使用data.table的方法:

dt[, x := rleid(values), by = .(id)]
dt[dt[values == "b", .(id, x=x-1, values="a")], 
   on = .(id, x, values), 
   values := "b"
   ][, x := NULL]
  • 创建一个名为"x"的新列,其中包含每个值的运行长度id,按id分组
  • 在自身上执行连接操作,同时修改运行长度id(x)以成为前面的值,并将值改变为"a"(您想要更改的特定值),然后使用"b"更新值
  • 之后删除列x

结果如下:

dt
#     id values
#  1:  1      a
#  2:  1      c
#  3:  1      b
#  4:  1      b
#  5:  1      a
#  6:  2      c
#  7:  2      c
#  8:  2      b
#  9:  2      b
# 10:  2      c
# 11:  3      c
# 12:  3      b
# 13:  3      b
# 14:  3      b
# 15:  3      b

以下是将值为 "a"、"x" 或 "y" 后面跟着 "b" 的情况替换为 "b" 的一般化方法:

dt[, x := rleid(values), by = .(id)]
dt[dt[values == "b", .(values=c("a", "x", "y")), by = .(id, x=x-1)], 
   on = .(id, x, values), 
   values := "b"
   ][, x := NULL]

4

我来晚了,已经有几个很好的运行长度替代方案提供了 ;) 所以这里我尝试使用nafill

(1) 创建一个变量 'v2',当 'values' 为 "a" 时它是 NA。(2) 通过向后传递下一个观测值来填充缺失值。(3) 当原始 'values' 为 "a" 并且相应填充到 'v2' 中的值为 "b" 时,使用 'v2' 更新 'v'。

# 1
dt[values != "a" , v2 := values]

# 2
d1[, v2 := v2[nafill(replace(seq_len(.N), is.na(v2), NA), type = "nocb")], by = id]

# 3
dt[values == "a" & v2 == "b", values := v2]

# clean-up
dt[ , v2 := NULL]

目前,nafill 只适用于数值变量,因此在代码块 # 2 中(修改自 @chinsoon12 的问题 nafill,setnafill 用于字符、因子和其他类型)需要使用 replace 步骤。

使用 zoo::nalocf 可以稍微缩短 NA 替换代码:

dt[, v2 := zoo::na.locf(v2, fromLast = TRUE, na.rm = FALSE), by = id]

但是,请注意,na.locf 更慢。


当比较较大的数据的答案时(data.table(id = rep(1:1e4, each = 1e4, replace = TRUE), values = sample(c("a", "b", "c"), 1e8, replace = TRUE)),结果表明,这种替代方法实际上比其他方法更快。


有趣的解决方案,谢谢!我真的很喜欢它,因为rle可能不明显对于阅读你的代码的每个人来说。可以通过使用zoo:na.locf使其更短(在你的示例中,ri索引列只是因为nafill不能与字符一起使用)。请参见:dt[values != "a" , v2 := values] ; dt[, v2 := zoo::na.locf(v2, fromLast = TRUE, na.rm = FALSE), by = "id"] ; dt[values == "a" & v2 == "b", values := v2] - Djpengo
非常感谢您的反馈,@Djpengo。在发布我的答案之前,我尝试过na.locf,但它的速度要慢得多,所以我决定将其排除在外(抱歉,我应该在帖子中提到这一点)。我可能会重新加入它。 - Henrik
我猜如果它在那里就很好,因为这取决于你要寻找什么,是简单性还是速度。使用DT时,确实是在寻找速度。 - Djpengo

2
这并不美观,但我认为这是您需要的内容:

这不是很好看,但我认为这就是您想要的:

dt[, .N, by = .(id, values = paste0(values, rleid(values)))
   ][, values := sub("[0-9]+", "", values)
     ][, values := fifelse(values == "a" & shift(values, -1L) == "b" & !is.na(shift(values, -1L)), "b", values), by = id
       ][, .SD[rep(seq_len(.N), N)]
         ][, !"N"]

    id values
 1:  1      a
 2:  1      c
 3:  1      b
 4:  1      b
 5:  1      a
 6:  2      c
 7:  2      c
 8:  2      b
 9:  2      b
10:  2      c
11:  3      c
12:  3      b
13:  3      b
14:  3      b
15:  3      b

1
你可以使用rle()注意:为了避免歧义,我将"values"列重命名为"var",因为rle()函数还会生成一个包含名为"values"的向量的列表。
dt[, new := with(rle(var), rep(ifelse(values == "a" & c(values[-1], "") == "b", "b", values), lengths)), by = id]
dt

#     id var new
#  1:  1   a   a
#  2:  1   c   c
#  3:  1   a   b
#  4:  1   b   b
#  5:  1   a   a
#  6:  2   c   c
#  7:  2   c   c
#  8:  2   b   b
#  9:  2   b   b
# 10:  2   c   c
# 11:  3   c   c
# 12:  3   a   b
# 13:  3   a   b
# 14:  3   a   b
# 15:  3   b   b

谢谢!看起来不错,你能帮我理解一下rle()函数中的lengths元素是如何使用的吗?我已经尝试了一段时间但还是不太明白... - Djpengo
例如,您可以运行 x <- c("a", "a", "b", "b", "b") ; rle(x)。它返回一个由2个向量组成的列表,一个是 values,另一个是 lengthslengths 表示 values 中元素的计数。如果您运行 with(rle(x), rep(values, lengths)),输出将与原始的 x 相同,因为 values 中的元素被重复了 lengths 次。 - Darren Tsai
1
谢谢,非常整洁的解决方案!对于任何阅读此内容的人,dt [,rle(var),by = id] 基本上创建了一个表格,显示没有重复的字符串序列,并且长度列显示每个字符串重复的次数。这是一个简单得多的情况,其他所有内容都可以从那里开始。 - Djpengo
这真的很棒,因为它完全遵循了所述条件,我百分之百确定它应该在我正在处理的更复杂的表格中按预期工作。 - Djpengo
我会实际使用dt$new <- dt[, rle(var), by = "id"][, rep(ifelse(values == "a" & shift(values, type= "lead") == "b", "b", values), lengths)],因为我认为这个命令序列最容易理解(如果您在一段时间后重新访问代码并忘记如何得出解决方案,则非常重要:))。 - Djpengo
1
@Djpengo 做得好。如果你使用 shift(),记得添加 fill = ""。否则,如果 dt$var 的最后一个元素是 "a",就会出现缺失值。你可以设置 dt$var[15] <- "a",然后再次运行你的代码。 - Darren Tsai

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接