Der schnellste Weg, NAs in einer großen Datentabelle zu ersetzen

150

Ich habe eine große Datentabelle mit vielen fehlenden Werten, die über die ~ 200.000 Zeilen und 200 Spalten verteilt sind. Ich möchte diese NA-Werte so effizient wie möglich auf Nullen umcodieren.

Ich sehe zwei Möglichkeiten:
1: Konvertieren zu einem data.frame und Verwendung etwas wie diese
2: Irgendeine Art von kühlem data.table Unterstellbefehl

Ich bin mit einer ziemlich effizienten Lösung vom Typ 1 zufrieden. Die Konvertierung in einen data.frame und dann zurück in eine data.table dauert nicht lange.

Zach
quelle
5
Warum wollen Sie das konvertieren data.tablezu einem data.frame? A data.table ist a data.frame. Jede data.frame-Operation funktioniert einfach.
Andrie
5
@ Andrea. Ein wesentlicher Unterschied besteht darin, dass Sie nicht auf eine Spalte in a zugreifen können, data.tableindem Sie die Spaltennummer angeben. so DT[,3]wird nicht die dritte Spalte geben. Ich denke, dies macht die im Link vorgeschlagene Lösung hier unrentabel. Ich bin sicher, es gibt einen eleganten Ansatz mit etwas data.tableZauberei!
Ramnath
6
@Ramnath, AFAIK, DT[, 3, with=FALSE]gibt die dritte Spalte zurück.
Andrie
2
@ Andrea. Aber es gibt immer noch ein Problem, mydf[is.na(mydf) == TRUE]das die Arbeit an Datenrahmen erledigt, während mydt[is.na(mydt) == TRUE]es mir etwas Seltsames gibt, selbst wenn ich es benutzewith=FALSE
Ramnath
2
@ Ramnath, Punkt genommen. Meine frühere Aussage war zu weit gefasst, dh ich habe mich geirrt. Es tut uns leid. Data.tables verhalten sich nur dann wie data.frames, wenn keine data.table-Methode vorhanden ist.
Andrie

Antworten:

183

Hier ist eine Lösung mit data.table ‚s :=Operator, aufbauend auf Andrie und Ramnath der Antworten.

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

Beachten Sie, dass f_dowle dt1 als Referenz aktualisiert hat. Wenn eine lokale Kopie erforderlich ist, ist ein expliziter Aufruf der copyFunktion erforderlich, um eine lokale Kopie des gesamten Datensatzes zu erstellen. data.table ist setkey, key<-und :=nicht Copy-on-Write.

Als nächstes wollen wir sehen, wo f_dowle seine Zeit verbringt.

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 ...

Dort würde ich mich auf na.replaceund konzentrieren is.na, wo es ein paar Vektorkopien und Vektorscans gibt. Diese können ziemlich einfach beseitigt werden, indem eine kleine na.replace C-Funktion geschrieben wird, die NAdurch Referenz im Vektor aktualisiert wird. Das würde mindestens die 20 Sekunden halbieren, denke ich. Existiert eine solche Funktion in einem R-Paket?

Der Grund dafür f_andriekann sein, dass einige Male das Ganze kopiert dt1oder eine logische Matrix erstellt wird, die so groß wie das Ganze dt1ist. Die anderen beiden Methoden arbeiten jeweils an einer Spalte (obwohl ich sie mir nur kurz angesehen habe NAToUnknown).

EDIT (elegantere Lösung, wie von Ramnath in Kommentaren gefordert):

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

Ich wünschte, ich hätte es so gemacht!

EDIT2 (über 1 Jahr später, jetzt)

