将 NA 替换为 0,仅在 data.table 中的数字列中

Replace NA with 0, only in numeric columns in data.table

我有一个包含不同数据类型的列的 data.table。我的目标是 select 只有数字列,并将这些列中的 NA 值替换为 0。 我知道用零替换 na-values 是这样的:

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

对于 select 只有数字列,我找到了这个解决方案,效果很好:

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

我明白了

"Error in [.data.table([...] i is invalid type (matrix)"

我错过了什么? 非常感谢任何帮助!!

我们可以使用set

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

或者为数字列创建一个索引,遍历它并且 set 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))

我想探索并可能改进@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)
  }
}

我认为上面的第一种方法真的很天才,因为它利用了 NA 是有类型的这一事实。

首先,即使 .SDi 参数中不可用,也可以使用 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  

您需要 tidyverse purrr 函数 map_ififelse 才能在一行代码中完成这项工作。

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 更简洁 :-)