Apakah R berlaku keluarga lebih dari gula sintaksis?

152

... mengenai waktu eksekusi dan / atau memori.

Jika ini tidak benar, buktikan dengan potongan kode. Perhatikan bahwa speedup oleh vektorisasi tidak masuk hitungan. Speedup harus berasal dari apply( tapply, sapply, ...) itu sendiri.

steffen
sumber

Jawaban:

152

The applyfungsi dalam R tidak memberikan peningkatan kinerja lebih fungsi perulangan lainnya (misalnya for). Satu pengecualian untuk ini adalah lapplyyang bisa sedikit lebih cepat karena ia bekerja lebih banyak dalam kode C daripada di R (lihat pertanyaan ini untuk contohnya ).

Namun secara umum, aturannya adalah Anda harus menggunakan fungsi terapkan untuk kejelasan, bukan untuk kinerja .

Saya ingin menambahkan ini yang menerapkan fungsi tidak memiliki efek samping , yang merupakan perbedaan penting ketika datang ke pemrograman fungsional dengan R. Ini dapat diganti dengan menggunakan assignatau <<-, tetapi itu bisa sangat berbahaya. Efek samping juga membuat program lebih sulit untuk dipahami karena keadaan variabel tergantung pada sejarah.

Edit:

Hanya untuk menekankan ini dengan contoh sepele yang secara rekursif menghitung urutan Fibonacci; ini dapat dijalankan beberapa kali untuk mendapatkan ukuran yang akurat, tetapi intinya adalah bahwa tidak ada metode yang memiliki kinerja yang sangat berbeda:

> 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 

Edit 2:

Mengenai penggunaan paket paralel untuk R (mis. Rpvm, rmpi, snow), ini biasanya menyediakan applyfungsi keluarga (bahkan foreachpaket itu pada dasarnya setara, terlepas dari namanya). Berikut adalah contoh sederhana dari sapplyfungsi di snow:

library(snow)
cl <- makeSOCKcluster(c("localhost","localhost"))
parSapply(cl, 1:20, get("+"), 3)

Contoh ini menggunakan soket cluster, yang tidak perlu diinstal perangkat lunak tambahan; jika tidak, Anda akan membutuhkan sesuatu seperti PVM atau MPI (lihat halaman pengelompokan Tierney ). snowmemiliki fungsi terapkan berikut ini:

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, ...)

Masuk akal bahwa applyfungsi harus digunakan untuk eksekusi paralel karena mereka tidak memiliki efek samping . Ketika Anda mengubah nilai variabel dalam satu forlingkaran, itu diatur secara global. Di sisi lain, semua applyfungsi dapat dengan aman digunakan secara paralel karena perubahan bersifat lokal pada pemanggilan fungsi (kecuali jika Anda mencoba menggunakan assignatau <<-, dalam hal ini Anda dapat menimbulkan efek samping). Tidak perlu dikatakan, sangat penting untuk berhati-hati tentang variabel lokal vs global, terutama ketika berhadapan dengan eksekusi paralel.

Edit:

Berikut adalah contoh sepele untuk menunjukkan perbedaan antara fordan *applysejauh menyangkut efek samping:

> 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

Perhatikan bagaimana dflingkungan induk diubah oleh fortetapi tidak *apply.

Shane
sumber
30
Sebagian besar paket multi-core untuk R juga mengimplementasikan paralelisasi melalui rangkaian applyfungsi. Oleh karena itu, penataan program yang mereka gunakan berlaku memungkinkan mereka diparalelkan dengan biaya marjinal yang sangat kecil.
Sharpie
Sharpie - terima kasih untuk itu! Ada ide untuk contoh yang menunjukkan itu (di windows XP)?
Tal Galili
5
Saya akan menyarankan melihat snowfallpaket dan mencoba contoh-contoh dalam sketsa mereka. snowfalldibangun di atas snowpaket dan abstrak detail dari paralelisasi bahkan membuatnya lebih mudah untuk mengeksekusi applyfungsi paralel .
Sharpie
1
@Sharpie tetapi catatan yang foreachtelah tersedia dan tampaknya banyak bertanya tentang SO.
Ari B. Friedman
1
@Shane, di bagian paling atas jawaban Anda, Anda menautkan ke pertanyaan lain sebagai contoh kasus di mana lapply"sedikit lebih cepat" daripada satu forlingkaran. Namun, di sana, saya tidak melihat ada yang menyarankan begitu. Anda hanya menyebutkan bahwa lapplylebih cepat daripada sapply, yang merupakan fakta terkenal karena alasan lain ( sapplymencoba menyederhanakan output dan karenanya harus melakukan banyak pengecekan ukuran data dan konversi potensial). Tidak ada yang terkait dengan for. Apakah saya melewatkan sesuatu?
flodel
70

