Ist Rs Anwendungsfamilie mehr als syntaktischer Zucker?


152

... bezüglich Ausführungszeit und / oder Speicher.

Wenn dies nicht der Fall ist, beweisen Sie es mit einem Code-Snippet. Beachten Sie, dass die Beschleunigung durch Vektorisierung nicht zählt. Der Speedup muss aus apply( tapply, sapply, ...) selbst.

Antworten:


152

Die applyFunktionen in R bieten keine verbesserte Leistung gegenüber anderen Schleifenfunktionen (z for. B. ). Eine Ausnahme ist lapplydie, die etwas schneller sein kann, da sie in C-Code mehr Arbeit leistet als in R ( ein Beispiel hierfür finden Sie in dieser Frage ).

Im Allgemeinen gilt jedoch die Regel, dass Sie aus Gründen der Übersichtlichkeit eine Apply-Funktion verwenden sollten, nicht aus Gründen der Leistung .

Ich möchte hinzufügen, dass Apply-Funktionen keine Nebenwirkungen haben , was ein wichtiger Unterschied bei der funktionalen Programmierung mit R ist. Dies kann durch die Verwendung von assignoder überschrieben werden <<-, aber das kann sehr gefährlich sein. Nebenwirkungen erschweren auch das Verständnis eines Programms, da der Status einer Variablen von der Historie abhängt.

Bearbeiten:

Nur um dies mit einem trivialen Beispiel zu betonen, das die Fibonacci-Sequenz rekursiv berechnet; Dies kann mehrmals ausgeführt werden, um eine genaue Messung zu erhalten. Der Punkt ist jedoch, dass keine der Methoden eine signifikant unterschiedliche Leistung aufweist:

> 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 

Bearbeiten 2:

In Bezug auf die Verwendung paralleler Pakete für R (z. B. rpvm, rmpi, snow) bieten diese im Allgemeinen applyFamilienfunktionen (selbst das foreachPaket ist trotz des Namens im Wesentlichen gleichwertig). Hier ist ein einfaches Beispiel für die sapplyFunktion in snow:

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

In diesem Beispiel wird ein Socket-Cluster verwendet, für den keine zusätzliche Software installiert werden muss. Andernfalls benötigen Sie etwas wie PVM oder MPI (siehe Tierneys Clustering-Seite ). snowhat die folgenden Funktionen anwenden:

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

Es ist sinnvoll, applyFunktionen für die parallele Ausführung zu verwenden, da sie keine Nebenwirkungen haben . Wenn Sie einen Variablenwert innerhalb einer forSchleife ändern , wird er global festgelegt. Auf der anderen Seite können alle applyFunktionen sicher parallel verwendet werden, da Änderungen lokal für den Funktionsaufruf sind (es sei denn, Sie versuchen assignoder oder <<-in diesem Fall können Sie Nebenwirkungen einführen). Es ist unnötig zu erwähnen, dass es wichtig ist, vorsichtig mit lokalen und globalen Variablen umzugehen, insbesondere wenn es um die parallele Ausführung geht.

Bearbeiten:

Hier ist ein triviales Beispiel, um den Unterschied zwischen forund *applyin Bezug auf Nebenwirkungen zu demonstrieren :

> 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

Beachten Sie, wie die dfin der übergeordneten Umgebung von geändert wird, foraber nicht *apply.


30
Die meisten Multi-Core-Pakete für R implementieren auch Parallelisierung über die applyFunktionsfamilie. Die Strukturierung von Programmen, die sie verwenden, ermöglicht daher die Parallelisierung zu sehr geringen Grenzkosten.
Sharpie

Sharpie - danke dafür! Irgendeine Idee für ein Beispiel, das das zeigt (unter Windows XP)?
Tal Galili

5
Ich würde vorschlagen, sich das snowfallPaket anzusehen und die Beispiele in ihrer Vignette auszuprobieren. snowfallbaut auf dem snowPaket auf und abstrahiert die Details der Parallelisierung noch weiter, was die Ausführung parallelisierter applyFunktionen kinderleicht macht .
Sharpie

1
@ Sharpie aber beachten Sie, dass foreachseitdem verfügbar geworden ist und auf SO viel gefragt zu sein scheint.
Ari B. Friedman

