HocThuatSo.comHọc Thuật Số
Gửi thư
R cho nghiên cứu

dplyr và tidyr: làm sạch dữ liệu khảo sát bằng chuỗi lệnh đọc được

Làm sạch một bảng khảo sát giả định bằng dplyr và tidyr: lọc phiếu, đổi mã 99 thành thiếu, đảo câu nghịch, tính điểm thang đo, nhóm tuổi, chuyển dạng dài và rộng.

dplyr và tidyr: làm sạch dữ liệu khảo sát bằng chuỗi lệnh đọc được

Làm sạch dữ liệu khảo sát bằng dplyr và tidyr nghĩa là viết từng bước thành một chuỗi lệnh nối bằng |>: lọc phiếu hợp lệ, đổi mã thiếu, đảo câu nghịch, tính điểm thang đo, rồi chuyển dạng dài hoặc rộng tuỳ phân tích. Đọc chuỗi lệnh là thấy được đã làm gì với dữ liệu.

Ví dụ dưới đây dùng một bảng khảo sát giả định về mức gắn bó công việc của giảng viên: cột id, dong_y (có đồng ý tham gia), tuoi, tinh gõ tay, năm câu Likert q1 đến q5 thang 1 đến 5, trong đó q3 là câu nghịch và 99 là “không trả lời”. Mã đã chạy thử với dplyr 1.2 và tidyr 1.3.

Sáu động từ dùng nhiều nhất

Hầu hết việc làm sạch xoay quanh một nhóm hàm nhỏ:

HàmLàm gìVí dụ trên bảng khảo sát
filter()Giữ dòng thoả điều kiệnGiữ phiếu có dong_y == 1
select()Chọn, đổi tên, sắp cộtBỏ cột thời gian nộp phiếu
mutate()Tạo hoặc sửa cộtĐảo mã q3, tính điểm trung bình
case_when()Gán giá trị theo nhiều điều kiệnChia nhóm tuổi
summarise() với .byTóm tắt theo nhómĐiểm trung bình theo tỉnh
pivot_longer(), pivot_wider()Chuyển dạng rộng sang dài và ngược lạiMỗi câu trả lời thành một dòng để vẽ hình

Chuỗi làm sạch từ đầu tới cuối

Đây là khung một kịch bản R/01-lam-sach.R, mỗi bước một dòng để dễ đọc và dễ tắt khi cần dò lỗi:

  1. Lọc phiếu hợp lệ: ks <- tho |> filter(dong_y == 1, !is.na(id)) |> distinct(id, .keep_all = TRUE). Dòng distinct() bỏ phiếu nộp trùng theo id, giữ phiếu đầu tiên.
  2. Đổi mã thiếu cho cả nhóm câu: mutate(across(q1:q5, \(x) na_if(x, 99))). across() áp một hàm cho nhiều cột, tránh chép năm dòng gần giống nhau.
  3. Đảo câu nghịch trên thang 1 đến 5: mutate(q3 = 6 - q3). Công thức chung là lấy tổng điểm nhỏ nhất và lớn nhất của thang trừ đi giá trị.
  4. Tính điểm thang đo kèm số câu đã trả lời: mutate(so_cau = rowSums(!is.na(pick(q1:q5))), gan_bo = if_else(so_cau >= 4, rowMeans(pick(q1:q5), na.rm = TRUE), NA)). Quy tắc “trả lời ít nhất 4 trên 5 câu mới tính điểm” phải ghi vào phần phương pháp.
  5. Chia nhóm tuổi: mutate(nhom_tuoi = case_when(tuoi < 35 ~ 'Dưới 35', tuoi < 50 ~ '35–49', tuoi >= 50 ~ '50 trở lên', .default = NA)). Các điều kiện được xét theo thứ tự từ trên xuống, dòng nào khớp trước thì lấy.
  6. Lưu kết quả: saveRDS(ks, here::here('du-lieu', 'xu-ly', 'khaosat-sach.rds')).

Khi cần soát, đặt |> print() hoặc |> count(nhom_tuoi) tạm sau bước nghi ngờ, chạy tới đó, xem, rồi xoá đi.

Đổi mã biến chữ gõ tay

Chuẩn hoá trước, đổi mã sau

