Tengo la costumbre de agrupar tareas similares en una sola línea. Por ejemplo, si necesito filtrar a
, b
y c
en una tabla de datos, voy a poner juntos en un solo []
con AND. Ayer, noté que en mi caso particular esto era increíblemente lento y probé encadenar filtros. He incluido un ejemplo a continuación.
Primero, siembro el generador de números aleatorios, cargo data.table y creo un conjunto de datos ficticios.
# Set RNG seed
set.seed(-1)
# Load libraries
library(data.table)
# Create data table
dt <- data.table(a = sample(1:1000, 1e7, replace = TRUE),
b = sample(1:1000, 1e7, replace = TRUE),
c = sample(1:1000, 1e7, replace = TRUE),
d = runif(1e7))
A continuación, defino mis métodos. El primer enfoque encadena los filtros juntos. El segundo Y une los filtros.
# Chaining method
chain_filter <- function(){
dt[a %between% c(1, 10)
][b %between% c(100, 110)
][c %between% c(750, 760)]
}
# Anding method
and_filter <- function(){
dt[a %between% c(1, 10) & b %between% c(100, 110) & c %between% c(750, 760)]
}
Aquí, compruebo que dan los mismos resultados.
# Check both give same result
identical(chain_filter(), and_filter())
#> [1] TRUE
Finalmente, los comparo.
# Benchmark
microbenchmark::microbenchmark(chain_filter(), and_filter())
#> Unit: milliseconds
#> expr min lq mean median uq max
#> chain_filter() 25.17734 31.24489 39.44092 37.53919 43.51588 78.12492
#> and_filter() 92.66411 112.06136 130.92834 127.64009 149.17320 206.61777
#> neval cld
#> 100 a
#> 100 b
Creado el 25/10/2019 por el paquete reprex (v0.3.0)
En este caso, el encadenamiento reduce el tiempo de ejecución en aproximadamente un 70%. ¿Por qué es este el caso? Quiero decir, ¿qué está pasando bajo el capó en la tabla de datos? No he visto ninguna advertencia contra el uso &
, por lo que me sorprendió que la diferencia sea tan grande. En ambos casos, evalúan las mismas condiciones, por lo que eso no debería ser una diferencia. En el caso AND, &
es un operador rápido y luego solo tiene que filtrar la tabla de datos una vez (es decir, usando el vector lógico resultante de los AND), en lugar de filtrar tres veces en el caso de encadenamiento.
Pregunta extra
¿Este principio es válido para las operaciones de la tabla de datos en general? ¿Las tareas de modularización son siempre una mejor estrategia?
fuente
base
observación similar con vectores haciendo lo siguiente:chain_vec <- function() { x <- which(a < .001); x[which(b[x] > .999)] }
yand_vec <- function() { which(a < .001 & b > .999) }
. (dondea
yb
son vectores de la misma longitud desderunif
- Utilicén = 1e7
para estos puntos de corte).Respuestas:
Principalmente, la respuesta se dio en los comentarios aleady: el "método de encadenamiento"
data.table
es más rápido en este caso que el "método de encadenamiento" ya que el encadenamiento ejecuta las condiciones una tras otra. A medida que cada paso reduce el tamaño deldata.table
hay menos para evaluar para el siguiente. "Anding" evalúa las condiciones para los datos de tamaño completo cada vez.Podemos demostrar esto con un ejemplo: cuando los pasos individuales NO disminuyen el tamaño de la
data.table
(es decir, las condiciones para verificar son las mismas para ambos retrasos):Usando los mismos datos pero el
bench
paquete, que comprueba automáticamente si los resultados son idénticos:Como puede ver aquí, el enfoque anding es 2.43 veces más rápido en este caso . Eso significa que el encadenamiento en realidad agrega algo de sobrecarga , lo que sugiere que generalmente el anding debería ser más rápido. EXCEPTO si las condiciones reducen el tamaño del
data.table
paso a paso. Teóricamente, el enfoque de encadenamiento podría incluso ser más lento (incluso dejando a un lado la sobrecarga), es decir, si una condición aumentara el tamaño de los datos. Pero prácticamente creo que eso no es posible ya que no se permite el reciclaje de vectores lógicosdata.table
. Creo que esto responde a tu pregunta extra.A modo de comparación, las funciones originales en mi máquina con
bench
:fuente