PHẦN 1: GIỚI THIỆU & THIẾT LẬP MÔI TRƯỜNG Chào mừng các bạn đến với Buổi 2! Hôm nay chúng ta sẽ học cách làm việc với dữ liệu lâm sàng thực tế. Dữ liệu thực tế thường không hề hoàn hảo: nó chứa đầy lỗi nhập liệu, giá trị khuyết (missing data), và những con số phi lý. Nhiệm vụ của chúng ta là dùng ngôn ngữ R để “khám bệnh” cho bộ dữ liệu này và “chữa” nó trước khi chạy bất kỳ mô hình thống kê nào. Trong nghiên cứu y khoa chuẩn Q1, tính tái lập (Reproducibility) là bắt buộc . File R Markdown này chính là một báo cáo tái lập: nó lưu lại mọi bước tư duy và thao tác của bạn. Cài đặt và Gọi thư viện (Packages) Thay vì dùng hàm library() lặp đi lặp lại cho từng gói, chúng ta sẽ dùng siêu công cụ pacman để tự động cài đặt (nếu chưa có) và tải các gói cần thiết chỉ với 1 dòng lệnh

# Cài đặt pacman nếu máy chưa có
if (!require("pacman")) install.packages("pacman")

# pacman::p_load sẽ tự động tìm, cài và gọi các packages
pacman::p_load(
  tidyverse,   # Chứa dplyr, ggplot2 để xử lý và vẽ biểu đồ
  rio,         # Siêu công cụ nhập dữ liệu (Excel, csv, SPSS)
  gtsummary,   # Lập bảng Table 1 chuẩn y khoa
  naniar       # Thăm dò dữ liệu khuyết (NA)
)
PHẦN 2: IMPORT DỮ LIỆU VÀO R Thay vì dùng nhiều hàm khác nhau cho từng loại đuôi file (.csv, .xlsx, .sav), chúng ta sử dụng hàm import() từ package rio. Hàm này đủ thông minh để tự nhận diện đuôi file và đọc chính xác dữ liệu . Mẹo kỹ thuật: Đường dẫn trong Windows dùng dấu , nhưng trong R, bạn phải đổi thành / hoặc \ để R hiểu đó là đường dẫn
PHẦN 3: KHÁM PHÁ DỮ LIỆU (EXPLORATORY DATA ANALYSIS) Trước khi can thiệp, bác sĩ phải khám bệnh. Trước khi xử lý (wrangling), nhà khoa học dữ liệu phải thăm dò (exploration). 3.1. Nhìn tổng quan cấu trúc Chúng ta dùng hàm glimpse() (nhìn lướt qua) để xem máy tính đang “hiểu” dữ liệu của chúng ta ra sao. glimpse(df_raw) Nhận xét lâm sàng: Biến Gender đang được máy tính hiểu là số (). Nếu để nguyên, R sẽ tính ra “Trung bình giới tính là 0.5” -> Vô nghĩa! Chúng ta phải chuyển nó về dạng phân loại (Factor). Biến Treatment (Phác đồ) là chữ (), cũng cần chuyển thành Factor. 3.2. Bắt mạch những con số phi lý (Biological Outliers) Hàm summary() giúp ta quét qua các chỉ số trung bình, min, max của toàn bộ dữ liệu. summary(df_raw) Phát hiện lỗi nguy hiểm (Outliers): Nhìn vào cột Age: Giá trị lớn nhất (Max) là 999 tuổi! Nhìn vào cột BMI: Giá trị lớn nhất (Max) là 150.5 và nhỏ nhất (Min) là 8.2. Đây chắc chắn là lỗi đánh máy (Typo error) từ người nhập liệu.

PHẦN 4: LÀM SẠCH VÀ NHÀO NẶN DỮ LIỆU (DATA WRANGLING) Đây là lúc chúng ta dùng hệ sinh thái dplyr với toán tử đường ống %>% (đọc là “và sau đó…”). Cấu trúc này giúp code R đọc mượt mà như một câu tiếng Anh , . Chúng ta sẽ dùng hàm mutate() để thay đổi và tạo mới biến số.

df_clean <- df_raw %>%
  mutate(
    # 1. Ép kiểu biến Gender từ 0/1 sang biến phân loại (Factor)
    Gender = factor(Gender, 
                    levels = c(0, 1), 
                    labels = c("Male", "Female")),
    
    Treatment = as.factor(Treatment),
    Outcome = factor(Outcome, levels=c(0,1), labels=c("Fail", "Success")),
    
    # 2. Xử lý Outlier sinh học: Nếu Age == 999 thì biến thành NA, ngược lại giữ nguyên Age
    Age = ifelse(Age == 999, NA, Age),
    
    # 3. Xử lý Outlier BMI: Chỉ chấp nhận BMI từ 10 đến 60, nằm ngoài khoảng này cho thành NA
    BMI = ifelse(BMI < 10 | BMI > 60, NA, BMI)
  )

