Il modo più veloce per sostituire i NA in un grande data.table


150

Ho un grande data.table , con molti valori mancanti sparsi nelle sue ~ 200k righe e 200 colonne. Vorrei ricodificare quei valori NA in zeri nel modo più efficiente possibile.

Vedo due opzioni:
1: Converti in un data.frame e usa qualcosa del genere
2: Qualche tipo di comando sub settato data.table

Sarò felice con una soluzione abbastanza efficiente di tipo 1. La conversione in un data.frame e quindi di nuovo in un data.table non richiederà troppo tempo.


5
Perché vuoi convertire il data.tablein a data.frame? A data.table è a data.frame. Qualsiasi operazione data.frame funzionerà.
Andrie,

5
@Andrie. una differenza fondamentale è che non puoi accedere a una colonna in a data.tablespecificando il numero di colonna. quindi DT[,3]non darà la terza colonna. penso che questo renda la soluzione proposta nel link non praticabile qui. sono sicuro che ci sia un approccio elegante usando un po 'di data.tablemagia!
Ramnath,

6
@Ramnath, AFAIK, DT[, 3, with=FALSE]restituisce la terza colonna.
Andrie,

2
@Andrie. ma c'è ancora un problema mydf[is.na(mydf) == TRUE]nel lavoro sui frame di dati, mentre mydt[is.na(mydt) == TRUE]mi dà qualcosa di strano anche se usowith=FALSE
Ramnath,

2
@Ramnath, punto preso. La mia precedente affermazione era troppo ampia, cioè mi sbagliavo. Scusate. Data.tables si comporta come data.frames solo quando non esiste un metodo data.table.
Andrie,

Risposte:


184

Ecco una soluzione che utilizza l' operatore di data.table:= , basandosi sulle risposte di 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

Nota che f_dowle ha aggiornato dt1 per riferimento. Se è necessaria una copia locale, è necessaria una chiamata esplicita alla copyfunzione per creare una copia locale dell'intero set di dati. data.table's setkey, key<-e :=non copia-su-scrivere.

Quindi, vediamo dove f_dowle sta trascorrendo il suo 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ì mi concentrerei su na.replacee is.na, dove ci sono alcune copie vettoriali e scansioni vettoriali. Questi possono essere facilmente eliminati scrivendo una piccola funzione na.replace C che si aggiorna NAper riferimento nel vettore. Ciò dimezzerebbe almeno i 20 secondi che penso. Una tale funzione esiste in qualsiasi pacchetto R?

Il motivo f_andriepotrebbe non essere dovuto al fatto che copia l'intero dt1, o crea una matrice logica grande quanto l'insieme dt1, alcune volte. Gli altri 2 metodi funzionano su una colonna alla volta (anche se ho guardato solo brevemente NAToUnknown).

EDIT (soluzione più elegante come richiesto da Ramnath nei commenti):

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

Vorrei averlo fatto in questo modo per iniziare!

EDIT2 (oltre 1 anno dopo, ora)

C'è anche set(). Questo può essere più veloce se ci sono molte colonne in loop, in quanto evita il (piccolo) overhead della chiamata [,:=,]in un loop. setè un loop :=. Vedere ?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)
}

5
+! Bella risposta! è possibile avere un equivalente più intuitivo delle eval(parse)...cose. su una nota più ampia, penso che sarebbe utile avere operazioni che funzionino su tutti gli elementi di data.table.
Ramnath,

1
Il tuo secondo blocco di codice sembra essere il modo più data.tableappropriato per farlo. Grazie!
Zach,

3
@Statwonk Immagino che tu DTabbia colonne di tipo logical, a differenza create_dt()dell'esempio per questo test. Cambia il quarto argomento della set()chiamata (che è 0nel tuo esempio e digita double in R) in FALSEe dovrebbe funzionare senza preavviso.
Matt Dowle,

2
@Statwonk E ho presentato una richiesta di funzione per rilassare questo caso e rilasciare quell'avvertimento quando si coprono i vettori 0 e 1 di lunghezza 1 su logici: # 996 . Potrebbe non farlo dal momento che, per velocità, vuoi essere avvisato di inutili costrizioni ripetitive.
Matt Dowle,

1
@StefanF Vero e preferisco seq_along(DT)anche io . Ma poi il lettore deve sapere che seq_alongsarebbe lungo le colonne e non lungo le file. seq_len(col(DT))un po 'più esplicito per quel motivo.
Matt Dowle,

28

Ecco il più semplice che potrei inventare:

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

È efficiente e non è necessario scrivere funzioni e altri codici di colla.


non funziona su set di dati di grandi dimensioni e normali computer workstation (errore di allocazione della memoria)
Jake,

3
@Jake su una macchina con 16 GB di RAM Sono stato in grado di eseguire questo su 31 M righe, ~ 20 colonne. YMMV ovviamente.
Bar

Rinvio alle tue prove empiriche. Grazie.
Jake,

