重要的是要理解,R
中的vectorization
意味着你不需要循环,B/C有人已经为你做了循环.重要的是,最终会有某种循环.
"经常"依赖现成的向量化比自己编写循环更快的原因是,前一种情况下的循环是在C
级上完成的,can级更快,但有时差异甚至不明显:
library(microbenchmark)
x <- seq_len(10000000L)
microbenchmark(
vec = sum(x),
lp = {sm <- 0; for(i in seq_along(x)) sm <- sm + x[i]; sm}
)
# Unit: nanoseconds
# expr min lq mean median uq max neval cld
# vec 100 200 2745 350 4450 19300 100 a
# lp 327515400 340921350 367242236 351582100 383396050 603997400 100 b
看起来很多,但要注意单位,所以基本上循环结束在0.32sec
,而R
循环需要0.0000001sec
.从相对值来看,这是一个巨大的进步,但从绝对值来看,你几乎看不出有什么不同.
底线是,你不应该害怕R
级循环,b/c它们比它们的声誉要好得多.如果循环有自然的替代品,那么一定要try ,但如果避免它们的代价是数据 struct 过于臃肿,请毫不犹豫地使用它们.
现在来谈谈你手头的问题.
could将数据转换为长格式,并逐个获取最后一行.您可以更进一步,使用带有适当密钥的data.table
:
library(microbenchmark)
library(tibble)
library(tidyr)
library(dplyr)
library(data.table)
create_data <- function(nr_indiv, max_per_indiv) {
l <- replicate(nr_indiv, sample(10000L, sample(max_per_indiv, 1L)))
tibble(id = seq_len(nr_indiv), load = l)
}
set.seed(20022024)
list_data <- create_data(10000, 1000)
lng_data <- unnest(list_data, cols = load)
dt_data <- copy(lng_data)
setDT(dt_data)
setkey(dt_data, "id")
my_check <- function(x) {
isTRUE(all.equal(x$lst, x$lng %>% pull(load), check.attributes = FALSE)) &&
isTRUE(all.equal(x$lst, x$dt[, load], check.attributes = FALSE)) &&
isTRUE(all.equal(x$lst, c(x$ta), check.attributes = FALSE)) &&
isTRUE(all.equal(x$lst, x$base, check.attributes = FALSE)) &&
isTRUE(all.equal(x$lst, x$flt %>% pull(load), check.attributes = FALSE))
}
microbenchmark(
lst = map_int(list_data$load, tail, n = 1L),
lng = slice_tail(lng_data, n = 1L, by = id),
dt = dt_data[, .(load = tail(load, 1L)), "id"],
ta = tapply(lng_data$load, lng_data$id, tail, n = 1L),
base = vapply(list_data$load, tail, integer(1L), n = 1L),
flt = lng_data %>% filter(ord == 1L),
check = my_check
)
# Unit: milliseconds
# expr min lq mean median uq max neval cld
# lst 70.5403 77.40405 93.87981 87.87985 101.53635 280.5060 100 a
# lng 916.2937 971.14420 1053.53934 1004.19125 1133.12665 1434.2933 100 b
# dt 18.4768 19.47870 28.12581 20.64565 37.49055 64.8045 100 c
# ta 315.0710 357.06000 407.04557 368.03110 453.28390 687.4518 100 d
# base 65.5528 75.40010 85.31279 85.87180 94.14265 116.5428 100 a
# flt 44.4467 47.46310 55.71809 50.85525 57.62820 244.5270 100 e
tidyverse
长格式解决方案比purrr
循环差,而data.table
长格式解决方案更好(不包括创建数据 struct 的开销),Filter方法介于两者之间,而基循环与purrr
相当,因此即使purrr
的轻微开销也可以忽略不计.
将会有更多的解决方案,但从第一次快速测试来看,在我看来,list
解决方案总体上并不那么糟糕,在我看来,同样(如果不是更重要的话),相当容易阅读和理解.
不要害怕循环,如果没有直接的自然向量化方法,一定要使用它们.