Data.table: por qué data.table es más rápido con un subconjunto de columnas vectorizadas que un subconjunto de columnas de lista

Creado en 27 mar. 2019  ·  3Comentarios  ·  Fuente: Rdatatable/data.table

Me gustan estas cosas de data.table, tanto por su velocidad de ejecución como por su forma parsimoniosa de secuencias de comandos.
Lo uso incluso en mesas pequeñas también.
Regularmente hago subconjuntos de tablas de esta manera: DT[, .(id1, id5)]
y no así: DT[, c("id1", "id5")]

Hoy medí la velocidad de los dos y me ha sorprendido la diferencia de velocidad en mesas pequeñas. El método parsimonioso es mucho más lento.

¿Esta diferencia es algo intencionado?

¿Hay aspiración de hacer que la vía parsimoniosa converja en velocidad de ejecución a la otra?
(Cuenta cuando tengo que crear subconjuntos de varias tablas pequeñas de forma repetitiva).

Ubuntu 18.04
R versión 3.5.3 (2019-03-11)
data.table 1.12.0
RAM 32GB
CPU Intel® Core™ i7-8565U a 1,80 GHz × 8

library(data.table)
library(microbenchmark)
N  <- 2e8
K  <- 100
set.seed(1)
DT <- data.table(
  id1 = sample(sprintf("id%03d", 1:K), N, TRUE),               # large groups (char)
  id2 = sample(sprintf("id%03d", 1:K), N, TRUE),               # large groups (char)
  id3 = sample(sprintf("id%010d", 1:(N/K)), N, TRUE),       # small groups (char)
  id4 = sample(K,   N, TRUE),                                           # large groups (int)
  id5 = sample(K,   N, TRUE),                                           # large groups (int)
  id6 = sample(N/K, N, TRUE),                                          # small groups (int)
  v1 =  sample(5,   N, TRUE),                                           # int in range [1,5]
  v2 =  sample(5,   N, TRUE),                                           # int in range [1,5]
  v3 =  sample(round(runif(100, max = 100), 4), N, TRUE) # numeric e.g. 23.5749
)

microbenchmark(
  DT[, .(id1, id5)],
  DT[, c("id1", "id5")]
)
Unit: seconds
                  expr      min       lq     mean   median       uq      max neval
     DT[, .(id1, id5)] 1.588367 1.614645 1.929348 1.626847 1.659698 12.33872   100
 DT[, c("id1", "id5")] 1.592154 1.613800 1.937548 1.628082 2.184456 11.74581   100


N  <- 2e5
DT2 <- data.table(
  id1 = sample(sprintf("id%03d", 1:K), N, TRUE),                 # large groups (char)
  id2 = sample(sprintf("id%03d", 1:K), N, TRUE),                 # large groups (char)
  id3 = sample(sprintf("id%010d", 1:(N/K)), N, TRUE),         # small groups (char)
  id4 = sample(K,   N, TRUE),                                             # large groups (int)
  id5 = sample(K,   N, TRUE),                                             # large groups (int)
  id6 = sample(N/K, N, TRUE),                                            # small groups (int)
  v1 =  sample(5,   N, TRUE),                                             # int in range [1,5]
  v2 =  sample(5,   N, TRUE),                                             # int in range [1,5]
  v3 =  sample(round(runif(100, max = 100), 4), N, TRUE)   # numeric e.g. 23.5749
)

microbenchmark(
  DT2[, .(id1, id5)],
  DT2[, c("id1", "id5")]
)
Unit: microseconds
                   expr      min       lq      mean    median        uq      max neval
DT2[, .(id1, id5)] 1405.042 1461.561 1525.5314 1491.7885 1527.8955 2220.860   100
DT2[, c("id1", "id5")]  614.624  640.617  666.2426  659.0175  676.9355  906.966   100

Comentario más útil

Puedes arreglar el formato de tu publicación usando una sola línea de tres acentos graves antes y después del fragmento de código:

```
code
```

Cuenta cuando tengo que crear subconjuntos de varias tablas pequeñas de forma repetitiva.

Supongo que seleccionar repetidamente columnas de tablas pequeñas es algo que debería y, en la mayoría de los casos, puede evitarse... Debido a que j en DT[i, j, by] admite y optimiza una variedad tan amplia de entradas, creo que es natural que haya algunos gastos generales al analizarlo.


Con respecto a otras formas de abordar su problema (y tal vez esto encajaría mejor con Stack Overflow si desea hablar más al respecto) ... Dependiendo de qué más quiera hacer con la tabla, podría eliminar las otras columnas , DT[, setdiff(names(DT), cols) := NULL] y continúe usando DT directamente.

Si aún prefiere tomar el subconjunto, capturar punteros de columna es mucho más rápido que cualquiera de las opciones que consideró aquí, aunque de esta manera las ediciones del resultado afectarán la tabla original:

library(data.table)
library(microbenchmark)
N <- 2e8
K <- 100
set.seed(1)
DT <- data.table(
id1 = sprintf("id%03d", 1:K), # large groups (char)
id2 = sprintf("id%03d", 1:K), # large groups (char)
id3 = sprintf("id%010d", 1:(N/K)), # small groups (char)
id4 = sample(K), # large groups (int)
id5 = sample(K), # large groups (int)
id6 = sample(N/K), # small groups (int)
v1 = sample(5), # int in range [1,5]
v2 = sample(5), # int in range [1,5]
v3 = round(runif(100, max = 100), 4), # numeric e.g. 23.5749
row = seq_len(N)
)

