Câu trả lời:
Các apply
hàm trong R không cung cấp hiệu suất được cải thiện so với các hàm lặp khác (ví dụ for
). Một ngoại lệ cho điều này là lapply
có thể nhanh hơn một chút vì nó hoạt động nhiều hơn trong mã C so với R (xem câu hỏi này để biết ví dụ về điều này ).
Nhưng nói chung, quy tắc là bạn nên sử dụng một hàm áp dụng cho rõ ràng, không phải cho hiệu suất .
Tôi sẽ thêm vào điều này rằng các hàm áp dụng không có tác dụng phụ , đó là một điểm khác biệt quan trọng khi nói đến lập trình chức năng với R. Điều này có thể được ghi đè bằng cách sử dụng assign
hoặc <<-
, nhưng điều đó có thể rất nguy hiểm. Tác dụng phụ cũng làm cho chương trình khó hiểu hơn vì trạng thái của biến phụ thuộc vào lịch sử.
Biên tập:
Chỉ cần nhấn mạnh điều này với một ví dụ tầm thường tính toán đệ quy chuỗi Fibonacci; điều này có thể được chạy nhiều lần để có được số đo chính xác, nhưng vấn đề là không có phương pháp nào có hiệu suất khác nhau đáng kể:
> 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
Chỉnh sửa 2:
Về việc sử dụng các gói song song cho R (ví dụ: rpvm, rmpi, snow), chúng thường cung cấp các apply
chức năng gia đình (ngay cả foreach
gói về cơ bản là tương đương, mặc dù tên). Đây là một ví dụ đơn giản về sapply
hàm trong snow
:
library(snow)
cl <- makeSOCKcluster(c("localhost","localhost"))
parSapply(cl, 1:20, get("+"), 3)
Ví dụ này sử dụng cụm socket, không cần cài đặt thêm phần mềm nào; nếu không, bạn sẽ cần một cái gì đó như PVM hoặc MPI (xem trang phân cụm của Tierney ). snow
có các chức năng áp dụng sau:
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, ...)
Nó có ý nghĩa rằng các apply
chức năng nên được sử dụng để thực hiện song song vì chúng không có tác dụng phụ . Khi bạn thay đổi một giá trị biến trong một for
vòng lặp, nó được đặt trên toàn cầu. Mặt khác, tất cả các apply
chức năng có thể được sử dụng song song một cách an toàn vì các thay đổi là cục bộ đối với lệnh gọi chức năng (trừ khi bạn cố gắng sử dụng assign
hoặc <<-
, trong trường hợp đó bạn có thể đưa ra các tác dụng phụ). Không cần phải nói, điều quan trọng là phải cẩn thận về các biến cục bộ và biến toàn cục, đặc biệt là khi xử lý thực thi song song.
Biên tập:
Đây là một ví dụ tầm thường để chứng minh sự khác biệt giữa for
và *apply
cho đến khi có liên quan đến tác dụng phụ:
> 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
Lưu ý làm thế nào df
trong môi trường cha mẹ bị thay đổi bởi for
nhưng không *apply
.
snowfall
gói và thử các ví dụ trong họa tiết của họ. snowfall
xây dựng trên đầu snow
gói và trừu tượng hóa các chi tiết song song thậm chí còn khiến việc thực hiện các apply
hàm song song trở nên đơn giản .
foreach
đã trở nên có sẵn và dường như được hỏi nhiều về SO.
lapply
"nhanh hơn một chút" so với for
vòng lặp. Tuy nhiên, ở đó, tôi không thấy bất cứ điều gì gợi ý như vậy. Bạn chỉ đề cập rằng lapply
nhanh hơn sapply
, đó là một thực tế nổi tiếng vì những lý do khác ( sapply
cố gắng đơn giản hóa đầu ra và do đó phải thực hiện nhiều kiểm tra kích thước dữ liệu và chuyển đổi tiềm năng). Không có gì liên quan đến for
. Tui bỏ lỡ điều gì vậy?
Đôi khi tăng tốc có thể là đáng kể, như khi bạn phải lồng các vòng lặp để lấy trung bình dựa trên một nhóm nhiều hơn một yếu tố. Ở đây bạn có hai cách tiếp cận cho bạn kết quả chính xác như nhau:
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)
Cả hai đều cho kết quả chính xác như nhau, là một ma trận 5 x 10 với mức trung bình và các hàng và cột được đặt tên. Nhưng :
> 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
Có bạn đi. Tôi đã giành được gì? ;-)
*apply
nhanh hơn Nhưng tôi nghĩ rằng điểm quan trọng hơn là tác dụng phụ (cập nhật câu trả lời của tôi với một ví dụ).
data.table
thậm chí còn nhanh hơn và tôi nghĩ "dễ dàng hơn". 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
là một chức năng chuyên biệt cho một nhiệm vụ cụ thể, đó là lý do tại sao nó nhanh hơn vòng lặp for. Nó không thể làm những gì một vòng lặp for có thể làm (trong khi thông thường apply
có thể). Bạn đang so sánh táo với cam.
... Và như tôi vừa viết ở nơi khác, vapply là bạn của bạn! ... nó giống như sapply, nhưng bạn cũng chỉ định loại giá trị trả về làm cho nó nhanh hơn nhiều.
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
Cập nhật ngày 1 tháng 1 năm 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
các vòng lặp nhanh hơn trên máy tính Windows 10, 2 lõi của tôi. Tôi đã làm điều này với 5e6
các yếu tố - một vòng lặp là 2,9 giây so với 3,1 giây vapply
.
Tôi đã viết ở một nơi khác rằng một ví dụ như Shane không thực sự nhấn mạnh sự khác biệt về hiệu suất giữa các loại cú pháp vòng lặp khác nhau bởi vì thời gian được sử dụng hết trong hàm thay vì thực sự nhấn mạnh vào vòng lặp. Hơn nữa, mã không công bằng so sánh một vòng lặp for không có bộ nhớ với các hàm gia đình áp dụng trả về một giá trị. Đây là một ví dụ hơi khác nhau nhấn mạnh điểm này.
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
Nếu bạn có kế hoạch lưu kết quả thì áp dụng các chức năng gia đình có thể nhiều hơn so với đường cú pháp.
(danh sách đơn giản của z chỉ là 0,2 giây nên tốc độ nhanh hơn rất nhiều. Khởi tạo z trong vòng lặp for khá nhanh vì tôi cho trung bình 5 trên 6 lần chạy cuối cùng để di chuyển ra ngoài hệ thống. hầu như không ảnh hưởng đến mọi thứ)
Một điều nữa cần lưu ý là có một lý do khác để sử dụng các chức năng gia đình áp dụng độc lập với hiệu suất, sự rõ ràng hoặc thiếu tác dụng phụ của chúng. Một for
vòng lặp thường khuyến khích đặt càng nhiều càng tốt trong vòng lặp. Điều này là do mỗi vòng lặp yêu cầu thiết lập các biến để lưu trữ thông tin (trong số các hoạt động có thể khác). Áp dụng các tuyên bố có xu hướng thiên vị theo cách khác. Thông thường, bạn muốn thực hiện nhiều thao tác trên dữ liệu của mình, một số thao tác có thể được vector hóa nhưng một số có thể không thực hiện được. Trong R, không giống như các ngôn ngữ khác, cách tốt nhất là tách các hoạt động đó ra và chạy các hoạt động không được vector hóa trong một câu lệnh áp dụng (hoặc phiên bản được vector hóa của hàm) và các hoạt động được vector hóa như các hoạt động vectơ thực sự. Điều này thường tăng tốc hiệu suất rất nhiều.
Lấy ví dụ về Joris Meys khi anh ta thay thế một vòng lặp truyền thống bằng hàm R tiện dụng, chúng ta có thể sử dụng nó để hiển thị hiệu quả của việc viết mã theo cách thân thiện hơn R để tăng tốc tương tự mà không cần chức năng chuyên biệt.
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
Điều này kết thúc nhanh hơn nhiều so với for
vòng lặp và chỉ chậm hơn một chút so với tapply
chức năng tối ưu hóa tích hợp. Không phải vì vapply
nó nhanh hơn nhiều for
mà bởi vì nó chỉ thực hiện một thao tác trong mỗi lần lặp của vòng lặp. Trong mã này, mọi thứ khác được vector hóa. Trong for
vòng lặp truyền thống của Joris Meys, nhiều thao tác (7?) Đang diễn ra trong mỗi lần lặp và có khá nhiều thiết lập chỉ để thực hiện. Cũng lưu ý rằng nó nhỏ gọn hơn bao nhiêu so với for
phiên bản.
2.798 0.003 2.803; 4.908 0.020 4.934; 1.498 0.025 1.528
, và vapply thậm chí còn tốt hơn:1.19 0.00 1.19
sapply
chậm hơn 50% so với for
và lapply
nhanh gấp đôi.
y
thành 1:1e6
, không numeric(1e6)
(một vectơ số 0). Đang cố gắng để phân bổ foo(0)
để z[0]
lặp đi lặp không minh họa cũng là một điển hình for
sử dụng vòng lặp. Thông điệp là tại chỗ trên.
Khi áp dụng các hàm trên các tập con của một vectơ, tapply
có thể nhanh hơn vòng lặp for. Thí dụ:
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
tuy nhiên, trong hầu hết các tình huống không cung cấp bất kỳ sự tăng tốc nào và trong một số trường hợp có thể chậm hơn rất nhiều:
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
Nhưng đối với những tình huống này, chúng tôi đã có colSums
và 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
nó chính xác hơn nhiều system.time
. Nếu bạn cố gắng so sánh system.time(f3(mat))
và system.time(f4(mat))
bạn sẽ nhận được kết quả khác nhau gần như mỗi lần. Đôi khi chỉ có một bài kiểm tra điểm chuẩn thích hợp là có thể hiển thị chức năng nhanh nhất.
apply
họ các hàm. Do đó, các chương trình cấu trúc để họ sử dụng áp dụng cho phép chúng được song song hóa với chi phí biên rất nhỏ.