R'ler uygulama ailesi sözdizimsel şekerden daha mı fazla?


152

... yürütme süresi ve / veya hafıza ile ilgili.

Bu doğru değilse, bir kod snippet'i ile kanıtlayın. Vektörleştirmeyle hızlanmanın sayılmadığını unutmayın. Hızlanma gelmelidir apply( tapply, sapply, ...) kendisi.

Yanıtlar:


152

applyR fonksiyonlar, diğer döngü fonksiyon (örneğin daha iyi performans sağlamamaktadır for). Bunun bir istisnası lapply, C kodunda R'den daha fazla çalıştığı için biraz daha hızlı olabilir ( bunun bir örneği için bu soruya bakın ).

Ancak genel olarak, kural, performans için değil, netlik için bir uygulama işlevi kullanmanızdır .

Bunu , R ile fonksiyonel programlama söz konusu olduğunda önemli bir ayrım olan uygulama fonksiyonlarının hiçbir yan etkisi olmadığını da ekleyeceğim. Bu, assignveya kullanılarak geçersiz kılınabilir <<-, ancak bu çok tehlikeli olabilir. Bir değişkenin durumu tarihe bağlı olduğu için yan etkiler de bir programı anlamayı zorlaştırır.

Düzenle:

Sadece bunu tekrarlayan Fibonacci dizisini hesaplayan önemsiz bir örnekle vurgulamak için; bu, doğru bir ölçüm almak için birden fazla kez çalıştırılabilir, ancak nokta, yöntemlerin hiçbirinin önemli ölçüde farklı performansa sahip olmamasıdır:

> 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 

Düzenleme 2:

R (örneğin rpvm, rmpi, kar) için paralel paketlerin kullanımı ile ilgili olarak, bunlar genellikle applyaile işlevleri sağlar (hatta foreachisme rağmen paket esasen eşdeğerdir). İşte sapplyfonksiyonun basit bir örneği snow:

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

Bu örnek için ek yazılımın yüklenmesi gerekmeyen bir soket kümesi kullanılır; aksi takdirde PVM veya MPI gibi bir şeye ihtiyacınız olacaktır (bkz. Tierney'in kümeleme sayfası ). snowaşağıdaki uygulama işlevlerine sahiptir:

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

Yan etkileri olmadığıapply için işlevlerin paralel yürütme için kullanılması mantıklıdır . Bir döngü içindeki bir değişken değeri değiştirdiğinizde , bu değer global olarak ayarlanır. Öte yandan, tüm işlevler güvenli bir şekilde paralel olarak kullanılabilir, çünkü değişiklikler işlev çağrısında yereldir (kullanmaya çalışmazsanız veya bu durumda yan etkiler getiremezseniz). Söylemeye gerek yok, özellikle paralel yürütme ile uğraşırken yerel ve küresel değişkenler konusunda dikkatli olmak çok önemlidir.forapplyassign<<-

Düzenle:

İşte arasındaki farkı göstermek için önemsiz bir örnek forve *applyböylece yan etkileri söz olarak:

> 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

dfAna ortamdaki öğenin nasıl değiştirildiğini forancak değiştirilmediğini unutmayın *apply.


30
R için çok çekirdekli paketlerin çoğu apply, işlev ailesi aracılığıyla paralelleştirme uygular . Bu nedenle, programları uygulayacakları şekilde yapılandırmak, çok düşük bir marjinal maliyetle paralel olmalarını sağlar.
Sharpie

Sharpie - bunun için teşekkürler! Bunu gösteren bir örnek için herhangi bir fikir (Windows XP'de)?
Tal Galili

5
snowfallPakete bakıp vinyetlerindeki örnekleri denemenizi öneririm . paketin snowfallüzerine snowkurulur ve paralelleştirmenin ayrıntılarını daha da soyutlayarak paralel applyişlevlerin yerine getirilmesini kolaylaştırır .
Sharpie

1
@Sharpie ama o foreachzamandan beri kullanılabilir hale geldi ve SO hakkında çok sorulmuş gibi görünüyor.
Ari B. Friedman

1
@Shane, cevabınızın en üstünde, lapplybir fordöngüden "biraz daha hızlı" olan bir duruma örnek olarak başka bir soruya bağlanırsınız . Ancak, orada, bunu öneren hiçbir şey görmüyorum. Sadece lapplybundan daha hızlı sapply, diğer nedenlerle iyi bilinen bir gerçektir ( sapplyçıktıyı basitleştirmeye çalışır ve bu nedenle çok fazla veri boyutu kontrolü ve potansiyel dönüşüm yapmak zorundadır). İle ilgili bir şey yok for. Bir şey mi kaçırıyorum?
flodel

70

Bazen hızlanma önemli olabilir, örneğin birden fazla faktöre sahip bir gruplamaya dayalı olarak ortalamayı almak için döngüler için iç içe geçirmeniz gerektiğinde. Burada size aynı sonucu veren iki yaklaşımınız var:

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)

Her ikisi de aynı sonucu verir, ortalamalar ve adlandırılmış satırlar ve sütunlar ile 5 x 10 matristir. Fakat :

> 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 

İşte böyle. Ne kazandım? ;-)