1
@Shane, ganz oben in Ihrer Antwort verlinken Sie auf eine andere Frage als Beispiel für einen Fall, in dem lapply"etwas schneller" als eine forSchleife ist. Dort sehe ich jedoch nichts, was darauf hindeutet. Sie erwähnen nur, dass dies lapplyschneller ist als sapply, was aus anderen Gründen eine bekannte Tatsache ist ( sapplyversucht, die Ausgabe zu vereinfachen und muss daher viele Datengrößenprüfungen und mögliche Konvertierungen durchführen). Nichts im Zusammenhang mit for. Vermisse ich etwas
Flodel

70

Manchmal kann die Beschleunigung erheblich sein, z. B. wenn Sie for-Schleifen verschachteln müssen, um den Durchschnitt basierend auf einer Gruppierung von mehr als einem Faktor zu erhalten. Hier haben Sie zwei Ansätze, mit denen Sie genau das gleiche Ergebnis erzielen:

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)

Beide ergeben genau das gleiche Ergebnis, nämlich eine 5 x 10-Matrix mit den Durchschnittswerten und benannten Zeilen und Spalten. Aber :

> 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 

Los geht's. Was habe ich gewonnen? ;-);


aah, so süß :-) Ich habe mich tatsächlich gefragt, ob jemals jemand auf meine ziemlich späte Antwort stoßen würde.
Joris Meys

1
Ich sortiere immer nach "aktiv". :) Ich bin mir nicht sicher, wie ich deine Antwort verallgemeinern soll. manchmal *applyist schneller. Aber ich denke, dass der wichtigere Punkt die Nebenwirkungen sind (aktualisierte meine Antwort mit einem Beispiel).
Shane

1
Ich denke, dass Apply besonders schneller ist, wenn Sie eine Funktion auf verschiedene Teilmengen anwenden möchten. Wenn es eine intelligente Anwendungslösung für eine verschachtelte Schleife gibt, wird die Anwendungslösung wahrscheinlich auch schneller sein. In den meisten Fällen gewinnt die Anwendung nicht viel an Geschwindigkeit, aber ich stimme den Nebenwirkungen definitiv zu.
Joris Meys

2
Dies ist ein wenig abseits des Themas, aber für dieses spezielle Beispiel data.tableist es noch schneller und ich denke "einfacher". 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
Dieser Vergleich ist absurd. tapplyist eine spezialisierte Funktion für eine bestimmte Aufgabe, das ist für Schleife , warum es schneller als ein. Es kann nicht das tun, was eine for-Schleife kann (während reguläre applykönnen). Sie vergleichen Äpfel mit Orangen.
Eddi

47

... und wie ich gerade anderswo geschrieben habe, ist vapply dein Freund! ... es ist wie sapply, aber Sie geben auch den Rückgabewerttyp an, wodurch es viel schneller wird.

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 

Update vom 1. Januar 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

Die ursprünglichen Ergebnisse scheinen nicht mehr wahr zu sein. forSchleifen sind auf meinem Windows 10 2-Core-Computer schneller. Ich habe dies mit 5e6Elementen gemacht - eine Schleife war 2,9 Sekunden gegenüber 3,1 Sekunden für vapply.
Cole

27

Ich habe an anderer Stelle geschrieben, dass ein Beispiel wie das von Shane den Leistungsunterschied zwischen den verschiedenen Arten der Schleifensyntax nicht wirklich betont, da die gesamte Zeit innerhalb der Funktion verbracht wird, anstatt die Schleife tatsächlich zu betonen. Darüber hinaus vergleicht der Code eine for-Schleife ohne Speicher unfair mit Funktionen der Apply-Familie, die einen Wert zurückgeben. Hier ist ein etwas anderes Beispiel, das den Punkt hervorhebt.

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 

Wenn Sie das Ergebnis speichern möchten, kann das Anwenden von Familienfunktionen viel mehr sein als syntaktischer Zucker.

(Die einfache Auflistung von z ist nur 0,2 s, daher ist das Lapply viel schneller. Das Initialisieren des z in der for-Schleife ist ziemlich schnell, da ich den Durchschnitt der letzten 5 von 6 Läufen so gebe, dass er sich außerhalb des Systems bewegt kaum etwas beeinflussen)

