Chuyển đến nội dung chính

Hướng dẫn về vòng lặp trong R - Cách dùng và lựa chọn thay thế

Hướng dẫn về vòng lặp trong R xem xét các cấu trúc vòng lặp sẵn có trong R. Khám phá các lựa chọn thay thế bằng tính năng vector hóa của R.
Đã cập nhật 5 thg 10, 2026  · 15 phút đọc

Khám phá cùng AI

ChatGPTClaudePerplexity

Hướng dẫn vòng lặp trong R này sẽ xem xét các cấu trúc vòng lặp sẵn có trong R, khi nào nên dùng chúng, và cách tận dụng các lựa chọn thay thế như tính năng vector hóa của R để thực hiện tác vụ lặp hiệu quả hơn.

Bài viết sẽ đưa ra một vài ví dụ về vòng lặp, sau đó phê bình và loại bỏ chúng để ủng hộ các lựa chọn thay thế dạng vector hóa phổ biến trong số rất nhiều phương án có trong hệ sinh thái thư viện phong phú của R.

Nói chung, lời khuyên của hướng dẫn này về vòng lặp trong R là: hãy học về vòng lặp. Chúng cho bạn cái nhìn chi tiết về những gì sẽ xảy ra ở mức cơ sở, đồng thời giúp bạn hiểu dữ liệu mà bạn đang thao tác.

Và sau khi bạn đã hiểu rõ về vòng lặp, hãy loại bỏ chúng.

Hãy đầu tư công sức để học về các lựa chọn thay thế dạng vector. Chúng mang lại lợi ích về hiệu năng.

Để luyện tập tương tác, hãy thử chương về vòng lặp trong khóa học R trung cấp của chúng tôi.

Vòng lặp là gì?

“Lặp”, “xoay vòng”, “lặp lại” hay chỉ đơn giản là nhân bản chỉ thị là thực hành có từ trước khi máy tính ra đời. Đó chỉ là việc tự động hóa một quy trình nhiều bước bằng cách tổ chức chuỗi hành động hay các quy trình “theo lô”, và gom nhóm các phần cần lặp lại.

Tất cả ngôn ngữ lập trình hiện đại đều cung cấp các cấu trúc đặc biệt cho phép lặp lại chỉ thị hoặc khối chỉ thị.

Nói rộng ra, có hai loại cấu trúc đặc biệt hay vòng lặp trong các ngôn ngữ hiện đại. Một số vòng lặp chạy một số lần định trước, được điều khiển bởi bộ đếm hoặc chỉ số, được tăng ở mỗi chu kỳ lặp. Đây là họ vòng lặp for.

Mặt khác, một số vòng lặp dựa trên sự khởi phát và kiểm tra của một điều kiện logic. Điều kiện được kiểm tra ở đầu hoặc cuối cấu trúc vòng lặp. Các biến thể này thuộc họ vòng lặp while hoặc repeat tương ứng.

Giới thiệu về vòng lặp trong R

Theo tài liệu R base, trong các lệnh điều khiển luồng, các cấu trúc vòng lặp gồm for, while và repeat, cùng với các mệnh đề bổ sung break và next.

Hãy nhớ rằng lệnh điều khiển luồng là các lệnh cho phép chương trình rẽ nhánh giữa các lựa chọn, hay “đưa ra quyết định”.

Bạn luôn có thể xem các lệnh điều khiển luồng này bằng cách gọi ?Control tại dòng lệnh RStudio.

?Control

Lưu đồ này cho thấy các cấu trúc vòng lặp trong R:

flow chart r loop structures

Vòng lặp For trong R

Các phần tiếp theo sẽ xem kỹ từng cấu trúc trong hình trên. Chúng ta sẽ bắt đầu với cấu trúc bên trái, và tiếp tục dần sang các cấu trúc bên phải.

Để xem video giới thiệu về for và bài tập theo sau, hãy thử chương For Loop trong khóa R trung cấp của chúng tôi.

Giải thích vòng lặp For

Cấu trúc vòng lặp này, gồm hộp chữ nhật ‘init’ (khởi tạo), hình thoi quyết định, và hộp chữ nhật i1, được thực thi một số lần đã biết.

Trong ngôn ngữ lưu đồ, hộp chữ nhật có nghĩa là “thực hiện một việc không bao hàm quyết định”. Ngược lại, hình thoi là “biểu tượng quyết định” và thể hiện câu hỏi chỉ có hai câu trả lời logic: Đúng (T) hoặc Sai (F).

Lưu ý để đơn giản hóa, các ký hiệu khác có thể có đã được lược bỏ khỏi hình.

Một hoặc nhiều chỉ thị trong khung khởi tạo được theo sau bởi việc đánh giá điều kiện trên một biến có thể nhận giá trị trong một dãy xác định. Trên hình, điều này được biểu diễn bằng hình thoi: các ký hiệu nghĩa là “giá trị hiện tại của biến v có thuộc dãy seq không?”.

Nói cách khác, bạn đang kiểm tra liệu giá trị hiện tại của v có nằm trong một khoảng xác định hay không. Bạn thường xác định khoảng này trong phần khởi tạo, như 1:100 để bảo đảm vòng lặp bắt đầu.