aah, çok tatlı :-) Aslında kimse benim oldukça geç cevabım gelip gelmeyeceğini merak ediyordum.
Joris Meys

1
Her zaman "aktif" olarak sıralarım. :) Cevabınızı nasıl genelleştireceğinizden emin değilsiniz; bazen *applydaha hızlıdır. Ama bence daha önemli olan nokta yan etkilerdir (cevabımı bir örnekle güncelledi).
Shane

1
Farklı altkümeler üzerine bir işlev uygulamak istediğinizde uygulamanın özellikle daha hızlı olduğunu düşünüyorum. Yuvalanmış bir döngü için akıllı bir uygulama çözümü varsa, sanırım uygulama çözümü de daha hızlı olacaktır. Çoğu durumda uygulamak çok fazla hız kazanmıyor sanırım, ama kesinlikle yan etkileri kabul ediyorum.
Joris Meys

2
Bu biraz konu dışı, ama bu özel örnek için, data.tabledaha da hızlı ve "daha kolay" düşünüyorum. 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
Bu karşılaştırma saçma. tapplybelirli bir görev için özel bir işlevdir, bu yüzden for döngüsünden daha hızlıdır. Bir for döngüsünün yapabileceklerini yapamaz (düzenli applyolabilir). Elmaları portakallarla karşılaştırıyorsunuz.
eddi

47

... ve başka bir yerde yazdığım gibi, vapply arkadaşın! ... aptal gibi, ama aynı zamanda çok daha hızlı yapan dönüş değeri türünü de belirlersiniz.

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 Ocak 2020 güncellemesi:

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

Orijinal bulgular artık doğru görünmüyor. forWindows 10, 2 çekirdekli bilgisayarımda döngüler daha hızlı. Bunu 5e6elementlerle yaptım - bir döngü 2.9 saniye vs. 3.1 saniye idi vapply.
Cole

27

Başka bir yerde yazdım, Shane's gibi bir örnek, çeşitli döngü sözdizimi arasındaki performans farkını gerçekten vurgulamıyor çünkü zaman aslında döngüyü vurgulamak yerine işlev içinde harcanıyor. Ayrıca, kod haksız yere bir for döngüsünü belleksiz ve bir değer döndüren uygulama ailesi işlevleriyle karşılaştırır. İşte konuyu vurgulayan biraz farklı bir örnek.

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 

Sonucu kaydetmeyi planlıyorsanız, aile işlevlerini sözdizimsel şekerden çok daha fazla olabilir .

(z'nin basit listesi sadece 0,2 saniyedir, bu yüzden lapply çok daha hızlıdır. for döngüsünde z'yi başlatmak oldukça hızlıdır, çünkü son 5 çalışmanın ortalamasını veriyorum, böylece sistemin dışına taşınıyor. işleri neredeyse hiç etkilemez)

