¿Qué no puedo hacer con dtplyr que pueda en data.table?

9

Debo invertir mi esfuerzo de aprendizaje para los datos de bregar en R, concretamente entre dplyr, dtplyry data.table?

  • Lo uso dplyrprincipalmente, pero cuando los datos son demasiado grandes para eso lo usaré data.table, lo cual es una ocurrencia rara. Entonces, ahora que dtplyrv1.0 está disponible como interfaz data.table, en la superficie parece que nunca tendré que preocuparme por usar la data.tableinterfaz nunca más.

  • Entonces, ¿cuáles son las características o aspectos más útiles data.tableque no se pueden usar dtplyren este momento y que probablemente nunca se puedan hacer dtplyr?

  • A primera vista, dplyrcon los beneficios de data.tablehace que suene como dtplyrsi fuera a superar dplyr. ¿Habrá alguna razón para usar dplyruna vez que dtplyrhaya madurado completamente?

Nota: no estoy preguntando sobre dplyrvs data.table(como en data.table vs dplyr: ¿puede uno hacer algo bien que el otro no puede o hace mal? ), Pero dado que uno es preferido sobre el otro para un problema en particular, ¿por qué no t dtplyrser la herramienta para su uso.

dule arnaux
fuente
1
¿Hay algo en lo que puedas hacer bien dplyrque no puedas hacerlo bien data.table? Si no, cambiar data.tablea será mejor que dtplyr.
sindri_baldur
2
Del dtplyrarchivo Léame, 'Algunas data.tableexpresiones no tienen dplyrequivalente directo . Por ejemplo, no hay forma de expresar uniones cruzadas o continuas dplyr''. y 'Para coincidir con la dplyrsemántica, mutate() no se modifica en su lugar de forma predeterminada. Esto significa que la mayoría de las expresiones involucradas mutate()deben hacer una copia que no sería necesaria si estuviera usando data.tabledirectamente '. Hay una forma de evitar esa segunda parte, pero teniendo en cuenta la frecuencia con la que mutatese usa, es un gran inconveniente para mí.
ClancyStats

Respuestas:

15

Intentaré dar mis mejores guías, pero no es fácil porque uno debe estar familiarizado con todos los {data.table}, {dplyr}, {dtplyr} y también {base R}. Utilizo {data.table} y muchos paquetes {tidy-world} (excepto {dplyr}). Me encantan ambos, aunque prefiero la sintaxis de data.table a la de dplyr. Espero que todos los paquetes de tidy-world usen {dtplyr} o {data.table} como backend cuando sea necesario.

Al igual que con cualquier otra traducción (piense en dplyr-to-sparkly / SQL), hay cosas que pueden o no pueden traducirse, al menos por ahora. Quiero decir, quizás algún día {dtplyr} pueda traducirlo al 100%, quién sabe. La lista a continuación no es exhaustiva ni es 100% correcta, ya que haré todo lo posible para responder en función de mi conocimiento sobre temas / paquetes / problemas relacionados, etc.

Es importante destacar que, para aquellas respuestas que no son del todo precisas, espero que le brinde algunas guías sobre los aspectos de {data.table} a los que debe prestar atención y, compárelos con {dtplyr} y descubra las respuestas usted mismo. No tome estas respuestas por sentado.

Y espero que esta publicación se pueda utilizar como uno de los recursos para todos los usuarios / creadores de {dplyr}, {data.table} o {dtplyr} para debates y colaboraciones, y mejorar aún más las #RStats.

{data.table} no solo se usa para operaciones rápidas y eficientes en memoria. Hay muchas personas, incluido yo mismo, que prefieren la elegante sintaxis de {data.table}. También incluye otras operaciones rápidas como funciones de series de tiempo como la familia rodante (es decir, frollapply) escritas en C. Se puede usar con cualquier función, incluido tidyverse. ¡Utilizo mucho {data.table} + {purrr}!

Complejidad de operaciones

Esto se puede traducir fácilmente

library(data.table)
library(dplyr)
library(flights)
data <- data.table(diamonds)

# dplyr 
diamonds %>%
  filter(cut != "Fair") %>% 
  group_by(cut) %>% 
  summarize(
    avg_price    = mean(price),
    median_price = as.numeric(median(price)),
    count        = n()
  ) %>%
  arrange(desc(count))

# data.table
data [
  ][cut != 'Fair', by = cut, .(
      avg_price    = mean(price),
      median_price = as.numeric(median(price)),
      count        = .N
    )
  ][order( - count)]

{data.table} es muy rápido y eficiente en memoria porque (¿casi?) todo está construido desde cero desde C con los conceptos clave de actualización por referencia , clave (piense en SQL) y su implacable optimización en todas partes del paquete (es decir fifelse, el fread/freadorden de clasificación de radix adoptado por la base R), mientras se asegura de que la sintaxis es concisa y consistente, por eso creo que es elegante.