Nếu điều kiện không thỏa và kết quả là False, vòng lặp sẽ không bao giờ chạy. Điều này được chỉ ra bởi mũi tên rời ở bên phải cấu trúc for. Chương trình sau đó sẽ thực thi chỉ thị đầu tiên sau khối vòng lặp.

Nếu điều kiện được xác nhận, một chỉ thị — hoặc khối chỉ thị — i1 sẽ được thực thi. Và có thể khối chỉ thị này lại là một vòng lặp khác. Trong trường hợp như vậy, ta gọi là vòng lặp lồng nhau.

Sau khi xong, điều kiện lại được đánh giá. Điều này được chỉ ra bằng các đường từ i1 quay lại phía trên, ngay sau hộp khởi tạo. Trong R — và trong Python — có thể diễn đạt điều này bằng tiếng Anh đơn giản, bằng cách hỏi biến của ta có thuộc một khoảng giá trị hay không.

Lưu ý rằng ở các ngôn ngữ khác, như C, điều kiện được làm rõ hơn với toán tử logic, như lớn hơn, nhỏ hơn, bằng, …

Dưới đây là một ví dụ for đơn giản:

# Create a vector filled with random normal values
u1 <- rnorm(30)
print("This loop calculates the square of the first 10 elements of vector u1")

# Initialize `usq`
usq <- 0. 

for (i in 1:10) {
    # i-th element of `u1` squared into `i`-th position of `usq`
    usq[i] <- u1[i] * u1[i]
    print(usq[i])
}

print(i)

Khối for được đặt trong ngoặc nhọn. Chúng có thể đặt ngay sau điều kiện kiểm tra hoặc xuống dòng bên dưới, tốt nhất là có thụt lề. Không bắt buộc, nhưng ngoặc nhọn chắc chắn tăng tính dễ đọc và giúp dễ dàng phát hiện khối vòng lặp cùng lỗi tiềm ẩn trong đó.

Lưu ý rằng vector bình phương, usq, được khởi tạo. Điều này không cần thiết trong mã RStudio thuần, nhưng ở phiên bản markup, knitr sẽ không biên dịch do tham chiếu đến vector chưa tồn tại trước khi dùng trong vòng lặp, gây lỗi trong RStudio. Để biết thêm về knitr, vào trang R Markdown.

Lồng vòng lặp For

Giờ bạn biết for có thể được lồng nhau, hẳn bạn thắc mắc tại sao và khi nào nên dùng điều này trong mã.

Giả sử bạn muốn thao tác một mảng hai chiều bằng cách gán phần tử của nó thành các giá trị cụ thể.

Lúc đó bạn có thể làm như sau:

# Insert your own integer here
my_int <- 42

nr <- as.integer(my_int)

# Create a `n` x `n` matrix with zeros
mymat <- matrix(0, nr, nr)

# For each row and for each column, assign values based on position
# These values are the product of two indexes
for (i in 1:dim(mymat)[1]) {
    for (j in 1:dim(mymat)[2]) {
        mymat[i, j] = i * j
    }
}

Mẹo: để biết thêm về hàm matrix(), truy cập R Documentation.

Bạn có hai vòng lặp for lồng nhau trong đoạn mã trên và do đó có hai cặp ngoặc nhọn, mỗi cái có khối riêng và được điều khiển bởi chỉ số riêng. Tức là i chạy qua các dòng và j chạy qua các cột.
 
Đã tạo ra gì?

Bạn đã tạo bảng cửu chương rất quen thuộc — thứ bạn nên thuộc lòng.

Bạn cũng có thể chọn một số nguyên rồi tạo bảng theo lựa chọn: bạn có thể gán một số nguyên cho một biến nếu bảng là vuông hoặc cho hai biến nếu bảng là hình chữ nhật. Biến này sẽ đóng vai trò là cận trên cho các chỉ số i và j.

# Show the first 10x10 chunk or the first `nr` x `nr` chunk
if (nr > 10) {
    mymat[1:10, 1:10]
} else
    mymat[1:nr, 1:nr]

Lưu ý để tránh làm ngập màn hình với các bảng khổng lồ, bạn đặt một điều kiện ở cuối để in ra khối 10 x 10 đầu tiên chỉ khi người dùng nhập số nguyên lớn hơn 10. Nếu không, sẽ in khối n x n.

Mã hoàn chỉnh như sau:

# Insert your own integer here
my_int <- 42

nr <- as.integer(my_int)

# Create a `n` x `n` matrix with zeroes
mymat <- matrix(0, nr, nr)

Vòng lặp While

Vòng lặp while, nằm giữa hình trên, gồm một khối khởi tạo như trước, theo sau là một điều kiện logic. Điều kiện này thường được biểu diễn bằng phép so sánh giữa biến điều khiển và một giá trị, dùng lớn hơn, nhỏ hơn hoặc bằng, nhưng bất kỳ biểu thức nào đánh giá ra giá trị logic True hoặc False đều hợp lệ.

Nếu kết quả là False (F), vòng lặp sẽ không bao giờ chạy, như mũi tên rời bên phải hình. Chương trình sau đó sẽ thực thi chỉ thị đầu tiên sau khối vòng lặp.

Nếu là True (T), chỉ thị hoặc khối chỉ thị i1 sẽ chạy tiếp theo.