Yine de dikkat edilmesi gereken bir şey de, aile işlevlerini performanslarından, netliklerinden veya yan etkilerden bağımsız olarak kullanmanın başka bir nedeni olduğudur. Bir fordöngü tipik olarak döngü içine mümkün olduğunca fazla koymayı teşvik eder. Bunun nedeni, her döngünün bilgileri depolamak için değişkenlerin kurulumunu gerektirmesidir (diğer olası işlemlerin yanı sıra). Uygulama ifadeleri diğer taraftan önyargılı olma eğilimindedir. Çoğu zaman verileriniz üzerinde birden fazla işlem gerçekleştirilebilir, bunlardan birkaçı vektörleştirilebilir, ancak bazıları gerçekleştirilemeyebilir. R'de, diğer dillerden farklı olarak, bu işlemleri ayırmak ve bir uygulama deyiminde (veya işlevin vektörleştirilmiş sürümü) vektörleştirilmemiş olanları ve gerçek vektör işlemleri olarak vektörlenenleri çalıştırmak en iyisidir. Bu genellikle performansı muazzam bir şekilde hızlandırır.

Geleneksel bir döngü yerine kullanışlı bir R işlevinin yerini aldığı Joris Meys örneğini ele alarak, özel bir işlev olmadan benzer bir hızlandırma için kod yazma verimliliğini daha R dostu bir şekilde göstermek için kullanabiliriz.

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

Bu, fordöngüden çok daha hızlı ve yerleşik optimize edilmiş tapplyişleve göre biraz daha yavaş olur . Çünkü vapplyçok daha hızlı fordeğil, çünkü döngünün her yinelemesinde sadece bir işlem gerçekleştiriyor. Bu kodda diğer her şey vektörleştirilir. Joris Meys geleneksel fordöngüsünde, her yinelemede birçok (7?) İşlem gerçekleşiyor ve sadece yürütmesi için biraz kurulum var. Bunun forsürümden ne kadar daha kompakt olduğunu da unutmayın .


4
Ancak Shane örneği, çoğu zaman döngüde değil , genellikle işlevde geçirildiği için gerçekçi .
hadley

9
kendiniz için konuşun ...:) ... Belki de Shane's belli bir anlamda gerçekçi ama aynı anlamda analiz tamamen işe yaramaz. İnsanlar çok fazla yineleme yapmak zorunda kaldıklarında yineleme mekanizmasının hızını umursarlar, aksi takdirde sorunları zaten başka yerlerde olur. Herhangi bir fonksiyon için doğrudur. Eğer 0.001 alır bir günah yazıp başka biri 0.002 alır bir kim yazar umurunda ?? Peki, bir demet yapmak zorunda kaldığın anda umursuyorsun.
John

2
12 çekirdek 3Ghz intel Xeon, 64bit, size oldukça farklı sayılar elde ediyorum - for döngüsü önemli ölçüde gelişir: üç 2.798 0.003 2.803; 4.908 0.020 4.934; 1.498 0.025 1.528testiniz için , alıyorum ve vapply daha da iyi:1.19 0.00 1.19
naught101 26:12

2
İşletim sistemi ve R sürümüne göre değişir ... ve mutlak anlamda CPU. Mac'te 2.15.2 ile koştum ve sapply% 50 daha yavaş forve lapplyiki kat daha hızlı aldım .
John

1
Senin örnekte, sete demek yiçin 1:1e6değil, numeric(1e6)(sıfır bir vektör). Tekrar tekrar tahsis foo(0)etmeye çalışmak z[0]tipik bir fordöngü kullanımını iyi göstermez . Mesaj başka şekilde açıktır.
flodel

3

Bir vektörün alt kümelerine fonksiyonlar uygulanırken, tapplyfor döngüsüne göre oldukça hızlı olabilir. Misal:

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

applyancak, çoğu durumda hız artışı sağlamaz ve bazı durumlarda çok daha yavaş olabilir:

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

Ancak bu durumlar için elimizdeki colSumsve 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
(Küçük kod parçaları için) microbenchmarkçok daha kesin olduğunu fark etmek önemlidir system.time. Karşılaştırmaya çalışırsanız system.time(f3(mat))ve system.time(f4(mat))neredeyse her seferinde farklı sonuçlar elde ederseniz. Bazen en uygun işlevi sadece uygun bir kıyaslama testi gösterebilir.
Michele
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.