Desde Introducción a data.table , las operaciones de manipulación de datos principales como subconjunto, grupo, actualización, unión, etc. se mantienen juntas durante

  • sintaxis concisa y consistente ...

  • realizar análisis de manera fluida sin la carga cognitiva de tener que mapear cada operación ...

  • optimizando automáticamente las operaciones internamente y de manera muy efectiva, al conocer con precisión los datos requeridos para cada operación, lo que lleva a un código muy rápido y eficiente en la memoria

El último punto, como ejemplo,

# Calculate the average arrival and departure delay for all flights with “JFK” as the origin airport in the month of June.
flights[origin == 'JFK' & month == 6L,
        .(m_arr = mean(arr_delay), m_dep = mean(dep_delay))]
  • Primero subconjunto en i para encontrar índices de fila coincidentes donde el aeropuerto de origen es igual a "JFK", y el mes es igual a 6L. No subconjustamos toda la tabla data.table correspondiente a esas filas todavía.

  • Ahora, miramos j y encontramos que usa solo dos columnas. Y lo que tenemos que hacer es calcular su media (). Por lo tanto, subconjustamos solo las columnas correspondientes a las filas coincidentes y calculamos su valor medio ().

Dado que los tres componentes principales de la consulta (i, j y by) están juntos dentro de [...] , data.table puede ver los tres y optimizar la consulta por completo antes de la evaluación, no cada uno por separado . Por lo tanto, podemos evitar todo el subconjunto (es decir, subconjunto de las columnas además de arr_delay y dep_delay), tanto para la velocidad como para la eficiencia de la memoria.

Dado que, para obtener los beneficios de {data.table}, la traducción de {dtplr} debe ser correcta en ese sentido. Cuanto más complejas son las operaciones, más difíciles son las traducciones. Para operaciones simples como las anteriores, ciertamente se puede traducir fácilmente. Para los complejos, o aquellos que no son compatibles con {dtplyr}, debe averiguarlo como se mencionó anteriormente, uno tiene que comparar la sintaxis traducida y el punto de referencia y estar familiarizado con los paquetes relacionados.

Para operaciones complejas u operaciones no admitidas, podría proporcionar algunos ejemplos a continuación. De nuevo, solo estoy haciendo lo mejor que puedo. Se gentil conmigo.

Actualización por referencia

No entraré en la introducción / detalles, pero aquí hay algunos enlaces

Recurso principal: semántica de referencia

Más detalles: Comprender exactamente cuándo un data.table es una referencia a (frente a una copia de) otro data.table

Actualización por referencia , en mi opinión, la característica más importante de {data.table} y eso es lo que la hace tan rápida y eficiente en memoria. dplyr::mutateno lo admite por defecto. Como no estoy familiarizado con {dtplyr}, no estoy seguro de cuánto y qué operaciones pueden o no ser compatibles con {dtplyr}. Como se mencionó anteriormente, también depende de la complejidad de las operaciones, lo que a su vez afecta las traducciones.

Hay dos formas de usar la actualización por referencia en {data.table}

  • operador de asignación de {data.table} :=

  • set-family: set, setnames, setcolorder, setkey, setDT, fsetdiff, y muchos más

:=se usa más comúnmente en comparación con set. Para conjuntos de datos complejos y grandes, la actualización por referencia es la clave para obtener la máxima velocidad y eficiencia de memoria. La forma fácil de pensar (no es 100% precisa, ya que los detalles son mucho más complicados que esto, ya que implica una copia impresa / superficial y muchos otros factores), dicen que se trata de un gran conjunto de datos de 10 GB, con 10 columnas y 1 GB cada una . Para manipular una columna, debe tratar solo con 1 GB.

El punto clave es que, con la actualización por referencia , solo necesita lidiar con los datos requeridos. Es por eso que cuando usamos {data.table}, especialmente cuando tratamos con grandes conjuntos de datos, usamos actualización por referencia todo el tiempo siempre que sea posible. Por ejemplo, manipular grandes conjuntos de datos de modelado

# Manipulating list columns

df <- purrr::map_dfr(1:1e5, ~ iris)
dt <- data.table(df)

# data.table
dt [,
    by = Species, .(data   = .( .SD )) ][,  # `.(` shorthand for `list`
    model   := map(data, ~ lm(Sepal.Length ~ Sepal.Width, data = . )) ][,
    summary := map(model, summary) ][,
    plot    := map(data, ~ ggplot( . , aes(Sepal.Length, Sepal.Width)) +
                           geom_point())]

# dplyr
df %>% 
  group_by(Species) %>% 
  nest() %>% 
  mutate(
    model   = map(data, ~ lm(Sepal.Length ~ Sepal.Width, data = . )),
    summary = map(model, summary),
    plot    = map(data, ~ ggplot( . , aes(Sepal.Length, Sepal.Width)) +
                          geom_point())
  )

