在data.table中,将数值列中的NA替换为0。

12

我有一个包含不同数据类型列的data.table。我的目标是仅选择数字列,并将这些列中的NA值替换为0。

我知道将na值替换为零的方法如下:

DT[is.na(DT)] <- 0

要选择只有数字的列,我发现这个解决方案非常有效:

DT[, as.numeric(which(sapply(DT,is.numeric))), with = FALSE]

我可以通过指派来实现我的目标。
DT2 <- DT[, as.numeric(which(sapply(DT,is.numeric))), with = FALSE]

然后执行:

DT2[is.na(DT2)] <- 0

当然,我希望通过引用修改我的原始DT。 不过,以下是需要注意的:

DT[, as.numeric(which(sapply(DT,is.numeric))), with = FALSE]
                 [is.na(DT[, as.numeric(which(sapply(DT,is.numeric))), with = FALSE])]<- 0

我遇到了以下错误:

"在 [.data.table([...] 中,i 的类型无效(为矩阵)"

请问我漏掉了什么?非常感谢您的帮助!!


你缺少data.table的基本语法,不支持DT[...] <- y。建议阅读vignettes https://github.com/Rdatatable/data.table/wiki/Getting-started,这是一种比“为每个步骤寻找解决方案”更高效的学习方式。下面的答案甚至不需要你发现的with=FALSE技巧。 - Frank
谢谢您的建议。您能详细说明一下基本语法错误"...which don't do DT[...] <- y"是什么意思吗?为什么在一个情况下赋值有效而在另一个情况下无效呢?我在指南中找不到相关内容,如果您能帮助我理解这个问题,那将对我非常有帮助。 - HannesZ
数据表不应该像 DT[...] <- y 这样使用,其中 ... 是你想到的任何内容。赋值应该使用 :=set 而不是 <-。箭头方式实际上在特殊情况下确实可以工作,也就是说表格被修改了,但它不是按引用方式工作的(据我所知),因此不是惯用方法。要使用 data.table,您需要学习一些它们的惯用法。如果您还不知道我所说的 := 是什么意思,那么查看vignettes是一个很好的理由。 - Frank
a) 在每个分组的j表达式内部计算数值列列表numeric_cols <- which(sapply(DT,is.numeric)),相比于在顶端一次性计算,效率更高。b) 然后只需引用DT[, numeric_cols]。c) 是的,在j表达式中放置函数调用很棘手,而且经常会触发语法错误。 - smci
3个回答

12

我们可以使用set

for(j in seq_along(DT)){
    set(DT, i = which(is.na(DT[[j]]) & is.numeric(DT[[j]])), j = j, value = 0)
 }

或者为数值列创建索引,通过循环遍历它,并将 NA 值 设置 为 0。

ind <-   which(sapply(DT, is.numeric))
for(j in ind){
    set(DT, i = which(is.na(DT[[j]])), j = j, value = 0)
}

数据

set.seed(24)
DT <- data.table(v1= c(NA, 1:4), v2 = c(NA, LETTERS[1:4]), v3=c(rnorm(4), NA))

set( ..., j = j, ...) 的意思是什么?所有列吗?毫无疑问,我们只需要在 OP 要求的数值子集上执行 set() - smci
1
@smci 不是所有的列。在代码中,我得到了ind,它获取“数字”列的列索引,因此只循环遍历这些列。 - akrun
好的。为什么不能通过使用“ind”索引到“names(DT)”来避免循环,并将其作为“set()”的j参数传递,从而获得列名列表?我猜查找NA的表达式然后需要是二维的。嗯,我想“set()”已经相当快了。 - smci
@smci 不确定我是否理解你的问题。j可以采用列名称或列索引。在这里,“ind”是索引。 - akrun
为什么不能避免循环 for(j in ind) { ... set(..., j=j, ...) }?一般情况下,不能直接执行 set(DT, j=ind) 吗? 我认为你可以这样做,但是循环 j 的唯一原因是用于查找该特定 j 的 NA 行的 i-expression 会发生变化。 - smci

4