Es gibt auch set(). Dies kann schneller sein, wenn viele Spalten durchlaufen werden, da der (geringe) Aufwand beim Aufrufen [,:=,]einer Schleife vermieden wird. setist eine Schleife :=. Siehe ?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
quelle
5
+! gute Antwort! Ist es möglich, ein intuitiveres Äquivalent zu dem eval(parse)...Zeug zu haben ? Im weiteren Sinne halte ich es für nützlich, Operationen zu haben, die auf allen Elementen des data.table.
Ramnath
1
Ihr zweiter Codeblock scheint der am besten data.tablegeeignete Weg zu sein, dies zu tun. Vielen Dank!
Zach
3
@Statwonk Ich denke, Sie DThaben Spalten vom Typ logical, im Gegensatz zum create_dt()Beispiel für diesen Test. Ändern Sie das 4. Argument des set()Aufrufs ( 0in Ihrem Beispiel und geben Sie double in R ein) in FALSEund es sollte ohne Warnung funktionieren.
Matt Dowle
2
@Statwonk Und ich habe eine Feature-Anfrage eingereicht, um diesen Fall zu lockern und diese Warnung zu löschen, wenn die Vektoren 0 und 1 der Länge 1 auf logisch gezwungen werden: # 996 . Möglicherweise nicht, da Sie aus Geschwindigkeitsgründen vor unnötigem, sich wiederholendem Erzwingen gewarnt werden möchten.
Matt Dowle
1
@StefanF Stimmt und ich bevorzuge seq_along(DT)auch. Aber dann muss der Leser wissen, dass seq_alongdies entlang der Spalten und nicht entlang der Zeilen erfolgt. seq_len(col(DT))ein kleines bisschen expliziter aus diesem Grund.
Matt Dowle
28

Hier ist die einfachste, die ich mir vorstellen kann:

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

Es ist effizient und es müssen keine Funktionen und kein anderer Klebercode geschrieben werden.