Kadang-kadang speedup bisa menjadi besar, seperti ketika Anda harus bersarang untuk-loop untuk mendapatkan rata-rata berdasarkan pengelompokan lebih dari satu faktor. Di sini Anda memiliki dua pendekatan yang memberi Anda hasil yang sama persis:

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)

Keduanya memberikan hasil yang persis sama, menjadi matriks 5 x 10 dengan rata-rata dan diberi nama baris dan kolom. Tapi:

> 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 

Ini dia. Apa yang saya menangkan? ;-)

Joris Meys
sumber
aah, sangat manis :-) Saya benar-benar bertanya-tanya apakah ada yang akan menemukan jawaban saya yang agak terlambat.
Joris Meys
1
Saya selalu mengurutkan berdasarkan "aktif". :) Tidak yakin bagaimana cara menggeneralisasi jawaban Anda; terkadang *applylebih cepat. Tetapi saya berpikir bahwa poin yang lebih penting adalah efek sampingnya (memperbarui jawaban saya dengan contoh).
Shane
1
Saya pikir itu berlaku lebih cepat ketika Anda ingin menerapkan fungsi pada himpunan bagian yang berbeda. Jika ada solusi terapkan cerdas untuk loop bersarang, saya kira solusi terapan akan lebih cepat juga. Dalam kebanyakan kasus, mendaftar tidak mendapatkan banyak kecepatan, saya kira, tapi saya setuju dengan efek sampingnya.
Joris Meys
2
Ini sedikit di luar topik, tetapi untuk contoh khusus ini, data.tablebahkan lebih cepat dan saya pikir "lebih mudah". 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
Perbandingan ini tidak masuk akal. tapplyadalah fungsi khusus untuk tugas tertentu, itu sebabnya lebih cepat daripada for loop. Itu tidak bisa melakukan apa yang bisa dilakukan untuk loop (sementara biasa applybisa). Anda membandingkan apel dengan jeruk.
eddi
47

... dan seperti yang baru saja saya tulis di tempat lain, vapply adalah teman Anda! ... itu seperti sapply, tetapi Anda juga menentukan tipe nilai pengembalian yang membuatnya jauh lebih cepat.

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 

Pembaruan 1 Januari 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
Tommy
sumber
Temuan asli tampaknya tidak lagi benar. forloop lebih cepat pada komputer Windows 10, 2-core saya. Saya melakukan ini dengan 5e6elemen - loop adalah 2,9 detik vs 3,1 detik untuk vapply.
Cole
27

Saya sudah menulis di tempat lain bahwa contoh seperti Shane's tidak benar-benar menekankan perbedaan kinerja di antara berbagai jenis sintaksis perulangan karena semua waktu dihabiskan dalam fungsi daripada benar-benar menekankan loop. Selain itu, kode ini secara tidak adil membandingkan loop untuk tanpa memori dengan menerapkan fungsi keluarga yang mengembalikan nilai. Inilah contoh yang sedikit berbeda yang menekankan intinya.

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 

Jika Anda berencana untuk menyimpan hasilnya kemudian menerapkan fungsi keluarga dapat menjadi jauh lebih dari sintaksis gula.

(unlist sederhana z hanya 0.2s sehingga hasilnya jauh lebih cepat. Menginisialisasi z dalam for loop cukup cepat karena saya memberikan rata-rata 5 dari 6 berjalan terakhir sehingga bergerak di luar system.time akan hampir tidak mempengaruhi hal-hal)

