Khóa học
Một ghi chú về Họ hàm Apply trong R
Họ hàm Apply trong R là tập các hàm cho phép bạn áp dụng một hàm lên các phần tử của vector, list hoặc matrix. Tuy nhiên, đây được xem là tính năng cũ và không nên dùng cho mã mới. Thay vào đó, khuyến nghị sử dụng gói purrr cho mọi thao tác lặp trong R. purrr cung cấp cú pháp thống nhất để làm việc với các hàm có nhiều đầu vào và đầu ra, giúp việc viết và đọc mã dễ dàng hơn. Ngoài ra, purrr còn có nhiều hàm được tối ưu cho môi trường tính toán hiện đại, nhanh và hiệu quả hơn so với họ Apply. Nhìn chung, dù họ Apply vẫn có thể hoạt động với một số trường hợp, nhưng tốt nhất bạn nên dùng purrr cho mọi dự án R mới.
Các hàm Apply như lựa chọn thay thế cho vòng lặp
Bài viết này sẽ hướng dẫn cách bạn có thể dùng hàm R apply(), các biến thể như mapply() và một vài họ hàng của apply(), áp dụng lên các cấu trúc dữ liệu khác nhau. Dĩ nhiên không thể bàn hết mọi biến thể, nhưng khi có thể, bạn sẽ được giới thiệu cách dùng các hàm này phối hợp với nhau qua vài ví dụ hơi “nặng ký” hơn.
Bạn cũng có thể thấy hữu ích khi xem qua khóa nhập môn R để hiểu rõ hơn về list, vector, array và dataframe, dù bạn không nhất thiết phải hoàn thành hướng dẫn đó để theo dõi bài viết này!
Họ apply()
Họ apply() thuộc gói base của R và bao gồm các hàm để thao tác các lát cắt dữ liệu từ matrix, array, list và dataframe theo cách lặp lại. Những hàm này cho phép “đi qua” dữ liệu theo nhiều cách và tránh phải dùng cấu trúc vòng lặp tường minh. Chúng nhận vào một list, matrix hoặc array và áp dụng một hàm đã đặt tên kèm theo một hoặc vài đối số tùy chọn.
Hàm được gọi có thể là:
- Một hàm gộp, như mean hoặc sum (trả về một số hay vô hướng);
- Các hàm biến đổi hoặc trích chọn khác; và
- Các hàm vector hóa khác, cho ra cấu trúc phức tạp hơn như list, vector, matrix và array.
Các hàm apply() là nền tảng cho các kết hợp phức tạp hơn và giúp thực hiện thao tác chỉ với rất ít dòng mã. Cụ thể hơn, họ này gồm các hàm apply(), lapply() , sapply(), vapply(), mapply(), rapply() và tapply().
Vậy dùng như thế nào và khi nào?
Điều này phụ thuộc vào cấu trúc dữ liệu bạn muốn thao tác và định dạng đầu ra bạn cần.
Cách dùng apply() trong R
Hãy bắt đầu với “bố già” của họ, apply(), hoạt động trên array. Để đơn giản, hướng dẫn này chỉ xét array 2 chiều, còn gọi là matrix.
Tài liệu base R cho biết cách gọi như sau: apply(X, MARGIN, FUN, ...)
trong đó:
Xlà một array hoặc matrix nếu array có số chiều là 2;MARGINlà biến xác định cách áp dụng hàm: khiMARGIN=1là theo hàng, cònMARGIN=2là theo cột. Lưu ý khi dùngMARGIN=c(1,2)thì áp dụng cho cả hàng và cột; vàFUNlà hàm bạn muốn áp dụng lên dữ liệu. Có thể là bất kỳ hàm R nào, kể cả hàm do người dùng định nghĩa (UDF).
Người mới có thể khó hình dung chuyện gì đang diễn ra, nên một hình minh họa và ít mã sẽ hữu ích.
Hãy tạo một matrix 5 x 6 và giả sử bạn muốn tính tổng các giá trị của từng cột.
Bạn có thể viết như sau:
# Construct a 5 x 6 matrix
my_matrix <- matrix(1:30, nrow = 5, ncol = 6)
# Calculate the sum of each column
col_sums <- apply(my_matrix, 2, sum)
# Print the result
print(col_sums)
Hàm matrix() tạo một matrix 5 x 6 với các giá trị từ 1 đến 30. Sau đó ta dùng hàm apply() để áp dụng hàm sum() lên từng cột của matrix (2 chỉ định áp dụng theo cột). Vector kết quả chứa tổng theo cột được lưu trong biến col_sums và in ra console.
Hãy nhớ trong R, một matrix có thể được xem là tập các vector hàng khi bạn đi từ trên xuống (theo chiều dọc 1, tức margin 1), hoặc là danh sách các vector cột khi bạn đi từ trái sang phải theo margin 2.
Nghĩa là câu lệnh bạn vừa nhập, như minh họa ở hình 1, có thể diễn đạt là: “áp dụng hàm ‘sum’ lên matrix X theo margin 2 (theo cột), cộng các giá trị của từng cột”.
Lưu ý để tránh rối hình, chỉ một cột được tô đậm.
Bạn thu được một vector hàng chứa tổng giá trị của mỗi cột.
Đầu ra ở trên, một vector hàng, cũng sẽ có được nếu bạn cộng theo từng hàng của matrix. Đây là cách R hiển thị kết quả.
Một lưu ý cho phần sau: trong đa số trường hợp, R có thể trả về một giá trị ngay cả khi bạn không chỉ định, hay nói chính xác hơn là không gán giá trị trả về của hàm cho biến nào. R đơn giản trả về đối tượng được đánh giá cuối cùng. Tuy nhiên, khi bạn muốn kiểm tra giá trị trả về hoặc cần thực hiện các thao tác tiếp theo lên chúng, tốt nhất là gán tường minh kết quả của hàm vào một biến.
Hàm lapply()
Bạn muốn áp dụng một hàm cho từng phần tử của một list và nhận về một list. Khi chạy ?lapply, bạn sẽ thấy cú pháp giống với apply().
Điểm khác là:
- Có thể dùng cho các đối tượng khác như dataframe, list hoặc vector; và
- Đầu ra là một list (chữ “l” trong tên hàm), có số phần tử bằng với đối tượng đầu vào.
Để xem hoạt động thế nào, hãy tạo vài matrix và trích từ mỗi matrix một cột nhất định.
Đây là thao tác khá phổ biến trên dữ liệu thực khi so sánh hoặc gộp từ các dataframe khác nhau.
Ví dụ minh họa trong hình 2 có thể mã hóa như sau:

Thao tác được minh họa ở phần bên trái của hình 2.
Một lần nữa, bạn bắt đầu bằng cách chỉ định đối tượng quan tâm, list Mylist. Bạn dùng toán tử chọn tiêu chuẩn của R là [ và bỏ trống tham số đầu (tương đương “bất kỳ”, nên bạn thấy hai dấu phẩy).
Tiếp theo, bạn chỉ định tham số thứ hai là 2: margin của chúng ta là ‘cột’. Vậy là bạn trích cột thứ hai từ tất cả các matrix trong list.
Một vài lưu ý về đoạn mã trên:
- Ký hiệu
[là toán tử chọn. Nhớ rằng, ví dụ, để trích tất cả phần tử ở hàng thứ ba của B cần:B[3,]; - Ký hiệu
[[ ]]biểu thị bạn đang làm việc với list: [[2]] nghĩa là phần tử thứ hai của list. Điều này cũng thể hiện ở đầu ra R trả về; - Đầu ra là một list có số phần tử bằng với đầu vào; và
- Lưu ý bạn cũng có thể trích một phần tử đơn từ mỗi matrix như sau:
lapply(MyList,"[", 1, 2)
Bên phải hình 2 là một cách trích khác: lần này bạn bỏ trống tham số đầu và lấy hàng đầu tiên từ mỗi matrix.
Hãy tự thử nhé! Chọn cột thứ hai từ mỗi matrix trong list:
Hàm sapply()
Hàm sapply() hoạt động như lapply(), nhưng cố gắng đơn giản hóa đầu ra về cấu trúc dữ liệu cơ bản nhất có thể. Thực ra, sapply() là một hàm “bao” (wrapper) cho lapply().
Một ví dụ sẽ giúp dễ hiểu hơn: giả sử bạn muốn lặp lại thao tác trích một phần tử như ví dụ trước, nhưng giờ lấy phần tử đầu của hàng thứ hai (chỉ số 2 và 1) cho mỗi matrix.
Dùng lapply() sẽ trả về một list, trừ khi bạn truyền simplify=FALSE làm tham số cho sapply(). Khi đó, một list sẽ được trả về. Xem cách hoạt động trong đoạn mã dưới đây:
Ngược lại, một hàm như unlist() có thể bảo lappy() trả về một vector!
Dù sao, để tránh nhầm lẫn, tốt nhất nên dùng những hàm này theo “định dạng gốc” của chúng và tránh chuyển đổi trừ khi thật cần thiết.
Hàm rep()
Một thứ thường dùng cùng với các hàm apply() là rep(). Khi áp dụng lên một vector hoặc factor x, hàm này nhân bản các giá trị của nó một số lần được chỉ định.
Hãy dùng một trong các vector mà bạn đã tạo ở trên bằng lapply() trong MyList.
Lần này, bạn chỉ chọn phần tử ở hàng đầu và cột đầu từ mỗi phần tử của list MyList (và dùng sapply() để lấy một vector):
Bạn thấy đoạn mã trên lặp lại các giá trị của Z một số lần như c(3,1,2) quy định: ba lần phần tử thứ nhất, một lần phần tử thứ hai và hai lần phần tử thứ ba:
Tiện lợi chứ?
Hàm mapply()
Hàm mapply() là viết tắt của ‘multivariate’ apply. Mục đích là vector hóa các đối số cho một hàm vốn dĩ không chấp nhận vector làm đối số.
Nói ngắn gọn, mapply() áp dụng một hàm cho nhiều đối số dạng List hoặc nhiều Vector.
Hãy xem một ví dụ mapply() nơi bạn tạo một matrix 4 x 4 bằng cách gọi lặp hàm rep():
Nhưng bạn sẽ thấy có cách hiệu quả hơn để “ghép” kết quả của hàm rep() thay vì dùng c(): khi gọi mapply(), bạn vector hóa hành động của hàm rep().
Các hàm liên quan đến apply()
Các hàm có cấu trúc tương tự đôi khi được dùng cùng với những phần tử trong họ apply(): hướng dẫn này chỉ điểm qua một vài hàm.
Hàm Sweep()
Hàm sweep() có lẽ là gần gũi nhất với họ apply(). Bạn dùng khi muốn lặp lại các phép toán khác nhau trên các phần tử MARGIN bạn đã chọn (giới hạn ở trường hợp matrix).
Một kịch bản điển hình là trong phân cụm, nơi bạn cần lặp đi lặp lại việc tạo dữ liệu chuẩn hóa và trung tâm hóa hay “chuẩn hóa z”.
Điều này nghĩa là gì?
Giả sử bạn có một số điểm dữ liệu trong một nhóm dữ liệu. Trước tiên bạn tìm tâm của dữ liệu (“trọng tâm”) và xem dữ liệu phân tán thế nào quanh tâm này. Hai đại lượng cơ bản cho bạn thông tin đó: mean và độ lệch chuẩn.
Giả sử các điểm dữ liệu là các vector cột trong một matrix dữ liệu, và dùng matrix B đã tạo ở đầu bài, giờ đặt tên là dataPoints.
Trước hết, bạn tìm mean theo cột và độ phân tán hoặc độ lệch chuẩn bằng một trong các hàm apply(). Sau đó bạn tịnh tiến tất cả các điểm về quanh tâm của chúng. Nghĩa là mean mà bạn vừa tìm sẽ được dùng để chuẩn hóa dữ liệu theo độ lệch chuẩn của chúng:
Bạn đã tạo ra các điểm đã được “trung tâm hóa” chỉ với một lần gọi sweep(). Hàm này nhận các phần tử sau:
- một array đầu vào, ở đây là matrix;
- MARGIN,
2để chỉ các cột; - một thống kê tóm tắt (ở đây là
mean); và - một phép toán cần áp dụng. Bạn dùng toán tử số học
“-”để trừ.
Nghĩa là: “lấy các phần tử theo cột của bộ dữ liệu MyPoints, và trừ đi mean, dataPoints_means, khỏi mỗi phần tử”.
Giờ bạn gọi sweep() lần nữa để chia tất cả giá trị vừa thu được cho độ lệch chuẩn tương ứng. Bước này gọi là “chuẩn hóa”. Lần nữa, bạn chọn MARGIN=2, rồi truyền vector độ lệch chuẩn dataPoints_sdev làm toán hạng. Tiếp theo truyền toán tử ‘chia cho’ “/”.
Bạn đang yêu cầu R: “lấy các phần tử theo cột của đối tượng mới vừa tạo, dataPoints_Trans1, và chia chúng (“/”) cho độ lệch chuẩn dataPoints_sdev”.
Dĩ nhiên bạn có thể đạt kết quả tương tự nhanh gọn hơn (như thường thấy trong R!) và không cần đặt nhiều tên khác nhau, chỉ với một dòng mã chứa lời gọi lồng nhau tới sweep():
Về mặt thống kê, bạn vừa tạo ra dữ liệu chuẩn hóa, là nền tảng cho nhiều quy trình phân tích nâng cao (như giảm chiều bằng PCA, phân tích tín hiệu, v.v.).
Hàm aggregate()
Hàm này nằm trong gói stats, bạn dùng như sau: aggregate(x, by, FUN, ..., simplify = TRUE).
Nói cách khác, nó hoạt động tương tự apply(): bạn chỉ định đối tượng, hàm và cho biết có muốn đơn giản hóa hay không, giống sapply(). Khác biệt quan trọng là mệnh đề by, thiết lập biến hoặc cột dataframe theo đó bạn muốn thực hiện phép gộp.
Phần tiếp theo sẽ minh họa cách hoạt động.
Ví dụ về aggregate()
Xét bộ dữ liệu ví dụ tên Mydf, chứa dữ liệu về doanh số một sản phẩm, trong đó một số giá trị ở cột biến DepPC lặp lại.
Biến này phân loại dữ liệu theo vị trí địa lý, như phần của mã bưu chính (ở đây các số tương ứng với các quận của Île-de-France, vùng bao gồm Paris).
Bạn muốn làm một số thống kê trên các cột doanh số. Đó là DProgr, một số thứ tự tăng dần theo thời gian, và doanh số sản phẩm (số lượng Qty), cùng một biến logic Delivered, cho biết sản phẩm đã được giao (T) hay chưa (F).
Đầu tiên, bạn có thể làm vài thao tác đơn giản để làm quen với dữ liệu, ngoài việc in toàn bộ bằng cách gõ tên của nó (ở đây chỉ có 120 bản ghi, nhưng hãy hình dung với tệp thực có hàng nghìn dòng!).
Khám phá dữ liệu:
Lưu ý nếu muốn xem số hàng và cột mà dataframe có, bạn cũng có thể gọi nrow(Mydf) và ncol(Mydf).
Còn rất nhiều cách thăm dò dữ liệu khác.
Ở đây, bạn quan tâm biết sản phẩm bán chạy nhất ở quận nào chẳng hạn. Vì vậy bạn nên nhóm dữ liệu theo quận, cộng dồn doanh số Qty cho mỗi quận DepPC bằng hàm aggregate():
Vậy, aggregate() bảo R rằng bạn muốn cộng tất cả Qty thuộc cùng một quận.
Lưu ý R gán tổng vào biến ‘x’ vì bạn chưa chỉ định khác.
Đầu ra khá dễ đọc, nhưng nếu số quận lớn, có thể sẽ kém trực quan. Khi đó, bạn có thể dùng đồ thị: vẽ kết quả bằng một hệ thống đồ họa của R kết hợp với aggregate():
Biểu đồ này cho bạn doanh số theo từng quận.
Bạn có thể hỏi cùng câu hỏi nhưng chỉ cho các hàng đã giao. Để làm vậy, trước tiên bạn lấy tập con dữ liệu với Delivered là true (T) bằng toán tử trích chọn quen thuộc "[".
Lưu ý ở đây bạn gán kết quả vào biến mới Y, là một dataframe mới thừa hưởng cùng tên cột từ dataframe gốc Mydf. Làm vậy để tránh lặp lại câu lệnh aggregate trong lúc gọi hàm vẽ cho dễ đọc:
Vậy là bạn có thể đặt nhiều câu hỏi khác nhau cho dữ liệu theo cách vector hóa như với aggregate(), và thường làm cùng với một hệ thống vẽ tiện lợi như ggplot2, ý tưởng là như vậy.
Lưu ý rằng để có được điều này, bạn chỉ cần rất ít dòng mã.
Vector hóa như một lựa chọn thay thế cho vòng lặp và các hàm Apply?
Bạn đã thấy vài biến tấu trên cùng một chủ đề: “thao tác trên một tập dữ liệu có cấu trúc theo cách lặp lại”. Theo nghĩa này, các hàm này không chỉ là lựa chọn thay thế cho vòng lặp mà còn là một dạng thực hiện theo kiểu vector hóa.
“Vector hóa” được hiểu theo nghĩa rộng, chúng ta sẽ không bàn luận xem các hàm apply() có thực sự vector hóa hay không (xem ví dụ thảo luận tại đây).
Trong thực tế, để chọn dùng hàm apply() nào, bạn cần cân nhắc:
- Kiểu dữ liệu đầu vào: đối tượng bạn sẽ thao tác (vector, matrix, array…, list, data frame hoặc kết hợp)
- Bạn định làm gì: hàm
FUNbạn muốn truyền vào - Tập con của dữ liệu: hàng, cột, hay tất cả?
- Bạn muốn kiểu dữ liệu đầu ra nào? Vì có thể bạn còn thao tác tiếp trên đó (và bạn muốn đối tượng mới hay muốn biến đổi trực tiếp đối tượng đầu vào?)
Đây là những câu hỏi khá tổng quát mà bạn cũng có thể đặt ra cho các hàm liên quan như aggregate(), by(), sweep(), v.v.
Nhưng còn nhiều nữa! Đừng dừng khám phá bây giờ! Hãy xem hướng dẫn Arrays in R của DataCamp.
Tiếp nối hướng dẫn này, hãy cân nhắc tham gia các khóa Introduction to R hoặc Intermediate R của DataCamp.
