6  Hàm names()

6.1 Vector có tên

Trong ví dụ dưới đây chúng ta có tên các biến và nhãn của chúng.

vars <- c("age", "sex", "bmi")
varnames <- c("Tuổi", "Giới", "BMI")

print(vars)
[1] "age" "sex" "bmi"
print(varnames)
[1] "Tuổi" "Giới" "BMI" 

Làm thế nào để chúng ta lấy được nhãn của biến age? Bạn sẽ cần sử dụng hàm which(). Trước hết hãy xem hàm which() làm gì.

which(vars == "age")
[1] 1

Như bạn đã thấy, hàm which() trả về danh sách các vị trí trong vector thỏa mãn điều kiện được khai báo trong hàm. Vậy để lấy nhãn của biến age, chúng ta sẽ làm như sau.

varnames[which(vars == "age")]
[1] "Tuổi"

R cung cấp một cách tham chiếu khác tới vector bằng tên gọi. Hãy “đặt tên” cho vector varnames và xem sự khác biệt.

names(varnames) <- vars
print(varnames)
   age    sex    bmi 
"Tuổi" "Giới"  "BMI" 

Chúng ta có một vector trong đó mỗi phần tử có một tên gọi riêng. Bây giờ chúng ta có thể tham chiếu tới từng phần tử bằng tên gọi tương tự như dictionary trong Python. Tuy nhiên, khác với Python, tất cả các tên trong R sẽ được tự động chuyển về kiểu kí tự.

varnames["age"]
   age 
"Tuổi" 

Nếu muốn xóa tên, chúng ta sẽ gán cho nó giá trị NULL.

names(varnames) <- NULL
varnames
[1] "Tuổi" "Giới" "BMI" 

6.1.1 Khai báo tên trực tiếp khi khai báo vector

Bạn cũng có thể khai báo tên ngay khi tạo vector. Nếu trong tên có kí tự đặc biệt không phải chữ cái, số, và dấu gạch nối (_), bạn có thể bao chúng trong cặp dấu ngoặc kép ("") hoặc phẩy trên trái (\``).

c(
    hb_d0 = "Hemoglobin (baseline)",
    "rbc d1" = "Red blood cell (day 1)",
    `plt d2` = "Platelet (day 2)"
)
                   hb_d0                   rbc d1                   plt d2 
 "Hemoglobin (baseline)" "Red blood cell (day 1)"       "Platelet (day 2)" 

6.1.2 Tạo tên gọi theo quy luật

Bạn không cần phải gõ tên cho từng phần tử nếu các phần tử có tên tuân theo một quy luật. Chẳng hạn, chúng ta có một danh sách các mốc thời gian, và tên gọi của mốc thời gian “X” là “Ngày X”.

timepoints <- seq_len(7)
names(timepoints) <- paste0("Ngày ", timepoints)
timepoints
Ngày 1 Ngày 2 Ngày 3 Ngày 4 Ngày 5 Ngày 6 Ngày 7 
     1      2      3      4      5      6      7 

6.1.3 Subset danh sách

Quay trở lại với ví dụ đầu tiên, làm sao để lấy được danh sách nhãn của một số biến?

vars_subset <- c("age", "bmi")
varnames[vars %in% vars_subset]
[1] "Tuổi" "BMI" 

Với tên gọi, bạn có thể gọi thẳng trực tiếp thay vì dùng toán tử matching %in%.

names(varnames) <- vars
varnames[vars_subset]
   age    bmi 
"Tuổi"  "BMI" 

6.2 List có tên

Bạn cũng có thể đặt cho danh sách. Thông thường chúng ta đặt tên cho danh sách trong lúc khai báo danh sách.

list(
    id = c(1, 2, 3),
    initials = c("PKL", "LHS", "MTNN")
)
$id
[1] 1 2 3

$initials
[1] "PKL"  "LHS"  "MTNN"

Nếu danh sách không có tên (ví dụ, được tạo ra từ hàm lặp), bạn có thể dùng hàm names() để đặt tên.

no_reps <- c(1, 4, 2)
results <- lapply(no_reps, function(x) rep(1, x))
print(results)
[[1]]
[1] 1

[[2]]
[1] 1 1 1 1

[[3]]
[1] 1 1
names(results) <- no_reps
print(results)
$`1`
[1] 1

$`4`
[1] 1 1 1 1

$`2`
[1] 1 1

Hoặc chúng ta đặt tên cho vector no_reps trước. Danh sách trả về sẽ sử dụng tên của vector này.

names(no_reps) <- c("1 element", "4 elements", "2 elements")
lapply(no_reps, function(x) rep(1, x))
$`1 element`
[1] 1

$`4 elements`
[1] 1 1 1 1

$`2 elements`
[1] 1 1

Hãy xem một ví dụ “nâng cao” để thấy giá trị của việc đặt tên. Trong ví dụ này, chúng ta sẽ viết một hàm để thống kê theo nhiều cách khác nhau, cách thức thống kê sẽ được quy định đối số method của hàm.

get_stats <- function(data, method = "mean") {
    switch(method,
        "mean" = sprintf("%.2f (%.2f)", mean(data, na.rm = TRUE), sd(data, na.rm = TRUE)),
        "median" = sprintf("%.2f [%.2f, %.2f]", median(data, na.rm = TRUE), quantile(data, .25, na.rm = TRUE), quantile(data, .75, na.rm = TRUE)),
        "minmax" = sprintf("%.2f-%.2f", min(data, na.rm = TRUE), max(data, na.rm = TRUE)),
    )
}

set.seed(0)
data <- rgamma(100, 2, 1)

get_stats(data)
[1] "1.85 (1.10)"
get_stats(data, "median")
[1] "1.63 [1.01, 2.55]"
get_stats(data, "minmax")
[1] "0.15-5.34"

