Menu

NA trong R: xử lý giá trị thiếu (is.na, na.rm, na.omit)

NA nghĩa là "không biết", và nó lan sang mọi phép tính nó chạm vào. Cách phát hiện NA bằng is.na(), bỏ qua bằng na.rm = TRUE, và loại bỏ hoặc thay thế NA một cách có chủ đích.

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.

NA nghĩa là "không biết", và nó lan ra

Dữ liệu thực tế luôn có lỗ hổng: câu hỏi khảo sát bị bỏ trống, cảm biến bỏ lỡ một lần đo. R biểu diễn mỗi lỗ hổng bằng NA, viết tắt của not available (không có sẵn). Mô hình tư duy quan trọng nhất: NA không phải số 0, không phải chuỗi rỗng, không phải một con số đặc biệt. Nó có nghĩa là "ở đây có một giá trị, nhưng ta không biết đó là gì."

Hiểu đúng điều này thì cách R hoạt động trở nên hợp lý. Một số không biết cộng một là bao nhiêu? Không biết. Trung bình của 4, 8 và một số không biết là bao nhiêu? Không biết, vì giá trị thiếu có thể là bất cứ số nào, nên trung bình cũng có thể là bất cứ số nào:

Cả ba dòng đều in ra NA. Tính lây lan này là tính năng, không phải bug: R từ chối âm thầm giả vờ biết một đáp án mà nó không biết. Một bảng tính tự động bỏ qua ô trống có thể che giấu việc nửa cột bị thiếu; R buộc bạn phải nhận ra và quyết định giá trị thiếu có ý nghĩa gì với phân tích của mình. Phần còn lại của bài viết là về cách đưa ra quyết định đó một cách có chủ đích.

Kiểm tra NA: is.na(), không bao giờ dùng ==

Đây là cái bẫy đầu tiên ai cũng mắc. Bạn muốn tìm các giá trị thiếu nên viết một phép so sánh, và nó không chạy đúng:

x == NA trả về NA NA NA, không có một TRUE nào. Tại sao? Hãy theo logic của "không biết": 4 có bằng một giá trị không biết không? Không thể nói được, vì giá trị đó có thể là 4. Ngay cả ô NA khi so sánh cũng cho ra NA: một giá trị không biết có bằng một giá trị không biết khác không? Không biết. So sánh với NA không bao giờ trả về TRUE hay FALSE, nên dùng làm phép kiểm tra là vô dụng, và tệ hơn, một mặt nạ toàn NA bên trong [ ] sẽ không chọn đúng những gì bạn mong đợi.

Công cụ đúng là is.na(), được tạo ra để trả lời chính câu hỏi này và trả về giá trị logic rõ ràng: FALSE TRUE FALSE. Từ đó có hai cách viết bạn sẽ dùng thường xuyên:

sum(is.na(x)) đếm số giá trị thiếu (TRUE được cộng như 1); hãy chạy nó trên mọi cột của một bộ dữ liệu mới trước khi làm bất cứ điều gì. which(is.na(x)) cho biết vị trí của chúng. Còn x[!is.na(x)] chỉ giữ lại các giá trị có thật, tức là loại bỏ thủ công bằng mặt nạ logic, cùng kiểu lọc vector quen thuộc.

Bỏ qua NA khi tóm tắt: na.rm = TRUE

Thường thì bạn không cần tự tay loại bỏ NA, vì các hàm tóm tắt của R có sẵn lối thoát: đối số na.rm (NA remove, tức bỏ NA):

Với na.rm = TRUE, hàm bỏ các giá trị thiếu và tính trên phần còn lại: trung bình của 4 và 8 là 6. sum(), sd(), median(), min(), max(), var(): cả nhóm hàm thống kê mô tả đều nhận đối số này.

