Câu trả lời:
Các applyhà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à lapplycó 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 assignhoặ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 applychức năng gia đình (ngay cả foreachgói về cơ bản là tương đương, mặc dù tên). Đây là một ví dụ đơn giản về sapplyhà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 ). snowcó 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 applychứ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 forvòng lặp, nó được đặt trên toàn cầu. Mặt khác, tất cả các applychứ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 assignhoặ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 forvà *applycho đế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 dftrong môi trường cha mẹ bị thay đổi bởi fornhưng không *apply.
snowfallgói và thử các ví dụ trong họa tiết của họ. snowfallxây dựng trên đầu snowgó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 applyhà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 forvò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 lapplynhanh hơn sapply, đó là một thực tế nổi tiếng vì những lý do khác ( sapplycố 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ì? ;-)
*applynhanh 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.tablethậ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")])
tapplylà 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 applycó 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
forcá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 5e6cá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 forvò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 forvòng lặp và chỉ chậm hơn một chút so với tapplychức năng tối ưu hóa tích hợp. Không phải vì vapplynó nhanh hơn nhiều formà 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 forvò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 forphiê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
sapplychậm hơn 50% so với forvà lapplynhanh gấp đôi.
ythà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 forsử 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ơ, tapplycó 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
applytuy 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ó colSumsvà 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
microbenchmarknó 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.
applyhọ 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ỏ.