Menu

apply, lapply, sapply trong R: giải thích họ hàm apply

Họ hàm apply, gồm apply, lapply, sapply, vapply, mapply và tapply, chạy một hàm qua mọi phần tử của dữ liệu. Đây là việc từng hàm làm và khi nào nên dùng.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

Họ hàm apply trong một câu

Mọi thành viên của họ hàm apply đều làm cùng một việc: nhận một hàm và chạy nó qua mọi phần tử của một cấu trúc nào đó, rồi gom kết quả lại. Điều khác nhau là hình dạng đầu vào và hình dạng đầu ra. Bản đồ như sau:

  • apply(m, MARGIN, FUN): các hàng hoặc cột của một ma trận.
  • lapply(x, FUN): từng phần tử của một list hoặc vector; luôn trả về list.
  • sapply(x, FUN): giống lapply, rồi đơn giản hóa thành vector hoặc ma trận khi có thể.
  • vapply(x, FUN, FUN.VALUE): sapply với kiểu trả về được khai báo và bắt buộc tuân theo.
  • mapply(FUN, x, y, ...): duyệt song song nhiều đầu vào, từng phần tử một.
  • tapply(values, groups, FUN): áp dụng FUN trong từng nhóm, mỗi nhóm một kết quả.

Tất cả đều nhận các hàm bạn tự viết, kể cả hàm ẩn danh, và đó là nơi sức mạnh của họ hàm này phát huy.

apply(): hàng và cột của ma trận

apply nhận một ma trận, một MARGIN (1 cho hàng, 2 cho cột) và một hàm:

Ma trận được điền theo từng cột, nên các hàng là 1 3 5 và 2 4 6: tổng theo hàng in ra 9 12 và tổng theo cột in ra 3 7 11. Với hai trường hợp phổ biến nhất, base R có sẵn các hàm chuyên dụng nhanh hơn là rowSums, colSums, rowMeans, colMeans, nên hãy để dành apply cho những hàm không có bản chuyên dụng, như apply(m, 2, max).

Một cái bẫy: apply trên data frame sẽ lặng lẽ chuyển nó thành ma trận trước, ép mọi cột về cùng một kiểu dữ liệu. Với data frame, hãy coi các cột như một list và dùng lapply/sapply.

lapply() luôn trả về list, sapply() thì đơn giản hóa

lapply ánh xạ một hàm qua từng phần tử và trả về một list cùng độ dài, trong mọi trường hợp:

Cùng phép tính, hai hình dạng: lapply trả về một list chứa 85 và 80; sapply nhận thấy mọi kết quả đều có độ dài 1 nên đơn giản hóa thành một vector số có tên, dễ đọc hơn nhiều và dễ đưa vào các phép tính tiếp theo.

Tuy vậy, sự tiện lợi này có một mặt sắc cạnh: kiểu trả về của sapply phụ thuộc vào dữ liệu. Chỉ cần một phần tử cho ra độ dài khác, việc đơn giản hóa thất bại và bạn lặng lẽ nhận lại một list:

Khi làm việc tương tác thì chẳng sao; nhưng trong một script, điều đó có nghĩa là code chạy tốt cả năm trời sẽ hỏng vào ngày dữ liệu đổi hình dạng. Đó chính là lý do vapply tồn tại.

vapply(): sapply an toàn về kiểu

vapply thêm đối số thứ ba, FUN.VALUE: một khuôn mẫu khai báo một kết quả phải trông như thế nào. integer(1) nghĩa là "mỗi lần gọi trả về đúng một số nguyên":

Bạn nhận được một vector số nguyên có tên, 5 6 6, và quan trọng hơn là một sự đảm bảo: nếu nchar có lúc trả về hai giá trị hay một chuỗi ký tự, vapply sẽ dừng với một lỗi ngay tại chỗ gọi thay vì để kết quả sai hình dạng trôi xuống các bước sau. Phần khai báo còn kiêm luôn vai trò tài liệu:

vapply(words, nchar, character(1))
# Error in vapply(words, nchar, character(1)) : values must be type 'character'

Quy tắc kinh nghiệm: sapply ở console, vapply trong các hàm và script phải chạy mà không có người trông chừng.

mapply() và tapply(): đầu vào song song và theo nhóm

lapply duyệt qua một cấu trúc. Khi mỗi lần gọi cần một phần tử từ nhiều cấu trúc ở cùng vị trí, hãy dùng mapply; lưu ý hàm được đặt trước:

Mỗi phần tử đầu ra ghép các giá trị thứ nhất, rồi các giá trị thứ hai, cứ thế tiếp tục: 10 200 3000 40000.

tapply là thành viên làm việc theo nhóm: nó chia values theo groups và áp dụng hàm trong từng nhóm, trả về mỗi nhóm một kết quả:

