Ответы:
Эти apply
функции в R не обеспечивают повышенную производительность по сравнению с другими сквозными функциями (например for
). Единственным исключением является то, lapply
что он может быть немного быстрее, потому что он выполняет больше работы в C-коде, чем в R (см. Этот вопрос для примера ).
Но в целом правило состоит в том, что вы должны использовать функцию применения для ясности, а не для производительности .
Я хотел бы добавить к этому, что применяемые функции не имеют побочных эффектов , что является важным отличием, когда речь идет о функциональном программировании на R. Это можно переопределить с помощью assign
или <<-
, но это может быть очень опасно. Побочные эффекты также затрудняют понимание программы, поскольку состояние переменной зависит от истории.
Редактировать:
Просто чтобы подчеркнуть это на тривиальном примере, который рекурсивно вычисляет последовательность Фибоначчи; это может быть выполнено несколько раз, чтобы получить точную меру, но дело в том, что ни один из методов не имеет существенно отличающихся характеристик:
> fibo <- function(n) {
+ if ( n < 2 ) n
+ else fibo(n-1) + fibo(n-2)
+ }
> system.time(for(i in 0:26) fibo(i))
user system elapsed
7.48 0.00 7.52
> system.time(sapply(0:26, fibo))
user system elapsed
7.50 0.00 7.54
> system.time(lapply(0:26, fibo))
user system elapsed
7.48 0.04 7.54
> library(plyr)
> system.time(ldply(0:26, fibo))
user system elapsed
7.52 0.00 7.58
Изменить 2:
Что касается использования параллельных пакетов для R (например, rpvm, rmpi, snow), они обычно предоставляют apply
семейные функции (даже foreach
пакет, по сути, эквивалентен, несмотря на название). Вот простой пример sapply
функции в snow
:
library(snow)
cl <- makeSOCKcluster(c("localhost","localhost"))
parSapply(cl, 1:20, get("+"), 3)
В этом примере используется кластер сокетов, для которого не требуется устанавливать дополнительное программное обеспечение; в противном случае вам понадобится что-то вроде PVM или MPI (см . страницу кластеризации Tierney ). snow
имеет следующие прикладные функции:
parLapply(cl, x, fun, ...)
parSapply(cl, X, FUN, ..., simplify = TRUE, USE.NAMES = TRUE)
parApply(cl, X, MARGIN, FUN, ...)
parRapply(cl, x, fun, ...)
parCapply(cl, x, fun, ...)
Имеет смысл использовать apply
функции для параллельного выполнения, поскольку они не имеют побочных эффектов . Когда вы изменяете значение переменной в for
цикле, оно устанавливается глобально. С другой стороны, все apply
функции можно безопасно использовать параллельно, потому что изменения являются локальными для вызова функции (если вы не пытаетесь использовать assign
или <<-
, в этом случае вы можете ввести побочные эффекты). Само собой разумеется, важно быть осторожным с локальными и глобальными переменными, особенно когда имеешь дело с параллельным выполнением.
Редактировать:
Вот простой пример , чтобы продемонстрировать разницу между for
и до *apply
сих пор , как побочные эффекты обеспокоены:
> df <- 1:10
> # *apply example
> lapply(2:3, function(i) df <- df * i)
> df
[1] 1 2 3 4 5 6 7 8 9 10
> # for loop example
> for(i in 2:3) df <- df * i
> df
[1] 6 12 18 24 30 36 42 48 54 60
Обратите внимание, как df
в родительской среде изменяется, for
но не изменяется *apply
.
snowfall
упаковку и попробовать примеры в их виньетке. snowfall
строится поверх snow
пакета и абстрагирует детали распараллеливания, что еще больше упрощает выполнение распараллеленных apply
функций.
foreach
тех пор он стал доступен и, похоже, очень интересуется SO.
lapply
«немного быстрее», чем for
цикл. Тем не менее, я не вижу ничего, что подсказывало бы это. Вы только упоминаете, что lapply
это быстрее, чем sapply
, что является общеизвестным фактом по другим причинам ( sapply
пытается упростить вывод и, следовательно, должен сделать много проверки размера данных и потенциальных преобразований). Ничего не связано с for
. Я что-то упускаю?
Иногда ускорение может быть значительным, например, когда вам приходится вкладывать циклы для получения среднего значения, основанного на группировке из более чем одного фактора. Здесь у вас есть два подхода, которые дают вам одинаковый результат:
set.seed(1) #for reproducability of the results
# The data
X <- rnorm(100000)
Y <- as.factor(sample(letters[1:5],100000,replace=T))
Z <- as.factor(sample(letters[1:10],100000,replace=T))
# the function forloop that averages X over every combination of Y and Z
forloop <- function(x,y,z){
# These ones are for optimization, so the functions
#levels() and length() don't have to be called more than once.
ylev <- levels(y)
zlev <- levels(z)
n <- length(ylev)
p <- length(zlev)
out <- matrix(NA,ncol=p,nrow=n)
for(i in 1:n){
for(j in 1:p){
out[i,j] <- (mean(x[y==ylev[i] & z==zlev[j]]))
}
}
rownames(out) <- ylev
colnames(out) <- zlev
return(out)
}
# Used on the generated data
forloop(X,Y,Z)
# The same using tapply
tapply(X,list(Y,Z),mean)
Оба дают одинаковый результат, будучи матрицей 5 x 10 со средними значениями и именованными строками и столбцами. Но :
> system.time(forloop(X,Y,Z))
user system elapsed
0.94 0.02 0.95
> system.time(tapply(X,list(Y,Z),mean))
user system elapsed
0.06 0.00 0.06
Вот и ты. Что я выиграл? ;-)
*apply
быстрее. Но я думаю, что более важным моментом являются побочные эффекты (обновил мой ответ на примере).
data.table
это еще быстрее, и я думаю, что "проще". library(data.table)
dt<-data.table(X,Y,Z,key=c("Y,Z"))
system.time(dt[,list(X_mean=mean(X)),by=c("Y,Z")])
tapply
является специализированной функцией для конкретной задачи, именно поэтому он быстрее , чем цикл. Он не может делать то, что может делать цикл for (в то время как обычный apply
может). Ты сравниваешь яблоки с апельсинами.
... и как я только что написал в другом месте, vapply твой друг! ... это как sapply, но вы также указываете тип возвращаемого значения, что делает его намного быстрее.
foo <- function(x) x+1
y <- numeric(1e6)
system.time({z <- numeric(1e6); for(i in y) z[i] <- foo(i)})
# user system elapsed
# 3.54 0.00 3.53
system.time(z <- lapply(y, foo))
# user system elapsed
# 2.89 0.00 2.91
system.time(z <- vapply(y, foo, numeric(1)))
# user system elapsed
# 1.35 0.00 1.36
1 января 2020 г., обновление:
system.time({z1 <- numeric(1e6); for(i in seq_along(y)) z1[i] <- foo(y[i])})
# user system elapsed
# 0.52 0.00 0.53
system.time(z <- lapply(y, foo))
# user system elapsed
# 0.72 0.00 0.72
system.time(z3 <- vapply(y, foo, numeric(1)))
# user system elapsed
# 0.7 0.0 0.7
identical(z1, z3)
# [1] TRUE
for
циклы быстрее на моем Windows 10, 2-ядерный компьютер. Я сделал это с 5e6
элементами - цикл составил 2,9 секунды против 3,1 секунды для vapply
.
В другом месте я писал, что пример, подобный Шейну, на самом деле не подчеркивает разницу в производительности между различными типами циклического синтаксиса, потому что все время затрачивается внутри функции, а не на нагрузку на цикл. Кроме того, код несправедливо сравнивает цикл for без памяти с функциями семейства apply, которые возвращают значение. Вот немного другой пример, который подчеркивает суть.
foo <- function(x) {
x <- x+1
}
y <- numeric(1e6)
system.time({z <- numeric(1e6); for(i in y) z[i] <- foo(i)})
# user system elapsed
# 4.967 0.049 7.293
system.time(z <- sapply(y, foo))
# user system elapsed
# 5.256 0.134 7.965
system.time(z <- lapply(y, foo))
# user system elapsed
# 2.179 0.126 3.301
Если вы планируете сохранить результат, то применять семейные функции можно гораздо больше, чем синтаксический сахар.
(простой unlist для z равен всего 0,2 с, поэтому задержка выполняется намного быстрее. Инициализация z в цикле for довольно быстрая, потому что я даю среднее значение за последние 5 из 6 запусков, поэтому перемещение за пределы system.time вряд ли повлияет на вещи)
Еще одна вещь, которую следует отметить, - это то, что есть еще одна причина использовать семейные функции независимо от их производительности, ясности или отсутствия побочных эффектов. for
Цикл , как правило , способствует положить в максимально возможной степени в пределах цикла. Это связано с тем, что каждый цикл требует настройки переменных для хранения информации (среди прочих возможных операций). Применить заявления, как правило, смещены в другую сторону. Часто вы хотите выполнить несколько операций с вашими данными, некоторые из которых могут быть векторизованы, но некоторые могут быть не в состоянии. В R, в отличие от других языков, лучше отделить те операции и запустить те, которые не векторизованы в операторе применения (или векторизованной версии функции), и те, которые векторизованы как истинные векторные операции. Это часто значительно повышает производительность.
На примере Joris Meys, где он заменяет традиционный цикл for на удобную R-функцию, мы можем использовать ее, чтобы показать эффективность написания кода более R-дружественным образом для аналогичного ускорения без специализированной функции.
set.seed(1) #for reproducability of the results
# The data - copied from Joris Meys answer
X <- rnorm(100000)
Y <- as.factor(sample(letters[1:5],100000,replace=T))
Z <- as.factor(sample(letters[1:10],100000,replace=T))
# an R way to generate tapply functionality that is fast and
# shows more general principles about fast R coding
YZ <- interaction(Y, Z)
XS <- split(X, YZ)
m <- vapply(XS, mean, numeric(1))
m <- matrix(m, nrow = length(levels(Y)))
rownames(m) <- levels(Y)
colnames(m) <- levels(Z)
m
Это оказывается намного быстрее, чем for
петля, и немного медленнее, чем встроенная оптимизированная tapply
функция. Это не потому, что vapply
он намного быстрее, for
а потому, что он выполняет только одну операцию в каждой итерации цикла. В этом коде все остальное векторизовано. В традиционном for
цикле Джориса Мейса в каждой итерации выполняется много (7?) Операций, и для его выполнения достаточно много настроек. Также обратите внимание, насколько компактнее эта for
версия.
2.798 0.003 2.803; 4.908 0.020 4.934; 1.498 0.025 1.528
, а vapply еще лучше:1.19 0.00 1.19
sapply
50% медленнее , чем for
и в lapply
два раза быстрее.
y
к 1:1e6
, а не numeric(1e6)
(вектор нулей). Попытка выделить foo(0)
на z[0]
снова и не хорошо иллюстрирует типичное с for
использованием цикла. В противном случае сообщение на месте.
При применении функций к подмножествам вектора, это tapply
может быть довольно быстро, чем цикл for. Пример:
df <- data.frame(id = rep(letters[1:10], 100000),
value = rnorm(1000000))
f1 <- function(x)
tapply(x$value, x$id, sum)
f2 <- function(x){
res <- 0
for(i in seq_along(l <- unique(x$id)))
res[i] <- sum(x$value[x$id == l[i]])
names(res) <- l
res
}
library(microbenchmark)
> microbenchmark(f1(df), f2(df), times=100)
Unit: milliseconds
expr min lq median uq max neval
f1(df) 28.02612 28.28589 28.46822 29.20458 32.54656 100
f2(df) 38.02241 41.42277 41.80008 42.05954 45.94273 100
apply
Однако в большинстве случаев увеличение скорости не обеспечивается, а в некоторых случаях может быть даже намного медленнее:
mat <- matrix(rnorm(1000000), nrow=1000)
f3 <- function(x)
apply(x, 2, sum)
f4 <- function(x){
res <- 0
for(i in 1:ncol(x))
res[i] <- sum(x[,i])
res
}
> microbenchmark(f3(mat), f4(mat), times=100)
Unit: milliseconds
expr min lq median uq max neval
f3(mat) 14.87594 15.44183 15.87897 17.93040 19.14975 100
f4(mat) 12.01614 12.19718 12.40003 15.00919 40.59100 100
Но для этих ситуаций у нас есть colSums
и rowSums
:
f5 <- function(x)
colSums(x)
> microbenchmark(f5(mat), times=100)
Unit: milliseconds
expr min lq median uq max neval
f5(mat) 1.362388 1.405203 1.413702 1.434388 1.992909 100
microbenchmark
это гораздо точнее, чем system.time
. Если вы попытаетесь сравнить, system.time(f3(mat))
и system.time(f4(mat))
вы получите разные результаты почти каждый раз. Иногда только надлежащий тест может показать самую быструю функцию.
apply
семейство функций. Следовательно, структурирование программ по их применению позволяет их распараллеливать с очень небольшими предельными издержками.