Si uso la sintaxis de dplyr sobre 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 incorrectamente 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 de 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) ]
r
data.table
dplyr
Polimerasa
fuente
fuente
dplyr
métodos para las tablas de datos, pero la tabla de datos también tiene sus propios métodos comparablesdplyr
se usa endata.frame
sy los correspondientesdata.table
, consulte aquí (y las referencias allí).Respuestas:
No hay una respuesta simple / directa porque las filosofías de ambos paquetes difieren en ciertos aspectos. Por eso, algunos compromisos son inevitables. Estas son algunas de las inquietudes que quizás deba abordar / considerar.
Operaciones que involucran
i
(==filter()
yslice()
en dplyr)Suponga
DT
con, digamos, 10 columnas. Considere estas expresiones de data.table:DT[a > 1, .N] ## --- (1) DT[a > 1, mean(b), by=.(c, d)] ## --- (2)
(1) da el número de filas en la
DT
columna wherea > 1
. (2) devuelvemean(b)
agrupados porc,d
para la misma expresión eni
que (1).Las
dplyr
expresiones de uso común 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 de data.table son más cortos. Además, también son más eficientes en memoria 1 . ¿Por qué? Porque 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 columnasb, c, d
para las operaciones sucesivas. Para superar esto, tenemosselect()
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)
Tenga en cuenta que en (5) y (6), todavía subconjuntamos columnas
a
que no requerimos. Pero no estoy seguro de cómo evitarlo. Si lafilter()
función tuviera un argumento para seleccionar las columnas a devolver, podríamos evitar este problema, pero entonces la función no hará una sola tarea (que también es una elección de diseño de dplyr).Subasignar por referencia
Por ejemplo, en data.table puede hacer:
DT[a %in% some_vals, a := NA]
que actualiza 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 columnas por referencia.
Otras funcionalidades
En data.table, puede agregar mientras se une, y esto es más fácil de entender y es eficiente en la memoria, ya que el resultado de la unión intermedia nunca se materializa. Consulte 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 sucesivas de data.table tampoco es compatible con la sintaxis de dplyr.
Recientemente implementamos uniones superpuestas en data.table para unir rangos de intervalo ( 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
[.data.table
para que podamos recopilar las otras características como agrupar, agregar al unirse, etc., que tendrán las mismas limitaciones descritas anteriormente.Desde 1.9.4, data.table implementa la indexación automática utilizando claves secundarias para subconjuntos basados en búsquedas binarias rápidas en la sintaxis R normal. Por ejemplo:
DT[x == 1]
yDT[x %in% some_vals]
creará automáticamente un índice en la primera ejecución, que luego se utilizará en subconjuntos sucesivos de la misma columna para un subconjunto rápido mediante la búsqueda binaria. Esta característica seguirá evolucionando. Consulte esta esencia para obtener una breve descripción general de esta función.Por la forma en que
filter()
se implementa para data.tables, no aprovecha esta característica.Una característica de dplyr es que también proporciona una interfaz a las bases de datos que utilizan la misma sintaxis, que data.table no hace 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 la eficiencia de la memoria afecta directamente la velocidad (especialmente a medida que los datos aumentan de tamaño), ya que el cuello de botella en la mayoría de los casos es mover los datos de la memoria principal a la caché (y hacer uso de los datos en la caché tanto como sea posible, reducir las pérdidas de caché - para reducir el acceso a la memoria principal). Sin entrar en detalles aquí.
fuente
filter()
plus eficientesummarise()
usando el mismo enfoque que usa dplyr para SQL, es decir, crear una expresión y luego ejecutar solo una vez a pedido. Es poco probable que esto se implemente en un futuro cercano porque dplyr es lo suficientemente rápido para mí e implementar un planificador / optimizador de consultas es relativamente difícil.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 según el comentario de Polymerase.
fuente
microbenchmark
paquete, descubrí que ejecutar eldplyr
código del OP en la versión original (marco de datos) dediamonds
tomó un tiempo promedio de 0.012 segundos, mientras que tomó un tiempo promedio de 0.024 segundos después de convertirdiamonds
a una tabla de datos. Ejecutar eldata.table
código de G. Grothendieck tomó 0.013 segundos. Al menos en mi sistema, parecedplyr
ydata.table
tiene aproximadamente el mismo rendimiento. Pero, ¿por qué seríadplyr
más lento cuando el marco de datos se convierte por primera vez en una tabla de datos?Para responder tu pregunta:
data.table
data.table
sintaxis puraEn muchos casos, esto será un compromiso aceptable para aquellos que quieran la
dplyr
sintaxis, aunque posiblemente será más lento quedplyr
con los marcos de datos simples.Un factor importante parece ser que
dplyr
se 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 tiene una velocidad comparable, pero la agrupación no lo es. Creo que el culpable es esta línea en
dplyr:::grouped_dt
:if (copy) { data <- data.table::copy(data) }
donde el valor
copy
predeterminado esTRUE
(y no se puede cambiar fácilmente a FALSO que puedo ver). Es probable que esto no represente el 100% de la diferencia, pero la sobrecarga general por sí sola en algo del tamañodiamonds
más probable no es la diferencia total.La cuestión es que para tener una gramática coherente, se
dplyr
hace la agrupación en dos pasos. Primero establece claves en una copia de la tabla de datos original que coinciden con los grupos, y solo más tarde 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 se debe asignar.Para su información, si a alguien le importa, encontré esto usando
treeprof
(install_github("brodieg/treeprof")
), un visor de árbol experimental (y todavía muy alfa) para laRprof
salida:Tenga en cuenta que lo anterior actualmente solo funciona en macs AFAIK. Además, desafortunadamente,
Rprof
registra las llamadas del tipopackagename::funname
como anónimas, por lo que en realidad podrían ser todas y cada una de lasdatatable::
llamadas internasgrouped_dt
las responsables, pero de las pruebas rápidas parecía que eradatatable::copy
la más grande.Dicho esto, puede ver rápidamente cómo no hay tanta sobrecarga alrededor de la
[.data.table
llamada, 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)
fuente
Puede usar dtplyr ahora, que es parte de tidyverse . Le permite usar declaraciones de estilo dplyr como de costumbre, pero utiliza una evaluación perezosa y traduce sus declaraciones a código data.table bajo el capó. La sobrecarga en la traducción es mínima, pero usted obtiene todos, si no, la mayoría de los beneficios de data.table. Más detalles en el repositorio oficial de git aquí y en la página de tidyverse .
fuente