Nhóm a có trung bình 30, nhóm b có trung bình 40. Đây là câu trả lời của base R cho bài toán "trung bình theo từng loại", cùng dạng câu hỏi mà group_by + summarize trả lời cho data frame. Nếu bạn đã làm việc với dplyr thì hãy dùng cách đó; tapply tỏa sáng khi bạn có hai vector trần và muốn giải quyết trong một dòng.

Có hai tiện ích dùng được cho cả họ hàm. Các đối số thêm sau hàm sẽ được chuyển tiếp cho hàm đó ở mỗi lần gọi:

Và hàm ẩn danh có thể chen vào bất cứ khi nào không có hàm dựng sẵn nào phù hợp: sapply(x, \(v) max(v) - min(v)) tính khoảng biến thiên cho từng phần tử mà không cần đặt tên cho một hàm phụ trước.

Họ hàm apply và vòng lặp for

Bạn sẽ nghe nói rằng vòng lặp for trong R chậm còn họ hàm apply thì nhanh. Phần lớn đó là lầm tưởng. Thứ thực sự chậm là nối dài kết quả bên trong vòng lặp: result <- c(result, new_value) sao chép toàn bộ vector ở mỗi lần lặp. Một vòng lặp cấp phát trước đầu ra thì chạy tốt:

Vậy nên hãy chọn theo độ dễ đọc, không phải hiệu năng. Phiên bản apply nói cái gì trong một dòng, "bình phương từng phần tử", và lo việc cấp phát giúp bạn, đó là lý do nó là cách viết chuẩn cho các thao tác đơn giản trên từng phần tử. Vòng lặp for phát huy giá trị khi các lần lặp phụ thuộc vào kết quả trước đó, khi bạn cần thoát sớm bằng break, hoặc khi thân vòng lặp đủ dài để một lambda gây hại nhiều hơn có lợi. Cả hai đều là R chính đáng.

Điều cần nhớ

  • Cả họ hàm chỉ là một ý tưởng: chạy một hàm qua mọi phần tử, gom kết quả lại.
  • apply dành cho các hàng (MARGIN = 1) và cột (MARGIN = 2) của ma trận; hãy ưu tiên rowSums/colMeans khi chúng có sẵn.
  • lapply luôn trả về list; sapply đơn giản hóa khi có thể, nghĩa là kiểu trả về của nó có thể thay đổi theo dữ liệu.
  • vapply khóa chặt kiểu trả về; hãy dùng nó trong code không được phép gây bất ngờ.
  • mapply ghép nhiều đầu vào lại với nhau; tapply tổng hợp giá trị trong từng nhóm.
  • Vòng lặp for vốn không chậm, việc nối dài vector mới chậm. Hãy chọn cách viết dễ đọc nhất.

Tiếp theo: pipe, toán tử nối các lời gọi này thành những pipeline dễ đọc, từng bước một.

Câu hỏi thường gặp

lapply và sapply trong R khác nhau thế nào?

lapply luôn trả về một list, không có ngoại lệ. sapply chạy cùng phép tính rồi cố đơn giản hóa kết quả: thành vector nếu mọi phần tử có độ dài 1, thành ma trận nếu chúng có cùng độ dài, và quay về list nếu không làm được. sapply dễ chịu hơn khi làm việc tương tác; lapply (hoặc vapply) an toàn hơn trong script vì kiểu trả về không bao giờ thay đổi.

Hàm apply() trong R làm gì?

apply(m, MARGIN, FUN) chạy FUN trên một ma trận: MARGIN = 1 áp dụng cho từng hàng, MARGIN = 2 cho từng cột. apply(m, 1, sum) cho tổng theo hàng, apply(m, 2, mean) cho trung bình theo cột. Hàm này dành cho ma trận và mảng; với list và vector, hãy dùng lapply/sapply.

Họ hàm apply có nhanh hơn vòng lặp for trong R không?

Thường thì không nhanh hơn bao nhiêu: đó là một lầm tưởng. Một vòng lặp for viết tốt với vector kết quả được cấp phát trước có hiệu năng tương đương. Những vòng lặp làm for mang tiếng xấu là những vòng lặp nối thêm kết quả từng phần tử một, sao chép mọi thứ ở mỗi lần lặp. Hãy chọn họ hàm apply vì sự ngắn gọn và rõ ý, không phải vì tốc độ.

vapply trong R dùng để làm gì?

vapply là sapply kèm một cam kết: bạn khai báo kiểu và độ dài của từng kết quả, ví dụ vapply(x, nchar, integer(1)). Nếu hàm trả về bất cứ thứ gì khác, R báo lỗi ngay lập tức thay vì lặng lẽ đưa cho bạn một cấu trúc không mong đợi. Hãy ưu tiên nó trong code phải chạy ổn định mà không có người trông chừng.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu