数据表计算每行非缺失值的数量

21
我尝试计算每行不包含NA值的列数,并将该值放入该行的新列中。
示例数据:
library(data.table)

a = c(1,2,3,4,NA)
b = c(6,NA,8,9,10)
c = c(11,12,NA,14,15)
d = data.table(a,b,c)

> d 
    a  b  c
1:  1  6 11
2:  2 NA 12
3:  3  8 NA
4:  4  9 14
5: NA 10 15

我希望输出结果中包含一个新列num_obs,该列包含每行非NA条目的数量:

    a  b  c num_obs
1:  1  6 11       3
2:  2 NA 12       2
3:  3  8 NA       2
4:  4  9 14       3
5: NA 10 15       2

我已经阅读了好几个小时,目前最好的解决办法就是循环遍历行,但这在R或data.table中都不是一个好的选择。我相信肯定还有更好的方法来解决这个问题,请给我指点迷津。

我的笨方法:

len = (1:NROW(d))
for (n in len) {
  d[n, num_obs := length(which(!is.na(d[n])))]
}

2
喜欢 d[, num_obs := sum(!is.na(.SD)), by = 1:nrow(d)][] 还是 d[, num_obs := rowSums(!is.na(d))][]?(不确定哪个更快。) - A5C1D2H2I1M1N2O1R2T1
是的!这个可行。你能解释一下吗?我以为如果我使用 by = 1:nrow(d).SD 将等于整个数据集。那么它是如何按行执行的呢? 另外,加上空链 [ ] 到底是做什么用的? - Reilstein
2个回答

23

试着使用Reduce+调用链接在一起:

d[, num_obs := Reduce(`+`, lapply(.SD,function(x) !is.na(x)))]

如果速度很重要,您可以采用Ananda建议的方法硬编码正在评估的列数,以获得更高效的结果:

如果速度很重要,你可以采用 Ananda 的建议,将被评估的列的数量硬编码,以获得更高的效率:

d[, num_obs := 4 - Reduce("+", lapply(.SD, is.na))]

使用 Ananda 的更大的 d 数据表进行基准测试:

fun1 <- function(indt) indt[, num_obs := rowSums(!is.na(indt))][]
fun3 <- function(indt) indt[, num_obs := Reduce(`+`, lapply(.SD,function(x) !is.na(x)))][]
fun4 <- function(indt) indt[, num_obs := 4 - Reduce("+", lapply(.SD, is.na))][]

library(microbenchmark)
microbenchmark(fun1(copy(d)), fun3(copy(d)), fun4(copy(d)), times=10L)

#Unit: milliseconds
#          expr      min       lq     mean   median       uq      max neval
# fun1(copy(d)) 3.565866 3.639361 3.912554 3.703091 4.023724 4.596130    10
# fun3(copy(d)) 2.543878 2.611745 2.973861 2.664550 3.657239 4.011475    10
# fun4(copy(d)) 2.265786 2.293927 2.798597 2.345242 3.385437 4.128339    10

也许根据OP的描述,这样做可以提高一些速度:indt[, num_obs := 4 - Reduce("+", lapply(.SD, is.na))][]。我已经硬编码了“4”,以避免按行重新计算。 - A5C1D2H2I1M1N2O1R2T1
@AnandaMahto - 它又节省了一点时间 - 我已经更新了上面的答案。 - thelatemail
@Reilstein - 这不是一个愚蠢的问题。我认为如果 i 中没有任何内容,.SD 实际上代表整个数据表 - 例如 d[,dim(.SD)] 返回行和列的计数。 - thelatemail
@thelatemail - 我明白了,那么在你的解决方案中,Reduce('+',lapply(.SD,function(x)!is.na(x)))语句是说+是函数,而lapply(.SD,function(x)!is.na(x))将由.SD给出的行转换为要操作的列表?我理解得对吗?我正在查看Reduce()帮助文件,并且我知道它接受一个函数、一个向量和一个R对象,所以我正在尝试拼凑出你的解决方案中哪个是哪个。再次感谢。 - Reilstein
好的,非常感谢。我开始理解它了。 - Reilstein
显示剩余3条评论

10

我能想到的两个选项是:

d[, num_obs := sum(!is.na(.SD)), by = 1:nrow(d)][]
d[, num_obs := rowSums(!is.na(d))][]

第一步是创建一个每组只有一行的“group”(1:nrow(d))。如果没有这个,它将仅对整个表格中的NA值进行求和。
第二步利用了一个已经非常高效的基本R函数rowSums
以下是大型数据的基准测试结果:
set.seed(1)
nrow = 10000
ncol = 15
d <- as.data.table(matrix(sample(c(NA, -5:10), nrow*ncol, TRUE), nrow = nrow, ncol = ncol))

fun1 <- function(indt) indt[, num_obs := rowSums(!is.na(indt))][]
fun2 <- function(indt) indt[, num_obs := sum(!is.na(.SD)), by = 1:nrow(indt)][]

library(microbenchmark)
microbenchmark(fun1(copy(d)), fun2(copy(d)))
# Unit: milliseconds
#           expr        min         lq       mean     median         uq      max neval
#  fun1(copy(d))   3.727958   3.906458   5.507632   4.159704   4.475201 106.5708   100
#  fun2(copy(d)) 584.499120 655.634889 684.889614 681.054752 712.428684 861.1650   100

顺便提一下,空的[]只是为了打印出结果为data.table。在想要从"data.table"中返回set*函数的输出时,这是必需的。

这太棒了,谢谢@AnandaMahto!我有点困惑你是如何使用1:nrow(d)创建一个一行的组。我的初步想法是这个语句会从第一行到文件的最后一行创建一个组。显然我没有理解by语句的工作原理或者其他什么。 - Reilstein
2
我的基准测试表明这个函数再次更快:fun3 <- function(indt) indt[, num_obs := Reduce(\+`, lapply(.SD,function(x) !is.na(x)))][]` - thelatemail
是的,在我的大数据集上,速度从115秒提高到了0.06秒,这是一个显著的改进!感谢您添加这个功能! - Reilstein
@Reilstein,出于好奇,您的较大数据集的尺寸是多少? - A5C1D2H2I1M1N2O1R2T1
我用于一些基准测试的“更大”的数据集是4列,150万行。最终,我需要在大约20列、600万行的数据上运行它,因此速度的提升非常有帮助:)。 - Reilstein
显示剩余3条评论

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