-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy path04_data_manipulation.qmd
More file actions
224 lines (167 loc) · 9.06 KB
/
04_data_manipulation.qmd
File metadata and controls
224 lines (167 loc) · 9.06 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# Tư duy thao tác dữ liệu trong R
```{r}
#| message: false
library(knitr)
opts_chunk$set(
message = FALSE
)
```
## 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.
```{r}
library(dplyr)
library(tidyr)
set.seed(0)
n <- 1000
d <- data.frame(
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)
)
d %>% head(5) %>% kable()
```
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.
## 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.
## 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.
### Bước 1: Reshaping
```{r}
d_long <- d %>%
select(gioi, do_tuoi, hailong) %>%
pivot_longer(cols = c(gioi, do_tuoi), names_to = "variable")
d_long %>% head() %>% kable()
```
### Bước 2: Grouping và Aggregation
```{r}
d_agg <- d_long %>%
group_by(variable, value) %>%
summarize(
mean = mean(hailong),
sd = sd(hailong)
)
d_agg %>% kable()
```
### Bước 3: Transformation
```{r}
d_agg %>%
mutate(
mean_sd = sprintf("%.1f (%.1f)", mean, sd)
) %>%
select(variable, value, mean_sd) %>%
kable()
```
## 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.
```{r}
library(rlang)
get_mean_sd <- function(d, group_vars, outcome_var) {
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)
}
group_vars <- c("gioi", "do_tuoi")
outcome_var <- "hailong"
get_mean_sd(d, group_vars, outcome_var) %>% kable()
```
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.
```{r}
library(purrr)
outcome_vars <- c("hailong", "qol")
map_df(outcome_vars, ~ get_mean_sd(d, group_vars, .x)) %>% kable()
```
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.
```{r}
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()
```
Và bạn có thể gói tiếp chức năng này trong một hàm như sau:
```{r}
get_mean_sd_all <- function(d, group_vars, outcome_vars) {
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()
```
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é.
```{r}
#| eval: false
library(dplyr)
library(tidyr)
library(rlang)
library(purrr)
# Tính mean (SD) cho một biến
get_mean_sd <- function(d, group_vars, outcome_var) {
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
get_mean_sd_all <- function(d, group_vars, outcome_vars) {
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)
}
group_vars <- c("gioi", "do_tuoi")
outcome_vars <- c("hailong", "qol")
get_mean_sd_all(d, group_vars, outcome_vars)
```