Відповіді:
Ці apply
функції в R не забезпечують підвищену продуктивність по порівнянні з іншими наскрізними функціями (наприклад for
). Один виняток із цього - це те, lapply
що може бути трохи швидше, оскільки це робить більше роботи в коді С, ніж у 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 (див . сторінку кластеризації Тірні ). 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 х 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
є спеціалізованою функцією для конкретного завдання, саме тому він швидше , ніж цикл. Він не може робити те, що може робити цикл (у той час як звичайний apply
). Ви порівнюєте яблука з апельсинами.
... і як я щойно писав деінде, вапплип - твій друг! ... це як 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
петлі швидші на моєму 2-ядерному комп'ютері Windows 10. Я зробив це з 5e6
елементами - цикл становив 2,9 секунди проти 3,1 секунди за vapply
.
Я писав в іншому місці, що такий приклад, як Шейн, насправді не підкреслює різницю в продуктивності між різними видами циклічного синтаксису, оскільки весь час витрачається в межах функції, а не насправді підкреслює цикл. Крім того, код несправедливо порівнює цикл для циклу без пам'яті з сімейними функціями застосувати, які повертають значення. Ось трохи інший приклад, який підкреслює суть.
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 секунди, так що lapply набагато швидше. Ініціалізація z в циклі for досить швидка, тому що я даю середнє значення за останні 5 з 6 пробіжок, так що рухаючись, що поза system.time навряд чи вплине на речі)
Ще одне, що слід зазначити, є те, що є ще одна причина використовувати сімейні функції незалежно від їх продуктивності, ясності або відсутності побічних ефектів. for
Цикл , як правило , сприяє покласти в максимально можливою мірою в межах циклу. Це пояснюється тим, що кожен цикл вимагає встановлення змінних для зберігання інформації (серед інших можливих операцій). Застосовувати висловлювання, як правило, упереджено інакше. Часто ви хочете виконати кілька операцій над вашими даними, кілька з яких можуть бути векторизованими, але деякі можуть бути не в змозі. У R, на відміну від інших мов, найкраще відокремити ті операції та виконати ті, які не векторизовані в операторі застосунку (або векторизованій версії функції) та ті, які векторизуються як справжні векторні операції. Це часто надзвичайно прискорює продуктивність.
Взявши приклад Joris Meys, коли він замінює традиційний для циклу зручний функцію 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
циклі Joris Meys багато операцій (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
може бути досить швидким, ніж для циклу. Приклад:
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
сімейство функцій. Тому структурування програм та їх використання застосовується дозволяє паралелізувати їх за дуже невеликих граничних витрат.