Quiero filtrar un data.table grande por grupo. Puedo usar .SD o .I y aunque personalmente creo que el primero es mucho más fácil de leer, el último es tremendamente más rápido / usa mucha menos memoria (a pesar de usar .SDcols).

Hasta cierto punto me queda claro por qué. Para .I solo necesitamos un vector por grupo, mientras que para .SD necesitamos un data.table completo. Pero pensé que al proporcionar un argumento significativo .SDcol podría acelerar / guardar algo de memoria.

Sin embargo, los puntos de referencia muestran que el enfoque .SD es aproximadamente 60 veces más lento y consume 300 veces más memoria. Por supuesto, una tabla de datos de {4 columnas .SD de 4 columnas necesitará más de 4 veces el tamaño de un vector. ¿Pero 60 veces más lento y 300 veces más memoria? ¿Podría alguien iluminarme, por qué el enfoque .SD consume tanta memoria y, por lo tanto, es mucho más lento? ¿Hay alguna forma de acelerar el enfoque .SD para que sea más rápido o es la única opción para recurrir al enfoque .I?

Configuración de datos

library(data.table)
## data set up

nr <- 1e6
nc <- 100
grp_perc <- .8
DT <- data.table(ids = sample(paste0("id", 
                                     seq(1, round(grp_perc * nr, 0))),
                              nr, TRUE))
cols <- paste("col", seq(1, nc), sep = "_")
DT[, (cols) := replicate(nc, sample(nr), simplify = FALSE)]

Puntos de referencia

results <- bench::mark(.I = DT[DT[, .(row_id = .I[which.min(col_1)]), 
                                  by = ids]$row_id, c("ids", cols[1:3]), with = FALSE],
                       .SD = DT[, .SD[which.min(col_1)], 
                                by = ids, .SDcols = cols[1:3]], 
                       iterations = 1, filter_gc = FALSE)

summary(results)
# A tibble: 2 x 13
  expression     min  median `itr/sec` mem_alloc `gc/sec` n_itr  n_gc total_time result         memory         time   gc       
  <bch:expr> <bch:t> <bch:t>     <dbl> <bch:byt>    <dbl> <int> <dbl>   <bch:tm> <list>         <list>         <list> <list>   
1 .I           2.64s   2.64s   0.378      34.4MB    0         1     0      2.64s <df[,4] [571,~ <df[,3] [1,41~ <bch:~ <tibble ~
2 .SD          2.73m   2.73m   0.00612     9.1GB    0.342     1    56      2.73m <df[,4] [571,~ <df[,3] [2,40~ <bch:~ <tibble ~
3
thothal 24 abr. 2020 a las 18:55

2 respuestas

La mejor respuesta

Aquí hay un enfoque que es más rápido que .I para este ejemplo en particular. Tenga en cuenta que esto también cambia el orden que puede no ser conveniente para usted.

DT[order(col_1), .SD[1L], by = ids, .SDcols = cols[1:3]]

Como @Ian Campbell menciona, este es un problema de Github. La buena noticia es que hay algunas optimizaciones, una de las cuales es .SD[1L]. La optimización es que el subconjunto se realiza todo en C, lo que lo hace muy rápido.

Estos son los puntos de referencia que incluyen la solución de @ sindri_baldur, pero eliminan su intento original de .SD. No quería esperar 3 minutos :).

# A tibble: 3 x 13
  expression    min median `itr/sec` mem_alloc `gc/sec` n_itr  n_gc total_time
  <bch:expr> <bch:> <bch:>     <dbl> <bch:byt>    <dbl> <int> <dbl>   <bch:tm>
1 .I          4.54s  4.54s    0.220       30MB    0.880     1     4      4.54s
2 self_join  11.32s 11.32s    0.0883    76.3MB    0         1     0     11.32s
3 use_order   3.55s  3.55s    0.282     58.3MB    0         1     0      3.55s

## show that it's equal but re-ordered:
all.equal(DT[DT[, .(row_id = .I[which.min(col_1)]), 
                by = ids]$row_id, c("ids", cols[1:3]), with = FALSE][order(col_1)],
          DT[order(col_1), .SD[1L], by = ids, .SDcols = cols[1:3]])

## [1] TRUE
3
Cole 25 abr. 2020 a las 10:44

Aquí hay una forma más rápida que todavía usa .SD.

DT[DT[, .(col_1 = min(col_1)), by = ids], 
   on = .(ids, col_1), 
   .SD, .SDcols = c("ids", cols[1:3])]
3
sindri_baldur 24 abr. 2020 a las 16:15