library(knitr)
$set(
opts_chunkmessage = FALSE
)
4 Tư duy thao tác dữ liệu trong R
4.1 Bài toán
Hôm nay mình sẽ giới thiệu với các bạn một bài toán phân tích số liệu đơn giản trong R. Bài toán này nhằm giúp các bạn hiểu rõ hơn cách thức tư duy khi giải quyết một bài toán bằng lập trình.
Chúng ta có một bộ số liệu với 4 biến là stt
(số thứ tự), gioi
(giới: Nam, Nữ), do_tuoi
(độ tuổi: <18, 18-45, >45), hailong
(điểm hài lòng, từ 0 đến 100), và qol
(điểm chất lượng cuộc sống, từ 0 đến 100). Đây là một bộ số liệu do mình tạo ra ngẫu nhiên ra thôi.
library(dplyr)
library(tidyr)
set.seed(0)
<- 1000
n
<- data.frame(
d stt = seq(n),
gioi = factor(sample(c(1, 2), n, TRUE),
levels = c(1, 2), labels = c("Nam", "Nu")),
do_tuoi = factor(sample(c(1, 2, 3), n, TRUE),
levels = c(1, 2, 3), labels = c("<18", "18-45", ">45")),
hailong = round(runif(n, 0, 100), 1),
qol = round(runif(n, 0, 100), 1)
)
%>% head(5) %>% kable() d
stt | gioi | do_tuoi | hailong | qol |
---|---|---|---|---|
1 | Nu | >45 | 33.0 | 76.3 |
2 | Nam | >45 | 69.7 | 23.6 |
3 | Nu | >45 | 35.4 | 28.6 |
4 | Nam | <18 | 40.6 | 31.8 |
5 | Nam | >45 | 30.8 | 92.2 |
Việc của chúng ta sẽ là tạo ra một bảng phân tích kết quả trông như sau:
Đặc điểm | Nhóm | Hài lòng, mean (SD) |
---|---|---|
Giới | Nam | … |
Giới | Nữ | … |
Độ tuổi | <18 | … |
Độ tuổi | 18-45 | … |
Độ tuổi | >45 | … |
Có nhiều cách để làm việc này. Cách mà mình giới thiệu hôm nay khá trực tiếp, mặc dù có thể không phải là cách tối ưu.
4.2 Các phép thao tác với số liệu
Có 4 phép thao tác (manipulate) số liệu chính:
- Tái cấu trúc (reshaping): chuyển số liệu từ dạng bảng dài sang dạng bảng ngang và ngược lại (pivot giữa long / wide data), xếp chồng các số liệu lên nhau (stacking / unstacking), v.v.. Thư viện sử dụng cho reshaping là
tidyr
. - Nhóm (grouping): các số liệu thuộc cùng một nhóm được xếp chung với nhau để phục vụ một mục đích nào đó. Bạn chắc đã làm quen với hàm
dplyr::group_by()
cho việc này. - Chuyển dạng (transformation): chuyển số liệu cá thể thành các giá trị mới dựa trên một phép biến đổi nào đó như chuẩn hóa (normalization), logarit, chia nhóm (categorization), v.v.. Mọi phép chuyển dạng đều thông qua hàm
dplyr::mutate()
và các biến thể của nó. - Tổng hợp (aggregation): tính toán các chỉ số tổng hợp (trung bình, tỉ lệ phần trăm, v.v.) từ số liệu cá thể. Hầu hết các phép tổng hợp đều thông qua hàm
dplyr::summarize()
và các biến thể của nó.
Bằng những phép thao tác số liệu này, chúng ta có thể tạo ra mọi kết quả mong muốn từ một bộ số liệu gốc.
4.3 Tư duy thao tác số liệu
Nhìn vào bộ số liệu gốc, mình nghĩ rằng sẽ cần tạo ra một (hoặc nhiều) bộ số liệu trung gian để phục vụ việc tính toán như trên. Bộ số liệu trung gian sẽ có cấu trúc như thế nào? Quan sát bảng phân tích kết quả, chúng ta thấy rằng:
- Cột “Đặc điểm” là tên các biến mà chúng ta có trong bộ số liệu gốc.
- Cột “Nhóm” là các giá trị của các biến “Đặc điểm” có trong bộ số liệu gốc.
- Cột “Hài lòng” là kết quả tổng hợp của điểm hài lòng trong bộ số liệu gốc.
Vậy bộ số liệu trung gian của mình có thể là kết quả chuyển từ dạng ngang (các biến xếp thành từng cột) sang dạng dài (các biến xếp chồng lên nhau) của hai biến gioi
và do_tuoi
, còn giữ lại biến hailong
. Ví dụ:
stt | variable | value | hailong |
---|---|---|---|
1 | gioi | Nu | 33.0 |
1 | do_tuoi | >45 | 33.0 |
2 | gioi | Nam | 69.7 |
2 | do_tuoi | >45 | 69.7 |
Sau đó mình chỉ việc tạo ra các nhóm của Đặc điểm (variable
) và Nhóm (value
) để tổng hợp (aggregate) cột hailong
. Hãy cùng xem chúng ta thực thi kế hoạch này.
4.3.1 Bước 1: Reshaping
<- d %>%
d_long select(gioi, do_tuoi, hailong) %>%
pivot_longer(cols = c(gioi, do_tuoi), names_to = "variable")
%>% head() %>% kable() d_long
hailong | variable | value |
---|---|---|
33.0 | gioi | Nu |
33.0 | do_tuoi | >45 |
69.7 | gioi | Nam |
69.7 | do_tuoi | >45 |
35.4 | gioi | Nu |
35.4 | do_tuoi | >45 |
4.3.2 Bước 2: Grouping và Aggregation
<- d_long %>%
d_agg group_by(variable, value) %>%
summarize(
mean = mean(hailong),
sd = sd(hailong)
)
%>% kable() d_agg
variable | value | mean | sd |
---|---|---|---|
do_tuoi | <18 | 49.97685 | 28.50213 |
do_tuoi | 18-45 | 50.87988 | 29.54522 |
do_tuoi | >45 | 49.84675 | 28.96249 |
gioi | Nam | 49.64345 | 29.21245 |
gioi | Nu | 50.84234 | 28.77515 |
4.3.3 Bước 3: Transformation
%>%
d_agg mutate(
mean_sd = sprintf("%.1f (%.1f)", mean, sd)
%>%
) select(variable, value, mean_sd) %>%
kable()
variable | value | mean_sd |
---|---|---|
do_tuoi | <18 | 50.0 (28.5) |
do_tuoi | 18-45 | 50.9 (29.5) |
do_tuoi | >45 | 49.8 (29.0) |
gioi | Nam | 49.6 (29.2) |
gioi | Nu | 50.8 (28.8) |
4.4 Module hóa công việc
Như ở trên, bạn đã thấy chúng ta thống kê được mean (SD) của điểm hài lòng. Nhưng nếu chúng ta muốn làm tương tự như vậy với điểm chất lượng cuộc sống và gộp chung kết quả với điểm hài lòng thì bạn sẽ làm thế nào? Tất nhiên, bạn hoàn toàn có thể thêm tạo ra các biến mean_qol
và sd_qol
cho điểm chất lượng cuộc sống trong Bước 2, nhưng nếu không phải là 2 biến mà là 20 biến, thì việc đó sẽ rất phiền toái, hoặc nếu bạn phải thay đổi kế hoạch phân tích, loại bỏ biến qol
và thêm biến khác vào. Đây là lúc bạn cần dùng đến hàm, và chúng ta gọi đây là module hóa công việc.
Ba bước ở trên có thể được tóm gọn trong một hàm như sau.
library(rlang)
<- function(d, group_vars, outcome_var) {
get_mean_sd %>%
d # Bước 1
select(all_of(c(group_vars, outcome_var))) %>%
pivot_longer(cols = all_of(group_vars), names_to = "variable") %>%
# Bước 2
group_by(variable, value) %>%
summarize(
mean = mean(!!sym(outcome_var)),
sd = sd(!!sym(outcome_var))
%>%
)
# Bước 3
mutate(
outcome = outcome_var,
mean_sd = sprintf("%.1f (%.1f)", mean, sd)
%>%
) select(variable, value, outcome, mean_sd)
}
<- c("gioi", "do_tuoi")
group_vars <- "hailong"
outcome_var get_mean_sd(d, group_vars, outcome_var) %>% kable()
variable | value | outcome | mean_sd |
---|---|---|---|
do_tuoi | <18 | hailong | 50.0 (28.5) |
do_tuoi | 18-45 | hailong | 50.9 (29.5) |
do_tuoi | >45 | hailong | 49.8 (29.0) |
gioi | Nam | hailong | 49.6 (29.2) |
gioi | Nu | hailong | 50.8 (28.8) |
Và chúng ta có thể tự động hóa việc tính toán này cho nhiều biến kết cục khác nhau.
library(purrr)
<- c("hailong", "qol")
outcome_vars map_df(outcome_vars, ~ get_mean_sd(d, group_vars, .x)) %>% kable()
variable | value | outcome | mean_sd |
---|---|---|---|
do_tuoi | <18 | hailong | 50.0 (28.5) |
do_tuoi | 18-45 | hailong | 50.9 (29.5) |
do_tuoi | >45 | hailong | 49.8 (29.0) |
gioi | Nam | hailong | 49.6 (29.2) |
gioi | Nu | hailong | 50.8 (28.8) |
do_tuoi | <18 | qol | 49.3 (30.0) |
do_tuoi | 18-45 | qol | 48.7 (30.4) |
do_tuoi | >45 | qol | 49.3 (30.7) |
gioi | Nam | qol | 49.9 (29.7) |
gioi | Nu | qol | 48.4 (31.0) |
Tất nhiên, nếu bạn muốn chuyển sang dạng nhiều cột kết quả của các biến kết cục thì cũng rất đơn giản, nó chỉ là pivot từ dạng long sang wide thôi.
map_df(outcome_vars, ~ get_mean_sd(d, group_vars, .x)) %>%
pivot_wider(id_cols = c(variable, value),
names_from = outcome, values_from = mean_sd) %>%
kable()
variable | value | hailong | qol |
---|---|---|---|
do_tuoi | <18 | 50.0 (28.5) | 49.3 (30.0) |
do_tuoi | 18-45 | 50.9 (29.5) | 48.7 (30.4) |
do_tuoi | >45 | 49.8 (29.0) | 49.3 (30.7) |
gioi | Nam | 49.6 (29.2) | 49.9 (29.7) |
gioi | Nu | 50.8 (28.8) | 48.4 (31.0) |
Và bạn có thể gói tiếp chức năng này trong một hàm như sau:
<- function(d, group_vars, outcome_vars) {
get_mean_sd_all map_df(outcome_vars, ~ get_mean_sd(d, group_vars, .x)) %>%
pivot_wider(id_cols = c(variable, value),
names_from = outcome, values_from = mean_sd)
}
get_mean_sd_all(d, group_vars, outcome_vars) %>% kable()
variable | value | hailong | qol |
---|---|---|---|
do_tuoi | <18 | 50.0 (28.5) | 49.3 (30.0) |
do_tuoi | 18-45 | 50.9 (29.5) | 48.7 (30.4) |
do_tuoi | >45 | 49.8 (29.0) | 49.3 (30.7) |
gioi | Nam | 49.6 (29.2) | 49.9 (29.7) |
gioi | Nu | 50.8 (28.8) | 48.4 (31.0) |
Những tính năng thuộc về lập trình cho dplyr
và purrr
như dấu chấm than kép (!!
), hàm rlang::sym()
, hàm dplyr::all_of()
, và hàm purrr:map_df()
mình sẽ giới thiệu cụ thể trong một bài khác. Chúng ta sẽ dừng lại bài này ở đây, vì hi vọng bạn đã hiểu rõ hơn cách chúng ta tư duy khi lập trình để thao tác với số liệu. Mình tổng hợp lại kết quả ở dưới đây nhé.
library(dplyr)
library(tidyr)
library(rlang)
library(purrr)
# Tính mean (SD) cho một biến
<- function(d, group_vars, outcome_var) {
get_mean_sd %>%
d # Bước 1
select(all_of(c(group_vars, outcome_var))) %>%
pivot_longer(cols = all_of(group_vars), names_to = "variable") %>%
# Bước 2
group_by(variable, value) %>%
summarize(
mean = mean(!!sym(outcome_var)),
sd = sd(!!sym(outcome_var))
%>%
)
# Bước 3
mutate(
outcome = outcome_var,
mean_sd = sprintf("%.1f (%.1f)", mean, sd)
%>%
) select(variable, value, outcome, mean_sd)
}
# Tính mean (SD) cho tất cả các biến
<- function(d, group_vars, outcome_vars) {
get_mean_sd_all map_df(outcome_vars, ~ get_mean_sd(d, group_vars, .x)) %>%
pivot_wider(id_cols = c(variable, value),
names_from = outcome, values_from = mean_sd)
}
<- c("gioi", "do_tuoi")
group_vars <- c("hailong", "qol")
outcome_vars get_mean_sd_all(d, group_vars, outcome_vars)