Antworten:
Die apply
Funktionen in R bieten keine verbesserte Leistung gegenüber anderen Schleifenfunktionen (z for
. B. ). Eine Ausnahme ist lapply
die, 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 assign
oder ü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 apply
Familienfunktionen (selbst das foreach
Paket ist trotz des Namens im Wesentlichen gleichwertig). Hier ist ein einfaches Beispiel für die sapply
Funktion 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 ). snow
hat 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, apply
Funktionen für die parallele Ausführung zu verwenden, da sie keine Nebenwirkungen haben . Wenn Sie einen Variablenwert innerhalb einer for
Schleife ändern , wird er global festgelegt. Auf der anderen Seite können alle apply
Funktionen sicher parallel verwendet werden, da Änderungen lokal für den Funktionsaufruf sind (es sei denn, Sie versuchen assign
oder 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 for
und *apply
in 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 df
in der übergeordneten Umgebung von geändert wird, for
aber nicht *apply
.
snowfall
Paket anzusehen und die Beispiele in ihrer Vignette auszuprobieren. snowfall
baut auf dem snow
Paket auf und abstrahiert die Details der Parallelisierung noch weiter, was die Ausführung parallelisierter apply
Funktionen kinderleicht macht .
foreach
seitdem verfügbar geworden ist und auf SO viel gefragt zu sein scheint.
lapply
"etwas schneller" als eine for
Schleife ist. Dort sehe ich jedoch nichts, was darauf hindeutet. Sie erwähnen nur, dass dies lapply
schneller ist als sapply
, was aus anderen Gründen eine bekannte Tatsache ist ( sapply
versucht, 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
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? ;-);
*apply
ist schneller. Aber ich denke, dass der wichtigere Punkt die Nebenwirkungen sind (aktualisierte meine Antwort mit einem Beispiel).
data.table
ist 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")])
tapply
ist 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 apply
können). Sie vergleichen Äpfel mit Orangen.
... 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
for
Schleifen sind auf meinem Windows 10 2-Core-Computer schneller. Ich habe dies mit 5e6
Elementen gemacht - eine Schleife war 2,9 Sekunden gegenüber 3,1 Sekunden für vapply
.
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 for
Schleife 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 for
Schleife und nur ein wenig langsamer als die eingebaute optimierte tapply
Funktion. Das liegt nicht daran, dass vapply
es so viel schneller ist als, for
sondern daran, dass in jeder Iteration der Schleife nur eine Operation ausgeführt wird. In diesem Code wird alles andere vektorisiert. In der traditionellen for
Schleife 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 for
Version.
2.798 0.003 2.803; 4.908 0.020 4.934; 1.498 0.025 1.528
und vapply ist sogar noch besser:1.19 0.00 1.19
sapply
50% langsamer als for
und lapply
doppelt so schnell geworden.
y
an 1:1e6
, nicht numeric(1e6)
(ein Vektor von Nullen). Der Versuch, immer wieder zuzuweisen foo(0)
, z[0]
veranschaulicht eine typische for
Schleifenverwendung nicht gut . Die Nachricht ist ansonsten genau richtig.
Wenn Funktionen über Teilmengen eines Vektors tapply
angewendet 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
apply
In 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 colSums
und 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
microbenchmark
viel 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.
apply
Funktionsfamilie. Die Strukturierung von Programmen, die sie verwenden, ermöglicht daher die Parallelisierung zu sehr geringen Grenzkosten.