La forma más rápida de reemplazar NA en una gran tabla de datos.


150

Tengo una gran tabla de datos , con muchos valores perdidos dispersos en sus ~ 200k filas y 200 columnas. Me gustaría volver a codificar esos valores de NA a ceros de la manera más eficiente posible.

Veo dos opciones:
1: convertir a data.frame y usar algo como esto
2: algún tipo de comando de subconfiguración de data.table genial

Estaré contento con una solución bastante eficiente del tipo 1. La conversión a un data.frame y luego de regreso a data.table no llevará demasiado tiempo.


55
¿Por qué quieres convertir el data.tablea a data.frame? A data.table es a data.frame. Cualquier operación data.frame simplemente funcionará.
Andrie

55
@Andrie. una diferencia clave es que no puede acceder a una columna en a data.tableespecificando el número de columna. entonces DT[,3]no dará la tercera columna. Creo que esto hace que la solución propuesta en el enlace sea inviable aquí. ¡Estoy seguro de que hay un enfoque elegante usando algo de data.tablemagia!
Ramnath

66
@Ramnath, AFAIK, DT[, 3, with=FALSE]devuelve la tercera columna.
Andrie

2
@Andrie. pero todavía hay un problema que mydf[is.na(mydf) == TRUE]hace el trabajo en marcos de datos, mientras mydt[is.na(mydt) == TRUE]que me da algo extraño incluso si lo usowith=FALSE
Ramnath

2
@Ramnath, punto tomado. Mi declaración anterior era demasiado amplia, es decir, estaba equivocado. Lo siento. Data.tables solo se comporta como data.frames cuando no hay un método data.table.
Andrie

Respuestas:


184

Aquí hay una solución que utiliza el operador de data.table:= , basándose en las respuestas de Andrie y 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

Tenga en cuenta que f_dowle actualizó dt1 por referencia. Si se requiere una copia local, entonces se necesita una llamada explícita a la copyfunción para hacer una copia local de todo el conjunto de datos. data.table's setkey, key<-y :=no copiar en escritura.

A continuación, veamos dónde f_dowle está gastando su tiempo.

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

Allí, me enfocaría en na.replacey is.na, donde hay algunas copias vectoriales y escaneos vectoriales. Esos pueden eliminarse fácilmente escribiendo una pequeña función na.replace C que se actualiza NApor referencia en el vector. Eso al menos reduciría a la mitad los 20 segundos, creo. ¿Existe tal función en algún paquete R?

La razón por la que f_andriefalla puede deberse a que copia la totalidad dt1o crea una matriz lógica tan grande como la totalidad de dt1algunas veces. Los otros 2 métodos funcionan en una columna a la vez (aunque solo lo vi brevemente NAToUnknown).

EDITAR (solución más elegante según lo solicitado por Ramnath en los comentarios):

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

¡Ojalá lo hiciera así para empezar!

EDIT2 (más de 1 año después, ahora)

También existe set(). Esto puede ser más rápido si hay muchas columnas en bucle, ya que evita la sobrecarga (pequeña) de llamar [,:=,]en un bucle. setEs un loopable :=. Ver ?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)
}

55
+! ¡gran respuesta! ¿Es posible tener un equivalente más intuitivo de las eval(parse)...cosas? En una nota más amplia, creo que sería útil tener operaciones que funcionen en todos los elementos de la data.table.
Ramnath

1
Su segundo bloque de código parece ser la forma más data.tableadecuada de hacer esto. ¡Gracias!
Zach

3
@Statwonk Supongo que DTtiene columnas de tipo logical, a diferencia del create_dt()ejemplo para esta prueba. Cambie el 4to argumento de la set()llamada (que está 0en su ejemplo y escriba double en R) a FALSEy debería funcionar sin previo aviso.
Matt Dowle

2
@Statwonk Y he presentado una solicitud de función para relajar este caso y descartar esa advertencia al obligar a los vectores de longitud 1 0 y 1 a lógico: # 996 . Es posible que no lo haga, ya que, para mayor velocidad, debe ser advertido sobre la coacción repetitiva innecesaria.
Matt Dowle el

1
@StefanF True y yo seq_along(DT)también prefiero . Pero entonces el lector tiene que saber que seq_alongsería a lo largo de las columnas y no en las filas. seq_len(col(DT))un poquito más explícito por esa razón.
Matt Dowle el

28

Aquí está el más simple que se me ocurrió:

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

Es eficiente y no necesita escribir funciones y otro código de pegamento.


no funciona en grandes conjuntos de datos y computadoras normales de estación de trabajo (error de asignación de memoria)
Jake

3
@Jake en una máquina con 16 GB de RAM Pude ejecutar esto en 31 millones de filas, ~ 20 columnas. YMMV por supuesto.
Bar

Me refiero a su evidencia empírica. Gracias.
Jake