Lưu ý rằng có thêm một chỉ thị hoặc khối chỉ thị i2: phần này cập nhật biến điều khiển, có thể thay đổi kết quả điều kiện ở đầu vòng lặp, nhưng không bắt buộc. Hoặc bạn muốn tăng một bộ đếm để theo dõi số lần lặp đã thực thi. Việc lặp dừng lại khi điều kiện đánh giá thành false.

Định dạng là while(cond) expr, trong đó cond là điều kiện kiểm tra và expr là một biểu thức.

Ví dụ sau yêu cầu người dùng, bằng một hàm do người dùng định nghĩa (UDF), nhập câu trả lời đúng cho câu hỏi về vũ trụ và mọi thứ. Nó sẽ tiếp tục cho đến khi người dùng trả lời đúng:

# Your User Defined Function
readinteger <- function(){
  n <- readline(prompt="Please, enter your ANSWER: ")
}

response <- as.integer(readinteger())

while (response!=42) {   
  print("Sorry, the answer to whatever the question MUST be 42");
  response <- as.integer(readinteger());
}

Ban đầu, dùng một hàm do người dùng định nghĩa để lấy đầu vào trước khi vào vòng lặp. Vòng lặp này sẽ tiếp tục miễn là câu trả lời không phải 42 như mong đợi.

Nói cách khác, bạn làm vậy vì nếu không, R sẽ phàn nàn về biểu thức bị thiếu, vốn đáng ra phải cung cấp giá trị True hoặc False — thực tế nó chưa biết ‘response’ trước khi dùng trong vòng lặp. Bạn cũng làm vậy vì nếu bạn trả lời đúng ngay lần đầu, vòng lặp sẽ không chạy chút nào.

Vòng lặp Repeat

Vòng lặp repeat nằm ngoài cùng bên phải của lưu đồ ở trên. Vòng lặp này tương tự while, nhưng được thiết kế sao cho các khối chỉ thị i1 và i2 chắc chắn được thực thi ít nhất một lần, bất kể kết quả điều kiện.

Theo các ngôn ngữ khác, có thể gọi vòng lặp này là “lặp cho đến khi” để nhấn mạnh việc các chỉ thị i1 và i2 được thực thi cho đến khi điều kiện còn False (F) hoặc tương đương, trở thành True (T) để thoát; nhưng dù sao cũng tối thiểu một lần.

Biến thể của ví dụ trước, bạn có thể viết:

readinteger <- function(){
  n <- readline(prompt="Please, enter your ANSWER: ") 
}

repeat {   
  response <- as.integer(readinteger());
  if (response == 42) {
    print("Well done!");
    break
  } else print("Sorry, the answer to whatever the question MUST be 42");
}

Sau hàm nhập quen thuộc, bạn có vòng lặp repeat với khối được thực thi ít nhất một lần và sẽ kết thúc bất cứ khi nào điều kiện if được thỏa.

Lưu ý bạn phải đặt một điều kiện trong vòng lặp để thoát bằng mệnh đề break. Mệnh đề này đưa ta đến khái niệm thoát hay ngắt vòng lặp.

Ngắt và thoát vòng lặp trong R

Vậy làm sao để thoát khỏi một vòng lặp?

Nói cách khác, ngoài việc kết thúc “tự nhiên” của vòng lặp — xảy ra do đạt số lần lặp quy định (for) hoặc gặp điều kiện (while, repeat) — bạn có thể dừng hay ngắt vòng lặp không?

Nếu có, làm thế nào?

Lệnh break trả lời câu hỏi đầu tiên: bạn đã thấy trong ví dụ cuối.

Ngắt vòng lặp với break

Khi trình thông dịch R gặp break, nó sẽ chuyển điều khiển đến chỉ thị ngay sau cuối vòng lặp (nếu có). Trong trường hợp vòng lặp lồng nhau, break chỉ thoát khỏi vòng lặp trong cùng.

Ví dụ dưới đây.

Đoạn mã này định nghĩa một ma trận m x n các số 0 rồi vào một vòng lặp for lồng để điền các vị trí của ma trận, nhưng chỉ khi hai chỉ số khác nhau. Mục đích là tạo ma trận tam giác dưới, tức là ma trận có phần tử dưới đường chéo chính khác 0. Các phần còn lại giữ nguyên giá trị 0 ban đầu.

Khi các chỉ số bằng nhau và điều kiện trong vòng lặp trong (chạy theo chỉ số cột j) được thỏa, một break sẽ được thực thi và vòng lặp trong bị ngắt, nhảy trực tiếp đến chỉ thị sau vòng lặp trong. Chỉ thị này là một lệnh print(). Sau đó, điều khiển trở lại điều kiện for ngoài (theo hàng, chỉ số i), và điều kiện lại được đánh giá.

Nếu các chỉ số khác nhau, phép gán được thực hiện và bộ đếm tăng thêm 1. Cuối cùng, chương trình in bộ đếm ctr, chứa số phần tử đã được gán.

# Make a lower triangular matrix (zeroes in upper right corner)
m = 10
n = 10

# A counter to count the assignment
ctr = 0

# Create a 10 x 10 matrix with zeroes
mymat = matrix(0, m, n)