Cột tinh gõ tay luôn có “Hà Nội”, “ha noi”, “ Hà nội” và “HN”. Bước đầu là gọt khoảng trắng và thống nhất chữ hoa: stringr::str_squish() bỏ khoảng trắng thừa. Cẩn thận với str_to_title(): nó biến “TP.HCM” thành “Tp.hcm”, nên chỉ dùng khi đã xem qua danh sách giá trị.

Bảng đổi mã rõ ràng

Với dplyr 1.2 trở lên, recode_values() cho viết từng cặp “giá trị cũ ~ giá trị mới”: mutate(tinh = recode_values(tinh, 'HN' ~ 'Hà Nội', 'ha noi' ~ 'Hà Nội', default = tinh)). Bản cũ hơn dùng case_match() với cú pháp tương tự hoặc case_when(). Khi danh sách dài, lưu bảng đổi mã ra một tệp CSV hai cột rồi nối bằng left_join(), như vậy người không viết mã cũng soát được.

Khi nào cần dạng dài, khi nào cần dạng rộng?

Bảng khảo sát thường ở dạng rộng: mỗi người một dòng, mỗi câu một cột. Dạng này hợp cho hồi quy, tính độ tin cậy và xuất sang SPSS. Dạng dài, mỗi cặp người – câu là một dòng, hợp cho vẽ phân bố từng câu bằng ggplot2, tính tỷ lệ chọn từng mức, và cho mô hình đa mức với số đo lặp.

Chuyển sang dài: dai <- ks |> select(id, q1:q5) |> pivot_longer(q1:q5, names_to = 'cau', values_to = 'diem'). Tỷ lệ chọn từng mức theo câu: dai |> count(cau, diem) |> mutate(ty_le = n / sum(n), .by = cau). Chuyển ngược: pivot_wider(names_from = cau, values_from = diem).

Với dữ liệu đo nhiều đợt có tên cột dạng diem_t1, diem_t2, tham số names_sep = '_' cùng names_to = c('bien', 'dot') tách tên cột thành hai cột thông tin.

Đổi tên cột hàng loạt mà vẫn giữ được bảng mã

Tệp xuất từ biểu mẫu trực tuyến thường có tên cột là nguyên câu hỏi tiếng Việt, có dấu, có khoảng trắng và dấu chấm hỏi. Gõ những tên đó trong mã vừa dài vừa dễ sai. janitor::clean_names() đổi một loạt về dạng chữ thường, không dấu, nối bằng gạch dưới: “Họ tên” thành ho_ten, “Điểm TB” thành diem_tb.

Với câu hỏi dài, tên tự sinh vẫn quá dài. Cách gọn hơn là đặt tên theo bảng mã: names(tho) <- bang_ma$ten_ngan, với điều kiện thứ tự cột không đổi, và kiểm bằng stopifnot(ncol(tho) == nrow(bang_ma)). Dùng rename_with() khi chỉ muốn đổi một nhóm cột theo quy tắc, ví dụ thêm tiền tố cho mọi cột bắt đầu bằng q.

Đừng vứt câu hỏi gốc. Gắn nó làm nhãn biến bằng labelled::var_label() ngay sau khi đổi tên; khi vẽ hình hay lập bảng, nhãn đó được lấy ra làm tiêu đề đầy đủ.

Câu nhiều lựa chọn: tách một ô thành nhiều cột

Biểu mẫu trực tuyến thường ghép mọi phương án người trả lời đã chọn vào một ô, ví dụ cột kenh chứa “Thư viện;Google Scholar”. Muốn đếm mỗi kênh được bao nhiêu người chọn, cần mỗi phương án một cột 0/1.

Với tidyr 1.3: ks |> select(id, kenh) |> separate_longer_delim(kenh, delim = ';') |> mutate(kenh = stringr::str_squish(kenh), co = 1L) |> pivot_wider(names_from = kenh, values_from = co, values_fill = 0L). Bước đầu tách mỗi phương án thành một dòng, bước sau trải ra thành cột, ô không chọn điền 0. Nối kết quả về bảng chính bằng left_join(by = 'id').

Chọn ký tự ngăn cách cẩn thận. Google Forms ngăn bằng dấu phẩy và khoảng trắng, nên một phương án có dấu phẩy bên trong sẽ bị tách đôi. Cách chắc nhất là đổi chữ phương án sang mã ngắn ngay khi thiết kế phiếu, hoặc thay các phương án có dấu phẩy bằng mã trước khi tách.

Bảng tóm tắt để soát sau khi làm sạch