Es list(.SD)posible que {dtlyr} no admita la operación de anidamiento como usan los usuarios tidyverse tidyr::nest? Por lo tanto, no estoy seguro de si las operaciones posteriores se pueden traducir ya que la forma de {data.table} es más rápida y menos memoria.

NOTA: el resultado de data.table está en "milisegundos", dplyr en "minutos"

df <- purrr::map_dfr(1:1e5, ~ iris)
dt <- copy(data.table(df))

bench::mark(
  check = FALSE,

  dt[, by = Species, .(data = list(.SD))],
  df %>% group_by(Species) %>% nest()
)
# # A tibble: 2 x 13
#   expression                                   min   median `itr/sec` mem_alloc `gc/sec` n_itr  n_gc
#   <bch:expr>                              <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl> <int> <dbl>
# 1 dt[, by = Species, .(data = list(.SD))] 361.94ms 402.04ms   2.49      705.8MB     1.24     2     1
# 2 df %>% group_by(Species) %>% nest()        6.85m    6.85m   0.00243     1.4GB     2.28     1   937
# # ... with 5 more variables: total_time <bch:tm>, result <list>, memory <list>, time <list>,
# #   gc <list>

Hay muchos casos de uso de actualización por referencia e incluso los usuarios de {data.table} no usarán la versión avanzada de ella todo el tiempo, ya que requieren más códigos. Si {dtplyr} es compatible con estos productos listos para usar, debe averiguarlo usted mismo.

Actualización múltiple por referencia para las mismas funciones

Recurso principal: asignación elegante de múltiples columnas en data.table con lapply ()

Esto implica ya sea el más comúnmente utilizado :=o set.

dt <- data.table( matrix(runif(10000), nrow = 100) )

# A few variants

for (col in paste0('V', 20:100))
  set(dt, j = col, value = sqrt(get(col)))

for (col in paste0('V', 20:100))
  dt[, (col) := sqrt(get(col))]

# I prefer `purrr::map` to `for`
library(purrr)
map(paste0('V', 20:100), ~ dt[, (.) := sqrt(get(.))])

Según el creador de {data.table} Matt Dowle

(Tenga en cuenta que puede ser más común establecer un bucle en una gran cantidad de filas que en una gran cantidad de columnas).

Unirse + setkey + actualizar por referencia

Necesitaba una unión rápida con datos relativamente grandes y patrones de unión similares recientemente, por lo que utilizo el poder de actualización por referencia , en lugar de combinaciones normales. Como requieren más códigos, los envuelvo en un paquete privado con una evaluación no estándar de reutilización y legibilidad donde lo llamo setjoin.

Hice algunos puntos de referencia aquí: data.table join + update-by-reference + setkey

Resumen

# For brevity, only the codes for join-operation are shown here. Please refer to the link for details

# Normal_join
x <- y[x, on = 'a']

# update_by_reference
x_2[y_2, on = 'a', c := c]

# setkey_n_update
setkey(x_3, a) [ setkey(y_3, a), on = 'a', c := c ]

NOTA: dplyr::left_jointambién se probó y es el más lento con ~ 9,000 ms, usa más memoria que las dos {data.table} update_by_referencey setkey_n_update, pero usa menos memoria que las normales de {data.table}. Consumió aproximadamente ~ 2.0GB de memoria. No lo incluí porque quiero centrarme únicamente en {data.table}.

Resultados clave

  • setkey + updatey updateson ~ 11 y ~ 6.5 veces más rápidos que normal join, respectivamente
  • en la primera unión, el rendimiento setkey + updatees similar a la updatesobrecarga de setkeycompensa en gran medida sus propias ganancias de rendimiento
  • en la segunda y posteriores uniones, como setkeyno es necesario, setkey + updatees más rápido que update~ 1.8 veces (o más rápido que normal join~ 11 veces)

Imagen

Ejemplos

Para combinaciones eficaces y de memoria eficiente, use uno updateo setkey + update, donde este último es más rápido a costa de más códigos.

Veamos algunos pseudocódigos , por brevedad. Las lógicas son las mismas.

Para una o algunas columnas

a <- data.table(x = ..., y = ..., z = ..., ...)
b <- data.table(x = ..., y = ..., z = ..., ...)

# `update`
a[b, on = .(x), y := y]
a[b, on = .(x),  `:=` (y = y, z = z, ...)]
# `setkey + update`
setkey(a, x) [ setkey(b, x), on = .(x), y := y ]
setkey(a, x) [ setkey(b, x), on = .(x),  `:=` (y = y, z = z, ...) ]

Para muchas columnas

cols <- c('x', 'y', ...)
# `update`
a[b, on = .(x), (cols) := mget( paste0('i.', cols) )]
# `setkey + update`
setkey(a, x) [ setkey(b, x), on = .(x), (cols) := mget( paste0('i.', cols) ) ]

Envoltorio para uniones rápidas y eficientes en memoria ... muchos de ellos ... con un patrón de unión similar, envuélvalos como setjoinarriba - con update - con o sinsetkey

setjoin(a, b, on = ...)  # join all columns
setjoin(a, b, on = ..., select = c('columns_to_be_included', ...))
setjoin(a, b, on = ..., drop   = c('columns_to_be_excluded', ...))
# With that, you can even use it with `magrittr` pipe
a %>%
  setjoin(...) %>%
  setjoin(...)

Con setkey, el argumento onpuede ser omitido. También se puede incluir para facilitar la lectura, especialmente para colaborar con otros.

Operación de hileras grandes

  • como se mencionó anteriormente, use set
  • rellenar previamente su tabla, utilice la actualización por referencia técnicas
  • subconjunto usando la tecla (es decir setkey)

Recurso relacionado: agregue una fila por referencia al final de un objeto data.table

Resumen de actualización por referencia

Estos son solo algunos casos de uso de actualización por referencia . Hay muchos más.

Como puede ver, para el uso avanzado de lidiar con datos de gran tamaño, existen muchos casos de uso y técnicas que utilizan la actualización por referencia para un gran conjunto de datos. No es tan fácil de usar en {data.table} y si {dtplyr} lo admite, puede averiguarlo usted mismo.

Me concentro en la actualización por referencia en esta publicación, ya que creo que es la característica más poderosa de {data.table} para operaciones rápidas y eficientes en memoria. Dicho esto, hay muchos, muchos otros aspectos que también lo hacen tan eficiente y creo que {dtplyr} no lo admite de forma nativa.

Otros aspectos clave

Lo que es / no es compatible, también depende de la complejidad de las operaciones y si involucra la característica nativa de data.table como actualización por referencia o setkey. Y si el código traducido es el más eficiente (uno que escribirían los usuarios de data.table) también es otro factor (es decir, el código se traduce, pero ¿es la versión eficiente?). Muchas cosas están interconectadas.

Muchos de estos aspectos están relacionados entre sí con los puntos mencionados anteriormente.

  • complejidad de las operaciones

  • actualización por referencia

Puede averiguar si {dtplyr} admite estas operaciones, especialmente cuando se combinan.

Otro truco útil cuando se trata de conjuntos de datos pequeños o grandes, durante la sesión interactiva, {data.table} realmente cumple con su promesa de reducir enormemente el tiempo de programación y computación .

Clave de configuración para la variable utilizada repetidamente tanto para la velocidad como para los 'nombres de fila sobrealimentados' (subconjunto sin especificar el nombre de la variable).

dt <- data.table(iris)
setkey(dt, Species) 

dt['setosa',    do_something(...), ...]
dt['virginica', do_another(...),   ...]
dt['setosa',    more(...),         ...]

# `by` argument can also be omitted, particularly useful during interactive session
# this ultimately becomes what I call 'naked' syntax, just type what you want to do, without any placeholders. 
# It's simply elegant
dt['setosa', do_something(...), Species, ...]

Si sus operaciones involucran solo las simples como en el primer ejemplo, {dtplyr} puede hacer el trabajo. Para los complejos / no compatibles, puede usar esta guía para comparar los traducidos de {dtplyr} con la forma en que los usuarios experimentados de data.table codificarían de manera rápida y eficiente en la memoria con la elegante sintaxis de data.table. La traducción no significa que sea la forma más eficiente, ya que puede haber diferentes técnicas para lidiar con diferentes casos de datos grandes. Para un conjunto de datos aún más grande, puede combinar {data.table} con {disk.frame} , {fst} y {drake} y otros paquetes increíbles para obtener lo mejor. También hay un {big.data.table} pero actualmente está inactivo.

Espero que ayude a todos. Que tengas un buen día ☺☺

K22
fuente
2

Me vienen a la mente uniones no equitativas y uniones rodantes. No parece haber planes para incluir funciones equivalentes en dplyr, por lo que no hay nada que dtplyr pueda traducir.

También hay remodelación (dcast optimizado y fusión equivalente a las mismas funciones en reshape2) que tampoco está en dplyr.

Todas las funciones * _if y * _at actualmente no se pueden traducir con dtplyr también, pero están en proceso.

EdTeD
fuente
0

Actualice una columna al unirse Algunos trucos .SD Muchas funciones f Y dios sabe qué más porque #rdatatable es más que una simple biblioteca y no se puede resumir con pocas funciones

Es un ecosistema completo por sí solo

Nunca he necesitado dplyr desde el día en que comencé R. Porque data.table es muy bueno

Vikram
fuente