Ответы:
Эти 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
sapply50% медленнее , чем 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семейство функций. Следовательно, структурирование программ по их применению позволяет их распараллеливать с очень небольшими предельными издержками.