Является ли семейство R больше, чем синтаксический сахар?

152

... относительно времени выполнения и / или памяти.

Если это не так, докажите это с помощью фрагмента кода. Обратите внимание, что ускорение векторизацией не считается. Убыстрение должны исходить из apply( tapply, sapply, ...) сама по себе.

Штеффен
источник

Ответы:

152

Эти 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.

Шейн
источник
30
Большинство многоядерных пакетов для R также реализуют распараллеливание через applyсемейство функций. Следовательно, структурирование программ по их применению позволяет их распараллеливать с очень небольшими предельными издержками.
Шарпи
Шарпи - спасибо тебе за это! Любая идея для примера, показывающего это (на Windows XP)?
Тал Галили
5
Я бы посоветовал взглянуть на snowfallупаковку и попробовать примеры в их виньетке. snowfallстроится поверх snowпакета и абстрагирует детали распараллеливания, что еще больше упрощает выполнение распараллеленных applyфункций.
Шарпи
1
@ Sharpie, но обратите внимание, что с foreachтех пор он стал доступен и, похоже, очень интересуется SO.
Ари Б. Фридман
1
@Shane, в самом верху своего ответа вы ссылаетесь на другой вопрос в качестве примера случая, когда lapply«немного быстрее», чем forцикл. Тем не менее, я не вижу ничего, что подсказывало бы это. Вы только упоминаете, что lapplyэто быстрее, чем sapply, что является общеизвестным фактом по другим причинам ( sapplyпытается упростить вывод и, следовательно, должен сделать много проверки размера данных и потенциальных преобразований). Ничего не связано с for. Я что-то упускаю?
flodel
70

Иногда ускорение может быть значительным, например, когда вам приходится вкладывать циклы для получения среднего значения, основанного на группировке из более чем одного фактора. Здесь у вас есть два подхода, которые дают вам одинаковый результат:

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 

Вот и ты. Что я выиграл? ;-)

Йорис Мейс
источник
ааа, так мило :-) Мне было интересно, если кто-нибудь когда-нибудь сталкивался с моим довольно поздним ответом.
Йорис Мейс
1
Я всегда сортирую по «активным». :) Не уверен, как обобщить свой ответ; иногда *applyбыстрее. Но я думаю, что более важным моментом являются побочные эффекты (обновил мой ответ на примере).
Шейн
1
Я думаю, что применять особенно быстрее, когда вы хотите применить функцию к различным подмножествам. Если есть разумное решение для применения для вложенного цикла, я думаю, что решение для применения также будет быстрее. Я полагаю, что в большинстве случаев применение не ускоряется, но я определенно согласен с побочными эффектами.
Йорис Мейс
2
Это немного не по теме, но для этого конкретного примера 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")])
dnlbrky
12
Это сравнение абсурдно. tapplyявляется специализированной функцией для конкретной задачи, именно поэтому он быстрее , чем цикл. Он не может делать то, что может делать цикл for (в то время как обычный applyможет). Ты сравниваешь яблоки с апельсинами.
Эдди
47

... и как я только что написал в другом месте, 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.
Коул
27

В другом месте я писал, что пример, подобный Шейну, на самом деле не подчеркивает разницу в производительности между различными типами циклического синтаксиса, потому что все время затрачивается внутри функции, а не на нагрузку на цикл. Кроме того, код несправедливо сравнивает цикл 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версия.

Джон
источник
4
Но пример Шейн является реалистичным в том , что большую часть времени это обычно проводится в функции, а не в петле.
хэдли
9
говорите за себя ...:) ... Может быть, Шейн реалистичен в определенном смысле, но в этом же смысле анализ совершенно бесполезен. Люди будут заботиться о скорости итерационного механизма, когда им приходится делать много итераций, иначе их проблемы все равно будут в другом месте. Это верно для любой функции. Если я напишу грех, который занимает 0,001 с, а кто-то другой напишет грех, который берет 0,002, кого это волнует? Ну, как только вы сделаете кучу из них, вам все равно.
Джон
2
на 12-ядерном 3 ГГц Intel Xeon, 64-битном, я получаю совершенно другие цифры для вас - цикл 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
naught101
2
Это зависит от версии ОС и R ... и в абсолютном смысле CPU. Я просто побежал с 2.15.2 на Mac и получил sapply50% медленнее , чем forи в lapplyдва раза быстрее.
Джон
1
В вашем примере, вы имеете в виду набор yк 1:1e6, а не numeric(1e6)(вектор нулей). Попытка выделить foo(0)на z[0]снова и не хорошо иллюстрирует типичное с forиспользованием цикла. В противном случае сообщение на месте.
Flodel
3

При применении функций к подмножествам вектора, это 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
Michele
источник
7
Важно отметить, что (для небольших фрагментов кода) microbenchmarkэто гораздо точнее, чем system.time. Если вы попытаетесь сравнить, system.time(f3(mat))и system.time(f4(mat))вы получите разные результаты почти каждый раз. Иногда только надлежащий тест может показать самую быструю функцию.
Мишель