Introduction

Bài giảng này giới thiệu một số hàm cơ bản của thư viện dplyr cho mục đích làm sạch - chuẩn bị dữ liệu. Các hàm cơ bản này cover hầu hết các tình huống thường gặp trong thực tế về làm sạch và chuẩn bị dữ liệu với một bộ dữ liệu từ Department of Health Economic - HMU.

Bộ dữ liệu là một file SPSS và để đọc bộ dữ liệu này vào R chúng ta sử dụng hàm read_sav() của thư viện haven như sau:

# Clear R Environment: 
rm(list = ls()) # Should use this command at the beginning of any project. 

# Load haven package: 
library(haven)

# Import data: 
stigma_data <- read_sav("C:/Users/Admin/Documents/[ver1.3]NC2.QOJ.Stigma.sav")

Lúc này ở cửa sổ có tên Global Environment ở bên phải, góc phía trên sẽ hiển thị một object có tên là stigma_data. Object này, theo cách gọi của R Users, là một Data Frame. Đến đây chúng ta có thể bắt đầu “chơi” với bộ dữ liệu này.

Warm Up

Tuy nhiên, trước khi chơi với dữ liệu này người học PHẢI (chữ này được được viết hoa với hàm ý nhấn mạnh) nắm được một số luật chơi cơ bản của ngôn ngữ R như sau:

  1. Trước hết là các quy tắc trình bày codes đúng cách. R Codes viết những phải đúng cú pháp (syntax) mà còn phải tuân thủ cách trình bày đúng như người viết văn phải tuân thủ các quy tắc chính tả vậy. Những quy tắc này người học phải TỰ học/tham khảo tại đây.

  2. Không sử dụng lệnh attach(). Một số tài liệu về R (như cuốn của tác giả NVT) thường xuyên sử dụng lệnh này. Cá nhân tôi lại cho rằng việc sử dụng lệnh này “làm hỏng” cách tư duy và làm việc với các ngôn ngữ OPP như R.

  3. Sử dụng phép gán. Phép gán của ngôn ngữ R có hai cách biểu diễn. Cách thứ nhất là sử dụng nũi tên đi từ phải sang trái, kí hiệu <- như sau:

# An example of assignment - method 1: 
stigma_data <- read_sav("C:/Users/Admin/Documents/[ver1.3]NC2.QOJ.Stigma.sav")

Hoặc sử dụng mũi tên theo chiều ngược lại (cách thứ hai) như sau:

# An example of assignment - method 2: 
read_sav("C:/Users/Admin/Documents/[ver1.3]NC2.QOJ.Stigma.sav") -> stigma_data
  1. Sử dụng toán tử pipe (pipe operator, kí hiệu %>%). Để minh họa ý nghĩa của toán tử này chúng ta xét bài toán đơn giản sau: tính giá trị của hàm y = tan(sin(x)) khi x = 10.

Để tính y khi x = 10 chúng ta có thể làm tuần tự từng bước như sau:

# Define x: 
x <- 10

# Calculate sin(x): 

value_sin10 <- sin(x)

# Calculate value of y: 

y <- tan(value_sin10)

# Print final result: 

y
## [1] -0.6049088

Kết quả -0.6049088 này có thể được tính toán với một cách trình bày khác với toán tử Pipe như sau:

# You must load magrittr package for using pipe operator: 
library(magrittr)

# Calculate y by using pipe operator: 
y_by_pipe <- x %>% sin() %>% tan() 

# Alternative coding style with -> assignment: 
x %>% sin() %>% tan() -> y_by_pipe.  

Ở các máy tính sử dụng hệ điều hành Window thì phím tắt cho toán tử pipe là tổ hợp phím Ctrl + Shift + M. Còn phím tắt cho -> là tổ hợp phím Alt + -.

Kiêm tra và so sánh y với y_by_pipe:

y_by_pipe
## [1] -0.6049088

Đoạn mã lệnh x %>% sin() %>% tan() có thể được diễn giải như sau:

  • x là nguyên liệu đầu vào của hàm sin(), được biểu diễn bằng x %>% sin().
  • x %>% sin() tạo ra một kết quả, một giá trị nào đó và kết quả/giá trị này lại là đầu vào của hàm tan() và sẽ được kí hiệu là x %>% sin() %>% tan().

Nếu chúng ta muốn, chẳng hạn, lấy giá trị tuyệt đối của y thì:

x %>% sin() %>% tan() %>% abs()
## [1] 0.6049088

Còn muốn khai căn bậc hai kết quả trên thì:

x %>% sin() %>% tan() %>% abs() %>% sqrt()
## [1] 0.7777588

Nếu không sử dụng toán tử pipe thì kết quả 0.7777588 ở trên được trình bày như sau:

sqrt(abs(tan(sin(x))))
## [1] 0.7777588

Cách trình bày là là khó đọc, khó theo dõi. Do vậy, bốn quy ước/luật chơi ở trên cần hiểu - nắm vững trước khi đi tiếp cuộc hành trình với R.

Glimse Your Data

Hiểu sơ bộ dữ liệu đang xử lí là điều cần thiết. Chúng ta có thể xem dữ liệu bằng lệnh View():

# Use pipe operator: 
stigma_data %>% View()

# Or not using pipe operator: 
View(stigma_data)

Kết quả như ta thấy dưới đây:

Để xem số dòng, số cột của bộ dữ liệu chúng ta có thể sử dụng một trong ba lệnh sau:

# Show number of rows: 
nrow(stigma_data)
## [1] 1121
# Show number of columns: 
ncol(stigma_data)
## [1] 153
# Show both: 
dim(stigma_data)
## [1] 1121  153

Your Assignment: Sử dụng pipe operator tính toán số dòng, số cột của stigma_data.

Chúng ta có thê xem 6 quan sát đầu/cuối của bộ dữ liệu:

head(stigma_data) # The first observations. 

tail(stigma_data) # The last observations. 

Để “extract” tên các cột biến của Data Frame, sử dụng lệnh names() như sau:

column_names <- names(stigma_data)

Để biết dạng dữ liệu/cấu trúc dữ liệu, ví dụ, của object có tên column_names chúng ta có thể sử dụng lệnh str():

str(column_names)

Kết quả sẽ là chr [1:153] “STT” … hàm ý rằng object này có kiểu dữ liệu là text/character - kí hiệu là chr. Còn phần [1:153] có nghĩa là text này gồm 153 phần tử - chính là số cột của bộ dữ liệu.

Your Assignment: Sử dụng lệnh str() cho stigma_data và đọc hiểu kết quả.

Object có tên column_names ở trên được gọi là một vector. Cụ thể hơn có thể gọi là vector text - vì dữ dạng dữ liệu text của nó. Chúng ta có thể accesss vào từng phần tử của một vector theo vị trí, chẳng hạn, vị trí số 1 của nó như sau:

column_names[1]
## [1] "STT"

Đây chính là tên của cột biến thứ nhất. Còn muốn access vào vị trí thứ 7:

column_names[7]
## [1] "BAge"

Access vào vị các vị trí liên tiếp từ 7 đến 12:

column_names[7:11]
## [1] "BAge"              "BMartialstatus"    "BEducationallevel"
## [4] "Professional"      "Position"

Your Assignment: Lệnh column_names[200] sẽ trả về kết quả là NA. Hãy giải thích tại sao?

Extract Description

Trước hết cần biết ý nghĩa của các cột biến là điều cần thiết. Sử dụng lệnh View() như ở trên chúng ta có thể biết, ví dụ, cột biến có tên A2 có nghĩa là “Tên đơn vị làm việc”. Tuy nhiên chúng ta có thể khai thác nhiều thông tin hơn về một cột biến cụ thể bằng lệnh attributes() như sau:

# Extract description for a given column: 
stigma_data$A2 %>% attributes() -> des_for_A2

# Show description: 
des_for_A2

Cho cột biến BSex:

# Extract description for BSex: 
stigma_data$BSex %>% attributes() -> des_for_BSex

# Show description: 
des_for_BSex

Đối với tình huống của BSex thì ngoài các miêu tả như đã có cho A2 chúng ta còn biết rằng giá trị 1 ở cột biến BSex là Nam và 2 là Nữ. Như thế cột biến BSex thuộc loại categorical - kiểu dữ liệu giống A2. Như vậy các giá trị 1 và 2 của cột biến này chỉ mang tính hình thức: không nên hiểu sai rằng BSex là biến numeric, và do vậy mọi phép tính với cột biến này, như lấy trung bình là không có ý nghĩa.

Hai objects (des_for_A2 và des_for_BSex) thuộc một kiểu tổ chức dữ liệu là list. Chúng ta có thể khai thác list thông qua kí hiệu $ như sau:

# Extract label: 

des_for_BSex$labels
## Nam N<U+1EEF> 
##   1   2
# Extract description for column: 

des_for_BSex$label
## [1] "Gi<U+1EDB>i tính"
# Extract data type: 

des_for_BSex$class
## [1] "haven_labelled" "vctrs_vctr"     "double"

Một lần nữa nhắc lại rằng mặc dù thông tin về kiểu dữ liệu là “double” nhưng đây chỉ mang ý nghĩa hình thức mà thôi: cột biến BSex là categorical - loại dữ liệu chỉ có ý nghĩa phân biệt một quan sát có phải là Nam/Nữ mà thôi.