Thay vì việc gọi hàm get_stats() 3 lần, chúng ta có thể sử dụng vòng lặp để cung cấp thông tin tự động cho đối số method.

methods <- c("mean", "minmax", "median")
sapply(methods, function(x) get_stats(data, x))
               mean              minmax              median 
      "1.85 (1.10)"         "0.15-5.34" "1.63 [1.01, 2.55]" 

Hàm này còn có thể mạnh hơn nữa khi lặp lại trên nhiều biến.

data2 <- data.frame(
    d0 = rgamma(100, 2, 1),
    d1 = rgamma(100, 3, 1.2),
    d2 = rgamma(100, 3.5, 2)
)

print(head(data2))
         d0       d1        d2
1 2.4998077 1.560672 1.1883247
2 1.4112553 2.871651 2.6535716
3 0.3700219 0.856785 1.8037974
4 1.1291394 3.366226 0.5186375
5 0.9739632 4.965208 2.3719618
6 2.2215693 2.454356 1.4778128
vars_to_get_stats <- c("d0", "d1", "d2")
sapply(vars_to_get_stats,
    function(x) sapply(methods, function(y) get_stats(data2[[x]], y))
) |> t() |> as.data.frame()   # Đổi hàng và cột cho nhau
          mean    minmax            median
d0 1.85 (1.39) 0.17-5.66 1.46 [0.81, 2.53]
d1 2.56 (1.41) 0.35-7.07 2.44 [1.47, 3.52]
d2 1.74 (1.09) 0.20-6.61 1.44 [1.00, 2.27]

Bạn có thể đi xa thêm một bước bằng cách đặt tên cho các vector methodsvars_to_get_stats. Chúng ta sẽ có một bảng tổng kết với tên gọi hoàn chỉnh sẵn sàng cho việc xuất bản.

names(methods) <- c("Mean (SD)", "Min-Max", "Median [Q1, Q3]")
names(vars_to_get_stats) <- c("Trước mổ", "Sau mổ - ngày 1", "Sau mổ - ngày 2")
sapply(vars_to_get_stats,
    function(x) sapply(methods, function(y) get_stats(data2[[x]], y))
) |> t() |> knitr::kable()
Mean (SD) Min-Max Median [Q1, Q3]
Trước mổ 1.85 (1.39) 0.17-5.66 1.46 [0.81, 2.53]
Sau mổ - ngày 1 2.56 (1.41) 0.35-7.07 2.44 [1.47, 3.52]
Sau mổ - ngày 2 1.74 (1.09) 0.20-6.61 1.44 [1.00, 2.27]

6.3 Giải nén tên gọi như đối số trong hàm

Nếu đã làm việc với thư viện dplyr, chắc hẳn bạn đã từng biết cú pháp của hàm rename(): rename(<tên_mới> = <tên_cũ>). Cá nhân mình thấy đây là một cú pháp ngớ ngẩn (logic thông thường là <tên_cũ> = <tên_mới>), nhưng dù là cú pháp nào thì bạn sẽ phải gõ bằng tay. Để tự động hóa việc này, chúng ta có thể sử dụng toán tử “ba chấm than” (!!!) để “giải nén” một vector có tên, tương tự như kĩ thuật dictionary unpacking (**kwargs) trong Python.

library(dplyr)

Attaching package: 'dplyr'
The following objects are masked from 'package:stats':

    filter, lag
The following objects are masked from 'package:base':

    intersect, setdiff, setequal, union
data2 %>% rename(!!!vars_to_get_stats) %>% head()
   Trước mổ Sau mổ - ngày 1 Sau mổ - ngày 2
1 2.4998077        1.560672       1.1883247
2 1.4112553        2.871651       2.6535716
3 0.3700219        0.856785       1.8037974
4 1.1291394        3.366226       0.5186375
5 0.9739632        4.965208       2.3719618
6 2.2215693        2.454356       1.4778128

Tương tự, bạn có thể thiết kế data dictionary để recode cho biến (sử dụng hàm dplyr::recode()).

datadict <- data.frame(
    var = c("sex", "sex", "has_insurance", "has_insurance"),
    code = c(1, 2, 0, 1),
    value = c("Female", "Male", "No", "Yes")
)

datadict
            var code  value
1           sex    1 Female
2           sex    2   Male
3 has_insurance    0     No
4 has_insurance    1    Yes
d <- data.frame(
    sex = sample(c(1, 2), 10, replace = TRUE),
    has_insurance = sample(c(0, 1), 10, replace = TRUE)
) %>% tibble::as_tibble()

d
# A tibble: 10 × 2
     sex has_insurance
   <dbl>         <dbl>
 1     2             1
 2     2             0
 3     2             0
 4     2             0
 5     1             1
 6     1             1
 7     2             1
 8     2             0
 9     1             1
10     1             0
recode_var <- function(d, datadict, varname) {
    # Lập từ điển recode cho biến
    codes <- datadict %>% filter(var == varname) %>% pull(code)
    values <- datadict %>% filter(var == varname) %>% pull(value)
    names(values) <- codes

    # Recode cho biến
    # Lưu ý: cú pháp của recode() ngược với rename()
    # recode(<giá_trị_cũ> = <giá_trị_mới>)
    d %>% mutate(!!varname := recode_factor(!!sym(varname), !!!values))
}

recode_var(d, datadict, "sex")
# A tibble: 10 × 2
   sex    has_insurance
   <fct>          <dbl>
 1 Male               1
 2 Male               0
 3 Male               0
 4 Male               0
 5 Female             1
 6 Female             1
 7 Male               1
 8 Male               0
 9 Female             1
10 Female             0