Satu hal lagi yang perlu diperhatikan adalah bahwa ada alasan lain untuk menggunakan fungsi keluarga yang berlaku terlepas dari kinerja, kejelasan, atau kurangnya efek sampingnya. SEBUAHfor loop biasanya mempromosikan penempatan sebanyak mungkin dalam loop. Ini karena setiap loop memerlukan pengaturan variabel untuk menyimpan informasi (di antara operasi lain yang mungkin). Pernyataan yang diterapkan cenderung bias sebaliknya. Sering kali Anda ingin melakukan beberapa operasi pada data Anda, beberapa di antaranya dapat di-vektor-kan tetapi beberapa mungkin tidak bisa. Dalam R, tidak seperti bahasa lain, yang terbaik adalah memisahkan operasi-operasi tersebut dan menjalankan yang tidak di-vektor-kan dalam pernyataan yang berlaku (atau versi fungsi yang di-vektor-kan) dan yang di-vektor-kan sebagai operasi vektor yang sebenarnya. Ini sering mempercepat kinerja luar biasa.

Mengambil contoh Joris Meys di mana ia menggantikan tradisional untuk loop dengan fungsi R praktis kita dapat menggunakannya untuk menunjukkan efisiensi penulisan kode dengan cara yang lebih ramah untuk speedup yang sama tanpa fungsi khusus.

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

Ini berakhir menjadi jauh lebih cepat daripada forloop dan hanya sedikit lebih lambat daripada fungsi built-in yang dioptimalkan tapply. Ini bukan karena vapplyjauh lebih cepat daripada fortetapi karena itu hanya melakukan satu operasi di setiap iterasi dari loop. Dalam kode ini segala sesuatu yang lain adalah vektor. Dalam forloop tradisional Joris Meys banyak (7?) Operasi yang terjadi di setiap iterasi dan ada cukup banyak pengaturan hanya untuk itu untuk dijalankan. Perhatikan juga seberapa kompaknya forversi ini.

John
sumber
4
Tetapi contoh Shane adalah realistis dalam sebagian besar waktu yang biasanya dihabiskan dalam fungsi, tidak dalam lingkaran.
Hadley
9
berbicara sendiri ...:) ... Mungkin Shane realistis dalam arti tertentu tetapi dalam arti yang sama analisisnya sama sekali tidak berguna. Orang akan peduli dengan kecepatan mekanisme iterasi ketika mereka harus melakukan banyak iterasi, jika tidak masalah mereka ada di tempat lain. Ini berlaku untuk semua fungsi. Jika saya menulis dosa yang membutuhkan 0,001 dan orang lain menulis dosa yang membutuhkan 0,002 siapa yang peduli ?? Nah, segera setelah Anda harus melakukan banyak dari mereka Anda peduli.
John
2
pada 12 inti 3Ghz intel Xeon, 64bit, saya mendapatkan angka yang sangat berbeda untuk Anda - loop untuk meningkatkan jauh: untuk tiga tes Anda, saya dapatkan 2.798 0.003 2.803; 4.908 0.020 4.934; 1.498 0.025 1.528, dan vapply bahkan lebih baik:1.19 0.00 1.19
naught101
2
Itu bervariasi dengan versi OS dan R ... dan dalam arti mutlak CPU. Saya hanya berlari dengan 2.15.2 di Mac dan mendapat sapply50% lebih lambat dari fordan lapplydua kali lebih cepat.
John
1
Dalam contoh Anda, Anda bermaksud mengatur yke 1:1e6, bukan numeric(1e6)(vektor nol). Mencoba untuk mengalokasikan foo(0)untuk z[0]berulang tidak menggambarkan dengan baik khas forpenggunaan lingkaran. Pesannya dinyatakan tepat.
flodel
3

Saat menerapkan fungsi pada subset vektor, tapplybisa jadi lebih cepat daripada for for. Contoh:

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

applyNamun, dalam kebanyakan situasi tidak memberikan peningkatan kecepatan, dan dalam beberapa kasus bahkan bisa lebih lambat:

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

Tetapi untuk situasi ini kita punya colSumsdan 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
sumber
7
Penting untuk memperhatikan bahwa (untuk potongan kode kecil) microbenchmarkitu jauh lebih tepat daripada system.time. Jika Anda mencoba membandingkan system.time(f3(mat))dan system.time(f4(mat))Anda akan mendapatkan hasil yang berbeda hampir setiap waktu. Terkadang hanya tes benchmark yang tepat yang mampu menunjukkan fungsi tercepat.
Michele