Maneira mais rápida de substituir NAs em um grande data.table

150

Eu tenho um grande data.table , com muitos valores ausentes espalhados por suas ~ 200k linhas e 200 colunas. Gostaria de codificar novamente esses valores de NA para zeros da maneira mais eficiente possível.

Eu vejo duas opções:
1: Converter em um data.frame e usar algo como este
2: Algum tipo de comando legal de subconfiguração data.table

Ficarei feliz com uma solução bastante eficiente do tipo 1. A conversão para um data.frame e depois para uma tabela de dados não levará muito tempo.

Zach
fonte
5
Por que você deseja converter o data.tablepara um data.frame? A data.table é a data.frame. Qualquer operação data.frame simplesmente funcionará.
Andrie
5
@Andrie. uma diferença importante é que você não pode acessar uma coluna em um data.tableespecificando o número da coluna. então DT[,3]não dará a terceira coluna. acho que isso inviabiliza a solução proposta no link aqui. Tenho certeza de que existe uma abordagem elegante usando algumas data.tablemagias!
Ramnath 29/08
6
@Ramnath, AFAIK, DT[, 3, with=FALSE]retorna a terceira coluna.
28811 Andrie
2
@Andrie. mas ainda há um problema mydf[is.na(mydf) == TRUE]é que o trabalho em quadros de dados, ao mesmo tempo mydt[is.na(mydt) == TRUE]dá-me algo estranho mesmo que eu usowith=FALSE
Ramnath
2
@ Ramnath, ponto levado. Minha declaração anterior era muito ampla, ou seja, eu estava errado. Desculpe. Data.tables só se comportam como data.frames quando não há um método data.table.
Andrie

Respostas:

184

Aqui está uma solução usando o operador data.table:= , com base nas respostas de Andrie e Ramnath.

require(data.table)  # v1.6.6
require(gdata)       # v2.8.2

set.seed(1)
dt1 = create_dt(2e5, 200, 0.1)
dim(dt1)
[1] 200000    200    # more columns than Ramnath's answer which had 5 not 200

f_andrie = function(dt) remove_na(dt)

f_gdata = function(dt, un = 0) gdata::NAToUnknown(dt, un)

f_dowle = function(dt) {     # see EDIT later for more elegant solution
  na.replace = function(v,value=0) { v[is.na(v)] = value; v }
  for (i in names(dt))
    eval(parse(text=paste("dt[,",i,":=na.replace(",i,")]")))
}

system.time(a_gdata = f_gdata(dt1)) 
   user  system elapsed 
 18.805  12.301 134.985 

system.time(a_andrie = f_andrie(dt1))
Error: cannot allocate vector of size 305.2 Mb
Timing stopped at: 14.541 7.764 68.285 

system.time(f_dowle(dt1))
  user  system elapsed 
 7.452   4.144  19.590     # EDIT has faster than this

identical(a_gdata, dt1)   
[1] TRUE

Observe que f_dowle atualizou o dt1 por referência. Se uma cópia local for necessária, será necessária uma chamada explícita para a copyfunção para fazer uma cópia local de todo o conjunto de dados. de data.table setkey, key<-e:= não copiar-on-write.

A seguir, vamos ver onde f_dowle está gastando seu tempo.

Rprof()
f_dowle(dt1)
Rprof(NULL)
summaryRprof()
$by.self
                  self.time self.pct total.time total.pct
"na.replace"           5.10    49.71       6.62     64.52
"[.data.table"         2.48    24.17       9.86     96.10
"is.na"                1.52    14.81       1.52     14.81
"gc"                   0.22     2.14       0.22      2.14
"unique"               0.14     1.36       0.16      1.56
... snip ...

Lá, eu focaria na.replacee is.na, onde existem algumas cópias e digitalizações de vetores. Esses podem ser facilmente eliminados escrevendo uma pequena função C na.replace que atualizaNA por referência no vetor. Pelo menos isso reduziria pela metade os 20 segundos. Essa função existe em algum pacote R?

O motivo da f_andriefalha pode ser porque ele copia o conjunto inteiro dt1ou cria uma matriz lógica do tamanho do conjunto dt1algumas vezes. Os outros 2 métodos funcionam em uma coluna de cada vez (embora eu tenha olhado brevemente NAToUnknown).

EDIT (solução mais elegante conforme solicitado por Ramnath nos comentários):

f_dowle2 = function(DT) {
  for (i in names(DT))
    DT[is.na(get(i)), (i):=0]
}