for (i in 1:m) {
    for (j in 1:n) {
        if (i == j) {
            break
        } else {
            # you assign the values only when i<>j
            mymat[i, j] = i * j
            ctr = ctr + 1
        }
    }
    print(i * j)
}

# Print how many matrix cells were assigned
print(ctr)

Lưu ý rằng có thể hơi cẩn thận quá mức khi đặt ngoặc nhọn ngay cả khi không thật sự cần. Bạn thường làm vậy để chắc rằng bất kỳ thứ gì mở bằng { đều được đóng bằng }. Vì vậy nếu bạn thấy số { hoặc } không khớp, bạn biết là có lỗi, dù chiều ngược lại không nhất thiết đúng!

Cách dùng next trong vòng lặp

next ngắt quãng một lần lặp cụ thể và nhảy sang chu kỳ kế tiếp. Thực tế, nó nhảy đến bước đánh giá điều kiện của vòng lặp hiện tại.

Trong các ngôn ngữ khác, bạn có thể gặp tương đương là “continue” (dễ gây nhầm), nghĩa là: ở bất cứ đâu, khi điều kiện được xác nhận, hãy nhảy đến phần đánh giá vòng lặp.

Một ví dụ đơn giản về tiếp tục vòng lặp nhưng bỏ qua một chu kỳ khi điều kiện xảy ra là:

m = 20

for (k in 1:m) {
    if (!k %% 2)
        next
    print(k)
}

Đoạn mã này in tất cả số lẻ trong khoảng 1:m (ở đây m=20). Nói cách khác, tất cả số nguyên trừ những số có phần dư khác 0 khi chia cho 2 (do dùng toán tử mô-đun %%), như chỉ định bởi kiểm tra if, sẽ được in.

Các số có phần dư bằng 0 sẽ không được in, vì chương trình nhảy đến đánh giá điều kiện i in 1:m và bỏ qua mọi chỉ thị theo sau. Trong trường hợp này, print(k) bị bỏ qua.

Tổng kết việc dùng vòng lặp trong R

  1. Cố gắng đặt càng ít mã càng tốt trong vòng lặp bằng cách đưa ra ngoài càng nhiều chỉ thị càng tốt. Hãy nhớ, mọi thứ bên trong vòng lặp sẽ lặp lại nhiều lần và có thể không cần thiết.
  2. Cẩn thận khi dùng repeat: hãy chắc chắn rằng điều kiện kết thúc được đặt rõ ràng, nếu không bạn có thể rơi vào vòng lặp vô hạn.
  3. Nếu một vòng lặp trở nên (quá) lớn, tốt hơn nên gọi một hoặc vài hàm bên trong vòng lặp. Các lời gọi hàm giúp người khác dễ theo dõi mã hơn. Nhưng việc dùng for lồng nhau để thực hiện thao tác ma trận hay mảng có lẽ là dấu hiệu bạn chưa triển khai theo cách tối ưu cho một ngôn ngữ dựa trên ma trận như R.
  4. Không khuyến khích “làm lớn dần” biến hay tập dữ liệu bằng cách gán ở mỗi lần lặp. Ở một số ngôn ngữ như Matlab, sẽ có cảnh báo: bạn có thể tiếp tục, nhưng nên cân nhắc lựa chọn khác. Một ví dụ điển hình sẽ được trình bày ở phần tiếp theo.

Khi nào nên dùng vòng lặp trong R

Tất cả điều này đều tốt, nhưng khi nào bạn nên dùng vòng lặp trong R và khi nào không?

Mỗi khi một (các) thao tác cần lặp lại, vòng lặp có thể hữu ích.

Bạn chỉ cần chỉ định lặp bao nhiêu lần hoặc theo điều kiện nào các thao tác đó cần được thực thi: bạn gán các giá trị ban đầu cho biến điều khiển vòng lặp, thực hiện vòng lặp và khi kết thúc, bạn thường làm gì đó với kết quả.

Nhưng khi nào bạn nên dùng vòng lặp?

Chẳng lẽ bạn không thể sao chép chỉ thị mong muốn đủ số lần cần thiết?

Một nguyên tắc kinh nghiệm là nếu bạn cần thực hiện hành động (ví dụ) từ ba lần trở lên, dùng vòng lặp sẽ tốt hơn. Nó làm mã gọn, dễ đọc, dễ bảo trì hơn và bạn có thể tiết kiệm gõ phím: giả sử bạn phát hiện một chỉ thị cần lặp thêm một lần so với dự kiến ban đầu: thay vì viết lại toàn bộ chỉ thị, bạn chỉ cần thay đổi giá trị của biến trong điều kiện kiểm tra.

Tuy nhiên, bản chất đặc thù của R gợi ý không dùng vòng lặp chút nào (!) bất cứ khi nào có lựa chọn thay thế.

May mắn là có một số lựa chọn!

R có một tính năng ít ngôn ngữ lập trình có, gọi là vector hóa.

Các lựa chọn thay thế cho vòng lặp trong R

Vector hóa là gì?

Như tên gọi gợi ý, vector hóa là việc chuyển các phép toán lặp trên những số đơn ("vô hướng") thành một phép toán đơn trên các vector hoặc ma trận. Bạn đã thấy vài ví dụ về điều này ở các tiểu mục trên.