Với những hiểu biết cơ bản về bộ số liệu như trên chúng ta có thể thực hiện công đoạn làm sạch và chuẩn bị dữ liệu với các hàm của thư viện/gói dplyr.

Function 1: select()

Lệnh này của dplyr cho phép “lựa chọn” cột biến theo những cách thức sau:

  1. Theo tên của cột biến. Giả sử chúng ta chỉ muốn chọn hai cột biến E1 và B9:
# Load dplyr package: 
library(dplyr)

# Method 1: 
stigma_data %>% select(E1, B9) -> df1_select_m1

# Method 2: 
some_columns <- c("E1", "B9")

stigma_data %>% select(some_columns) -> df_select_m2
  1. Theo vị trí của cột biến. Giả sử muốn lấy các cột ở vị trí số 1 và 7:
stigma_data %>% select(1, 7) -> df1_select_positioned
  1. Chỉ lấy các cột biến có dạng dữ liệu, ví dụ, là numeric:
# Only select numeric columns: 
stigma_data %>% select_if(is.numeric) -> df_only_numeric
  1. Theo một pattern nào đó của tên biến. Giả sử chúng ta muốn lấy ra tất cả các cột biến mà tên biến có cụm từ “hailong”:
stigma_data %>% select(contains("hailong")) -> df_contains_hailong
  1. Một công dụng khác của lệnh select() là xóa một cột biến cụ thể. Muốn xóa cột biến STT chẳng hạn:
stigma_data %>% select(-STT) -> stigma_data_not_STT

Your Assignment: Hãy xóa đồng thời hai cột biến là STT và BAge.

Function 2: rename()

Như tên của lệnh này ngụ ý: đổi tên cho cột biến. Lệnh này sử dụng theo cú pháp như sau:

# Rename for STT: 
df1_select_positioned %>% rename(so_thu_tu = STT)
## # A tibble: 1,121 x 2
##    so_thu_tu  BAge
##        <dbl> <dbl>
##  1         1    43
##  2         2    30
##  3         3    30
##  4         4    32
##  5         5    49
##  6         6    46
##  7         7    37
##  8         8    31
##  9         9    30
## 10        10    34
## # ... with 1,111 more rows
# Rename for both: 
df1_select_positioned %>% rename(so_thu_tu = STT, tuoi = BAge)
## # A tibble: 1,121 x 2
##    so_thu_tu  tuoi
##        <dbl> <dbl>
##  1         1    43
##  2         2    30
##  3         3    30
##  4         4    32
##  5         5    49
##  6         6    46
##  7         7    37
##  8         8    31
##  9         9    30
## 10        10    34
## # ... with 1,111 more rows

Một cách thức khác để đổi tên cho cả hai cột biến (và rất tiện dụng trong nhiều trường hợp) như sau:

# Prepare new names: 
new_names <- c("so_thu_tu", "tuoi")

# Rename for df1_select_positioned: 
names(df1_select_positioned) <- new_names

# Check: 
df1_select_positioned %>% head()
## # A tibble: 6 x 2
##   so_thu_tu  tuoi
##       <dbl> <dbl>
## 1         1    43
## 2         2    30
## 3         3    30
## 4         4    32
## 5         5    49
## 6         6    46

Bộ dữ liệu gốc có cả tên tiếng Anh (ngôn ngữ không dấu) và tiếng Việt (có dấu) lẫn lộn nhau. Cái này gọi là Inconsistency - một điều tệ nên tránh. Chúng ta có thể “chuẩn hóa” bằng cách đổi tên cho tất cả về chữ không dấu như sau:

# Load stringi package: 
library(stringi)

# Convert to Latin character: 
stri_trans_general(column_names, "Latin-ASCII") -> column_names_latin

# Rename for all columns: 
names(stigma_data) <- column_names_latin

Lúc này chúng ta có thể xem 6 tên cột biến đầu tiên như sau:

stigma_data %>% names() %>% head()
## [1] "STT"           "Dauthoigian"   "A1"            "A2"           
## [5] "AEconomicarea" "BSex"

Function 3: filter()

Sử dụng lệnh này để:

  1. Lấy ra một số quan sát thỏa mãn, ví dụ, BAge lớn hơn 30:
stigma_data %>% filter(BAge > 30) -> df_age_over30
  1. Lấy ra các quan sát mà đồng thời thỏa mãn: (1) BAge > 30, và (2) BSex == 1:
stigma_data %>% filter(BAge > 30, BSex == 1) -> df_age_over30_sex1
  1. Lấy ra các quan sát mà chính xác có cụm từ “Bệnh viện Tâm thần tỉnh Nghệ An” ở cột biến A2:
stigma_data %>% 
  filter(A2 == "Bệnh viện Tâm thần tỉnh Nghệ An") -> df_nghean
  1. Lấy ra các quan sát chỉ cần có cụm từ “Nghệ An” ở cột biến A2:
# Load stringr package: 
library(stringr)

# Observations contain "Nghệ An": 

stigma_data %>% 
  filter(str_detect(A2, "Nghệ An")) -> df_only_nghean
  1. Lấy ra các quan sát mà không có dữ liệu (missing data) về tuổi:
df1_select_positioned %>% 
  filter(is.na(tuoi))
## # A tibble: 1 x 2
##   so_thu_tu  tuoi
##       <dbl> <dbl>
## 1       765    NA

Kết quả này cho ta biết quan sát ở dòng thứ 756 không có dữ liệu về tuổi. Nguyên nhân có thể là do sai sót của khâu nhập dữ liệu. Ngược lại, nếu chúng ta muốn lấy ra các quan sát mà có dữ liệu về tuổi thì:

df1_select_positioned %>% 
  filter(!is.na(tuoi))
## # A tibble: 1,120 x 2
##    so_thu_tu  tuoi
##        <dbl> <dbl>
##  1         1    43
##  2         2    30
##  3         3    30
##  4         4    32
##  5         5    49
##  6         6    46
##  7         7    37
##  8         8    31
##  9         9    30
## 10        10    34
## # ... with 1,110 more rows

Function 4: mutate()

Sử dụng lệnh này để:

  1. Tạo ra một/hoặc nhiều cột biến mới:
# Create a new column tuoi2 from df1_select_positioned: 
df1_select_positioned %>% 
  mutate(tuoi2 = tuoi - 2)
## # A tibble: 1,121 x 3
##    so_thu_tu  tuoi tuoi2
##        <dbl> <dbl> <dbl>
##  1         1    43    41
##  2         2    30    28
##  3         3    30    28
##  4         4    32    30
##  5         5    49    47
##  6         6    46    44
##  7         7    37    35
##  8         8    31    29
##  9         9    30    28
## 10        10    34    32
## # ... with 1,111 more rows
# Or create two new columns: 
df1_select_positioned %>% 
  mutate(tuoi2 = tuoi - 1, stt2 = so_thu_tu*so_thu_tu)
## # A tibble: 1,121 x 4
##    so_thu_tu  tuoi tuoi2  stt2
##        <dbl> <dbl> <dbl> <dbl>
##  1         1    43    42     1
##  2         2    30    29     4
##  3         3    30    29     9
##  4         4    32    31    16
##  5         5    49    48    25
##  6         6    46    45    36
##  7         7    37    36    49
##  8         8    31    30    64
##  9         9    30    29    81
## 10        10    34    33   100
## # ... with 1,111 more rows
  1. Xóa một cột biến đã có:
# Drop so_thu_tu column: 
df1_select_positioned %>% 
  mutate(so_thu_tu = NULL)
## # A tibble: 1,121 x 1
##     tuoi
##    <dbl>
##  1    43
##  2    30
##  3    30
##  4    32
##  5    49
##  6    46
##  7    37
##  8    31
##  9    30
## 10    34
## # ... with 1,111 more rows

Function 5: top_n()

Lệnh này để, ví dụ, lấy ra 5 quan sát mà tuổi là cao nhất:

df1_select_positioned %>% 
  top_n(n = 5, wt = tuoi)
## # A tibble: 10 x 2
##    so_thu_tu  tuoi
##        <dbl> <dbl>
##  1       310    60
##  2       419    59
##  3       450    67
##  4       510    59
##  5       545    63
##  6       564    59
##  7       622    59
##  8       669    59
##  9       945    60
## 10       981    59

Lệnh này không nhất thiết phải lấy ra đúng 5 quan sát có tuổi cao nhất. Các quan sát có tuổi như nhau (59 chẳng hạn) thì đều được chọn.

Function 6: arrange()

Sử dụng lệnh này để sắp xếp lại các quan sát theo chiều tăng dần của, ví dụ, tuổi:

df1_select_positioned %>% 
  arrange(tuoi)
## # A tibble: 1,121 x 2
##    so_thu_tu  tuoi
##        <dbl> <dbl>
##  1       636    21
##  2       698    22
##  3       738    22
##  4       741    22
##  5       912    22
##  6       171    23
##  7       550    23
##  8       602    23
##  9       733    23
## 10       848    23
## # ... with 1,111 more rows

Còn nếu sắp xếp theo chiều giảm dần của tuổi:

df1_select_positioned %>% 
  arrange(-tuoi)
## # A tibble: 1,121 x 2
##    so_thu_tu  tuoi
##        <dbl> <dbl>
##  1       450    67
##  2       545    63
##  3       310    60
##  4       945    60
##  5       419    59
##  6       510    59
##  7       564    59
##  8       622    59
##  9       669    59
## 10       981    59
## # ... with 1,111 more rows