system.time(f_dowle2(dt1))
  user  system elapsed 
 6.468   0.760   7.250   # faster, too

identical(a_gdata, dt1)   
[1] TRUE

Eu gostaria de fazê-lo dessa maneira para começar!

EDIT2 (mais de um ano depois, agora)

Há também set(). Isso pode ser mais rápido se houver muitas colunas em loop, pois evita a sobrecarga (pequena) da chamada [,:=,]em um loop. seté um loopable :=. Veja ?set.

f_dowle3 = function(DT) {
  # either of the following for loops

  # by name :
  for (j in names(DT))
    set(DT,which(is.na(DT[[j]])),j,0)

  # or by number (slightly faster than by name) :
  for (j in seq_len(ncol(DT)))
    set(DT,which(is.na(DT[[j]])),j,0)
}
Matt Dowle
fonte
5
+! Ótima resposta! é possível ter um equivalente mais intuitivo do eval(parse)...material. em uma nota mais ampla, acho que seria útil ter operações que funcionem em todos os elementos do data.table.
Ramnath
1
Seu segundo bloco de código parece ser a data.tablemaneira mais apropriada de fazer isso. Obrigado!
Zach
3
@ Statwonk Eu acho que o seu DTtem colunas do tipo logical, ao contrário do create_dt()exemplo para este teste. Altere o quarto argumento da set()chamada (que está 0no seu exemplo e digite duas vezes em R) para FALSEe deve funcionar sem aviso.
precisa saber é o seguinte
2
@Statwonk E eu enviei uma solicitação de recurso para relaxar esse caso e soltar esse aviso ao coagir os vetores 1 e 1 de comprimento 1 para a lógica: # 996 . Pode não ser, pois, por velocidade, você deseja ser avisado sobre coação repetitiva desnecessária.
quer
1
@StefanF True e eu seq_along(DT)também prefiro . Mas então o leitor precisa saber que seq_alongseria ao longo das colunas e não nas linhas. seq_len(col(DT))um pouco mais explícito por esse motivo.
precisa
28

Aqui está o mais simples que eu poderia criar:

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

É eficiente e não há necessidade de escrever funções e outros códigos de cola.

