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/fread
orden 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::mutate
no 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_join
también se probó y es el más lento con ~ 9,000 ms, usa más memoria que las dos {data.table} update_by_reference
y 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 + update
y update
son ~ 11 y ~ 6.5 veces más rápidos que normal join
, respectivamente
- en la primera unión, el rendimiento
setkey + update
es similar a la update
sobrecarga de setkey
compensa en gran medida sus propias ganancias de rendimiento
- en la segunda y posteriores uniones, como
setkey
no es necesario, setkey + update
es más rápido que update
~ 1.8 veces (o más rápido que normal join
~ 11 veces)
Ejemplos
Para combinaciones eficaces y de memoria eficiente, use uno update
o 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 setjoin
arriba - 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 on
puede 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.
setkey
. Ver claves y subconjunto basado en búsqueda binaria rápida
- Índices secundarios e indexación automática
- Usando .SD para el análisis de datos
- funciones de series de tiempo: pensar
frollapply
. funciones rodantes, agregados rodantes, ventana deslizante, promedio móvil
- unión rodante , unión no equi , (algunas) unión "cruzada"
- {data.table} ha construido las bases en velocidad y eficiencia de la memoria, en el futuro, puede extenderse para incluir muchas funciones (como la forma en que implementan las funciones de series de tiempo mencionadas anteriormente)
- en general, las operaciones más complejas sobre la data.table
i
, j
o by
las operaciones (se puede utilizar casi cualquier expresión de allí), creo que los más difíciles las traducciones, sobre todo cuando se combinan con la actualización por referencia , setkey
y otra data.table nativa funciones comofrollapply
- Otro punto está relacionado con el uso de la base R o tidyverse. Uso ambos data.table + tidyverse (excepto dplyr / readr / tidyr). Para operaciones grandes, a menudo comparo, por ejemplo, las
stringr::str_*
funciones de familia contra base R y encuentro que la base R es más rápida hasta cierto punto y las uso. El punto es, no te limites a tidyverse o data.table o ..., explora otras opciones para hacer el trabajo.
Muchos de estos aspectos están relacionados entre sí con los puntos mencionados anteriormente.
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 ☺☺
dplyr
que no puedas hacerlo biendata.table
? Si no, cambiardata.table
a será mejor quedtplyr
.