Eine weitere zu beachtende Sache ist jedoch, dass es einen weiteren Grund gibt, Familienfunktionen unabhängig von ihrer Leistung, Klarheit oder dem Fehlen von Nebenwirkungen anzuwenden. Eine forSchleife fördert normalerweise das Einfügen so viel wie möglich in die Schleife. Dies liegt daran, dass für jede Schleife Variablen eingerichtet werden müssen, um Informationen zu speichern (unter anderem mögliche Operationen). Apply-Anweisungen sind in der Regel voreingenommen. Oft möchten Sie mehrere Vorgänge mit Ihren Daten ausführen, von denen einige vektorisiert werden können, andere jedoch möglicherweise nicht. In R ist es im Gegensatz zu anderen Sprachen am besten, diese Operationen zu trennen und diejenigen auszuführen, die nicht in einer apply-Anweisung (oder einer vektorisierten Version der Funktion) vektorisiert sind, und diejenigen, die als echte Vektoroperationen vektorisiert sind. Dies beschleunigt die Leistung oft enorm.

Am Beispiel von Joris Meys, in dem er eine herkömmliche for-Schleife durch eine praktische R-Funktion ersetzt, können wir damit die Effizienz des Schreibens von Code auf eine R-freundlichere Weise für eine ähnliche Beschleunigung ohne die spezielle Funktion demonstrieren.

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

Dies ist viel schneller als die forSchleife und nur ein wenig langsamer als die eingebaute optimierte tapplyFunktion. Das liegt nicht daran, dass vapplyes so viel schneller ist als, forsondern daran, dass in jeder Iteration der Schleife nur eine Operation ausgeführt wird. In diesem Code wird alles andere vektorisiert. In der traditionellen forSchleife von Joris Meys treten in jeder Iteration viele (7?) Operationen auf, und es gibt einiges an Setup, nur damit es ausgeführt werden kann. Beachten Sie auch, wie viel kompakter dies ist als die forVersion.


4
Aber Shanes Beispiel ist realistisch, dass die meiste Zeit wird in der Regel in der Funktion verwendet, nicht in der Schleife.
Hadley

9
sprechen Sie für sich selbst ... :) ... Vielleicht ist Shane's in gewissem Sinne realistisch, aber in demselben Sinne ist die Analyse völlig nutzlos. Die Leute werden sich um die Geschwindigkeit des Iterationsmechanismus kümmern, wenn sie viele Iterationen durchführen müssen, sonst sind ihre Probleme sowieso woanders. Es gilt für jede Funktion. Wenn ich eine Sünde schreibe, die 0,001 Sekunden dauert, und jemand anderes eine Sünde schreibt, die 0,002 Sekunden dauert, wen interessiert das? Nun, sobald Sie ein paar von ihnen machen müssen, interessiert es Sie.
John

2
Auf einem 12-Kern-3-GHz-Intel-Xeon, 64-Bit, bekomme ich ganz andere Zahlen als Sie - die for-Schleife verbessert sich erheblich: Für Ihre drei Tests bekomme ich 2.798 0.003 2.803; 4.908 0.020 4.934; 1.498 0.025 1.528und vapply ist sogar noch besser:1.19 0.00 1.19
naught101

2
Es variiert je nach Betriebssystem und R-Version ... und im absoluten Sinne CPU. Ich bin gerade mit 2.15.2 auf dem Mac gelaufen und bin sapply50% langsamer als forund lapplydoppelt so schnell geworden.
John

1
In Ihrem Beispiel bedeuten Sie Satz yan 1:1e6, nicht numeric(1e6)(ein Vektor von Nullen). Der Versuch, immer wieder zuzuweisen foo(0), z[0]veranschaulicht eine typische forSchleifenverwendung nicht gut . Die Nachricht ist ansonsten genau richtig.
Flodel

3

Wenn Funktionen über Teilmengen eines Vektors tapplyangewendet werden , kann dies ziemlich schnell sein als eine for-Schleife. Beispiel:

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

applyIn den meisten Situationen wird jedoch keine Geschwindigkeitssteigerung erzielt, und in einigen Fällen kann dies sogar viel langsamer sein:

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

Aber für diese Situationen haben wir colSumsund 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
Es ist wichtig zu beachten, dass es (für kleine Codeteile) microbenchmarkviel genauer ist als system.time. Wenn Sie versuchen , zu vergleichen system.time(f3(mat))und system.time(f4(mat))Sie anderes Ergebnis fast jedes Mal zu bekommen. Manchmal kann nur ein richtiger Benchmark-Test die schnellste Funktion zeigen.
Michele
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.