Barra
fonte
não funciona em grandes conjuntos de dados e estações de trabalho normais (memória de erro de alocação)
Jake
3
@ Jake em uma máquina com 16 GB de RAM, eu era capaz de executar isso em 31 milhões de linhas, ~ 20 colunas. YMMV é claro.
Bar
Adio a sua evidência empírica. Obrigado.
Jake
10
Infelizmente nas últimas versões do data.table, ele não funciona. Ele diz Erro em [.data.table(dt, is.na (dt)): i é do tipo inválido (matriz). Talvez no futuro uma matriz de 2 colunas possa retornar uma lista de elementos de TD (no espírito de A [B] na FAQ 2.14). Informe a ajuda da tabela de dados se você gostaria disso ou adicione seus comentários à FR # 657. >
skan
isto é interessante! Eu sempre useiset
marbel
14

Funções dedicadas ( nafille setnafill) para esse fim estão disponíveis no data.tablepacote (versão> = 1.12.4):

Ele processa colunas em paralelo tão bem que aborda os benchmarks publicados anteriormente, abaixo do tempo em relação à abordagem mais rápida até agora, e também ampliou, usando uma máquina de 40 núcleos.

library(data.table)
create_dt <- function(nrow=5, ncol=5, propNA = 0.5){
  v <- runif(nrow * ncol)
  v[sample(seq_len(nrow*ncol), propNA * nrow*ncol)] <- NA
  data.table(matrix(v, ncol=ncol))
}
f_dowle3 = function(DT) {
  for (j in seq_len(ncol(DT)))
    set(DT,which(is.na(DT[[j]])),j,0)
}

set.seed(1)
dt1 = create_dt(2e5, 200, 0.1)
dim(dt1)
#[1] 200000    200
dt2 = copy(dt1)
system.time(f_dowle3(dt1))
#   user  system elapsed 
#  0.193   0.062   0.254 
system.time(setnafill(dt2, fill=0))
#   user  system elapsed 
#  0.633   0.000   0.020   ## setDTthreads(1) elapsed: 0.149
all.equal(dt1, dt2)
#[1] TRUE

set.seed(1)
dt1 = create_dt(2e7, 200, 0.1)
dim(dt1)
#[1] 20000000    200
dt2 = copy(dt1)
system.time(f_dowle3(dt1))
#   user  system elapsed 
# 22.997  18.179  41.496
system.time(setnafill(dt2, fill=0))
#   user  system elapsed 
# 39.604  36.805   3.798 
all.equal(dt1, dt2)
#[1] TRUE
jangorecki
fonte
Esse é um ótimo recurso! Você está planejando adicionar suporte para colunas de caracteres? Então poderia ser usado aqui .
Ismirsehregal 26/11/19
1
@ismirsehregal sim, você pode acompanhar esse recurso aqui github.com/Rdatatable/data.table/issues/3992
jangorecki
12
library(data.table)

DT = data.table(a=c(1,"A",NA),b=c(4,NA,"B"))

DT
    a  b
1:  1  4
2:  A NA
3: NA  B

DT[,lapply(.SD,function(x){ifelse(is.na(x),0,x)})]
   a b
1: 1 4
2: A 0
3: 0 B

Apenas para referência, mais lento em comparação com gdata ou data.matrix, mas usa apenas o pacote data.table e pode lidar com entradas não numéricas.

Andreas Rhode
fonte
5
Você provavelmente poderia evitar ifelsee atualizar por referência fazendo isso DT[, names(DT) := lapply(.SD, function(x) {x[is.na(x)] <- "0" ; x})]. E duvido que seja mais lento que as respostas que você mencionou.
David Arenburg
11

Aqui está uma solução usando NAToUnknownno gdatapacote. Usei a solução da Andrie para criar uma enorme tabela de dados e também incluí comparações de tempo com a solução da Andrie.

# CREATE DATA TABLE
dt1 = create_dt(2e5, 200, 0.1)

# FUNCTIONS TO SET NA TO ZERO   
f_gdata  = function(dt, un = 0) gdata::NAToUnknown(dt, un)
f_Andrie = function(dt) remove_na(dt)

# COMPARE SOLUTIONS AND TIMES
system.time(a_gdata  <- f_gdata(dt1))

user  system elapsed 
4.224   2.962   7.388 

system.time(a_andrie <- f_Andrie(dt1))

 user  system elapsed 
4.635   4.730  20.060 

identical(a_gdata, g_andrie)  

TRUE
Ramnath
fonte
+1 Boa localização. Interessante - é a primeira vez que vejo horários com horários semelhantes, usermas realmente há uma grande diferença no elapsedtempo.
Andrie
@ Andrew Tentei usar rbenchmarkpara comparar soluções usando mais replicações, mas obtive um erro de falta de memória, possivelmente devido ao tamanho do quadro de dados. se você pode executar benchmarkem ambas estas soluções com várias repetições, esses resultados seria interessante como eu não sou realmente certo porque eu estou recebendo um aumento de velocidade 3x
Ramnath
@ Ramnath Para acertar as coisas, o tempo nesta resposta é para ncol=5eu acho (deve levar muito mais tempo) devido ao erro create_dt.
Matt Dowle
5

Por uma questão de completude, outra maneira de substituir NAs por 0 é usar

f_rep <- function(dt) {
dt[is.na(dt)] <- 0
return(dt)
}

Para comparar resultados e tempos, incorporei todas as abordagens mencionadas até agora.

set.seed(1)
dt1 <- create_dt(2e5, 200, 0.1)
dt2 <- dt1
dt3 <- dt1

system.time(res1 <- f_gdata(dt1))
   User      System verstrichen 
   3.62        0.22        3.84 
system.time(res2 <- f_andrie(dt1))
   User      System verstrichen 
   2.95        0.33        3.28 
system.time(f_dowle2(dt2))
   User      System verstrichen 
   0.78        0.00        0.78 
system.time(f_dowle3(dt3))
   User      System verstrichen 
   0.17        0.00        0.17 
system.time(res3 <- f_unknown(dt1))
   User      System verstrichen 
   6.71        0.84        7.55 
system.time(res4 <- f_rep(dt1))
   User      System verstrichen 
   0.32        0.00        0.32 

identical(res1, res2) & identical(res2, res3) & identical(res3, res4) & identical(res4, dt2) & identical(dt2, dt3)
[1] TRUE

Portanto, a nova abordagem é um pouco mais lenta do que f_dowle3mas mais rápida que todas as outras abordagens. Mas, para ser sincero, isso é contrário à minha intuição da sintaxe data.table e não tenho idéia do por que isso funciona. Alguém pode me esclarecer?

bratwoorst711
fonte
1
Sim, eu os verifiquei, é por isso que incluí os idênticos aos pares.
Bratwoorst711
1
Aqui está uma razão pela qual não é a maneira idiomática - stackoverflow.com/a/20545629
Naumz
4

Meu entendimento é que o segredo para operações rápidas em R é utilizar vetores (ou matrizes, que são vetores sob o capô).

Nesta solução, uso um data.matrixque é um, arraymas se comporta um pouco como um data.frame. Por ser uma matriz, você pode usar uma substituição de vetor muito simples para substituir os NAs:

Uma pequena função auxiliar para remover os NA. A essência é uma única linha de código. Eu só faço isso para medir o tempo de execução.

remove_na <- function(x){
  dm <- data.matrix(x)
  dm[is.na(dm)] <- 0
  data.table(dm)
}

Uma pequena função auxiliar para criar um data.tabletamanho específico.

create_dt <- function(nrow=5, ncol=5, propNA = 0.5){
  v <- runif(nrow * ncol)
  v[sample(seq_len(nrow*ncol), propNA * nrow*ncol)] <- NA
  data.table(matrix(v, ncol=ncol))
}

Demonstração em uma pequena amostra:

library(data.table)
set.seed(1)
dt <- create_dt(5, 5, 0.5)

dt
            V1        V2        V3        V4        V5
[1,]        NA 0.8983897        NA 0.4976992 0.9347052
[2,] 0.3721239 0.9446753        NA 0.7176185 0.2121425
[3,] 0.5728534        NA 0.6870228 0.9919061        NA
[4,]        NA        NA        NA        NA 0.1255551
[5,] 0.2016819        NA 0.7698414        NA        NA

remove_na(dt)
            V1        V2        V3        V4        V5
[1,] 0.0000000 0.8983897 0.0000000 0.4976992 0.9347052
[2,] 0.3721239 0.9446753 0.0000000 0.7176185 0.2121425
[3,] 0.5728534 0.0000000 0.6870228 0.9919061 0.0000000
[4,] 0.0000000 0.0000000 0.0000000 0.0000000 0.1255551
[5,] 0.2016819 0.0000000 0.7698414 0.0000000 0.0000000
Andrie
fonte
Esse é um exemplo muito bom de conjunto de dados. Vou tentar melhorar remove_na. Esse tempo de 21.57s inclui o create_dt(incluindo runife sample) junto com o remove_na. Alguma chance de você editar para dividir as duas vezes?
quer
Existe um pequeno erro create_dt? Parece para criar sempre um data.table 5 coluna independentemente do ncolpassado.
Matt Dowle
@MatthewDowle Bem visto. Erro removido (bem como os horários) #
Andrie
A conversão em matriz só funcionará corretamente se todas as colunas forem do mesmo tipo.
skan
2

Para generalizar para muitas colunas, você pode usar essa abordagem (usando dados de amostra anteriores, mas adicionando uma coluna):

z = data.table(x = sample(c(NA_integer_, 1), 2e7, TRUE), y = sample(c(NA_integer_, 1), 2e7, TRUE))

z[, names(z) := lapply(.SD, function(x) fifelse(is.na(x), 0, x))]

Mas não testou a velocidade

arono686
fonte
1
> DT = data.table(a=LETTERS[c(1,1:3,4:7)],b=sample(c(15,51,NA,12,21),8,T),key="a")
> DT
   a  b
1: A 12
2: A NA
3: B 15
4: C NA
5: D 51
6: E NA
7: F 15
8: G 51
> DT[is.na(b),b:=0]
> DT
   a  b
1: A 12
2: A  0
3: B 15
4: C  0
5: D 51
6: E  0
7: F 15
8: G 51
> 
Hao
fonte
3
E como você generalizaria isso para mais de uma coluna?
David Arenburg
@DavidArenburg basta escrever um loop for. Esta deve ser a resposta aceita: é a mais simples!
baibo 23/06
1

Usando a fifelsefunção das data.tableversões mais recentes 1.12.6, é até 10 vezes mais rápido que NAToUnknownno gdatapacote:

z = data.table(x = sample(c(NA_integer_, 1), 2e7, TRUE))
system.time(z[,x1 := gdata::NAToUnknown(x, 0)])

#   user  system elapsed 
#  0.798   0.323   1.173 
system.time(z[,x2:= fifelse(is.na(x), 0, x)])

#   user  system elapsed 
#  0.172   0.093   0.113 
Miao Cai
fonte
Você pode adicionar algumas comparações de tempo a esta resposta? Acho que f_dowle3ainda será mais rápido: stackoverflow.com/a/7249454/345660
Zach