# Kiểm tra lại sau khi dọn dẹp
summary(df_clean)
##   Patient_ID             Age           Gender         BMI       
##  Length:1000        Min.   :18.00   Male  :520   Min.   :10.30  
##  Class :character   1st Qu.:47.00   Female:480   1st Qu.:22.50  
##  Mode  :character   Median :55.00                Median :25.00  
##                     Mean   :55.16                Mean   :24.96  
##                     3rd Qu.:62.00                3rd Qu.:27.50  
##                     Max.   :96.00                Max.   :35.10  
##                                                  NA's   :60     
##   Cholesterol     Treatment      Outcome   
##  Min.   :1.860   Drug_A:498   Fail   :405  
##  1st Qu.:4.350   Drug_B:502   Success:595  
##  Median :5.090                             
##  Mean   :5.114                             
##  3rd Qu.:5.860                             
##  Max.   :8.390                             
##  NA's   :80

Ghi chú: Bạn có thể thấy các giá trị 999 và 150.5 đã biến mất và được chuyển thành NA (Missing).

PHẦN 5: BẢN CHẤT VÀ CÁCH XỬ LÝ DỮ LIỆU KHUYẾT (MISSING DATA) Dữ liệu khuyết không chỉ đơn thuần là những ô trống. Theo lý thuyết dịch tễ học, chúng được chia làm 3 loại cơ chế: MCAR (Missing Completely At Random): Mất ngẫu nhiên hoàn toàn. Ví dụ: Rớt vỡ ống nghiệm sinh hóa. MAR (Missing At Random): Mất ngẫu nhiên có điều kiện. Ví dụ: Phụ nữ thường ngại khai báo cân nặng hơn nam giới (phụ thuộc vào biến Gender). MNAR (Missing Not At Random): Mất không ngẫu nhiên. Ví dụ: Bệnh nhân béo phì giấu chỉ số BMI của mình. 5.1 Xử lý bằng cách Xóa (Listwise Deletion) Nếu tỉ lệ Missing < 5% và cơ chế là MCAR, chúng ta có thể an tâm xóa bỏ các dòng chứa NA bằng lệnh drop_na().
r # Tạo một tập data mới bằng cách loại bỏ những bệnh nhân thiếu Cholesterol df_complete <- df_clean %>% drop_na(Cholesterol) # Từ 1000 bệnh nhân, ta chỉ còn lại bao nhiêu? nrow(df_complete)
## [1] 920
5.2 Xử lý bằng cách Điền khuyết (Imputation) CẢNH BÁO HỌC THUẬT QUAN TRỌNG: Dưới đây tôi hướng dẫn các bạn cách dùng giá trị Trung bình (Mean Imputation) để điền vào ô trống. Tuy nhiên, trong các bài báo Q1, TUYỆT ĐỐI KHÔNG DÙNG CÁCH NÀY. Việc điền số trung bình sẽ bóp méo phương sai và phá vỡ mối tương quan sinh học thật sự của dữ liệu. Thay vào đó, giới hàn lâm bắt buộc dùng Multiple Imputation (ví dụ gói mice).
``` r df_imputed <- df_clean %>% mutate( # Nếu BMI bị khuyết, điền số trung bình của toàn bộ cột BMI vào, nếu không thì giữ nguyên BMI_filled = ifelse(is.na(BMI), mean(BMI, na.rm = TRUE), BMI) )
# So sánh sự thay đổi của cột BMI gốc và BMI đã điền khuyết summary(df_imputed %>% select(BMI, BMI_filled)) ```
## BMI BMI_filled ## Min. :10.30 Min. :10.30 ## 1st Qu.:22.50 1st Qu.:22.70 ## Median :25.00 Median :24.96 ## Mean :24.96 Mean :24.96 ## 3rd Qu.:27.50 3rd Qu.:27.30 ## Max. :35.10 Max. :35.10 ## NA's :60

PHẦN 6: BÁO CÁO KẾT QUẢ ĐẦU RA (TABLE 1) Sau khi bộ dữ liệu đã được làm sạch chuẩn mực, phần thưởng của chúng ta là bảng Table 1 (Baseline Characteristics). Với package gtsummary, chúng ta chỉ cần 2 dòng lệnh thay vì mất hàng giờ ngồi đếm thủ công trên SPSS hay Excel.

df_clean %>%
  # Bỏ cột Patient_ID vì không có ý nghĩa thống kê
  select(-Patient_ID) %>% 
  
  # Tạo bảng tóm tắt, phân tầng theo kết cục điều trị (Outcome)
  tbl_summary(
    by = Outcome,
    missing = "no", # Ẩn các dòng hiển thị số NA để bảng gọn hơn
    statistic = list(all_continuous() ~ "{mean} ({sd})") # Chỉnh định dạng Mean (SD)
  ) %>%
  add_p() %>% # Tự động chạy T-test hoặc Chi-square để so sánh 2 nhóm
  bold_labels() %>%
  modify_caption("**Bảng 1: Đặc điểm lâm sàng nền của bệnh nhân theo kết cục điều trị**")
Bảng 1: Đặc điểm lâm sàng nền của bệnh nhân theo kết cục điều trị
Characteristic Fail
N = 405
1
Success
N = 595
1
p-value2
Age 55 (12) 55 (12) 0.5
Gender

0.8
    Male 209 (52%) 311 (52%)
    Female 196 (48%) 284 (48%)
BMI 24.9 (3.7) 25.0 (3.9) 0.7
Cholesterol 5.00 (1.04) 5.19 (1.15) 0.005
Treatment

0.2
    Drug_A 211 (52%) 287 (48%)
    Drug_B 194 (48%) 308 (52%)
1 Mean (SD); n (%)
2 Wilcoxon rank sum test; Pearson’s Chi-squared test