Vector là cấu trúc dữ liệu cơ bản trong R và là “một thực thể đơn bao gồm một tập hợp các thứ”, theo tài liệu R base.

Vì vậy, một tập hợp số là một vector số.

Nếu bạn kết hợp các vector (cùng độ dài), bạn thu được một ma trận. Bạn có thể làm điều này theo chiều dọc hoặc ngang, dùng các lệnh R khác nhau. Do đó, trong R, ma trận được xem là tập hợp các vector ngang hoặc dọc. Mở rộng ra, bạn có thể vector hóa các phép toán lặp trên vector.

Nhiều cấu trúc vòng lặp ở trên có thể được làm ngầm đi bằng cách dùng vector hóa.

Tôi nói “ngầm”, vì chúng không thực sự biến mất. Ở mức thấp hơn, dạng vector hóa thay thế được dịch thành mã sẽ chứa một hoặc nhiều vòng lặp trong ngôn ngữ cấp thấp mà dạng đó được triển khai và biên dịch (Fortran, C, hoặc C++).

Những thứ này ẩn với người dùng và thường nhanh hơn mã R tường minh tương đương, nhưng trừ khi bạn định tự triển khai hàm R bằng một trong các ngôn ngữ đó, điều này hoàn toàn trong suốt với bạn.

Trường hợp cơ bản nhất bạn có thể nghĩ đến là cộng hai vector v1 và v2 thành vector v3, có thể làm từng phần tử bằng vòng lặp for:

v1 <- c(3,4,5)
v2 <- c(3,4,6)
v3 <- c(0,0,0)
n = length(v1)

for (i in 1:n) {
    v3[i] <- v1[i] + v2[i]
}
v3

Hoặc bạn cũng có thể dùng dạng vector hóa “nguyên bản”:

v3 = v1 + v2
v3

Lưu ý gọi là “nguyên bản” vì R nhận biết tất cả toán tử số học hoạt động trên vector và ma trận.

Tương tự, với hai ma trận A và B, thay vì cộng các phần tử A[i,j] và B[i,j] ở vị trí tương ứng, mà bạn cần để ý đến hai chỉ số i và j, bạn chỉ cần bảo R làm như sau:

A <- matrix(1:6, nrow = 3, ncol = 2)
B <- matrix(c(2,4,3,1,5,7), nrow=3, ncol=2)

C= A + B
C

Và điều này thực sự rất đơn giản!

Giải thích vector hóa trong R

Tại sao vector hóa lại chạy nhanh hơn, khi số phép toán cơ bản dường như như nhau?

Điều này được giải thích tốt nhất bằng cách nhìn vào “ngóc ngách” nội bộ của R, vốn cần một bài riêng, nhưng tóm lược: trước hết, R là ngôn ngữ thông dịch, vì vậy mọi chi tiết về định nghĩa biến được trình thông dịch xử lý. Bạn không cần chỉ định một số là số thực dấu phẩy động hay cấp phát bộ nhớ bằng con trỏ, chẳng hạn.

Trình thông dịch R “hiểu” các vấn đề này từ ngữ cảnh khi bạn nhập lệnh, nhưng làm vậy theo từng lệnh. Do đó, nó phải xử lý các định nghĩa đó mỗi lần bạn ra lệnh, ngay cả khi bạn chỉ lặp lại lệnh đó.

Ngược lại, một trình biên dịch giải quyết mọi định nghĩa và khai báo tại thời điểm biên dịch trên toàn bộ mã; mã sau đó được dịch thành mã nhị phân gọn nhẹ và tối ưu trước khi bạn thực thi bất cứ điều gì. Nay, vì các hàm R được viết bằng một trong những ngôn ngữ cấp thấp này, chúng hiệu quả hơn.

Thực tế, nếu nhìn vào mã cấp thấp, bạn sẽ thấy các lời gọi đến C hoặc C++, thường được triển khai trong cái gọi là mã bao (wrapper).

Thứ hai, trong các ngôn ngữ hỗ trợ vector hóa (như R hoặc Matlab), mọi lệnh dùng dữ liệu số đều hoạt động trên một đối tượng vốn được định nghĩa nguyên bản là vector, ngay cả khi chỉ gồm một phần tử. Đây là mặc định khi bạn định nghĩa, ví dụ, một biến số đơn: biểu diễn bên trong của nó trong R luôn là một vector, dù chỉ có một số.

Các vòng lặp vẫn tồn tại “dưới nắp capo”, nhưng ở mức C/C++ biên dịch thấp và nhanh hơn nhiều. Lợi thế của việc có một vector là các định nghĩa được trình thông dịch giải quyết chỉ một lần, trên toàn bộ vector, bất kể kích thước, trái với vòng lặp trên vô hướng, nơi định nghĩa và cấp phát phải làm theo từng phần tử — và điều này chậm hơn.

Cuối cùng, làm việc với định dạng vector nguyên bản cho phép tận dụng các thư viện Đại số tuyến tính rất hiệu quả (như BLAS), nên khi thực thi các lệnh vector hóa, R dựa trên các thủ tục số hiệu quả này. Vậy thông điệp là, nếu có thể, hãy xử lý toàn bộ cấu trúc dữ liệu trong một lời gọi hàm duy nhất để tránh mọi thao tác sao chép.

