在单个R data.table中按组有效地定位

car*_*son 11 r dataframe rcpp dplyr data.table

我有一个大的,宽的data.table(20米行)由一个人ID键入,但有很多列(~150)有很多空值.每列都是我希望为每个人继承的记录状态/属性.每个人可能有10到10,000个观察点,并且该集合中有大约500,000人.来自一个人的值不能"流血"到下一个人,因此我的解决方案必须适当地尊重人员ID列和组.

出于演示目的 - 这是一个非常小的示例输入:

DT = data.table(
  id=c(1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3),
  aa=c("A", NA, "B", "C", NA, NA, "D", "E", "F", NA, NA, NA),
  bb=c(NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA),
  cc=c(1, NA, NA, NA, NA, 4, NA, 5, 6, NA, 7, NA)
)
Run Code Online (Sandbox Code Playgroud)

它看起来像这样:

    id aa bb cc
 1:  1  A NA  1
 2:  1 NA NA NA
 3:  1  B NA NA
 4:  1  C NA NA
 5:  2 NA NA NA
 6:  2 NA NA  4
 7:  2  D NA NA
 8:  2  E NA  5
 9:  3  F NA  6
10:  3 NA NA NA
11:  3 NA NA  7
12:  3 NA NA NA
Run Code Online (Sandbox Code Playgroud)

我的预期输出如下:

    id aa bb cc
 1:  1  A NA  1
 2:  1  A NA  1
 3:  1  B NA  1
 4:  1  C NA  1
 5:  2 NA NA NA
 6:  2 NA NA  4
 7:  2  D NA  4
 8:  2  E NA  5
 9:  3  F NA  6
10:  3  F NA  6
11:  3  F NA  7
12:  3  F NA  7
Run Code Online (Sandbox Code Playgroud)

我找到了一个有效的data.table解决方案,但是我的大型数据集速度非常慢:

DT[, na.locf(.SD, na.rm=FALSE), by=id]
Run Code Online (Sandbox Code Playgroud)

我发现使用同样慢的dplyr的等效解决方案.

GRP = DT %>% group_by(id)
data.table(GRP %>% mutate_each(funs(blah=na.locf(., na.rm=FALSE))))
Run Code Online (Sandbox Code Playgroud)

我希望我可以使用这个data.table功能提出一个滚动的"自我"联接,但我似乎无法正确使用它(我怀疑我需要使用.N但我还没想到它).

在这一点上,我想我将不得不在Rcpp中写一些东西来有效地应用分组的locf.

我是R的新手,但我不是C++的新手 - 所以我有信心我能做到.我觉得应该有一种有效的方法在R中使用它data.table.

ale*_*laz 19

na.locf可以通过转发(cummax)非NA索引((!is.na(x)) * seq_along(x))和相应的子集来构建一个非常简单的:

x = c(1, NA, NA, 6, 4, 5, 4, NA, NA, 2)
x[cummax((!is.na(x)) * seq_along(x))]
# [1] 1 1 1 6 4 5 4 4 4 2
Run Code Online (Sandbox Code Playgroud)

这种复制na.locfna.rm = TRUE参数,以获得na.rm = FALSE行为,我们只需要确保的第一个元素的cummaxTRUE:

x = c(NA, NA, 1, NA, 2)
x[cummax(c(TRUE, tail((!is.na(x)) * seq_along(x), -1)))]
#[1] NA NA  1  1  2
Run Code Online (Sandbox Code Playgroud)

在这种情况下,我们不仅需要考虑非NA索引,还要考虑(有序或有序)"id"列更改值的索引:

id = c(10, 10, 11, 11, 11, 12, 12, 12, 13, 13)
c(TRUE, id[-1] != id[-length(id)])
# [1]  TRUE FALSE  TRUE FALSE FALSE  TRUE FALSE FALSE  TRUE FALSE
Run Code Online (Sandbox Code Playgroud)

结合以上内容:

id = c(10, 10, 11, 11, 11, 12, 12, 12, 13, 13)
x =  c(1,  NA, NA, 6,  4,  5,  4,  NA, NA, 2)

x[cummax(((!is.na(x)) | c(TRUE, id[-1] != id[-length(id)])) * seq_along(x))]
# [1]  1  1 NA  6  4  5  4  4 NA  2
Run Code Online (Sandbox Code Playgroud)

注意,这里我们OR第一个元素TRUE,即使它等于TRUE,从而获得na.rm = FALSE行为.

对于这个例子:

id_change = DT[, c(TRUE, id[-1] != id[-.N])]
DT[, lapply(.SD, function(x) x[cummax(((!is.na(x)) | id_change) * .I)])]
#    id aa bb cc
# 1:  1  A NA  1
# 2:  1  A NA  1
# 3:  1  B NA  1
# 4:  1  C NA  1
# 5:  2 NA NA NA
# 6:  2 NA NA  4
# 7:  2  D NA  4
# 8:  2  E NA  5
# 9:  3  F NA  6
#10:  3  F NA  6
#11:  3  F NA  7
#12:  3  F NA  7
Run Code Online (Sandbox Code Playgroud)

  • 对我来说,downvote是非常不明显的,并且可以理解一些解释 (7认同)
  • 我认为很好的答案 - 这不仅是常规“na.locf”的更快版本,而且还添加了针对每个组执行此操作的修改(假设已排序的组),**无需**实际执行“by”循环(这会为每个组引入额外的“eval”并会减慢速度)。除非我遗漏了一些东西 - 这应该是标准的“na.locf”实现,而不是“zoo”所做的“rle”东西。 (2认同)
  • 我可能会补充一点,对于 20m 行的原始测试集,第一个建议的“lapply”解决方案需要 40 小时才能完成。新代码只需 4 分钟!我怀疑 Rcpp 能做得更好。 (2认同)