我希望能够探索并可能改进@akrun上述优秀答案中使用的数据。以下是他在示例中使用的数据:

library(data.table)

set.seed(24)
DT <- data.table(v1= c(NA, 1:4), v2 = c(NA, LETTERS[1:4]), v3=c(rnorm(4), NA))
DT

#>    v1   v2         v3
#> 1: NA <NA> -0.5458808
#> 2:  1    A  0.5365853
#> 3:  2    B  0.4196231
#> 4:  3    C -0.5836272
#> 5:  4    D         NA

以下是他建议使用的两种方法:

fun1 <- function(x){
  for(j in seq_along(x)){
  set(x, i = which(is.na(x[[j]]) & is.numeric(x[[j]])), j = j, value = 0)
  }
}

fun2 <- function(x){
  ind <-   which(sapply(x, is.numeric))
  for(j in ind){
    set(x, i = which(is.na(x[[j]])), j = j, value = 0)
  }
}

我认为上述第一种方法非常聪明,因为它利用了NAs的类型信息。

首先,尽管在i参数中不可用.SD,但可以使用get()提取列名,因此我认为可以以这种方式进行子赋值data.table

fun3 <- function(x){
  nms <- names(x)[sapply(x, is.numeric)]
  for(j in nms){
    x[is.na(get(j)), (j):=0]
  }
}

通常情况下,需要依赖.SD.SDcols仅在数字列上工作。

fun4 <- function(x){
  nms <- names(x)[sapply(x, is.numeric)]
  x[, (nms):=lapply(.SD, function(i) replace(i, is.na(i), 0)), .SDcols=nms]  
}

但是我突然想到:“嘿,谁说我们不能使用基础的R语言进行这种操作呢?”下面是一个简单的lapply()函数和条件语句,包装在setDT()函数中。

fun5 <- function(x){
setDT(
  lapply(x, function(i){
    if(is.numeric(i))
         i[is.na(i)]<-0
    i
  })
)
}

最后,我们可以使用条件的相同思路来限制应用set()的列。
fun6 <- function(x){
  for(j in seq_along(x)){
    if (is.numeric(x[[j]]) )
      set(x, i = which(is.na(x[[j]])), j = j, value = 0)
  }
}

这是基准测试结果:
microbenchmark::microbenchmark(
  for.set.2cond = fun1(copy(DT)),
  for.set.ind = fun2(copy(DT)),
  for.get = fun3(copy(DT)),
  for.SDcol = fun4(copy(DT)),
  for.list = fun5(copy(DT)),
  for.set.if =fun6(copy(DT))
)

#> Unit: microseconds
#>           expr     min      lq     mean   median       uq      max neval cld
#>  for.set.2cond  59.812  67.599 131.6392  75.5620 114.6690 4561.597   100 a  
#>    for.set.ind  71.492  79.985 142.2814  87.0640 130.0650 4410.476   100 a  
#>        for.get 553.522 569.979 732.6097 581.3045 789.9365 7157.202   100   c
#>      for.SDcol 376.919 391.784 527.5202 398.3310 629.9675 5935.491   100  b 
#>       for.list  69.722  81.932 137.2275  87.7720 123.6935 3906.149   100 a  
#>     for.set.if  52.380  58.397 116.1909  65.1215  72.5535 4570.445   100 a  

2
您需要使用tidyverse中的purrr函数map_if,以及ifelse来在一行代码中完成任务。
library(tidyverse)
set.seed(24)
DT <- data.table(v1= sample(c(1:3,NA),20,replace = T), v2 = sample(c(LETTERS[1:3],NA),20,replace = T), v3=sample(c(1:3,NA),20,replace = T))

下面的单行代码采用一个具有数字和非数字列的DT,并仅对数字列进行操作,以将NA替换为0:
DT %>% map_if(is.numeric,~ifelse(is.na(.x),0,.x)) %>% as.data.table

所以,有时候tidyverse比data.table更简洁易懂 :-)

我可以问一下为什么我的回答被踩了吗?它不起作用吗? - Lazarus Thurston
谢谢,帮了我很多! - Lucas Aschenbach

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