Nhưng tạm dừng phần lan man, hãy làm một ví dụ tổng quát về vector hóa, rồi ở tiểu mục tiếp theo, bạn sẽ đi sâu vào các hàm vector hóa cụ thể và phổ biến trong R để thay thế vòng lặp.

Ví dụ về vector hóa trong R

Quay lại khái niệm “tăng dần dữ liệu”, một cách điển hình là kém hiệu quả để “cập nhật” data frame.

Đầu tiên, bạn tạo một ma trận m x n với replicate(m, rnorm(n)) cùng m=10 vector cột có n=10 phần tử mỗi vector, được tạo bằng rnorm(n) tạo số chuẩn ngẫu nhiên.

Sau đó bạn chuyển thành data frame (tức 10 quan sát của 10 biến) và thực hiện phép toán đại số trên từng phần tử bằng một vòng lặp for lồng: ở mỗi lần lặp, một hàm hình sin tăng thêm vào mỗi phần tử được tham chiếu bởi hai chỉ số.

Ví dụ sau hơi nhân tạo, nhưng có thể đại diện cho việc thêm một tín hiệu vào nhiễu ngẫu nhiên:

# This is a bad loop with 'growing' data
set.seed(42)
m = 10
n = 10

# Create matrix of normal random numbers
mymat <- replicate(m, rnorm(n))

# Transform into data frame
mydframe <- data.frame(mymat)

for (i in 1:m) {
  for (j in 1:n) {
    mydframe[i, j] <- mydframe[i, j] + 10 * sin(0.75 * pi)
    print(mydframe)
  }
}

Ở đây, phần lớn thời gian thực thi là sao chép và quản lý vòng lặp.

Hãy xem giải pháp vector hóa trông như thế nào:

set.seed(42)
m = 10
n = 10
mymat <- replicate(m, rnorm(n))
mydframe <- data.frame(mymat)
mydframe <- mydframe + 10 * sin(0.75 * pi)
mydframe

Trông đơn giản hơn: dòng cuối thay thế vòng lặp for lồng. Lưu ý việc dùng set.seed() để đảm bảo hai cách triển khai cho kết quả y hệt.

Giờ hãy định lượng thời gian thực thi cho hai giải pháp.

Bạn có thể làm điều này bằng cách dùng các lệnh hệ thống của R như system.time(), cho phép truyền vào một đoạn mã như sau:

Mẹo: chỉ cần đặt đoạn mã bạn muốn đo vào trong dấu ngoặc của hàm system.time().

# Insert `system.time()` to measure loop execution
for (i in 1:m) {
  for (j in 1:n) {
    mydframe[i, j] <- mydframe[i, j] + 10 * sin(0.75 * pi)
  }
}

# Add `system.time()` to measure vectorized execution
mydframe <- mydframe + 10 * sin(0.75 * pi) 

Trong đoạn mã trên, bạn chỉ thực hiện việc chọn m và n, tạo ma trận và chuyển thành data frame một lần ở đầu, rồi đánh giá khối for so với “một dòng” của phiên bản vector hóa bằng hai lần gọi system.time() riêng biệt.

Bạn sẽ thấy ngay cả với thiết lập tối thiểu m=n=10 phiên bản vector hóa đã nhanh hơn 7 lần, dù với giá trị thấp như vậy, điều này hầu như không quan trọng với người dùng.

Sự khác biệt trở nên đáng chú ý (ở thang đo con người) nếu đặt m=n=100, trong khi tăng lên 1000 khiến vòng lặp for có vẻ “treo” hàng chục giây, còn dạng vector hóa vẫn chạy trong chớp mắt.

Với m=n=10000 vòng lặp for treo hơn một phút trong khi dạng vector hóa mất 2,54 giây. Tất nhiên, các phép đo này chỉ mang tính minh họa và tùy thuộc cấu hình phần cứng/phần mềm, tránh việc máy tính bị quá tải bởi hàng chục tab trình duyệt và nhiều ứng dụng chạy nền; nhưng chúng cho thấy sự khác biệt.

Công bằng mà nói, tăng m và n cũng ảnh hưởng mạnh đến việc tạo ma trận như bạn có thể thấy bằng cách đặt thêm một lần gọi system.time() quanh hàm replicate.

Hãy thử thay đổi m và n để xem thời gian thực thi thay đổi thế nào, bằng cách vẽ thời gian theo tích m x n. Đây là chỉ báo phù hợp, vì nó biểu diễn kích thước ma trận được tạo, cũng như định lượng số lần lặp cần thiết để hoàn thành tác vụ thông qua vòng lặp for lồng.

Đó là một ví dụ về vector hóa. Nhưng còn nhiều ví dụ khác. Trong R News, bản tin của dự án R ở trang 46, có các hàm rất hiệu quả để tính tổng và trung bình theo chiều của mảng hay ma trận, như: rowSums(), colSums(), rowMeans() và colMeans().

Hơn nữa, bản tin cũng nhắc rằng “... các hàm trong họ ‘apply’, tên là [s,l,m,t]apply, được cung cấp để áp dụng một hàm khác lên các phần/tầng của đối tượng. Các hàm ‘apply’ này cung cấp cú pháp gọn gàng cho những tác vụ đôi khi khá phức tạp, dễ đọc hơn và nhanh hơn so với các vòng lặp viết kém.”