10
Sfortunatamente nelle ultime versioni di data.table non funziona. Dice Errore in [.data.table(dt, is.na (dt)): i è un tipo non valido (matrice). Forse in futuro una matrice a 2 colonne potrebbe restituire un elenco di elementi di DT (nello spirito di A [B] nella FAQ 2.14). Per favore informa datatable-help se desideri questo, o aggiungi i tuoi commenti a FR # 657. >
skan

questo è interessante! Ho sempre usatoset
marbel il

15

Le funzioni dedicate ( nafille setnafill) a tale scopo sono disponibili nel data.tablepacchetto (versione> = 1.12.4):

Elabora parallelamente le colonne indirizzando così bene i benchmark precedentemente pubblicati, al di sotto dei suoi tempi rispetto all'approccio più veloce fino ad ora, e anche ridimensionato, utilizzando 40 core.

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

Questa è una grande caratteristica! Stai pensando di aggiungere il supporto per le colonne di caratteri? Quindi potrebbe essere usato qui .
Ismirsehregal,

1
@ismirsehregal sì, puoi tenere traccia di questa funzione qui 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

Solo per riferimento, più lento rispetto a gdata o data.matrix, ma utilizza solo il pacchetto data.table e può gestire voci non numeriche.


5
Probabilmente potresti evitare ifelsee aggiornare per riferimento facendo DT[, names(DT) := lapply(.SD, function(x) {x[is.na(x)] <- "0" ; x})]. E dubito che sarà più lento delle risposte che hai citato.
David Arenburg,

11

Ecco una soluzione che utilizza NAToUnknownnel gdatapacchetto. Ho usato la soluzione di Andrie per creare un'enorme tabella di dati e ho incluso anche confronti temporali con la soluzione di 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

+1 Buona scoperta. Interessante: è la prima volta che vedo orari con tempi simili userma con una differenza davvero grande nel elapsedtempo.
Andrie,

@Andrie Ho provato a utilizzare le rbenchmarksoluzioni di benchmark utilizzando più repliche, ma ho riscontrato un errore di memoria insufficiente probabilmente a causa delle dimensioni del frame di dati. se è possibile eseguire benchmarkentrambe queste soluzioni con più repliche, quei risultati sarebbero interessanti in quanto non sono davvero sicuro del motivo per cui sto ottenendo un 3 volte accelerazione
Ramnath

@Ramnath Per ottenere le cose corrette, i tempi in questa risposta sono per ncol=5penso (dovrebbe richiedere molto più tempo) a causa del bug in create_dt.
Matt Dowle,

5

Per completezza, utilizzare un altro modo per sostituire le NA con 0

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

Per confrontare risultati e tempi ho incorporato tutti gli approcci menzionati finora.

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

Quindi il nuovo approccio è leggermente più lento di f_dowle3ma più veloce di tutti gli altri approcci. Ma ad essere sincero, ciò è contrario alla mia intuizione della sintassi data.table e non ho idea del perché funzioni. Qualcuno può illuminarmi?


1
Sì, li ho controllati, ecco perché ho incluso gli identici a coppie.
Bratwoorst711,

1
Ecco un motivo per cui non è il modo idiomatico - stackoverflow.com/a/20545629
Naumz

4

La mia comprensione è che il segreto delle operazioni veloci in R è utilizzare il vettore (o le matrici, che sono vettori sotto il cofano).

In questa soluzione faccio uso di un data.matrixche è un arrayma mi comporto un po 'come un data.frame. Poiché si tratta di un array, è possibile utilizzare una sostituzione vettoriale molto semplice per sostituire NAs:

Una piccola funzione di aiuto per rimuovere la NAs. L'essenza è una singola riga di codice. Lo faccio solo per misurare i tempi di esecuzione.

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

Una piccola funzione di aiuto per creare una data.tabledi una determinata dimensione.

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))
}

Dimostrazione su un piccolo campione:

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

Questo è un set di dati di esempio molto carino. Proverò a migliorare remove_na. Tale tempistica di 21.57 include il create_dt(incluso runife sample) insieme al remove_na. Qualche possibilità che potresti modificare per dividere le 2 volte?
Matt Dowle,

C'è un piccolo bug dentro create_dt? Sembra sempre creare un data.table a 5 colonne, indipendentemente dal fatto che sia ncolpassato.
Matt Dowle,

@MatthewDowle Ben notato. Errore rimosso (così come i tempi)
Andrie

La conversione in matrice funzionerà correttamente solo se tutte le colonne sono dello stesso tipo.
skan

2

Per generalizzare a molte colonne è possibile utilizzare questo approccio (utilizzando dati di esempio precedenti ma aggiungendo una colonna):

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))]

Non ho provato per la velocità però


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
> 

3
E come lo generalizzeresti a più di una colonna però?
David Arenburg,

@DavidArenburg basta scrivere un ciclo for. Questa dovrebbe essere la risposta accettata: è la più semplice!
baibo,

1

Utilizzando la fifelsefunzione delle data.tableversioni più recenti 1.12.6, è persino 10 volte più veloce rispetto NAToUnknownal gdatapacchetto:

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 

Puoi aggiungere alcuni confronti temporali a questa risposta? Penso che f_dowle3sarà ancora più veloce: stackoverflow.com/a/7249454/345660
Zach
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.