Trước khi phân tích, in một bảng tóm tắt để soát bằng mắt. ks |> summarise(n = n(), thieu_diem = sum(is.na(gan_bo)), tb = mean(gan_bo, na.rm = TRUE), .by = nhom_tuoi) cho biết mỗi nhóm có bao nhiêu người, bao nhiêu người không đủ câu để tính điểm, và điểm trung bình. Tham số .by thay cho cặp group_by() và ungroup(), không để lại bảng còn nhóm gây bất ngờ ở bước sau.

So các con số với những gì bạn biết về mẫu: tổng số phiếu bằng số người đồng ý tham gia trừ phiếu trùng, không có ai dưới tuổi tối thiểu, điểm nằm trong khoảng 1 đến 5. Một bảng soát như vậy trong phụ lục hoặc tài liệu bổ sung cũng giúp người phản biện tin hơn vào khâu xử lý.

Lỗi hay gặp trong chuỗi lệnh

Phần lớn lỗi không làm R báo đỏ mà lặng lẽ cho ra số sai. Những chỗ đáng soát:

  • Dùng mean(c(q1, q2, q3)) trong mutate(): ra một con số chung cho cả cột chứ không phải trung bình theo từng người. Dùng rowMeans(pick(...)).
  • Đảo mã hai lần vì chạy lại một phần kịch bản trên đối tượng đã đổi. Luôn tạo bảng mới từ bảng thô, không ghi đè cùng tên rồi chạy từng đoạn.
  • filter(x != 99) cũng loại luôn các dòng NA của x, vì so sánh với NA cho NA. Muốn loại đúng dòng có 99, dùng filter_out(x == 99) (từ dplyr 1.2) hoặc viết rõ filter(is.na(x) | x != 99).
  • Quên .default trong case_when(): mọi trường hợp không khớp thành NA, có thể đúng ý, có thể không.
  • Nối bảng bằng left_join() mà khoá bên phải bị trùng: số dòng tăng lên. Thêm relationship = 'many-to-one' để R báo lỗi ngay khi khoá trùng.

Cuối kịch bản nên có vài dòng kiểm tra tự động như stopifnot(n_distinct(ks$id) == nrow(ks), all(ks$q3 %in% c(1:5, NA))). Điều kiện sai là kịch bản dừng, không để số sai đi tiếp vào bảng kết quả.

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

Nên dùng |> hay %>%?

Ống |> có sẵn trong R từ bản 4.1, không cần nạp gói. %>% của magrittr vẫn chạy tốt. Trong một dự án nên thống nhất dùng một loại để mã dễ đọc.

across có dùng được với summarise không?

Được. Ví dụ summarise(across(q1:q5, \(x) mean(x, na.rm = TRUE)), .by = tinh) tính trung bình từng câu theo tỉnh trong một dòng lệnh.

Có nên thay giá trị thiếu bằng trung bình cho gọn không?

Không nên làm tự động. Cách xử lý giá trị thiếu là quyết định phương pháp, cần lý do và ghi vào bài. Trong khâu làm sạch chỉ nên đổi mã thiếu về NA cho đúng.

Làm sao biết bước nào làm thay đổi số dòng?

Chạy chuỗi tới từng bước rồi dùng nrow, hoặc chèn tạm count sau bước nghi ngờ. Gói tidylog in ra số dòng bị lọc hoặc thêm sau mỗi lệnh nếu muốn tự động.

Dữ liệu lớn thì dplyr có chậm không?

Với vài trăm nghìn dòng khảo sát, dplyr đủ nhanh. Dữ liệu hàng chục triệu dòng có thể dùng data.table, hoặc duckdb và arrow mà vẫn viết bằng cú pháp dplyr.

Cần tư vấn cụ thể cho trường hợp của bạn?

Chúng tôi sẽ liên hệ trong vòng 24 giờ.

Đăng ký tư vấn ngay

Bài viết liên quan

🧭
Bạn đang ở chặng nào của đường học vị?
Nhập chỗ bạn đang đứng và đích bạn nhắm — công cụ trả về số năm, chi phí và việc phải làm từng chặng.
Xem lộ trình của tôi →
Miễn phí, không cần tài khoản. Xem tất cả công cụ

Bạn cần hỗ trợ thêm?

Để lại thông tin, chúng tôi sẽ liên hệ trong 24 giờ.

hoặc
[email protected]