Nếu biến được chọn để sắp xếp là categorical thì thứ tự xuất hiện sẽ theo trật tự của alphabet. Ví dụ là sắp xếp lại thứ tự các quan sát theo A2- tức là nơi làm việc người được khảo sát:

stigma_data %>% 
  arrange(A2)

Function 7: slice()

Lệnh này cho phép lấy ra một số quan sát theo vị trí của dòng. Ví dụ, muốn lấy ra các quan sát ở dòng 1, 10, và 100 từ df1_select_positioned:

df1_select_positioned %>% 
  slice(c(1, 10, 100))
## # A tibble: 3 x 2
##   so_thu_tu  tuoi
##       <dbl> <dbl>
## 1         1    43
## 2        10    34
## 3       100    32

Hoặc lấy ra 5 quan sát liên tiếp từ 11 đến 15:

df1_select_positioned %>% 
  slice(c(11:15))
## # A tibble: 5 x 2
##   so_thu_tu  tuoi
##       <dbl> <dbl>
## 1        11    48
## 2        12    35
## 3        13    37
## 4        14    34
## 5        15    32

Hoặc lấy ra 5 quan sát liên tiếp từ 11 đến 15 ở vị trí dòng 21:

df1_select_positioned %>% 
  slice(c(11:15, 21))
## # A tibble: 6 x 2
##   so_thu_tu  tuoi
##       <dbl> <dbl>
## 1        11    48
## 2        12    35
## 3        13    37
## 4        14    34
## 5        15    32
## 6        21    54

Lệnh slice() này có thể được sử dụng để trả lời câu hỏi lấy ra 3 quan sát có tuổi lớn nhất. Trước hết ta sắp xếp lại các quan sát theo chiều giảm của tuổi:

# Stage 1: 
df1_select_positioned %>% 
  arrange(-tuoi) -> df_arranged

Sau đó lấy ra ba quan sát liên tiếp từ vị trí 1 đến 3:

# Stage 2: 
df_arranged %>% 
  slice(c(1:3))
## # A tibble: 3 x 2
##   so_thu_tu  tuoi
##       <dbl> <dbl>
## 1       450    67
## 2       545    63
## 3       310    60

Đến đây chúng ta trở lại với tính y = tan(sin(x)) một lần nữa. Điểm tương tự là cách làm thông qua hai bước như trên chúng ta phải tạo ra một object trung gian là df_arranged. Tương tự như vấn đề tính y = tan(sin(x)), chúng ta có thể không cần phải tạo ra object trung gian df_arranged mà vẫn có câu trả lời nhờ sử dụng toán tử pipe như sau:

df1_select_positioned %>% 
  arrange(-tuoi) %>% 
  slice(c(1:3))
## # A tibble: 3 x 2
##   so_thu_tu  tuoi
##       <dbl> <dbl>
## 1       450    67
## 2       545    63
## 3       310    60

Your Assignment: Giải thích ý nghĩa của đoạn R codes dưới đây:

stigma_data %>% 
  select(STT, BAge) %>% 
  rename(tuoi = BAge) %>% 
  mutate(tuoi_binh_phuong = tuoi^2)

Function 8: count()

Lệnh count() như tên gọi của nó gợi ý - là đếm quan sát. Tuy nhiên nó hiếm khi được sử dụng đơn lẻ mà thường được kết hợp với lệnh group_by(). Chẳng hạn, chúng ta muốn biết trình độ học vấn của các quan sát xuất hiện với tần suất bao nhiêu:

stigma_data %>% 
  group_by(BEducationallevel) %>% 
  count() 

Nghĩa là: trình độ học vấn được mã hóa là 1 xuất hiện 2 lần còn trình độ học vấn được mã hóa là 3 xuất hiện với tần suất lớn nhất là 505.

Nếu chúng ta muốn sắp xếp theo chiều giảm dần của tần suất xuất hiện của BEducationallevel thì:

# Count frequencies by BEducationallevel: 
stigma_data %>% 
  group_by(BEducationallevel) %>% 
  count(sort = TRUE) -> education_report

Đến đây nếu muốn tính tỉ lệ các nhóm trình độ học vấn thì:

# Method 1: 
education_report %>% 
  mutate(ti_le = n / 1121)

Cách tính này rõ ràng chúng ta phải nhớ con số 1121 - chính là số dòng của bộ dữ liệu. Đây là cách làm không được khuyến khích. Nên tuân thủ cách làm thứ hai dưới đây:

# Method 2: 
education_report %>% 
  ungroup() %>% 
  mutate(n_obs = sum(n)) %>% 
  mutate(rate =  n / n_obs)