Để ý rằng giá trị mặc định là FALSE. Đó là R đang chủ động bảo vệ bạn: nó muốn việc bỏ qua dữ liệu thiếu phải là một lựa chọn tường minh bạn viết ra, không phải một mặc định âm thầm. Khi gõ na.rm = TRUE, bạn đang khẳng định "ở đây bỏ qua các giá trị thiếu là an toàn". Điều đó đúng khi một cảm biến ngẫu nhiên bỏ lỡ vài lần đo, và sai một cách nguy hiểm khi, chẳng hạn, những người thu nhập thấp nhất bỏ qua câu hỏi về thu nhập. Đối số này buộc bạn chịu trách nhiệm cho quyết định đó.

Loại bỏ hàng thiếu dữ liệu: na.omit() và complete.cases()

Trong một data frame, giá trị thiếu nằm ở từng ô, nhưng phân tích thường làm việc theo từng hàng, nên thao tác phổ biến là loại bỏ các hàng có bất kỳ NA nào:

na.omit(df) trả về data frame sau khi bỏ mọi hàng chứa ít nhất một NA; ở đây chỉ còn Rosa. complete.cases(df) trả về mặt nạ logic theo hàng (TRUE FALSE FALSE) đứng sau cùng ý tưởng đó, và lập chỉ mục bằng nó cho kết quả giống hệt nhưng có hai lợi thế: bạn có thể đếm trước số hàng sắp mất (sum(!complete.cases(df))), và có thể giới hạn những cột cần xét. df[complete.cases(df[, "age"]), ] chỉ bỏ những hàng thiếu age, nên vẫn giữ Mia dù điểm của cô ấy không xác định.

Dạng giới hạn theo cột này quan trọng hơn vẻ ngoài của nó: na.omit() trên một data frame nhiều cột có thể âm thầm loại bỏ phần lớn số hàng chỉ vì NA ở những cột bạn không hề định phân tích. Hãy biết mình đang bỏ bao nhiêu hàng, và vì sao, trước khi bỏ.

Thay thế NA

Đôi khi cách đúng là lấp lỗ hổng thay vì xóa hàng. Cách viết thường dùng kết hợp is.na() với phép gán:

Hãy đọc là: "ở những ô mà visits bị thiếu, đặt giá trị 0." Cách này hợp lệ đúng khi NA biểu thị một giá trị đã biết: một khách hàng không có bản ghi lượt ghé thăm thực sự có 0 lượt ghé thăm.

Nhưng hãy trung thực về việc khi nào điều đó đúng. Nếu NA nghĩa là "ta đã không đo được", thì thay bằng 0 là bịa dữ liệu: thay điểm thi bị thiếu bằng 0 sẽ kéo trung bình xuống như thể những học sinh đó được điểm 0, trong khi thật ra bạn không biết họ được bao nhiêu. Hãy so trung bình ở trên (2,4) với trung bình dùng na.rm của dữ liệu gốc (4): cùng dữ liệu, hai khẳng định khác nhau. Thay bằng trung bình hoặc trung vị ít méo hơn nhưng vẫn làm giảm độ biến thiên. Quy tắc: chỉ điền một giá trị khi bạn có thể giải thích bằng lời đơn giản vì sao đó chính là giá trị thật của ô bị thiếu. Nếu không, hãy giữ NA và dùng na.rm: "không biết" thường là giá trị trung thực nhất trong bộ dữ liệu.

NA, NULL, NaN và Inf

R có bốn giá trị đặc biệt trông giống nhau nhưng mang ý nghĩa thực sự khác nhau:

Giá trịÝ nghĩaĐộ dàiNguồn thường gặp
NACó giá trị nhưng không biết là gì1 (chiếm một ô)Dữ liệu bị thiếu
NULLHoàn toàn không có đối tượng0 (không chiếm ô)Phần tử list đã bị xóa, kết quả rỗng
NaNPhép toán có kết quả không xác định10 / 0, log(-1)
InfSố vượt quá khả năng biểu diễn11 / 0, tràn số

