¿Cómo eliminar una fila por referencia en data.table?


150

Mi pregunta está relacionada con la asignación por referencia versus la copia en data.table. Quiero saber si uno puede eliminar filas por referencia, similar a

DT[ , someCol := NULL]

Quiero saber sobre

DT[someRow := NULL, ]

Supongo que hay una buena razón por la cual esta función no existe, por lo que tal vez podría señalar una buena alternativa al enfoque de copia habitual, como se muestra a continuación. En particular, yendo con mi favorito de ejemplo (data.table),

DT = data.table(x = rep(c("a", "b", "c"), each = 3), y = c(1, 3, 6), v = 1:9)
#      x y v
# [1,] a 1 1
# [2,] a 3 2
# [3,] a 6 3
# [4,] b 1 4
# [5,] b 3 5
# [6,] b 6 6
# [7,] c 1 7
# [8,] c 3 8
# [9,] c 6 9

Digamos que quiero eliminar la primera fila de esta tabla de datos. Sé que puedo hacerlo:

DT <- DT[-1, ]

pero a menudo queremos evitar eso, porque estamos copiando el objeto (y eso requiere aproximadamente 3 * N de memoria, si es N object.size(DT), como se señaló aquí . Ahora descubrí set(DT, i, j, value). Sé cómo establecer valores específicos (como aquí: establecer todo valores en filas 1 y 2 y columnas 2 y 3 a cero)

set(DT, 1:2, 2:3, 0) 
DT
#      x y v
# [1,] a 0 0
# [2,] a 0 0
# [3,] a 6 3
# [4,] b 1 4
# [5,] b 3 5
# [6,] b 6 6
# [7,] c 1 7
# [8,] c 3 8
# [9,] c 6 9

Pero, ¿cómo puedo borrar las dos primeras filas, por ejemplo? Haciendo

set(DT, 1:2, 1:3, NULL)

establece todo el DT en NULL.

Mi conocimiento de SQL es muy limitado, así que ustedes me dicen: dado data.table usa tecnología SQL, ¿hay un equivalente al comando SQL?

DELETE FROM table_name
WHERE some_column=some_value

en data.table?


17
No creo que sea que data.table()use la tecnología SQL tanto como se pueda establecer un paralelismo entre las diferentes operaciones en SQL y los diversos argumentos de a data.table. Para mí, la referencia a "tecnología" de alguna manera implica que data.tablese encuentra en la parte superior de una base de datos SQL en algún lugar, lo que AFAIK no es el caso.
Chase

1
Gracias Chase. Sí, supongo que la analogía SQL fue una suposición descabellada.
Florian Oswald

1
A menudo debería ser suficiente definir un indicador para mantener filas, como DT[ , keep := .I > 1], luego, subconjunto para operaciones posteriores: DT[(keep), ...]quizás incluso setindex(DT, keep)la velocidad de este subconjunto. No es una panacea, pero vale la pena considerarla como una opción de diseño en su flujo de trabajo: ¿realmente desea eliminar todas esas filas de la memoria o preferiría excluirlas? La respuesta difiere según el caso de uso.
MichaelChirico

Respuestas:


125

Buena pregunta. data.tableNo se pueden eliminar filas por referencia todavía.

data.tablepuede agregar y eliminar columnas por referencia, ya que sobreasigna el vector de punteros de columna, como ya sabe. El plan es hacer algo similar para las filas y permitir rápido inserty delete. Una eliminación de fila se usaría memmoveen C para presupuestar los elementos (en todas y cada una de las columnas) después de las filas eliminadas. Eliminar una fila en el medio de la tabla aún sería bastante ineficiente en comparación con una base de datos de almacén de filas como SQL, que es más adecuada para insertar y eliminar rápidamente filas dondequiera que esas filas estén en la tabla. Pero aún así, sería mucho más rápido que copiar un nuevo objeto grande sin las filas eliminadas.

Por otro lado, dado que los vectores de columna se sobreasignarían, las filas podrían insertarse (y eliminarse) al final , al instante; por ejemplo, una serie temporal en crecimiento.


Se presenta como un problema: elimine las filas por referencia .


1
@Matthew Dowle ¿Hay alguna noticia sobre esto?
statquant

15
@statquant Creo que debería corregir los 37 errores y terminar freadprimero. Después de eso es bastante alto.
Matt Dowle

15
@MatthewDowle seguro, gracias de nuevo por todo lo que estás haciendo.
statquant

1
@rbatt Correcto. DT[b<8 & a>3]devuelve un nuevo data.table. Nos gustaría agregar delete(DT, b>=8 | a<=3)y DT[b>=8 | a<=8, .ROW:=NULL]. La ventaja de este último sería combinar con otras características [], como los números de fila i, unirse iy rollbeneficiarse de la [i,j,by]optimización.
Matt Dowle

2
@charliealpha Sin actualización. Contribuciones bienvenidas. Estoy dispuesto a guiarte. Necesita habilidades de C, de nuevo, estoy dispuesto a guiar.
Matt Dowle

29

El enfoque que he tomado para hacer que el uso de la memoria sea similar a la eliminación en el lugar es subconjugar una columna a la vez y eliminar. no es tan rápido como una solución C memmove adecuada, pero el uso de memoria es todo lo que me importa aquí. algo como esto:

DT = data.table(col1 = 1:1e6)
cols = paste0('col', 2:100)
for (col in cols){ DT[, (col) := 1:1e6] }
keep.idxs = sample(1e6, 9e5, FALSE) # keep 90% of entries
DT.subset = data.table(col1 = DT[['col1']][keep.idxs]) # this is the subsetted table
for (col in cols){
  DT.subset[, (col) := DT[[col]][keep.idxs]]
  DT[, (col) := NULL] #delete
}

55
+1 Buen enfoque de memoria eficiente. Así que, idealmente, necesitamos eliminar un conjunto de filas por referencia, ¿no es cierto? No había pensado en eso. Tendrá que ser una serie de memmoves para reducir las brechas, pero está bien.
Matt Dowle

¿Funcionaría esto como una función, o el uso en una función y el retorno lo obligan a hacer copias de memoria?
russellpierce

1
funcionaría en una función, ya que data.tables siempre son referencias.
vc273

1
gracias agradable Para acelerar un poco (sobre todo con muchas columnas) cambia DT[, col:= NULL, with = F]enset(DT, NULL, col, NULL)
Michele

2
Actualización a la luz del cambio de idioma y advertencia "with = FALSE junto con: = fue desaprobado en v1.9.4 lanzado en octubre de 2014. Ajuste el LHS de: = con paréntesis; por ejemplo, DT [, (myVar): = sum (b) , by = a] para asignar a los nombres de columna contenidos en la variable myVar. Ver? ': =' para otros ejemplos. Como se advirtió en 2014, esto ahora es una advertencia ".
Frank

6

Aquí hay una función de trabajo basada en la respuesta de @ vc273 y los comentarios de @ Frank.

delete <- function(DT, del.idxs) {           # pls note 'del.idxs' vs. 'keep.idxs'
  keep.idxs <- setdiff(DT[, .I], del.idxs);  # select row indexes to keep
  cols = names(DT);
  DT.subset <- data.table(DT[[1]][keep.idxs]); # this is the subsetted table
  setnames(DT.subset, cols[1]);
  for (col in cols[2:length(cols)]) {
    DT.subset[, (col) := DT[[col]][keep.idxs]];
    DT[, (col) := NULL];  # delete
  }
   return(DT.subset);
}

Y ejemplo de su uso:

dat <- delete(dat,del.idxs)   ## Pls note 'del.idxs' instead of 'keep.idxs'

Donde "dat" es un data.table. Eliminar 14k filas de 1.4M filas toma 0.25 segundos en mi computadora portátil.

> dim(dat)
[1] 1419393      25
> system.time(dat <- delete(dat,del.idxs))
   user  system elapsed 
   0.23    0.02    0.25 
> dim(dat)
[1] 1404715      25
> 

PD. Como soy nuevo en SO, no pude agregar comentarios al hilo de @ vc273 :-(


Comenté bajo la respuesta de vc explicando la sintaxis cambiada para (col): =. Es extraño tener una función llamada "eliminar" pero un argumento relacionado con qué guardar. Por cierto, generalmente se prefiere usar un ejemplo reproducible en lugar de mostrar tenue para sus propios datos. Podría reutilizar DT de la pregunta, por ejemplo.
Frank

No entiendo por qué lo haces por referencia, pero luego utilizas una tarea dat <-
skan

1
@skan, esa asignación asigna "dat" para apuntar a la tabla de datos modificada que se ha creado al subconjugar la tabla de datos original. La evaluación <- no hace una copia de los datos devueltos, solo le asigna un nuevo nombre. enlace
Jarno P.

@ Frank, he actualizado la función para la rareza que señaló.
Jarno P.

OK gracias. Dejo el comentario, ya que todavía creo que vale la pena señalar que no se recomienda mostrar la salida de la consola en lugar de un ejemplo reproducible aquí. Además, un punto de referencia único no es tan informativo. Si también midió el tiempo necesario para el subconjunto, sería más informativo (ya que la mayoría de nosotros no sabemos intuitivamente cuánto tiempo lleva, mucho menos cuánto tiempo lleva su compilación). De todos modos, no quiero sugerir que esta sea una mala respuesta; Soy uno de sus votantes.
Frank

4

En su lugar, o tratando de establecer NULL, intente configurar NA (haciendo coincidir el tipo NA para la primera columna)

set(DT,1:2, 1:3 ,NA_character_)

3
Sí, eso funciona, supongo. Mi problema es que tengo muchos datos y quiero deshacerme exactamente de esas filas con NA, posiblemente sin tener que copiar DT para deshacerme de esas filas. gracias por tu comentario de todos modos!
Florian Oswald

4

El tema sigue siendo interesante para muchas personas (incluido yo).

¿Qué hay de eso? Solía assignreemplazar glovalenvel código y el descrito anteriormente. Sería mejor capturar el entorno original, pero al menos en globalenvél es eficiente en memoria y actúa como un cambio por ref.

delete <- function(DT, del.idxs) 
{ 
  varname = deparse(substitute(DT))

  keep.idxs <- setdiff(DT[, .I], del.idxs)
  cols = names(DT);
  DT.subset <- data.table(DT[[1]][keep.idxs])
  setnames(DT.subset, cols[1])

  for (col in cols[2:length(cols)]) 
  {
    DT.subset[, (col) := DT[[col]][keep.idxs]]
    DT[, (col) := NULL];  # delete
  }

  assign(varname, DT.subset, envir = globalenv())
  return(invisible())
}

DT = data.table(x = rep(c("a", "b", "c"), each = 3), y = c(1, 3, 6), v = 1:9)
delete(DT, 3)

Para ser claros, esto no se elimina por referencia (basado en address(DT); delete(DT, 3); address(DT)), aunque puede ser eficiente en algún sentido.
Frank

1
No, no lo hace. Emula el comportamiento y es eficiente en memoria. Por eso dije: actúa así . Pero estrictamente hablando tienes razón, la dirección cambió.
JRR

3

Aquí hay algunas estrategias que he usado. Creo que puede venir una función .ROW. Ninguno de estos enfoques a continuación son rápidos. Estas son algunas estrategias un poco más allá de los subconjuntos o el filtrado. Traté de pensar como dba solo tratando de limpiar los datos. Como se señaló anteriormente, puede seleccionar o eliminar filas en data.table:

data(iris)
iris <- data.table(iris)

iris[3] # Select row three

iris[-3] # Remove row three

You can also use .SD to select or remove rows:

iris[,.SD[3]] # Select row three

iris[,.SD[3:6],by=,.(Species)] # Select row 3 - 6 for each Species

iris[,.SD[-3]] # Remove row three

iris[,.SD[-3:-6],by=,.(Species)] # Remove row 3 - 6 for each Species

Nota: .SD crea un subconjunto de los datos originales y le permite hacer bastante trabajo en j o data.table posterior. Ver https://stackoverflow.com/a/47406952/305675 . Aquí ordené mis iris por Sepal Length, tome un Sepal.Length especificado como mínimo, seleccione los tres primeros (por Sepal Length) de todas las especies y devuelva todos los datos que lo acompañan:

iris[order(-Sepal.Length)][Sepal.Length > 3,.SD[1:3],by=,.(Species)]

Los enfoques, sobre todo, reordenan una tabla de datos secuencialmente al eliminar filas. Puede transponer una tabla de datos y eliminar o reemplazar las filas antiguas que ahora son columnas transpuestas. Cuando se usa ': = NULL' para eliminar una fila transpuesta, también se elimina el siguiente nombre de columna:

m_iris <- data.table(t(iris))[,V3:=NULL] # V3 column removed

d_iris <- data.table(t(iris))[,V3:=V2] # V3 column replaced with V2

Cuando transpone el data.frame de nuevo a un data.table, es posible que desee cambiar el nombre del data.table original y restaurar los atributos de la clase en caso de eliminación. La aplicación de ": = NULL" a un data.table ahora transpuesto crea todas las clases de caracteres.

m_iris <- data.table(t(d_iris));
setnames(d_iris,names(iris))

d_iris <- data.table(t(m_iris));
setnames(m_iris,names(iris))

Es posible que solo desee eliminar filas duplicadas que puede hacer con o sin una clave:

d_iris[,Key:=paste0(Sepal.Length,Sepal.Width,Petal.Length,Petal.Width,Species)]     

d_iris[!duplicated(Key),]

d_iris[!duplicated(paste0(Sepal.Length,Sepal.Width,Petal.Length,Petal.Width,Species)),]  

También es posible agregar un contador incremental con '.I'. Luego puede buscar claves o campos duplicados y eliminarlos eliminando el registro con el contador. Esto es computacionalmente costoso, pero tiene algunas ventajas ya que puede imprimir las líneas que se eliminarán.

d_iris[,I:=.I,] # add a counter field

d_iris[,Key:=paste0(Sepal.Length,Sepal.Width,Petal.Length,Petal.Width,Species)]

for(i in d_iris[duplicated(Key),I]) {print(i)} # See lines with duplicated Key or Field

for(i in d_iris[duplicated(Key),I]) {d_iris <- d_iris[!I == i,]} # Remove lines with duplicated Key or any particular field.

También puede completar una fila con 0 o NA y luego usar una consulta i para eliminarlos:

 X 
   x v foo
1: c 8   4
2: b 7   2

X[1] <- c(0)

X
   x v foo
1: 0 0   0
2: b 7   2

X[2] <- c(NA)
X
    x  v foo
1:  0  0   0
2: NA NA  NA

X <- X[x != 0,]
X <- X[!is.na(x),]

Esto realmente no responde la pregunta (sobre la eliminación por referencia) y el uso ten un marco data.frame no suele ser una buena idea; verifique str(m_iris)que todos los datos se hayan convertido en una cadena / carácter. Por cierto, también puede obtener números de fila utilizando d_iris[duplicated(Key), which = TRUE]sin hacer una columna de contador.
Frank

1
Sí, tiene usted razón. No contesto la pregunta específicamente. Pero eliminar una fila por referencia aún no tiene funcionalidad oficial o documentación, y muchas personas van a venir a esta publicación en busca de una funcionalidad genérica para hacer exactamente eso. Podríamos crear una publicación para responder la pregunta sobre cómo eliminar una fila. El desbordamiento de pila es muy útil y realmente entiendo la necesidad de mantener las respuestas exactas a la pregunta. Sin embargo, a veces, creo que SO puede ser un poco fascista a este respecto ... pero tal vez haya una buena razón para eso.
rferrisx

Ok, gracias por explicarlo. Creo que por ahora nuestra discusión aquí es suficiente señal para cualquiera que se confunda en este caso.
Frank
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.