Cách thay thế các giá trị bị thiếu (NA) trong R: na.omit & na.rm
⚡ Tóm tắt thông minh
Bài viết "Thay thế giá trị thiếu trong R" hướng dẫn cách phát hiện các giá trị NA, loại bỏ các hàng không đầy đủ bằng hàm na.omit() và điền vào chỗ trống bằng giá trị trung bình hoặc trung vị thông qua hàm mutate(). Bài hướng dẫn này sử dụng tập dữ liệu Titanic, trong đó cả tuổi và giá vé đều có dữ liệu bị thiếu.
Giá trị thiếu trong R là gì?
Giá trị thiếu xuất hiện khi một quan sát không có giá trị được ghi lại trong một cột, hoặc khi một ký tự giữ chỗ không phải số nằm ở vị trí đáng lẽ phải là số. Chúng phải được loại bỏ hoặc thay thế trước khi thực hiện bất kỳ phép tính nào, vì hầu hết các hàm trong R đều trả về NA ngay khi có giá trị thiếu.
Hướng dẫn này trình bày cách xử lý các giá trị thiếu bằng thư viện dplyr, một phần của hệ sinh thái tidyverse dành cho phân tích dữ liệu.
Bước đầu tiên luôn là tìm ra có bao nhiêu giá trị bị thiếu và ở đâu.
Cách phát hiện và đếm các giá trị thiếu trong R
Trước khi quyết định phải làm gì với các giá trị bị thiếu, bạn cần biết có bao nhiêu giá trị như vậy và chúng nằm ở đâu. R cung cấp bốn cách kiểm tra, từ câu trả lời đơn giản "có" hoặc "không" đến việc thống kê đầy đủ từng cột.
# 1. Is there any NA at all? anyNA(df_titanic) # 2. How many in total? sum(is.na(df_titanic)) # 3. How many per column, the most useful view colSums(is.na(df_titanic)) # 4. Percentage missing per column round(colMeans(is.na(df_titanic)) * 100, 1)
Hàm `colSums()` là hàm nên dùng trong thực tế. Nó trả về một vectơ có tên với một giá trị đếm cho mỗi cột, cho thấy ngay lập tức liệu một biến có thiếu một vài giá trị hay hầu hết là trống hay không.
Đếm số hàng hoàn chỉnh. complete.cases() trả về TRUE cho các hàng không có giá trị thiếu ở bất kỳ đâu, điều này cho bạn biết trước lượng dữ liệu mà na.omit() sẽ loại bỏ:
sum(complete.cases(df_titanic)) # rows that would survive sum(!complete.cases(df_titanic)) # rows that would be dropped
Cảnh báo về các phần giữ chỗ. R chỉ nhận diện NA. Các tập dữ liệu thường mã hóa các giá trị thiếu dưới dạng chuỗi rỗng, dấu cách, “N/A”, “-“, hoặc số đặc biệt như -99 hoặc 999. Những giá trị này sẽ vượt qua mọi bước kiểm tra ở trên mà không bị phát hiện. Hãy chuyển đổi chúng khi nhập dữ liệu để phần còn lại của quy trình hoạt động bình thường:
df <- read.csv(PATH, na.strings = c("", " ", "NA", "N/A", "-", "-99"))
Luôn luôn quét phạm vi của từng cột số sau khi nhập dữ liệu. Tuổi -99 hoặc giá vé 9999 nguy hiểm hơn nhiều so với giá trị NA thông thường, vì không có chức năng nào sẽ cảnh báo bạn về điều đó.
Các loại dữ liệu bị thiếu: MCAR, MAR và MNAR
Lý do giá trị bị thiếu quyết định việc có nên điền giá trị đó vào hay không. Các nhà thống kê nhận ra ba cơ chế.
- MCAR, mất tích hoàn toàn ngẫu nhiên. Xác suất bị mất dữ liệu không liên quan đến bất cứ điều gì, dù được quan sát hay không, ví dụ như một cảm biến bị lỗi ngẫu nhiên. (Drop)ping Việc điền các hàng này không gây ra sai lệch nào, chỉ làm giảm độ chính xác.
- MAR, thiếu ngẫu nhiên. Xác suất phụ thuộc vào các biến quan sát được khác nhưng không phụ thuộc vào chính giá trị bị thiếu. Nếu hành khách lớn tuổi ít có khả năng được ghi lại tuổi, thì tuổi là MAR (Missing At Random - Giá trị bị thiếu ngẫu nhiên) dựa trên các cột dữ liệu khác. Phương pháp điền dữ liệu sử dụng các cột đó sẽ xử lý vấn đề này tốt.
- MNAR, dữ liệu thiếu không phải ngẫu nhiên. Xác suất phụ thuộc vào chính giá trị không quan sát được, ví dụ như những người có thu nhập cao từ chối khai báo thu nhập của họ. Không có phương pháp điền dữ liệu nào có thể khắc phục điều này chỉ dựa trên dữ liệu, và bản thân dữ liệu thiếu mang thông tin đáng được ghi lại trong cột đánh dấu.
Phương pháp gán giá trị trung bình, như được sử dụng trong hướng dẫn này, chỉ khả thi trong trường hợp MCAR và MAR đơn giản. Ngay cả khi đó, nó cũng có một nhược điểm đã biết: việc điền vào mọi khoảng trống bằng cùng một số sẽ làm giảm phương sai của cột và làm suy yếu mối tương quan của nó với mọi thứ khác. Đối với công việc nghiêm túc, hãy sử dụng phương pháp dựa trên mô hình như gói mice, và luôn giữ một cột cờ để đánh dấu các giá trị đã được gán.
đột biến ()
mutate() là dplyr Động từ này tạo ra một biến mới hoặc ghi đè lên một biến hiện có, do đó nó là công cụ tự nhiên để tạo một bản sao đã được làm sạch của một cột.
Chúng ta sẽ tiến hành theo hai phần. Chúng ta sẽ học cách:
- loại trừ các giá trị bị thiếu khỏi khung dữ liệu
- gán các giá trị còn thiếu bằng giá trị trung bình và trung vị
Động từ mutate() rất dễ sử dụng. Chúng ta có thể tạo một biến mới theo cú pháp sau:
mutate(df, name_variable_1 = condition, ...) arguments: -df: Data frame used to create a new variable -name_variable_1: Name and the formula to create the new variable -...: No limit constraint. Possibility to create more than one variable inside mutate()
Loại bỏ các giá trị thiếu (NA)
na.omit() là một hàm cơ bản của R, không phải là một lệnh của dplyr, nhưng nó vẫn hoạt động trơn tru. Nó loại bỏ mọi hàng chứa ít nhất một giá trị NA. Đó là lựa chọn nhanh nhất và hiếm khi là lựa chọn tốt nhất, bởi vì chỉ cần một giá trị thiếu cũng đủ để loại bỏ toàn bộ quan sát.
Để giải quyết vấn đề thiếu quan sát, chúng tôi sẽ sử dụng bộ dữ liệu titanic. Trong tập dữ liệu này, chúng tôi có quyền truy cập vào thông tin của hành khách trên tàu trong thảm kịch. Tập dữ liệu này có nhiều NA cần được quan tâm.
Tải tệp CSV từ internet, sau đó liệt kê các cột chứa giá trị NA:
PATH <- "https://raw.githubusercontent.com/guru99-edu/R-Programming/master/test.csv" df_titanic <- read.csv(PATH, sep = ",") # Return the column names containing missing observations list_na <- colnames(df_titanic)[ apply(df_titanic, 2, anyNA) ] list_na
Đầu ra:
## [1] "age" "fare"
Ở đây,
colnames(df_titanic)[apply(df_titanic, 2, anyNA)]
Trả về tên của các cột chứa ít nhất một giá trị bị thiếu.
Cột tuổi và giá vé bị thiếu giá trị.
Chúng ta có thể loại bỏ chúng bằng na.omit().
library(dplyr) # Exclude the missing observations df_titanic_drop <-df_titanic %>% na.omit() dim(df_titanic_drop)
Đầu ra:
## [1] 1045 13
Tập dữ liệu mới chứa 1045 hàng so với 1309 hàng của tập dữ liệu gốc.
Điền dữ liệu thiếu bằng giá trị trung bình và trung vị.
Bạn cũng có thể điền vào các giá trị bị thiếu bằng giá trị trung bình hoặc trung vị. Một cách làm tốt là tạo hai biến riêng biệt cho giá trị trung bình và trung vị. Sau khi tạo xong, chúng ta có thể thay thế các giá trị bị thiếu bằng các biến mới được tạo.
Chúng ta sẽ sử dụng phương thức apply để tính giá trị trung bình của cột với NA. Hãy xem một ví dụ
Bước 1) Trước đó trong hướng dẫn, chúng tôi đã lưu trữ tên cột có giá trị bị thiếu trong danh sách có tên list_na. Chúng tôi sẽ sử dụng danh sách này
Bước 2) Tính giá trị trung bình với đối số na.rm = TRUE. Đối số này là bắt buộc vì các cột có dữ liệu bị thiếu, và nó cho R biết cần bỏ qua các dữ liệu đó.
# Create mean average_missing <- apply(df_titanic[,colnames(df_titanic) %in% list_na], 2, mean, na.rm = TRUE) average_missing
Code Giải thích:
Chúng ta truyền 4 đối số trong phương thức apply.
- df: df_titanic[,colnames(df_titanic) %in% list_na]. Mã này sẽ trả về tên cột từ đối tượng list_na (tức là “tuổi” và “giá vé”)
- 2: Tính hàm trên các cột
- ý nghĩa: Tính giá trị trung bình
- na.rm = TRUE: Bỏ qua các giá trị còn thiếu
Đầu ra:
## age fare ## 29.88113 33.29548
Chúng tôi đã tạo thành công giá trị trung bình của các cột chứa các quan sát bị thiếu. Hai giá trị này sẽ được sử dụng để thay thế các quan sát còn thiếu.
Bước 3) Thay thế các giá trị NA
Động từ mutate từ thư viện dplyr rất hữu ích trong việc tạo một biến mới. Chúng tôi không nhất thiết muốn thay đổi cột ban đầu để có thể tạo một biến mới mà không cần NA. mutate rất dễ sử dụng, chúng ta chỉ cần chọn tên biến và xác định cách tạo biến này. Đây là mã hoàn chỉnh
# Create a new variable with the mean and median df_titanic_replace <- df_titanic %>% mutate(replace_mean_age = ifelse(is.na(age), average_missing[1], age), replace_mean_fare = ifelse(is.na(fare), average_missing[2], fare))
Code Giải thích:
Chúng tôi tạo hai biến, thay thế_mean_age và thay thế_mean_fare như sau:
- thay thế_mean_age = ifelse(is.na(age), Average_missing[1], age)
- thay thế_mean_fare = ifelse(is.na(giá vé), Average_missing[2], giá vé)
Nếu cột tuổi thiếu giá trị thì thay thế bằng phần tử đầu tiên của Average_missing (tuổi trung bình), nếu không thì giữ nguyên giá trị ban đầu. Logic tương tự cho giá vé
sum(is.na(df_titanic_replace$age))
Đầu ra:
## [1] 263
Sau khi thay thế, cột mới không còn giá trị nào bị thiếu:
sum(is.na(df_titanic_replace$replace_mean_age))
Đầu ra:
## [1] 0
Cột tuổi ban đầu có 263 giá trị bị thiếu, trong khi cột replace_mean_age mới đã điền vào tất cả các giá trị đó bằng tuổi trung bình.
Bước 4) Chúng ta cũng có thể thay thế các quan sát còn thiếu bằng giá trị trung vị.
median_missing <- apply(df_titanic[,colnames(df_titanic) %in% list_na], 2, median, na.rm = TRUE) df_titanic_replace <- df_titanic %>% mutate(replace_median_age = ifelse(is.na(age), median_missing[1], age), replace_median_fare = ifelse(is.na(fare), median_missing[2], fare)) head(df_titanic_replace)
Đầu ra:
Bước 5) Với tập dữ liệu lớn, phương pháp từng bước trở nên rườm rà. Hàm sapply() rút gọn toàn bộ quy trình thành một câu lệnh duy nhất, nhưng đổi lại là người dùng sẽ không bao giờ nhìn thấy các giá trị được gán.
sapply không tạo ra một khung dữ liệu, vì vậy chúng ta có thể gói hàm sapply() bên trong data.frame() để tạo đối tượng khung dữ liệu.
# Quick code to replace missing values with the mean df_titanic_impute_mean <- data.frame( sapply( df_titanic, function(x) ifelse(is.na(x), mean(x, na.rm = TRUE), x)))
Phương pháp điền dữ liệu hiện đại với replace_na() và across()
Các phương pháp apply() và sapply() nêu trên vẫn hoạt động, nhưng tidyr và dplyr hiện thể hiện các thao tác tương tự một cách an toàn hơn và dễ đọc hơn.
replace_na() cho các giá trị cố định. Hàm tidyr::replace_na() nhận vào một danh sách các cột được đặt tên và các cột thay thế cho chúng:
library(tidyr) df_titanic %>% replace_na(list(age = 29.88, fare = 33.30))
across() cho mỗi cột số. Đây là phương pháp thay thế trực tiếp, an toàn về kiểu dữ liệu cho lệnh sapply() một dòng, và không giống như sapply(), nó không bao giờ can thiệp vào các cột kiểu ký tự hoặc kiểu factor:
library(dplyr) # Mean-impute every numeric column df_titanic %>% mutate(across(where(is.numeric), ~ ifelse(is.na(.x), mean(.x, na.rm = TRUE), .x))) # Median-impute instead df_titanic %>% mutate(across(where(is.numeric), ~ ifelse(is.na(.x), median(.x, na.rm = TRUE), .x)))
⚠️ Vì sao phím tắt sapply() tiềm ẩn rủi ro: Ví dụ sapply() một dòng chạy quá nhiều lần. mỗi cột, bao gồm cả các cột kiểu ký tự và kiểu factor. Gọi hàm mean() trên các cột đó sẽ trả về NA kèm theo cảnh báo, và hàm sapply() sau đó sẽ chuyển đổi toàn bộ kết quả sang kiểu ký tự. Phiên bản across(where(is.numeric), …) ở trên hoàn toàn tránh được điều này.
coalesce() cho các cột dự phòng. Khi cột thứ hai có thể cung cấp giá trị bị thiếu, hàm coalesce() sẽ trả về mục không bị thiếu đầu tiên trong số các đối số của nó:
df %>% mutate(age_final = coalesce(age, age_estimated, 0))
Hãy ghi lại những gì bạn đã thay đổi. Thêm một cờ trước khi điền giá trị, để bất kỳ mô hình nào sau này cũng có thể học hỏi từ thực tế rằng giá trị đó bị thiếu:
df_titanic %>%
mutate(age_was_missing = is.na(age),
age = ifelse(is.na(age), mean(age, na.rm = TRUE), age))
Xử lý giá trị thiếu trong R: Tài liệu tham khảo về phương pháp
Bài hướng dẫn này sẽ đề cập đến ba phương pháp:
- Loại trừ tất cả các quan sát còn thiếu
- Đổ tội với ý nghĩa
- Tranh chấp với số trung vị
Bảng dưới đây tóm tắt các phương pháp phát hiện và loại bỏ:
| Thư viện | Mục tiêu | Code |
|---|---|---|
| cơ sở | Liệt kê các quan sát còn thiếu |
colnames(df)[apply(df, 2, anyNA)] |
| cơ sở | Xóa mọi hàng có chứa giá trị NA. |
na.omit(df) |
Việc tính toán bằng giá trị trung bình hoặc trung vị có thể được thực hiện theo hai cách
- Sử dụng áp dụng
- Sử dụng sapply
| Phương pháp | Chi Tiết | Ưu điểm | Nhược điểm |
|---|---|---|---|
| Từng bước áp dụng | Kiểm tra các cột bị thiếu, tính giá trị trung bình/trung vị, lưu trữ giá trị, thay thế bằng mutate() | Bạn có thể thấy giá trị trung bình hoặc trung vị được ước tính. | Thêm thời gian thực hiện. Có thể chậm với tập dữ liệu lớn |
| Cách nhanh chóng với sapply | Sử dụng sapply() và data.frame() để tự động tìm kiếm và thay thế các giá trị bị thiếu bằng giá trị trung bình/trung vị | Mã ngắn và nhanh | Các giá trị được gán không bao giờ được hiển thị. |




