seleccionar - subset en r español
Cómo acelerar el subconjunto por grupos (2)
Solía lograr que mis datos se disputaran con dplyr, pero algunos de los cálculos son "lentos". En particular, subgrupo por grupos, leí que dplyr es lento cuando hay muchos grupos y, según este punto de referencia data.table podría ser más rápido, así que comencé a aprender data.table.
Aquí es cómo reproducir algo cercano a mis datos reales con 250k filas y aproximadamente 230k grupos.
Me gustaría agrupar por id1, id2 y subconjunto de las filas con el
max(datetime)
para cada grupo.
Datos
# random datetime generation function by Dirk Eddelbuettel
# https://stackoverflow.com/questions/14720983/efficiently-generate-a-random-sample-of-times-and-dates-between-two-dates
rand.datetime <- function(N, st = "2012/01/01", et = "2015/08/05") {
st <- as.POSIXct(as.Date(st))
et <- as.POSIXct(as.Date(et))
dt <- as.numeric(difftime(et,st,unit="sec"))
ev <- sort(runif(N, 0, dt))
rt <- st + ev
}
set.seed(42)
# Creating 230000 ids couples
ids <- data.frame(id1 = stringi::stri_rand_strings(23e4, 9, pattern = "[0-9]"),
id2 = stringi::stri_rand_strings(23e4, 9, pattern = "[0-9]"))
# Repeating randomly the ids[1:2000, ] to create groups
ids <- rbind(ids, ids[sample(1:2000, 20000, replace = TRUE), ])
# Adding random datetime variable and dummy variables to reproduce real datas
datas <- transform(ids,
datetime = rand.datetime(25e4),
var1 = sample(LETTERS[1:6], 25e4, rep = TRUE),
var2 = sample(c(1:10, NA), 25e4, rep = TRUE),
var3 = sample(c(1:10, NA), 25e4, rep = TRUE),
var4 = rand.datetime(25e4),
var5 = rand.datetime(25e4))
datas.tbl <- tbl_df(datas)
datas.dt <- data.table(datas, key = c("id1", "id2"))
No pude encontrar la forma directa de subconjuntos por grupos con data.table, así que hice esta pregunta: Filtrar filas por grupos con data.table
Sugerimos que use .SD:
datas.dt[, .SD[datetime == max(datetime)], by = c("id1", "id2")]
Pero tengo dos problemas, funciona con fecha pero no con POSIXct ("Error en UseMethod (" as.data.table "): ningún método aplicable para ''as.data.table'' aplicado a un objeto de la clase" c ('' POSIXct '','' POSIXt '') ""), y esto es muy lento. Por ejemplo con fechas:
> system.time({
+ datas.dt[, .SD[as.Date(datetime) == max(as.Date(datetime))], by = c("id1", "id2")]
+ })
utilisateur système écoulé
207.03 0.00 207.48
Así que encontré otra forma mucho más rápida de lograr esto (y mantener las fechas y horas) con data.table:
Las funciones
f.dplyr <- function(x) x %>% group_by(id1, id2) %>% filter(datetime == max(datetime))
f.dt.i <- function(x) x[x[, .I[datetime == max(datetime)], by = c("id1", "id2")]$V1]
f.dt <- function(x) x[x[, datetime == max(datetime), by = c("id1", "id2")]$V1]
Pero luego pensé que data.table sería mucho más rápido, la diferencia horaria con dplyr no es significativa.
Microbenchmark
mbm <- microbenchmark(
dplyr = res1 <- f.dplyr(datas.tbl),
data.table.I = res2 <- f.dt.i(datas.dt),
data.table = res3 <- f.dt(datas.dt),
times = 50L)
Unit: seconds
expr min lq mean median uq max neval
dplyr 31.84249 32.24055 32.59046 32.61311 32.88703 33.54226 50
data.table.I 30.02831 30.94621 31.19660 31.17820 31.42888 32.16521 50
data.table 30.28923 30.84212 31.09749 31.04851 31.40432 31.96351 50
¿Me falta / uso incorrecto de algo con data.table? ¿Tienes ideas para acelerar este cálculo?
Cualquier ayuda sería muy apreciada ! Gracias
Editar: Algunas precisiones sobre el sistema y las versiones de paquetes utilizados para el microbenchmark. (La computadora no es una máquina de guerra, 12Go i5)
Sistema
sessionInfo()
R version 3.1.3 (2015-03-09)
Platform: x86_64-w64-mingw32/x64 (64-bit)
Running under: Windows 7 x64 (build 7601) Service Pack 1
locale:
[1] LC_COLLATE=French_France.1252 LC_CTYPE=French_France.1252
[3] LC_MONETARY=French_France.1252 LC_NUMERIC=C
[5] LC_TIME=French_France.1252
attached base packages:
[1] stats graphics grDevices utils datasets methods base
other attached packages:
[1] readr_0.1.0 ggplot2_1.0.1 microbenchmark_1.4-2
[4] data.table_1.9.4 dplyr_0.4.1 plyr_1.8.2
loaded via a namespace (and not attached):
[1] assertthat_0.1 chron_2.3-45 colorspace_1.2-6 DBI_0.3.1
[5] digest_0.6.8 grid_3.1.3 gtable_0.1.2 lazyeval_0.1.10
[9] magrittr_1.5 MASS_7.3-39 munsell_0.4.2 parallel_3.1.3
[13] proto_0.3-10 Rcpp_0.11.5 reshape2_1.4.1 scales_0.2.4
[17] stringi_0.4-1 stringr_0.6.2 tools_3.1.3
> packageVersion("data.table")
[1] ‘1.9.4’
> packageVersion("dplyr")
[1] ‘0.4.1’
¿Qué tal resumir data.table y
join
datos originales?
system.time({
datas1 <- datas.dt[, list(datetime=max(datetime)), by = c("id1", "id2")] #summarize the data
setkey(datas1, id1, id2, datetime)
setkey(datas.dt, id1, id2, datetime)
datas2 <- datas.dt[datas1]
})
# user system elapsed
# 0.083 0.000 0.084
que filtra correctamente los datos
system.time(dat1 <- datas.dt[datas.dt[, .I[datetime == max(datetime)], by = c("id1", "id2")]$V1])
# user system elapsed
# 23.226 0.000 23.256
all.equal(dat1, datas2)
# [1] TRUE
Apéndice
setkey
argumento
setkey
es superfluo si está utilizando la
versión
de
desarrollo
de
data.table
(Gracias a @akrun por el puntero)
system.time({
datas1 <- datas.dt[, list(datetime=max(datetime)), by = c("id1", "id2")] #summarize the data
datas2 <- datas.dt[datas1, on=c(''id1'', ''id2'', ''datetime'')]
})
Gran pregunta!
Asumiré que
df
y
dt
son los nombres de los objetos para una escritura fácil / rápida.
df = datas.tbl
dt = datas.dt
Comparación a nivel de optimización
-O3
:
Primero, aquí está el tiempo en mi sistema en la versión CRAN actual de
dplyr
y la versión de desarrollo de
data.table
.
La versión de desarrollo de
dplyr
parece sufrir regresiones de rendimiento (y está siendo
dplyr
por Romain).
system.time(df %>% group_by(id1, id2) %>% filter(datetime == max(datetime)))
# 25.291 0.128 25.610
system.time(dt[dt[, .I[datetime == max(datetime)], by = c("id1", "id2")]$V1])
# 17.191 0.075 17.349
Ejecuté esto varias veces, y parece que no cambia.
Sin embargo, compilo todos los paquetes con el indicador de optimización
-O3
(estableciendo
~/.R/Makevars
adecuadamente).
Y he observado que el rendimiento de
data.table
mejora mucho más que otros paquetes que lo he comparado con
-O3
.
Comparación de velocidad de agrupamiento
En segundo lugar, es importante entender la razón de tal lentitud. Primero, comparemos el tiempo para agrupar .
system.time(group_by(df, id1, id2))
# 0.303 0.007 0.311
system.time(data.table:::forderv(dt, by = c("id1", "id2"), retGrp = TRUE))
# 0.002 0.000 0.002
Aunque hay un total de 250,000 filas, su tamaño de datos es de aproximadamente ~ 38MB. Con este tamaño, es poco probable que vea una diferencia notable en la velocidad de agrupación.
La agrupación de
data.table
es
>100x
más rápida aquí, claramente no es la razón de tal lentitud ...
¿Por qué es lento?
Entonces, ¿cuál es la razón?
datatable.verbose
opción
datatable.verbose
y verifiquemos nuevamente:
options(datatable.verbose = TRUE)
dt[dt[, .I[datetime == max(datetime)], by = c("id1", "id2")]$V1]
# Detected that j uses these columns: datetime
# Finding groups (bysameorder=TRUE) ... done in 0.002secs. bysameorder=TRUE and o__ is length 0
# lapply optimization is on, j unchanged as ''.I[datetime == max(datetime)]''
# GForce is on, left j unchanged
# Old mean optimization is on, left j unchanged.
# Starting dogroups ...
# memcpy contiguous groups took 0.097s for 230000 groups
# eval(j) took 17.129s for 230000 calls
# done dogroups in 17.597 secs
¡Así que
eval(j)
solo tomó ~ 97% del tiempo!
La expresión que proporcionamos en
j
se evalúa para
cada grupo
.
Como tiene 230,000 grupos y hay una penalización en la llamada
eval()
, eso se suma.
Evitar la penalización
eval()
Como somos conscientes de esta penalización, seguimos adelante y comenzamos a implementar versiones internas de algunas funciones de uso común:
sum
,
mean
,
min
,
max
.
Esto / debería expandirse a tantas otras funciones como sea posible (cuando encontremos el tiempo).
Entonces, intentemos calcular el tiempo solo para obtener
max(datetime)
primero:
dt.agg = dt[, .(datetime = max(datetime)), by = .(id1, id2)]
# Detected that j uses these columns: datetime
# Finding groups (bysameorder=TRUE) ... done in 0.002secs. bysameorder=TRUE and o__ is length 0
# lapply optimization is on, j unchanged as ''list(max(datetime))''
# GForce optimized j to ''list(gmax(datetime))''
Y es instantáneo.
¿Por qué?
Debido a que
max()
se optimiza internamente a
gmax()
y no hay una llamada
eval()
para cada uno de los 230K grupos.
Entonces, ¿por qué
datetime == max(datetime)
instantáneo?
Porque es más complicado analizar tales expresiones y optimizarlas internamente, y aún no lo hemos logrado.
Solución alternativa
Entonces, ahora que conocemos el problema y una forma de solucionarlo, usémoslo.
dt.agg = dt[, .(datetime = max(datetime)), by = .(id1, id2)]
dt[dt.agg, on = c("id1", "id2", "datetime")] # v1.9.5+
Esto toma ~ 0.14 segundos en mi Mac.
Tenga en cuenta que esto solo es rápido
porque
la expresión se optimiza a
gmax()
.
Compáralo con:
dt[, .(datetime = base::max(datetime)), by = .(id1, id2)]
Estoy de acuerdo en que optimizar las expresiones más complicadas para evitar la penalización
eval()
sería la solución ideal, pero aún no hemos llegado.