Rộng và dài: cùng dữ liệu, hai hình dạng
Mọi câu hỏi về chuyển dạng dữ liệu đều quy về một khác biệt duy nhất, nên hãy cùng nhìn nó. Đây là một bộ dữ liệu nhỏ, doanh số theo quý của hai thành phố, được tạo ở cả hai dạng:
Thông tin giống hệt, hình dạng khác nhau. Dạng rộng có mỗi thành phố một hàng và mỗi quý một cột: quý nằm trong tên cột. Dạng dài có mỗi quan sát (thành phố × quý) một hàng, với quý bị hạ xuống thành một cột bình thường và mọi số đo nằm chung trong một cột sales.
Không dạng nào là "đúng": chúng phục vụ những mục đích khác nhau. Dạng rộng là thứ con người đọc và bảng tính xuất ra: gọn, dễ so sánh trong nháy mắt. Dạng dài là thứ các công cụ xử lý, và việc chuyển đổi giữa hai dạng gọi là pivot (hay reshape, cùng một nghĩa).
Vì sao dạng dài thắng thế khi phân tích
Hãy xem dạng rộng đã làm gì với khái niệm "quý": nó xé một biến thành các tên cột. Không gì có thể tính toán với tên cột. Ở dạng dài, quarter lại là dữ liệu, và mọi công cụ phân tích theo nhóm đều hoạt động:
aggregate(sales ~ quarter, ...)hoặcgroup_by(quarter): không thể làm ở dạng rộng, nơi mỗi quý là một cột riêng mà bạn phải xử lý thủ công (tóm tắt theo nhóm đều giả định dữ liệu dạng dài).- ggplot2 ánh xạ cột vào các thuộc tính thẩm mỹ:
aes(x = quarter, y = sales, color = city)cần ba cột đó tồn tại. Vẽ dữ liệu dạng rộng là một cuộc vật lộn liên tục; vẽ dữ liệu dạng dài chỉ cần một dòng. - Thêm Q3 là thêm hàng, không đổi cấu trúc, trong khi dạng rộng phải thêm một cột mới mà mọi bước xử lý phía sau đều phải biết đến.
Quy tắc thực tế: lưu trữ và phân tích ở dạng dài, trình bày ở dạng rộng. Đó là lý do hai phép chuyển đổi dưới đây được dùng nhiều đến vậy: dữ liệu đến từ bảng tính ở dạng rộng, được kéo dài ra để phân tích, rồi được trải rộng lại cho bảng cuối cùng trong báo cáo.
Rộng sang dài: pivot_longer()
Package tidyr (anh em của dplyr trong tidyverse) nắm giữ cách chuyển dạng hiện đại. Các đoạn code của nó ở đây là khối tĩnh, vì sandbox chỉ chạy base R. Kéo dài dữ liệu cần ba quyết định, mỗi quyết định là một đối số:
library(tidyr)
long <- pivot_longer(wide,
cols = c(Q1, Q2), # which columns to stack
names_to = "quarter", # new column that receives the old NAMES
values_to = "sales" # new column that receives the cell VALUES
)
Hãy đi qua từng bước: cols chỉ ra các cột bị gộp lại (cú pháp khoảng Q1:Q2 và các hàm hỗ trợ như starts_with("Q") cũng dùng được, rất tiện khi có hai mươi cột). Mỗi ô được chọn trở thành một hàng; tên cột chứa nó đi vào quarter, còn con số đi vào sales. Những cột không được liệt kê trong cols (ở đây là city) được coi là định danh và được lặp lại theo các hàng. Kết quả chính xác là data frame long đã in ở trên.
Dài sang rộng: pivot_wider()
Chiều ngược lại cần hai quyết định: tên cột mới lấy từ đâu, và lấy gì để điền vào:
wide <- pivot_wider(long,
names_from = quarter, # distinct values here become new columns
values_from = sales # these values fill the cells
)
Mỗi giá trị khác nhau của quarter (Q1, Q2) trở thành một cột; mỗi ô được điền bằng giá trị sales từ hàng khớp với thành phố và quý đó. Các cột còn lại (city) đóng vai trò định danh hàng: mỗi tổ hợp khác nhau cho một hàng kết quả. Thành phố nào thiếu một quý sẽ nhận NA ở ô đó (đối số values_fill thay bằng giá trị khác, ví dụ values_fill = 0 cho dữ liệu đếm).
Bạn cũng sẽ gặp thế hệ trước trong các bài hướng dẫn cũ: spread() là pivot_wider() và gather() là pivot_longer(); chúng đã bị thay thế từ tidyr 1.0, vẫn chạy được, nhưng không đáng học kỹ ngoài việc nhận ra chúng.
Base R có reshape(): một lời cảnh báo thẳng thắn
Base R có thể làm việc này mà không cần package, bằng reshape(), một hàm nổi tiếng khó hiểu đến mức chính tài liệu của nó từng phải xin lỗi. Hàm được thiết kế quanh thuật ngữ của nghiên cứu theo chiều dọc (idvar, timevar, direction), tên các đối số khớp một cách gượng gạo với dữ liệu thường ngày, và ai cũng quên chúng giữa hai lần dùng. Nhưng nó vẫn hoạt động:
Để ý tên cột kết quả: sales.Q1, sales.Q2, tên cột giá trị bị ghép vào từng cột. Dùng tạm thì ổn, trong môi trường không có phụ thuộc nào, hoặc khi bạn gặp nó trong code cũ. Nhưng nếu bạn chuyển dạng dữ liệu nhiều hơn một lần mỗi năm, install.packages("tidyr") là lời khuyên thật lòng: đây là việc xử lý dữ liệu duy nhất mà công cụ của base R thực sự tốn công hơn cái phụ thuộc nó giúp bạn tránh.
Cạm bẫy khóa trùng lặp khi trải rộng
Trải rộng giả định rằng mỗi tổ hợp định danh + tên xác định một giá trị. Nếu Lima có hai hàng Q1, con số nào sẽ vào ô Q1 duy nhất? pivot_wider() từ chối đoán: nó đưa ra cảnh báo (values are not uniquely identified) và đặt một list-column vào ô đó, tức một data frame có list lồng bên trong, thứ sẽ làm hỏng bước tiếp theo bạn làm với nó.
Khi thấy cảnh báo đó, đừng vội dùng lối thoát values_fn; hãy hỏi vì sao lại có trùng lặp. Thường thì dữ liệu chi tiết hơn bạn nghĩ (các hàng theo ngày chứ không phải theo quý, nên hãy tóm tắt trước rồi mới trải rộng), hoặc một phép join ở bước trước đã nhân đôi các hàng. values_fn = mean (hoặc sum) chỉ là cách sửa hợp lệ khi bạn nói rõ được các bản trùng lặp nên được gộp theo phép tổng hợp nào. reshape() còn tệ hơn ở điểm này: nó âm thầm giữ bản trùng lặp đầu tiên và bỏ phần còn lại, chỉ kèm một cảnh báo rất dễ bị lướt qua.
Những điều cần nhớ
- Dạng rộng: biến ẩn trong tên cột, phù hợp để đọc. Dạng dài: mỗi quan sát một hàng, phù hợp để phân tích và dùng với ggplot2.
- Lưu trữ và phân tích ở dạng dài, trình bày ở dạng rộng.
pivot_longer(cols, names_to, values_to)xếp các cột thành hàng; các cột không được liệt kê trở thành định danh được lặp lại.pivot_wider(names_from, values_from)trải các hàng thành cột; chỗ trống được điềnNA.spread()/gather()là tên cũ;reshape()của base R chạy được nhưng nổi tiếng là khó dùng.- Các cặp định danh + tên bị trùng khiến việc trải rộng trở nên mơ hồ: hãy tóm tắt trước, đừng chỉ tắt cảnh báo.
Tiếp theo: đọc dữ liệu thật từ tệp với read.csv() và những điểm dễ vấp của nó.
Câu hỏi thường gặp
Dữ liệu dạng rộng và dạng dài trong R khác nhau thế nào?
Cùng dữ liệu, khác hình dạng. Dạng rộng trải một biến ra nhiều cột (ví dụ mỗi quý một cột), mỗi đối tượng một hàng, phù hợp để người đọc. Dạng dài xếp biến đó thành các hàng, mỗi quan sát một hàng, với một cột tên và một cột giá trị, phù hợp cho phân tích theo nhóm và ggplot2.
Làm sao để chuyển dữ liệu dạng rộng sang dạng dài trong R?
Hàm pivot_longer(df, cols = Q1:Q4, names_to = "quarter", values_to = "sales") của tidyr xếp chồng các cột được chọn thành hai cột mới: tên các cột cũ đi vào cột names_to, giá trị trong các ô đi vào cột values_to.
Làm sao để chuyển dữ liệu dạng dài sang dạng rộng trong R?
Hàm pivot_wider(df, names_from = quarter, values_from = sales) của tidyr trải các hàng thành cột: mỗi giá trị khác nhau của names_from trở thành một cột, được điền bằng giá trị values_from tương ứng. Những tổ hợp bị thiếu sẽ thành NA.
Hàm nào đã thay thế spread và gather trong R?
pivot_wider() thay thế spread() và pivot_longer() thay thế gather() từ tidyr 1.0 (2019). Các hàm cũ vẫn chạy được, và bạn sẽ gặp chúng trong các bài hướng dẫn cũ, nhưng mọi code mới nên dùng cặp pivot_*, vốn có đối số rõ ràng hơn và nhiều tính năng hơn.