Họ apply: chỉ là vòng lặp ẩn?

Họ hàm apply rất phong phú và mạnh mẽ là các hàm mang tính vector hóa nội tại. Nếu thoạt nhìn chúng dường như không chứa vòng lặp, đặc điểm này sẽ lộ rõ khi bạn nhìn kỹ “dưới nắp”.

Lệnh apply, hay đúng hơn là họ lệnh, thuộc gói base của R. Nó bao gồm một số hàm ([s,l,m,r, t,v]apply) để thao tác các “lát cắt” dữ liệu dạng ma trận hay mảng theo cách lặp đi lặp lại, cho phép băng qua dữ liệu và tránh dùng vòng lặp tường minh. Các hàm hoạt động trên một ma trận hay mảng đầu vào và áp dụng một hàm được chỉ định, kèm một hoặc vài đối số tùy chọn.

Lưu ý đó là lý do chúng thuộc nhóm ‘functional’ (theo trang Advanced R của Hadley Wickham).

Hàm được gọi có thể là hàm tổng hợp, như trung bình đơn giản, hoặc một hàm biến đổi hay trích lọc khác.

Lưu ý rằng áp dụng các hàm này không nhất thiết dẫn đến thực thi nhanh hơn: khác biệt không lớn; thay vào đó, nó giúp tránh việc viết các vòng lặp cồng kềnh, giảm khả năng mắc lỗi.

Các hàm trong họ gồm: apply(), sapply(), lapply(), mapply(), rapply(), tapply(), vapply().

Nhưng khi nào và cách dùng chúng ra sao?

Đáng chú ý là một gói như plyr bao quát chức năng của họ; dù nhớ hết mà không cần tài liệu chính thức có thể khó, nhưng các hàm này là nền tảng cho các kết hợp phức tạp và hữu ích hơn.

Ba hàm đầu được dùng thường xuyên nhất:

  1. apply()

Bạn muốn áp dụng một hàm lên các hàng (chỉ số “1”) hoặc cột (chỉ số “2”) của ma trận. Xem trang này để biết thêm.

  1. lapply()

Bạn muốn áp dụng một hàm lên từng phần tử của một list và nhận về một list (chữ “l” trong tên hàm là vì vậy). Tìm hiểu thêm trên R documentation.

  1. sapply()

Bạn muốn áp dụng một hàm lên từng phần tử của một list nhưng muốn nhận về một vector thay vì list. Cần biết thêm? Hãy truy cập trang tài liệu R.

Các hàm liên quan gồm sweep(), by() và aggregate() và đôi khi được dùng cùng với các phần tử của họ apply().

Tôi giới hạn thảo luận trong bài viết này ở hàm apply() (bàn luận mở rộng có thể là chủ đề của bài viết tương lai).

Cho một ma trận M, lời gọi apply(M,1,fun) hoặc apply(M, 2,fun) sẽ áp dụng hàm fun chỉ định lên các hàng của M nếu chỉ định 1; hoặc lên các cột của M khi chỉ định 2. Đối số số này gọi là “margin” và chỉ nhận giá trị 1 và 2 vì hàm hoạt động trên ma trận. Tuy nhiên, bạn cũng có thể có mảng đến 8 chiều.

Hàm có thể là bất kỳ hàm R hợp lệ, hoặc là một hàm do người dùng định nghĩa (UDF), thậm chí viết ngay bên trong apply(), rất tiện lợi.

apply(): ví dụ

# define matrix `mymat` by replicating the sequence `1:5` for `4` times and transforming into a matrix
mymat <- matrix(rep(seq(5), 4), ncol = 5)

# `mymat` sum on rows
apply(mymat, 1, sum)

# `mymat` sum on columns
apply(mymat, 2, sum)

# With user defined function within the apply that adds any number `y` to the sum of the row
# `y` is set at `4.5`
apply(mymat, 1, function(x, y)
  sum(x) + y, y = 4.5)

# Or produce a summary column wise for each column
apply(mymat, 2, function(x, y)
  summary(mymat))

Bạn thường dùng data frame: trong trường hợp cụ thể này, bạn phải bảo đảm dữ liệu cùng kiểu, nếu không sẽ xảy ra ép kiểu, điều mà hầu như bạn không muốn. Ví dụ, trong một data frame trộn văn bản và số, dữ liệu số sẽ bị chuyển thành chuỗi ký tự.

Các cân nhắc cuối cùng về việc dùng vòng lặp và lựa chọn thay thế trong R

Hành trình này đã đưa chúng ta từ các cấu trúc vòng lặp nền tảng dùng trong lập trình đến (khái niệm cơ bản về) vector hóa và đến một ví dụ về dùng một trong các hàm của họ apply(), thứ xuất hiện thường xuyên trong R.

Về điều khiển luồng mã, bạn chỉ xử lý vòng lặp: for, while, repeat và cách ngắt/thoát chúng.

Khi các tiểu mục cuối ám chỉ rằng nên tránh dùng vòng lặp trong R, bạn có thể tự hỏi vì sao lại phải học về chúng.