Bar
quelle
funktioniert nicht auf großen Datenmengen und normalen Workstation-Computern (Speicherzuordnungsfehler)
Jake
3
@ Jake auf einem Computer mit 16 GB RAM Ich konnte dies auf 31 Millionen Zeilen, ~ 20 Spalten, ausführen. YMMV natürlich.
Bar
Ich verweise auf Ihre empirischen Beweise. Vielen Dank.
Jake
10
Leider funktioniert es in den neuesten Versionen von data.table nicht. Es heißt Fehler in [.data.table(dt, is.na (dt)): i ist ein ungültiger Typ (Matrix). Vielleicht könnte eine 2-Spalten-Matrix in Zukunft eine Liste von DT-Elementen zurückgeben (im Sinne von A [B] in FAQ 2.14). Bitte teilen Sie datatable-help mit, ob Sie dies möchten, oder fügen Sie Ihre Kommentare zu FR # 657 hinzu. >
Skan
das ist interessant! Ich habe immerset
Marbel
14

Spezielle Funktionen ( nafillund setnafill) für diesen Zweck sind im data.tablePaket verfügbar (Version> = 1.12.4):

Es verarbeitet Spalten parallel, um zuvor veröffentlichte Benchmarks so gut zu adressieren, dass sie unter den Zeitpunkten im Vergleich zum bisher schnellsten Ansatz liegen, und skaliert sie auch mithilfe einer 40-Kerne-Maschine.

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
quelle
Das ist eine großartige Funktion! Planen Sie Unterstützung für Zeichenspalten hinzuzufügen? Dann könnte es hier verwendet werden .
Ismirsehregal
1
@ismirsehregal ja, Sie können diese Funktion hier verfolgen 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

Nur als Referenz, langsamer im Vergleich zu gdata oder data.matrix, verwendet jedoch nur das Paket data.table und kann nicht numerische Einträge verarbeiten.

Andreas Rhode
quelle
5
Sie könnten dies wahrscheinlich sowohl vermeiden ifelseals auch durch Referenz aktualisieren DT[, names(DT) := lapply(.SD, function(x) {x[is.na(x)] <- "0" ; x})]. Und ich bezweifle, dass es langsamer sein wird als die Antworten, die Sie erwähnt haben.
David Arenburg
11

Hier ist eine Lösung, die NAToUnknownim gdataPaket verwendet wird. Ich habe Andries Lösung verwendet, um eine riesige Datentabelle zu erstellen, und auch Zeitvergleiche mit Andries Lösung aufgenommen.

# 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
quelle
+1 Guter Fund. Interessant - es ist das erste Mal, dass ich Timings mit ähnlicher userZeit, aber wirklich großem Zeitunterschied sehe elapsed.
Andrie
@Andrie Ich habe versucht rbenchmark, Lösungen mit mehr Replikationen zu vergleichen, habe jedoch möglicherweise aufgrund der Größe des Datenrahmens einen Speicherfehler festgestellt . Wenn Sie benchmarkbeide Lösungen mit mehreren Replikationen ausführen können, wären diese Ergebnisse interessant, da ich nicht sicher bin, warum ich eine dreifache Beschleunigung erhalte
Ramnath
@Ramnath Um die Dinge richtig zu machen, sind die Timings in dieser Antwort ncol=5meiner Meinung nach (sollte viel länger dauern) aufgrund des Fehlers in create_dt.
Matt Dowle
5

Der Vollständigkeit halber können Sie NAs auch durch 0 ersetzen

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

Um Ergebnisse und Zeiten zu vergleichen, habe ich alle bisher genannten Ansätze berücksichtigt.

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

Der neue Ansatz ist also etwas langsamer als, f_dowle3aber schneller als alle anderen Ansätze. Aber um ehrlich zu sein, ist dies gegen meine Intuition der data.table-Syntax und ich habe keine Ahnung, warum dies funktioniert. Kann mich jemand aufklären?

bratwoorst711
quelle
1
Ja, ich habe sie überprüft. Deshalb habe ich die paarweisen Identitäten eingefügt.
Bratwoorst711
1
Hier ist ein Grund, warum es nicht der idiomatische Weg ist - stackoverflow.com/a/20545629
Naumz
4

Mein Verständnis ist, dass das Geheimnis schneller Operationen in R darin besteht, Vektoren (oder Arrays, die Vektoren unter der Haube sind) zu verwenden.

In dieser Lösung verwende ich ein, data.matrixdas ein ist array, sich aber ein bisschen wie ein verhalte data.frame. Da es sich um ein Array handelt, können Sie eine sehr einfache Vektorsubstitution verwenden, um das NAs zu ersetzen :

Eine kleine Hilfsfunktion zum Entfernen des NAs. Die Essenz ist eine einzelne Codezeile. Ich mache das nur, um die Ausführungszeit zu messen.

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

Eine kleine Hilfsfunktion zum Erstellen data.tableeiner bestimmten Größe.

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

Demonstration an einer winzigen Probe:

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
quelle
Das ist ein sehr schöner Beispieldatensatz. Ich werde versuchen, mich zu verbessern remove_na. Dieser Zeitpunkt von 21,57 Sekunden beinhaltet das create_dt(einschließlich runifund sample) zusammen mit dem remove_na. Gibt es eine Chance, die Sie bearbeiten könnten, um die 2-mal aufzuteilen?
Matt Dowle
Gibt es einen kleinen Fehler create_dt? Es scheint immer eine 5-Spalten-Datentabelle zu erstellen, unabhängig davon, ob sie ncolübergeben wurde.
Matt Dowle
@ MatthewDowle Gut entdeckt. Fehler entfernt (sowie die Timings)
Andrie
Das Konvertieren in eine Matrix funktioniert nur dann ordnungsgemäß, wenn alle Spalten vom gleichen Typ sind.
Skan
2

Um auf viele Spalten zu verallgemeinern, können Sie diesen Ansatz verwenden (unter Verwendung vorheriger Beispieldaten, aber Hinzufügen einer Spalte):

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

Ich habe die Geschwindigkeit allerdings nicht getestet

arono686
quelle
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
quelle
3
Und wie würden Sie dies auf mehr als eine Spalte verallgemeinern?
David Arenburg
@ DavidArenburg schreibe einfach eine for-Schleife. Dies sollte die akzeptierte Antwort sein: Es ist die einfachste!
Baibo
1

Mit der fifelseFunktion aus den neuesten data.tableVersionen 1.12.6 ist sie sogar zehnmal schneller als NAToUnknownim gdataPaket:

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
quelle
Können Sie dieser Antwort einige Zeitvergleiche hinzufügen? Ich denke, es f_dowle3wird noch schneller gehen: stackoverflow.com/a/7249454/345660
Zach