Чи застосовується сім'я R більше, ніж синтаксичний цукор?


152

... щодо часу виконання та / або пам'яті.

Якщо це неправда, доведіть це за допомогою фрагмента коду. Зауважте, що прискорення за допомогою векторизації не враховується. Прискорення повинні виходити з apply( tapply, sapply, ...) сама по собі.

Відповіді:


152

Ці 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.


30
Більшість багатоядерних пакетів для R також здійснюють паралелізацію через applyсімейство функцій. Тому структурування програм та їх використання застосовується дозволяє паралелізувати їх за дуже невеликих граничних витрат.
Шарпі

Шарпі - дякую за це! Будь-яка ідея для прикладу, що показує це (у Windows XP)?
Тал Галілі

5
Я б запропонував подивитися snowfallпакет і спробувати приклади у їхній віньєтці. snowfallбудується на вершині snowпакета і ще більше абстрагує деталі паралелізації, роблячи її простою для виконання паралелізованих applyфункцій.
Шарпі

1
@Sharpie, але зауважте, що з foreachтих пір стали доступними і, здається, багато запитують про SO.
Арі Б. Фрідман

1
@Shane, у самому верху вашої відповіді ви посилаєтесь на інше запитання як приклад випадку, коли lapply"трохи швидше", ніж forцикл. Однак там я не бачу нічого, що це напрошувало б. Ви лише згадуєте, що lapplyце швидше sapply, що добре відомий факт з інших причин ( sapplyнамагається спростити вихід, а значить, доведеться зробити багато перевірки розміру даних та потенційних перетворень). Нічого, пов’язаного з цим for. Я щось пропускаю?
флодель

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 х 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
Я думаю, що застосувати особливо швидше, коли потрібно застосувати функцію до різних підмножин. Якщо для вкладеного циклу є розумне рішення застосувати, я думаю, що застосувати рішення також буде швидше. Я думаю, у більшості випадків не набирає великої швидкості, але я безумовно згоден на побічні ефекти.
Joris Meys

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є спеціалізованою функцією для конкретного завдання, саме тому він швидше , ніж цикл. Він не може робити те, що може робити цикл (у той час як звичайний apply). Ви порівнюєте яблука з апельсинами.
Едді

47

... і як я щойно писав деінде, вапплип - твій друг! ... це як 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.
Коул

27

Я писав в іншому місці, що такий приклад, як Шейн, насправді не підкреслює різницю в продуктивності між різними видами циклічного синтаксису, оскільки весь час витрачається в межах функції, а не насправді підкреслює цикл. Крім того, код несправедливо порівнює цикл для циклу без пам'яті з сімейними функціями застосувати, які повертають значення. Ось трохи інший приклад, який підкреслює суть.

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версія.


4
Але приклад Шейн є реалістичним в тому , що більшу частину часу це зазвичай проводиться в функції, а не в петлі.
Хадлі

9
говоріть самі ...:) ... Можливо, Шейн реалістичний у певному сенсі, але в цьому ж сенсі аналіз є абсолютно марним. Люди будуть піклуватися про швидкість механізму ітерації, коли їм доведеться зробити багато ітерацій, інакше їхні проблеми в іншому випадку. Це правда для будь-якої функції. Якщо я пишу гріх, який займає 0,001s, а хтось інший пише той, який займає 0,002, хто піклується ?? Ну, як тільки вам доведеться зробити купу з них, ви піклуєтесь.
Джон

2
на 12-ядерному 3Ghz intel Xeon, 64bit, я отримую для вас зовсім інші цифри - цикл 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 ... і в абсолютному сенсі процесора. Я просто біг з 2.15.2 на Mac і на sapply50% повільніше forі lapplyвдвічі швидше.
Джон

1
У вашому прикладі ви маєте на увазі встановити yзначення « 1:1e6не» numeric(1e6)(вектор нулів). Спроба виділити foo(0)на z[0]знову і не добре ілюструє типове з forвикористанням циклу. В іншому випадку повідомлення не вказано.
флодель

3

Застосовуючи функції над підмножинами вектора, 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

7
Важливо зауважити, що (для невеликих фрагментів коду) microbenchmarkце набагато точніше, ніж system.time. Якщо спробувати порівняти system.time(f3(mat))і system.time(f4(mat))ви отримаєте інший результат майже кожен раз. Іноді лише правильний тест на еталон здатний показати найшвидшу функцію.
Мішель
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.