r data.table dplyr

dplyr en data.table, ¿realmente estoy usando data.table?



(3)

No hay una respuesta directa / simple porque las filosofías de ambos paquetes difieren en ciertos aspectos. Entonces, algunos compromisos son inevitables. Estas son algunas de las inquietudes que puede necesitar abordar / considerar.

Operaciones que involucran i (== filter() y slice() en dplyr)

Suponga DT con digamos 10 columnas. Considere estas expresiones data.table:

DT[a > 1, .N] ## --- (1) DT[a > 1, mean(b), by=.(c, d)] ## --- (2)

(1) da el número de filas en DT donde la columna a > 1 . (2) devuelve la mean(b) agrupada por c,d para la misma expresión en i que (1).

Las expresiones dplyr comúnmente utilizadas serían:

DT %>% filter(a > 1) %>% summarise(n()) ## --- (3) DT %>% filter(a > 1) %>% group_by(c, d) %>% summarise(mean(b)) ## --- (4)

Claramente, los códigos data.table son más cortos. Además, también son más eficientes en memoria 1 . ¿Por qué? Debido a que tanto en (3) como en (4), filter() devuelve filas para las 10 columnas primero, cuando en (3) solo necesitamos el número de filas, y en (4) solo necesitamos las columnas b, c, d para Las sucesivas operaciones. Para superar esto, tenemos que select() columnas a priori:

DT %>% select(a) %>% filter(a > 1) %>% summarise(n()) ## --- (5) DT %>% select(a,b,c,d) %>% filter(a > 1) %>% group_by(c,d) %>% summarise(mean(b)) ## --- (6)

Es esencial destacar una gran diferencia filosófica entre los dos paquetes:

  • En data.table , nos gusta mantener juntas estas operaciones relacionadas, y eso permite ver la j-expression (de la misma llamada a la función) y darnos cuenta de que no hay necesidad de columnas en (1). La expresión en i se calcula, y .N es solo la suma de ese vector lógico que da el número de filas; todo el subconjunto nunca se realiza. En (2), solo las columnas b,c,d se materializan en el subconjunto, otras columnas se ignoran.

  • Pero en dplyr , la filosofía es hacer que una función haga precisamente una cosa bien . No hay (al menos actualmente) ninguna forma de saber si la operación después de filter() necesita todas esas columnas que filtramos. Tendrá que pensar con anticipación si desea realizar tales tareas de manera eficiente. Personalmente, me parece contraintuitivo en este caso.

Tenga en cuenta que en (5) y (6), todavía subconjunto la columna a que no requerimos. Pero no estoy seguro de cómo evitar eso. Si la función filter() tuviera un argumento para seleccionar las columnas a devolver, podríamos evitar este problema, pero entonces la función no realizará solo una tarea (que también es una opción de diseño dplyr).

Subasignar por referencia

dplyr nunca se actualizará por referencia. Esta es otra gran diferencia (filosófica) entre los dos paquetes.

Por ejemplo, en data.table puedes hacer:

DT[a %in% some_vals, a := NA]

que actualiza la columna a por referencia solo en aquellas filas que satisfacen la condición. Por el momento, dplyr copia en profundidad toda la tabla de datos internamente para agregar una nueva columna. @BrodieG ya mencionó esto en su respuesta.

Pero la copia profunda se puede reemplazar por una copia superficial cuando se implementa FR # 617 . También relevante: dplyr: FR # 614 . Tenga en cuenta que aún así, la columna que modifique siempre se copiará (por lo tanto, un poco más lenta / menos eficiente en memoria). No habrá forma de actualizar las columnas por referencia.

Otras funcionalidades

  • En data.table, puede agregarse mientras se une, y esto es más fácil de entender y es eficiente en memoria ya que el resultado de la unión intermedia nunca se materializa. Mira esta publicación para ver un ejemplo. No puede (¿en este momento?) Hacer eso usando la sintaxis data.table / data.frame de dplyr.

  • La función de combinaciones continuas de data.table tampoco es compatible con la sintaxis de dplyr.

  • Recientemente implementamos uniones superpuestas en data.table para unir en intervalos de intervalos ( aquí hay un ejemplo ), que es una función separada foverlaps() en este momento, y por lo tanto podría usarse con los operadores de tubería (magrittr / pipeR? - nunca lo probé yo mismo).

    Pero, en última instancia, nuestro objetivo es integrarlo en [.data.table para que podamos aprovechar las otras características como agrupar, agregar y unir, etc., que tendrán las mismas limitaciones descritas anteriormente.

  • Desde 1.9.4, data.table implementa la indexación automática usando claves secundarias para subconjuntos basados ​​en búsqueda binaria rápida en sintaxis R regular. Ej: DT[x == 1] y DT[x %in% some_vals] crearán automáticamente un índice en la primera ejecución, que luego se usará en subconjuntos sucesivos de la misma columna para subconjuntos rápidos mediante búsqueda binaria. Esta característica continuará evolucionando. Consulte esta información general para obtener una breve descripción general de esta función.

    Por la forma en que se implementa filter() para data.tables, no aprovecha esta característica.

  • Una característica de dplyr es que también proporciona una interfaz para bases de datos que usan la misma sintaxis, que data.table no tiene en este momento.

Por lo tanto, tendrá que sopesar estos (y probablemente otros puntos) y decidir en función de si estas compensaciones son aceptables para usted.

HTH

(1) Tenga en cuenta que el uso eficiente de la memoria afecta directamente la velocidad (especialmente a medida que los datos se hacen más grandes), ya que el cuello de botella en la mayoría de los casos está moviendo los datos de la memoria principal a la memoria caché (y haciendo uso de los datos en la memoria caché tanto como sea posible - reduzca las pérdidas de memoria caché) - para reducir el acceso a la memoria principal). No voy a entrar en detalles aquí.