Theo tôi, bạn nên học các cấu trúc lập trình này vì:

  1. Khả năng cao R sẽ không phải là ngôn ngữ duy nhất của bạn trong khoa học dữ liệu hay ở nơi khác, và nắm bắt các cấu trúc chung như vòng lặp là điều hữu ích để bỏ túi kỹ năng. Cú pháp vòng lặp có thể thay đổi theo ngôn ngữ, nhưng khi bạn đã làm chủ ở một ngôn ngữ, bạn sẽ dễ dàng áp dụng cho ngôn ngữ khác.
  2. Vũ trụ R rất rộng và rất khó, nếu không muốn nói là bất khả, để biết hết mọi hàm trong R. Có nhiều cách để làm việc, vài cách hiệu quả hoặc tao nhã hơn, và đường cong học tập của bạn sẽ tăng dần; khi bạn dùng chúng, bạn sẽ tự hỏi về những cách hiệu quả hơn và cuối cùng sẽ gặp những hàm mà trước đó bạn chưa từng nghe. Đến lúc bạn đọc bài này, vài thư viện mới có thể đã được phát triển, bản khác phát hành, nên cơ hội biết hết là rất thấp, trừ khi bạn dành toàn bộ thời gian làm chuyên gia R.
  3. Cuối cùng, ít nhất khi không làm việc với các tập dữ liệu có số chiều đặc biệt cao, một giải pháp vòng lặp sẽ dễ viết và dễ đọc. Và có thể, với tư cách nhà khoa học dữ liệu, bạn được yêu cầu dựng một bản thử nghiệm “làm được việc”. Có thể bạn không quan tâm đến cách tinh vi hay tao nhã để ra kết quả. Có thể, trong quy trình phân tích dữ liệu, bạn chỉ cần thể hiện chuyên môn lĩnh vực và tập trung vào nội dung. Người khác, nhiều khả năng là chuyên gia backend, sẽ lo giai đoạn sản xuất (và có thể họ sẽ viết bằng ngôn ngữ khác!)

Nếu vòng lặp for trong R không còn là thử thách với bạn sau khi đọc hướng dẫn này, bạn có thể cân nhắc tham gia khóa Intermediate R của DataCamp. Khóa học này sẽ củng cố kiến thức của bạn về các chủ đề ở R trung cấp với một loạt bài tập mới và thú vị. Nếu vòng lặp đã không còn bí mật nào với bạn, khóa Writing Functions in R do Hadley và Charlotte Wickham giảng dạy có thể khiến bạn quan tâm. Ngoài ra, hãy xem hướng dẫn For Loops in R.

Chạy và chỉnh sửa mã từ hướng dẫn trực tuyến này

Chạy mã

Câu hỏi thường gặp về vòng lặp trong R

Vòng lặp trong R là gì và vì sao hữu ích?

Vòng lặp là cách lặp lại một khối mã nhiều lần. Chúng hữu ích khi bạn muốn thực hiện cùng một thao tác trên nhiều điểm dữ liệu hoặc đối tượng, hoặc khi bạn muốn lặp cho đến khi một điều kiện nhất định được đáp ứng.

Các loại vòng lặp trong R gồm những gì?

Có hai loại vòng lặp chính trong R: for và while.

  • For lặp lại một khối mã cho mỗi phần tử trong một danh sách hoặc dãy. Ví dụ, bạn có thể dùng for để in ra tất cả phần tử của một vector, hoặc áp dụng một hàm cho từng phần tử của một list.
  • While lặp lại một khối mã miễn là một điều kiện nào đó đúng. Ví dụ, bạn có thể dùng while để tiếp tục yêu cầu người dùng nhập cho đến khi họ đưa ra phản hồi hợp lệ.

Làm sao dừng một vòng lặp trong R?

Để dừng một vòng lặp trong R, bạn có thể dùng lệnh break. Lệnh này sẽ lập tức thoát vòng lặp và tiếp tục chạy phần mã phía sau.

Làm sao bỏ qua lần lặp hiện tại và sang lần tiếp theo trong R?

Để bỏ qua và chuyển sang vòng lặp kế tiếp trong R, bạn có thể dùng lệnh next. Lệnh này sẽ bỏ qua phần còn lại của lần lặp hiện tại và chuyển sang lần lặp tiếp theo.

Tôi có thể đặt vòng lặp bên trong vòng lặp khác trong R không?

Có, bạn có thể đặt vòng lặp bên trong vòng lặp khác trong R. Cách làm này gọi là “lồng” vòng lặp. Ví dụ, bạn có thể dùng for bên trong một for khác để duyệt qua các phần tử của một ma trận.

Chủ đề
R
Khoa học Dữ liệu

Tìm hiểu thêm về R

Khóa học

Nhập môn viết hàm trong R

4 giờ
54.1K
Nâng cao kỹ năng R bằng cách viết các hàm hiệu quả, có thể tái sử dụng.
Xem chi tiếtRight Arrow
Bắt Đầu Khóa Học
Xem thêmRight Arrow
Liên quan

blog

Claude Opus 4.6: Tính năng, Điểm chuẩn, Bài kiểm tra thực hành và hơn thế nữa

Mô hình mới nhất của Anthropic dẫn đầu ở mã hóa tác tử và lập luận phức tạp. Thêm vào đó, nó có cửa sổ ngữ cảnh 1M.
Matt Crabtree's photo

Matt Crabtree

10 phút

Xem ThêmXem Thêm