## # A tibble: 5 x 4
##          BEducationallevel     n n_obs    rate
##                  <dbl+lbl> <int> <int>   <dbl>
## 1 3 [Cao d<U+1EB3>ng tru<U+1EDD>ng ngh<U+1EC1>]   505  1121 0.450  
## 2 4 [Ð<U+1EA1>i h<U+1ECD>c]                391  1121 0.349  
## 3 5 [Sau d<U+1EA1>i h<U+1ECD>c]            130  1121 0.116  
## 4 2 [Trung h<U+1ECD>c Ph<U+1ED5> Thông]     93  1121 0.0830 
## 5 1 [Trung h<U+1ECD>c co s<U+1EDF>]          2  1121 0.00178

Cách làm này chúng ta sử dụng lệnh ungroup() để vô hiệu hóa hiệu lực của group_by() rồi tạo cột biến mới có tên n_obs - tức là tổng số quan sát. Cuối cùng lấy số quan sát n chia cho tổng số quan sát n_obs để tính ra tỉ lệ của các nhóm học vấn.

Function 9: case_when()

Sử dụng lệnh này (đồng thời kết hợp với các lệnh khác) để:

  1. Phân thành các nhóm (hay rời rạc hóa) biến liên tục. Chẳng hạn chúng ta tạo cột biến mới là nhom_tuoi theo luật chơi sau:
  • Nhỏ hơn 30 là Group1.
  • Từ 30 đến thấp hơn 40 là Group2.
  • Từ 40 trở lên là Group3.

Dưới đây là R codes thực hiện phân loại này theo hai cách:

# Method 1: 
df1_select_positioned %>% 
  mutate(nhom_tuoi = case_when(tuoi < 30 ~ "Group1", 
                               tuoi >= 30 & tuoi < 40 ~ "Group2", 
                               tuoi >= 40 ~ "Group3")) -> df_ageGrouped

# Method 2: 

df1_select_positioned %>% 
  mutate(nhom_tuoi = case_when(tuoi < 30 ~ "Group1", 
                               tuoi >= 30 & tuoi < 40 ~ "Group2", 
                               TRUE ~ "Group3")) -> df_ageGrouped

Đến đây chúng ta có thể đếm tần suất của các nhóm tuổi và sắp xếp theo tần suất giảm dần:

df_ageGrouped %>% 
  group_by(nhom_tuoi) %>% 
  count(sort = TRUE)
## # A tibble: 3 x 2
## # Groups:   nhom_tuoi [3]
##   nhom_tuoi     n
##   <chr>     <int>
## 1 Group2      537
## 2 Group3      375
## 3 Group1      209

Your Assignment: Nhóm tuổi có tên “Group3” trong đoạn R codes dưới đây xuất hiện bao nhiêu lần:

df1_select_positioned %>% 
  mutate(nhom_tuoi = case_when(tuoi < 30 ~ "Group1", 
                               tuoi >= 30 & tuoi < 40 ~ "Group2", 
                               TRUE ~ "Group3")) %>% 
  group_by(nhom_tuoi) %>% 
  count(sort = TRUE)

Your Assignment: Tính tỉ lệ các nhóm tuổi theo cách phân loại ở trên.

  1. Mã hóa lại (recode) các biến rời rạc. Chảng hạn chúng ta muốn mã hóa lại trình độ học vấn BEducationallevel bằng cách tạo thành một cột biến mới có tên edu_level. Nhắc lại rằng chúng ta có thể khai thác các thông tin/ý nghĩa của cột biến bằng lệnh attributes() đã biết:
stigma_data$BEducationallevel %>% attributes()

Dựa trên hiểu biết / ý nghĩa của cột biến BEducationallevel này chúng ta có thể mã hóa lại 5 nhóm học vấn như sau:

stigma_data %>% 
  mutate(edu_level = case_when(BEducationallevel == 1 ~ "Trung_hocCS", 
                               BEducationallevel == 2 ~ "Trung_hocPT", 
                               BEducationallevel == 3 ~ "Cao_dang/Nghe", 
                               BEducationallevel == 4 ~ "DaiHoc", 
                               TRUE ~ "SauDaiHoc")) -> stigma_data_new

Nếu muốn tính tỉ lệ của các nhóm học vấn thì:

stigma_data_new %>% 
  group_by(edu_level) %>% 
  count(sort = TRUE) %>% 
  ungroup() %>% 
  mutate(total = sum(n)) %>% 
  mutate(ti_le_edu = n / total)