cols = c("id1", "id5")
microbenchmark(times = 3,
  expression = DT[, .(id1, id5)],
  index = DT[, c("id1", "id5")],
  dotdot = DT[, ..cols],
  oddball = setDT(lapply(setNames(cols, cols), function(x) DT[[x]]))[],
  oddball2 = setDT(unclass(DT)[cols])[]
)

Unit: microseconds
       expr         min           lq         mean      median           uq         max neval
 expression 1249753.580 1304355.3415 1417166.9297 1358957.103 1500873.6045 1642790.106     3
      index 1184056.302 1191334.4835 1396372.3483 1198612.665 1502530.3715 1806448.078     3
     dotdot 1084521.234 1240062.2370 1439680.6980 1395603.240 1617260.4300 1838917.620     3
    oddball      92.659     171.8635     568.5317     251.068     806.4680    1361.868     3
   oddball2      66.582     125.9505     150.7337     185.319     192.8095     200.300     3

(Eliminé la aleatorización de su ejemplo y reduje # veces en el punto de referencia porque estaba impaciente).

Nunca encontré una manera de llamar directamente al subconjunto de la lista de R (que se usa después de los unclass anteriores).

Con respecto a "las ediciones del resultado modificarán la tabla original", quiero decir:

myDT = data.table(a = 1:2, b = 3:4)

# standard way
res <- myDT[, "a"]
res[, a := 0]
myDT
#    a b
# 1: 1 3
# 2: 2 4

# oddball, grabbing pointers
res2 <- setDT(unclass(myDT)["a"])
res2[, a := 0]
myDT
#    a b
# 1: 0 3
# 2: 0 4

Todos 3 comentarios

Puedes arreglar el formato de tu publicación usando una sola línea de tres acentos graves antes y después del fragmento de código:

```
code
```

Cuenta cuando tengo que crear subconjuntos de varias tablas pequeñas de forma repetitiva.

Supongo que seleccionar repetidamente columnas de tablas pequeñas es algo que debería y, en la mayoría de los casos, puede evitarse... Debido a que j en DT[i, j, by] admite y optimiza una variedad tan amplia de entradas, creo que es natural que haya algunos gastos generales al analizarlo.


Con respecto a otras formas de abordar su problema (y tal vez esto encajaría mejor con Stack Overflow si desea hablar más al respecto) ... Dependiendo de qué más quiera hacer con la tabla, podría eliminar las otras columnas , DT[, setdiff(names(DT), cols) := NULL] y continúe usando DT directamente.

Si aún prefiere tomar el subconjunto, capturar punteros de columna es mucho más rápido que cualquiera de las opciones que consideró aquí, aunque de esta manera las ediciones del resultado afectarán la tabla original:

library(data.table)
library(microbenchmark)
N <- 2e8
K <- 100
set.seed(1)
DT <- data.table(
id1 = sprintf("id%03d", 1:K), # large groups (char)
id2 = sprintf("id%03d", 1:K), # large groups (char)
id3 = sprintf("id%010d", 1:(N/K)), # small groups (char)
id4 = sample(K), # large groups (int)
id5 = sample(K), # large groups (int)
id6 = sample(N/K), # small groups (int)
v1 = sample(5), # int in range [1,5]
v2 = sample(5), # int in range [1,5]
v3 = round(runif(100, max = 100), 4), # numeric e.g. 23.5749
row = seq_len(N)
)

cols = c("id1", "id5")
microbenchmark(times = 3,
  expression = DT[, .(id1, id5)],
  index = DT[, c("id1", "id5")],
  dotdot = DT[, ..cols],
  oddball = setDT(lapply(setNames(cols, cols), function(x) DT[[x]]))[],
  oddball2 = setDT(unclass(DT)[cols])[]
)

Unit: microseconds
       expr         min           lq         mean      median           uq         max neval
 expression 1249753.580 1304355.3415 1417166.9297 1358957.103 1500873.6045 1642790.106     3
      index 1184056.302 1191334.4835 1396372.3483 1198612.665 1502530.3715 1806448.078     3
     dotdot 1084521.234 1240062.2370 1439680.6980 1395603.240 1617260.4300 1838917.620     3
    oddball      92.659     171.8635     568.5317     251.068     806.4680    1361.868     3
   oddball2      66.582     125.9505     150.7337     185.319     192.8095     200.300     3

(Eliminé la aleatorización de su ejemplo y reduje # veces en el punto de referencia porque estaba impaciente).

Nunca encontré una manera de llamar directamente al subconjunto de la lista de R (que se usa después de los unclass anteriores).

Con respecto a "las ediciones del resultado modificarán la tabla original", quiero decir:

myDT = data.table(a = 1:2, b = 3:4)

# standard way
res <- myDT[, "a"]
res[, a := 0]
myDT
#    a b
# 1: 1 3
# 2: 2 4

# oddball, grabbing pointers
res2 <- setDT(unclass(myDT)["a"])
res2[, a := 0]
myDT
#    a b
# 1: 0 3
# 2: 0 4

Ok, he aprendido algo nuevo y rápido (los bichos raros) hoy y he estado tomando nota de que existe una compensación entre la velocidad y la codificación parsimoniosa. ¡Así que el vaso está medio lleno! ¡Gracias!

Supongo que # 852 relacionado

¿Fue útil esta página
0 / 5 - 0 calificaciones