基于所选列中的值对数据表进行子集化的有效方法

6

假设我有一个包含6列的data.table

library(data.table)
set.seed(123)
dt <- data.table( id = 1:100,
                  p1 = sample(1:10, 100, replace = TRUE ),
                  p2 = sample(1:10, 100, replace = TRUE ),
                  p3 = sample(1:10, 100, replace = TRUE ),
                  p4 = sample(1:10, 100, replace = TRUE ),
                  p5 = sample(1:10, 100, replace = TRUE ) )

现在,我想要对这个数据表进行子集操作,仅保留 p1 - pn 列(这里是 p1-p5)中 任何一个 包含值为 10 的行。

对于这个小样本数据表,可以使用以下代码手动完成此操作:

test1 <- dt[ p1 == 10 | p2 == 10 | p3 == 10 | p4 == 10 | p5 == 10, ]

但我的生产数据包含许多p列,因此手动键入所有内容将是一种痛苦...

我目前的解决方案是首先创建一个向量,其中包含我需要的列名:

cols <- grep( "^p", names( dt ), value = TRUE )

然后使用apply进行子集处理:

test2 <- dt[ apply( dt[, ..cols ], 1, function(r) any( r == 10 ) ), ]

检查:

identical(test1, test2)
# TRUE

我的实际问题

上面的解决方案(使用apply)对我来说已经足够快了.. 但是我不确定它是否是最优解。相比其他一些在这里的人,我对data.table还很新,而且这可能不是我想要的子集的最有效/最优雅的方式。

我来这里学习,所以有没有更加优雅/更好/更快速的方法来解决我的子集问题?

更新

该问题被标记为重复... 但我仍然会在这里发布我的答案:

我发现@Marcus的答案是最好(=可读性强)的代码,而@akrun的答案则是最快的。

基准测试

data.table包含100万行和50个感兴趣的列(即p列)

#create sample data
set.seed( 123 )
n   <- 1000000
k   <- 100
dat <- sample( 1:100, n * k, replace = TRUE )
DT  <- as.data.table( matrix( data = dat, nrow = n, ncol = k ) )
setnames( DT, names( DT ), c( paste0( "p", 1:50 ), paste( "r", 1:50 ) ) )

#vector with columns starting with "p"
cols <- grep( "^p", names( DT ), value = TRUE )

apply_method   <- DT[ apply( DT[, ..cols ], 1, function(x) any( x == 10 ) ), ]
reduce_method  <- DT[ DT[, Reduce(`|`, lapply(.SD, `==`, 10)), .SDcols = cols]]
rowsums_method <- DT[ rowSums( DT[ , ..cols ] == 10, na.rm = TRUE ) >= 1 ]

identical(  apply_method, rowsums_method )

microbenchmark::microbenchmark(
  apply   = DT[ apply( DT[ , ..cols ], 1, function(x) any( x == 10 ) ), ],
  reduce  = DT[ DT[, Reduce( `|`, lapply( .SD, `==`, 10 ) ), .SDcols = cols ] ],
  rowSums = DT[ rowSums( DT[ , ..cols ] == 10, na.rm = TRUE ) >= 1, ],
  times = 10
)

#    expr       min        lq      mean    median        uq       max neval
#   apply 3352.0640 3441.7760 3665.5004 3662.7666 3760.7553 4325.9125    10
#  reduce  408.6349  437.6806  552.8850  572.2012  657.6072  710.7699    10
# rowSums  619.2594  663.7325  784.2389  850.0963  868.2096  892.7469    10

这个 test3 <- dt[rowSums(dt[, ..cols ] == 10) >= 1]; identical(test1, test3) 怎么样? - markus
@markus,这是目前为止最快的...还有:代码很简短..可惜我不能接受.. - Wimpel
@markus,当您不是在寻找值10而是在寻找字符串(例如cols列中的test)时,是否也可以调整此方法? - Wimpel
像这样 dt <- data.table(col1 = c("test", "no_test"), col2 = 1:4); cols = c("col1", "col2"); dt[rowSums(dt[, ..cols ] == "test") >= 1] - markus
1
忽略我之前(已删除的)评论。将 na.rm = TRUE 添加到解决方案中就可以了!因为我的生产数据中也包含感兴趣的列中的 NA 值! - Wimpel
1个回答

5

一种选项是在.SDcols中指定感兴趣的列,循环遍历数据表子集(.SD),生成一个逻辑向量的list,使用(|)将其Reduce为单一逻辑向量,并将其用于对行进行子集筛选。

i1 <- dt[, Reduce(`|`, lapply(.SD, `==`, 10)), .SDcols = cols]
test2 <- dt[i1]
identical(test1, test2)
#[1] TRUE

1
有趣的方法...我以前从未使用过Reduce... 速度与原始的apply解决方案大致相同 - Wimpel
1
更新:当数据表变得更大时,reduce + lapply 是明显的速度赢家。请参见问题中的更新以获取基准测试。 - Wimpel
很想理解这个答案,但是 |== 是什么意思? - W Barker

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