## # A tibble: 5 x 4
##   edu_level         n total ti_le_edu
##   <chr>         <int> <int>     <dbl>
## 1 Cao_dang/Nghe   505  1121   0.450  
## 2 DaiHoc          391  1121   0.349  
## 3 SauDaiHoc       130  1121   0.116  
## 4 Trung_hocPT      93  1121   0.0830 
## 5 Trung_hocCS       2  1121   0.00178

Function 10: summarise()

Lệnh summarise() thường được sử dụng kết hợp với các lệnh khác để tính toán các thống kê hoặc bất kì chỉ số nào theo nhóm. Chẳng hạn chúng ta muốn tính min, max, mean, median, standard deviation của mucdohailongveluong của các nhóm học vấn:

stigma_data_new %>% 
  group_by(edu_level) %>% 
  summarise(min_HL = min(mucdohailongveluong), 
            max_HL = max(mucdohailongveluong), 
            avg_HL = mean(mucdohailongveluong), 
            med_HL = median(mucdohailongveluong), 
            sd_HL = sd(mucdohailongveluong))
## # A tibble: 5 x 6
##   edu_level     min_HL max_HL avg_HL med_HL sd_HL
## * <chr>          <dbl>  <dbl>  <dbl>  <dbl> <dbl>
## 1 Cao_dang/Nghe    0      5     3.66   3.75  1.16
## 2 DaiHoc           0      5     3.73   4     1.09
## 3 SauDaiHoc        1      5     3.66   3.75  1.03
## 4 Trung_hocCS      2.5    4.5   3.5    3.5   1.41
## 5 Trung_hocPT      1      5     3.73   3.75  1.19

Kết quả này cho thấy, ví dụ, nhóm Trung_hocCS có mức độ hài lòng trung bình là 3.5. Tuy nhiên con số này không nên được dựa vào để đưa ra bất kì kết luận hay khuyến nghị nào. Vì rằng: số quan sát của nhóm học vấn này chỉ là 2.

Do vậy, có lẽ nên đưa thêm thông tin về số lượng của các nhóm quan sát như sau:

stigma_data_new %>% 
  group_by(edu_level) %>% 
  summarise(min_HL = min(mucdohailongveluong), 
            max_HL = max(mucdohailongveluong), 
            avg_HL = mean(mucdohailongveluong), 
            med_HL = median(mucdohailongveluong), 
            sd_HL = sd(mucdohailongveluong), 
            n_obs = n()) -> hailong_income_by_educ

hailong_income_by_educ
## # A tibble: 5 x 7
##   edu_level     min_HL max_HL avg_HL med_HL sd_HL n_obs
## * <chr>          <dbl>  <dbl>  <dbl>  <dbl> <dbl> <int>
## 1 Cao_dang/Nghe    0      5     3.66   3.75  1.16   505
## 2 DaiHoc           0      5     3.73   4     1.09   391
## 3 SauDaiHoc        1      5     3.66   3.75  1.03   130
## 4 Trung_hocCS      2.5    4.5   3.5    3.5   1.41     2
## 5 Trung_hocPT      1      5     3.73   3.75  1.19    93

Nếu cần báo cáo mức độ hài lòng về lương của các nhóm cho bộ trưởng Y tế thì chúng ta có thể lưu lại báo cáo này ở dạng, ví dụ, định dạng csv (để có thể đọc bằng phần mềm Excel) như sau:

write.csv(hailong_income_by_educ, "F:/hailong_income_by_educ.csv")

Lúc này tại ổ cứng F của máy tính sẽ có một file có tên là hailong_income_by_educ.csv có thể mở bằng Excel hoặc các phần mềm phân tích dữ liệu khác như SPSS, Stata. Lưu ý rằng không phải máy tính của ai cũng có ổ F. Và do vậy đường dẫn trong lệnh lưu dữ liệu trên có thể phải được thay đổi cho phù hợp.

Function 11: pull()

Lệnh này cho phép tách ra một cột biến từ một data frame đã có. Ví dụ, tách ra BAge:

stigma_data %>% pull(BAge) -> vector_tuoi_m1

Object có tên vector_tuoi_m1 ở trên là một vector trong khi stigma_data là một data frame. Chúng ta cũng có thể sử dụng dấu $ để hoàn thành công việc này:

stigma_data$BAge -> vector_tuoi_m2

Capstone Project

11 lệnh/hàm của dplyr được giới thiệu ở trên có thể giải quyết hầu hết các tình huống của nhóm công việc báo cáo. Để minh họa, chúng ta sẽ thực hiện tái tạo lại một phần báo cáo ở Bảng 2 dưới đây:

Giả sử rằng thông tin về cái gọi là “vị trí làm việc” nằm ở cột biến Professional thì việc đầu tiên là chúng ta cần kiểm tra lại một lần nữa về biến số này:

stigma_data$Professional %>% attributes()