10
Desafortunadamente, en las últimas versiones de data.table no funciona. Dice Error en [.data.table(dt, is.na (dt)): i es un tipo no válido (matriz). Quizás en el futuro una matriz de 2 columnas pueda devolver una lista de elementos de DT (en el espíritu de A [B] en la pregunta frecuente 2.14). Informe a datatable-help si desea esto o agregue sus comentarios a FR # 657. >
skan

¡esto es interesante! Siempre uséset
marbel

14

Las funciones dedicadas ( nafilly setnafill) para ese propósito están disponibles en el data.tablepaquete (versión> = 1.12.4):

Procesa columnas en paralelo para abordar de forma correcta los puntos de referencia publicados anteriormente, por debajo de sus tiempos frente al enfoque más rápido hasta ahora, y también se amplió, utilizando una 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

Esa es una gran característica! ¿Planea agregar soporte para columnas de caracteres? Entonces podría usarse aquí .
ismirsehregal

1
@ismirsehregal sí, puede rastrear esta característica aquí 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 como referencia, más lento en comparación con gdata o data.matrix, pero usa solo el paquete data.table y puede manejar entradas no numéricas.


55
Probablemente podría evitar ifelsey actualizar por referencia haciendo DT[, names(DT) := lapply(.SD, function(x) {x[is.na(x)] <- "0" ; x})]. Y dudo que sea más lento que las respuestas que has mencionado.
David Arenburg

11

Aquí hay una solución que se usa NAToUnknownen el gdatapaquete. He usado la solución de Andrie para crear una gran tabla de datos y también incluí comparaciones de tiempo con la solución de 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 Buen hallazgo. Interesante: es la primera vez que veo tiempos con un usertiempo similar pero con una gran diferencia de elapsedtiempo.
Andrie

@Andrie Intenté usar rbenchmarkpara comparar soluciones usando más réplicas, pero obtuve un error de falta de memoria posiblemente debido al tamaño del marco de datos. si puede ejecutar benchmarkambas soluciones con múltiples réplicas, esos resultados serían interesantes ya que no estoy realmente seguro de por qué estoy obteniendo una aceleración 3x
Ramnath

@Ramnath Para que las cosas sean correctas, ncol=5creo que los tiempos en esta respuesta (deberían tomar mucho más tiempo) debido al error create_dt.
Matt Dowle

5

En aras de la integridad, otra forma de reemplazar NAs con 0 es usar

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

Para comparar resultados y tiempos, he incorporado todos los enfoques mencionados hasta ahora.

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

Entonces, el nuevo enfoque es un poco más lento f_dowle3pero más rápido que todos los otros enfoques. Pero para ser honesto, esto va en contra de mi intuición de la sintaxis data.table y no tengo idea de por qué funciona. ¿Alguien puede iluminarme?


1
Sí, los revisé, es por eso que he incluido los idénticos por pares.
bratwoorst711

1
Aquí hay una razón por la cual no es la forma idiomática - stackoverflow.com/a/20545629
Naumz

4

Tengo entendido que el secreto para las operaciones rápidas en R es utilizar vectores (o matrices, que son vectores bajo el capó).

En esta solución, utilizo un data.matrixque es un arraypero se comporta un poco como a data.frame. Debido a que es una matriz, puede usar una sustitución de vectores muy simple para reemplazar el NAs:

Una pequeña función auxiliar para eliminar el NAs. La esencia es una sola línea de código. Solo hago esto para medir el tiempo de ejecución.

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

Una pequeña función auxiliar para crear una data.tablede un tamaño determinado.

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

Demostración en una pequeña muestra:

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

Ese es un conjunto de datos de ejemplo muy bueno. Lo intentaré y mejoraré remove_na. Ese momento de 21.57 incluye el create_dt(incluyendo runify sample) junto con el remove_na. ¿Alguna posibilidad de que puedas editar para dividir las 2 veces?
Matt Dowle

¿Hay un pequeño error en create_dt? Parece que siempre crea una tabla de datos de 5 columnas, independientemente de lo que se haya ncolpasado.
Matt Dowle

@MatthewDowle Bien visto. Error eliminado (así como los horarios)
Andrie

La conversión a matriz solo funcionará correctamente si todas las columnas son del mismo tipo.
skan

2

Para generalizar a muchas columnas, puede usar este enfoque (usando datos de muestra anteriores pero agregando una columna):

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

Aunque no probé la velocidad


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
¿Y cómo generalizarías esto a más de una columna?
David Arenburg

@DavidArenburg solo escribe un bucle for. Esta debería ser la respuesta aceptada: ¡es la más simple!
baibo

1

Usando la fifelsefunción de las data.tableversiones más recientes 1.12.6, es incluso 10 veces más rápido que NAToUnknownen el gdatapaquete:

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 

¿Puedes agregar algunas comparaciones de tiempo a esta respuesta? Creo f_dowle3que aún será más rápido: stackoverflow.com/a/7249454/345660
Zach
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.