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.