Như vậy có 10 nhóm khác nhau được mã hóa từ 1 đến 10 nhưng báo cáo trên chỉ phân thành 4 nhóm là “Bác sĩ tâm thần”, “Điều dưỡng”, “Cán bộ chăm sóc sức khỏe” và “Khác”. Nhóm có tên “Chung” có lẽ là không tính toán theo bốn nhóm này mà là gộp chung tất cả lại.

Trước hết tính cho nhóm có tên “Chung”:

stigma_data %>% 
  summarise(Mean_HL_luong = mean(mucdohailongveluong), 
            SD_HL_luong = sd(mucdohailongveluong), 
            Mean_TTien = mean(mucdohailongcohoithangtien), 
            SD_TTien = sd(mucdohailongcohoithangtien))
## # A tibble: 1 x 4
##   Mean_HL_luong SD_HL_luong Mean_TTien SD_TTien
##           <dbl>       <dbl>      <dbl>    <dbl>
## 1          3.69        1.12       3.30     1.11

Trung bình thì mức độ hài lòng về lương là 3.69 tuy nhiên kết quả trong bảng 2 lại là 18.8. Đây là một kết quả không chính xác vì rằng mucdohailongveluong (cũng như các biến có chữ hailong khác) có giá trị lớn nhất là 5. Do vậy trung bình của nó không thể lớn hơn 5 được.

Kế tiếp mã hóa lại vị trí công việc thành 4 nhóm như trong báo cáo này với giả định rằng “Cận lâm sàng” được mã hóa lại thành “Can_bo_cham_soc”:

stigma_data %>% 
  mutate(job_position = case_when(Professional == 2 ~ "Bac_si_tam_than", 
                                  Professional == 3 ~ "Dieu_duong", 
                                  Professional == 4 ~ "Can_bo_cham_soc", 
                                  TRUE ~ "Khac")) -> stigmaDataNew

Rồi tính toán Mean, SD cho bốn nhóm này và có cả thông tin về số lượng quan sát của mỗi nhóm:

stigmaDataNew %>%
  group_by(job_position) %>% 
  summarise(Mean_HL_luong = mean(mucdohailongveluong), 
            SD_HL_luong = sd(mucdohailongveluong), 
            Mean_TTien = mean(mucdohailongcohoithangtien), 
            SD_TTien = sd(mucdohailongcohoithangtien), 
            n_obs = n())
## # A tibble: 4 x 6
##   job_position    Mean_HL_luong SD_HL_luong Mean_TTien SD_TTien n_obs
## * <chr>                   <dbl>       <dbl>      <dbl>    <dbl> <int>
## 1 Bac_si_tam_than          3.49        1.10       3.29     1.16   181
## 2 Can_bo_cham_soc          3.87        1.29       3.63     1.09    31
## 3 Dieu_duong               3.69        1.14       3.28     1.12   578
## 4 Khac                     3.78        1.08       3.31     1.05   331

Your Assignment: Thực hiện tính toán các kết quả còn lại như ở Bảng 2.

Final Notes

  1. Bài giảng này giới thiệu chỉ 11 lệnh của dplyr. Đây là những lệnh quan trọng thường sử dụng và chúng cover hầu hết các tình huống của công việc báo cáo.

  2. Trình bày cách sử dụng chỉ một số lệnh (trong số 11 lệnh) để tạo ra báo cáo như Bảng 2. Có bằng chứng rõ ràng rằng các kết quả tính toán trong Bảng 2 là không đúng. Lí do đã phân tích ở trên.

  3. Các câu hỏi về đo lường cái gọi là “Hài Lòng” ở bộ dữ liệu này có lẽ sử dụng thang đo Likert - là loại dữ liệu thuộc nhóm Ordinal data nên cần có những mô hình thống kê chuyên biệt cho loại dữ liệu này.

Chúng ta có thể kiểm tra một số biến thuộc thang đo Likert (hay loại biến là Ordinal data) như sau:

sapply(stigma_data %>% select(E1a:E3a), function(x) {attributes(x)$labels})
##                   E1a E2a E3a
## Strongly agree      1   1   1
## Agree               2   2   2
## Somewhat agree      3   3   3
## Somewhat disagree   4   4   4
## Disagree            5   5   5
## Strongly disagree   6   6   6

Thông thường các biến là thang đo Likert sẽ sử dụng một số lẻ (3, 5, 7) thang đo để sẽ có một điểm gọi là trung dung (neutral) nhằm ghi nhận thông tin khi mà thái độ của người phỏng vấn không nghiêng hẳn về bên nào. Phổ biến là 5. Nhưng bộ dữ liệu này lại sử dụng 6 ngưỡng - một điều rất hiếm thấy (thực tế tôi chưa gặp nghiên cứu nào sử dụng Likert 6).