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 laj-expression
(de la misma llamada a la función) y darnos cuenta de que no hay necesidad de columnas en (1). La expresión eni
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 columnasb,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 defilter()
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]
yDT[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.