Si uso la sintaxis de dplyr encima de una tabla de datos , ¿obtengo todos los beneficios de velocidad de la tabla de datos mientras sigo usando la sintaxis de dplyr? En otras palabras, ¿uso mal la tabla de datos si la consulto con la sintaxis dplyr? ¿O necesito usar una sintaxis de tabla de datos pura para aprovechar todo su poder?

Gracias de antemano por cualquier consejo. Ejemplo de código:

library(data.table) library(dplyr) diamondsDT <- data.table(ggplot2::diamonds) setkey(diamondsDT, cut) diamondsDT %>% filter(cut != "Fair") %>% group_by(cut) %>% summarize(AvgPrice = mean(price), MedianPrice = as.numeric(median(price)), Count = n()) %>% arrange(desc(Count))

Resultados:

# cut AvgPrice MedianPrice Count # 1 Ideal 3457.542 1810.0 21551 # 2 Premium 4584.258 3185.0 13791 # 3 Very Good 3981.760 2648.0 12082 # 4 Good 3928.864 3050.5 4906

Aquí está la equivalencia de tabla de datos que se me ocurrió. No estoy seguro si cumple con las buenas prácticas de DT. Pero me pregunto si el código es realmente más eficiente que la sintaxis dplyr detrás de escena:

diamondsDT [cut != "Fair" ] [, .(AvgPrice = mean(price), MedianPrice = as.numeric(median(price)), Count = .N), by=cut ] [ order(-Count) ]


Para responder tu pregunta:

  • Sí, estás usando data.table
  • Pero no tan eficientemente como lo haría con la sintaxis pura de data.table

En muchos casos, este será un compromiso aceptable para aquellos que desean la sintaxis dplyr , aunque posiblemente será más lento que dplyr con marcos de datos simples.

Un gran factor parece ser que dplyr copiará data.table de forma predeterminada al agrupar. Considere (usando microbenchmark):

Unit: microseconds expr min lq median diamondsDT[, mean(price), by = cut] 3395.753 4039.5700 4543.594 diamondsDT[cut != "Fair"] 12315.943 15460.1055 16383.738 diamondsDT %>% group_by(cut) %>% summarize(AvgPrice = mean(price)) 9210.670 11486.7530 12994.073 diamondsDT %>% filter(cut != "Fair") 13003.878 15897.5310 17032.609

El filtrado es de velocidad comparable, pero la agrupación no. Creo que el culpable es esta línea en dplyr:::grouped_dt :

if (copy) { data <- data.table::copy(data) }

donde la copy defecto es TRUE (y no se puede cambiar fácilmente a FALSO como puedo ver). Es probable que esto no represente el 100% de la diferencia, pero la sobrecarga general sola en algo del tamaño de los diamonds probablemente no sea la diferencia completa.

El problema es que para tener una gramática consistente, dplyr agrupa en dos pasos. Primero establece las claves en una copia de la tabla de datos original que coinciden con los grupos, y solo más tarde se agrupa. data.table solo asigna memoria para el grupo de resultados más grande, que en este caso es solo una fila, por lo que hace una gran diferencia en la cantidad de memoria que debe asignarse.

FYI, si a alguien le importa, encontré esto usando treeprof ( install_github("brodieg/treeprof") ), un visualizador de árbol experimental (y todavía muy alfa) para la salida de Rprof :

Tenga en cuenta que lo anterior solo funciona actualmente en Mac AFAIK. Además, desafortunadamente, Rprof registra las llamadas del tipo packagename::funname como anónimas, por lo que en realidad podría ser cualquiera y todas las datatable:: llamadas dentro de grouped_dt que son responsables, pero de las pruebas rápidas parecía datatable::copy es el gran uno.

Dicho esto, puede ver rápidamente cómo no hay tanta sobrecarga alrededor de la llamada [.data.table , pero también hay una rama completamente separada para la agrupación.

EDITAR : para confirmar la copia:

> tracemem(diamondsDT) [1] "<0x000000002747e348>" > diamondsDT %>% group_by(cut) %>% summarize(AvgPrice = mean(price)) tracemem[0x000000002747e348 -> 0x000000002a624bc0]: <Anonymous> grouped_dt group_by_.data.table group_by_ group_by <Anonymous> freduce _fseq eval eval withVisible %>% Source: local data table [5 x 2] cut AvgPrice 1 Fair 4358.758 2 Good 3928.864 3 Very Good 3981.760 4 Premium 4584.258 5 Ideal 3457.542 > diamondsDT[, mean(price), by = cut] cut V1 1: Ideal 3457.542 2: Premium 4584.258 3: Good 3928.864 4: Very Good 3981.760 5: Fair 4358.758 > untracemem(diamondsDT)


Solo inténtalo.

library(rbenchmark) library(dplyr) library(data.table) benchmark( dplyr = diamondsDT %>% filter(cut != "Fair") %>% group_by(cut) %>% summarize(AvgPrice = mean(price), MedianPrice = as.numeric(median(price)), Count = n()) %>% arrange(desc(Count)), data.table = diamondsDT[cut != "Fair", list(AvgPrice = mean(price), MedianPrice = as.numeric(median(price)), Count = .N), by = cut][order(-Count)])[1:4]

En este problema, parece que data.table es 2.4 veces más rápido que dplyr usando data.table:

test replications elapsed relative 2 data.table 100 2.39 1.000 1 dplyr 100 5.77 2.414

Revisado en base al comentario de la polimerasa.