Những khác biệt quan trọng trong thực tế: NULL biến mất bên trong vector (c(1, NULL, 3) có hai phần tử, nên nó không thể biểu diễn một quan sát bị thiếu), còn NA vẫn giữ chỗ của mình. NaN được tính là thiếu (is.na(NaN) là TRUE, nên na.rm cũng loại bỏ nó), nhưng chiều ngược lại thì sai: is.nan(NA) là FALSE. Còn Inf không phải giá trị thiếu: nó là một số thật, so sánh được (Inf > 1e300 là TRUE), nên na.rm không loại bỏ nó; hãy dùng is.finite() để chỉ giữ lại các số bình thường.

Để đầy đủ: NA âm thầm có nhiều dạng theo kiểu (NA_integer_, NA_real_, NA_character_) để có thể nằm trong bất kỳ vector nào mà không phá vỡ quy tắc một kiểu dữ liệu. Bạn hiếm khi phải gõ chúng, nhưng sẽ gặp chúng trong code của package và thông báo lỗi của dplyr.

Những điều cần nhớ

  • NA nghĩa là "không biết", và cái không biết thì lây lan: mọi phép tính chạm vào NA đều trả về NA, và đó là thiết kế có chủ ý.
  • Kiểm tra bằng is.na(), không bao giờ bằng == NA; đếm lỗ hổng bằng sum(is.na(x)).
  • na.rm = TRUE khiến các hàm tóm tắt bỏ qua NA, một quyết định tường minh cho từng lần gọi rằng các giá trị thiếu có thể bỏ qua.
  • na.omit() loại bỏ toàn bộ các hàng thiếu dữ liệu; complete.cases() cho bạn mặt nạ, đếm được và giới hạn được theo những cột quan trọng.
  • Chỉ thay NA (x[is.na(x)] <- value) khi bạn giải thích được giá trị bị thiếu thực sự là gì.
  • NA ≠ NULL ≠ NaN ≠ Inf: giá trị thiếu, đối tượng vắng mặt, phép toán không xác định, số không giới hạn.

Tiếp theo: hàm (function), đóng gói logic thành những phần có tên và dùng lại được.

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

Tại sao mean() trả về NA trong R?

Vì có ít nhất một giá trị trong vector là NA, và NA có tính lây lan: nếu một đầu vào bất kỳ không xác định, R coi kết quả cũng không xác định. Truyền na.rm = TRUE vào, tức mean(x, na.rm = TRUE), để tính trung bình của những giá trị đang có. Hầu hết các hàm tóm tắt (sum, sd, median, min, max) đều nhận đối số này.

Làm sao để kiểm tra NA trong R?

Dùng is.na(x), hàm này trả về TRUE ở mọi vị trí bị thiếu giá trị. Đừng bao giờ kiểm tra bằng x == NA: so sánh bất cứ thứ gì với một giá trị không biết sẽ cho ra NA, không phải TRUE hay FALSE, nên phép kiểm tra âm thầm thất bại. sum(is.na(x)) đếm số giá trị thiếu; which(is.na(x)) tìm vị trí của chúng.

Làm sao để xóa các hàng có NA khỏi data frame trong R?

na.omit(df) loại bỏ mọi hàng chứa ít nhất một NA. Muốn kiểm soát nhiều hơn thì complete.cases(df) trả về một vector logic đánh dấu các hàng đầy đủ dữ liệu, nên df[complete.cases(df), ] làm đúng việc đó một cách tường minh, và bạn có thể chỉ áp dụng cho những cột quan trọng, ví dụ df[complete.cases(df[, c("age", "score")]), ].

NA và NULL trong R khác nhau thế nào?

NA là một giá trị bị thiếu: nó chiếm một ô trong vector và có độ dài 1. NULL là sự vắng mặt của một đối tượng: nó có độ dài 0 và biến mất khi được đưa vào vector, nên c(1, NULL, 3) chỉ có hai phần tử. Dùng NA cho một điểm dữ liệu bị thiếu; NULL xuất hiện khi xóa phần tử của list hoặc khi hàm trả về rỗ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