Giao diện
↑ R1 · ← Workflow & Style · → Types & Missingness
🎯 Mục tiêu
Bạn sẽ gặp 80% bug R “khó chịu” từ 3 thứ: (1) chọn sai data structure, (2) subsetting trả về kiểu khác bạn nghĩ, (3) recycling xảy ra âm thầm. Bài này chốt cả 3.
1) Mental model: 4 data structures, 1 triết lý
R có rất nhiều class, nhưng nền tảng bạn cần chốt là:
- Atomic vector: 1 chiều, 1 kiểu (numeric/character/logical/...) → máy chạy nhanh.
- List: 1 chiều, phần tử có thể khác kiểu → linh hoạt.
- Matrix: atomic vector +
dim→ vẫn “atomic”, chỉ khác shape. - Data frame: list của các cột (vectors) có cùng length → dữ liệu dạng bảng.
Nếu bạn nhớ được 2 câu:
- Matrix không “mới”, nó là vector có
dim. - Data frame không “mới”, nó là list các cột có cùng số dòng.
…thì bạn sẽ debug nhanh hơn hẳn.
1.1 So sánh nhanh (bảng quyết định)
| Cấu trúc | Lưu trữ | Đồng nhất kiểu? | Khi nên dùng |
|---|---|---|---|
vector | 1D | Có | cột dữ liệu, tính toán vectorized |
list | 1D | Không | record, config, output nhiều thành phần |
matrix | 2D | Có | đại số tuyến tính, numeric computation |
data.frame | 2D | Theo cột | dữ liệu tabular, join/filter/summarise |
⚠️ Cạm bẫy
Nhiều người dùng list như “data frame DIY” hoặc dùng data.frame như “mảng 2D” thuần. Hai cái đó đều dẫn đến code khó đọc và khó kiểm soát kiểu trả về.
2) Vectors & Lists: đồng nhất vs linh hoạt
2.1 Atomic vector: coercion luôn xảy ra
Atomic vector chỉ có một kiểu. Khi bạn trộn kiểu, R sẽ coercion theo “độ bao dung”:
r
c(1, 2, 3) # numeric
c(TRUE, FALSE, TRUE) # logical
c(1, "2", 3) # character (numeric bị nâng lên character)Điều này cực tiện, nhưng cũng là nguồn bug “im lặng” khi bạn tưởng mình đang làm toán.
2.2 List: container của containers
List cho phép mỗi phần tử là bất cứ thứ gì:
r
x <- list(
user_id = 42L,
features = c(0.1, 0.2),
meta = list(source = "api", ts = Sys.time())
)List rất hợp để:
- trả về nhiều output từ một function,
- đóng gói config,
- parse JSON.
3) Matrix: “vector có dim” và luật drop
Matrix vẫn là atomic vector. Vì thế:
- Chỉ có 1 kiểu cho toàn bộ phần tử.
- Tất cả quy tắc vector (coercion, recycling) vẫn áp dụng.
Subsetting matrix có một bẫy lớn: drop = TRUE là mặc định.
r
m <- matrix(1:6, nrow = 2)
m[1, ] # vector (bị drop chiều)
m[1, , drop=FALSE] # vẫn là matrix 1x3⚠️ Cạm bẫy
Khi bạn lấy 1 hàng/1 cột từ matrix và bị drop về vector, code downstream (kỳ vọng matrix) sẽ vỡ ở những chỗ rất xa nguồn gốc.
4) Data frame: list các cột (bảng), không phải mảng 2D thuần
Data frame có 2 chỉ số (row, col) nhưng bản chất là list-of-columns:
r
df <- data.frame(user_id = c(1L, 2L, 3L), score = c(10, 20, 30))
str(df)Điểm cần nhớ:
- Mỗi cột là một vector.
- Mọi cột phải có cùng số dòng.
df[[...]]/df$...trả về cột (vector), còndf[...]thường trả về data.frame.
5) Subsetting: [], [[]], $ và hệ quả
Đây là phần bạn phải “thuộc lòng” vì nó quyết định kiểu trả về.
5.1 Quy tắc chung
[luôn trả về “cùng loại container”:- vector → vector
- list → list
- data.frame → data.frame (thường vậy)
[[lấy “một phần tử” bên trong container (không còn bọc list/data.frame).$lấy phần tử theo tên (chỉ dùng cho list/data.frame), và có những hành vi dễ gây surprise.
5.2 List subsetting: [ vs [[ (bẫy kinh điển)
r
x <- list(a = 10, b = 20)
x["a"] # list(a = 10)
x[["a"]] # 10
x$a # 10Hệ quả:
x["a"]vẫn là list → downstream nếu kỳ vọng numeric sẽ fail.x[["a"]]là element → đúng khi bạn cần giá trị.
5.3 Data frame subsetting: cột vs bảng
r
df <- data.frame(user_id = 1:3, score = c(10, 20, 30))
df["score"] # data.frame 1 cột
df[["score"]] # vector
df$score # vector
df[, "score"] # vector (mặc định drop)
df[, "score", drop = FALSE] # data.frame 1 cộtKhuyến nghị thực dụng:
- Khi muốn “bảng” → dùng
df[ , , drop = FALSE]hoặcdf["col"]. - Khi muốn “cột” → dùng
df[["col"]](rõ ràng nhất).
5.4 $ và rủi ro
$ tiện, nhưng dễ dính vấn đề:
- Nếu tên cột đổi (refactor),
$có thể trảNULLvà bug chạy tiếp. - Một số môi trường có partial matching (tùy cấu hình/đối tượng), dễ gây surprise.
Rule: dùng $ cho interactive exploration; dùng [[ cho code “ship”.
6) Recycling rules: khi nào bug xuất hiện
Recycling là cơ chế “tự lặp lại” vector ngắn để phù hợp với vector dài trong phép toán/subsetting.
6.1 Trường hợp hợp lệ (length 1)
r
x <- 1:5
x + 10
x * 2Đây là “broadcast” phiên bản R, thường là ý định đúng.
6.2 Trường hợp nguy hiểm (không phải bội số)
r
1:10 + 1:3R sẽ cảnh báo vì 10 không chia hết cho 3, nhưng vẫn cho ra kết quả. Trong pipeline dài, cảnh báo dễ bị bỏ qua.
6.3 Bug hay gặp nhất: logical index bị recycle
r
x <- 1:10
mask <- c(TRUE, FALSE) # length 2
x[mask] # mask bị recycle thành length 10Nếu bạn tưởng mask là “chọn 2 phần tử đầu”, bạn vừa tạo bug logic mà không hề biết.
✅ Checklist triển khai
Chặn recycling bug
- Với phép toán giữa 2 vector:
stopifnot(length(a) == length(b) || length(a) == 1 || length(b) == 1) - Với logical mask:
stopifnot(length(mask) == length(x))(trừ khi bạn cố ý broadcast) - Nếu cố ý recycle: dùng
rep_len()để thể hiện ý định
7) Mini cheat-sheet (để dán cạnh màn hình)
| Bạn muốn… | Dùng | Kết quả |
|---|---|---|
| Lấy “1 cột” từ data.frame | df[["col"]] | vector |
| Giữ data.frame 1 cột | df["col"] hoặc df[, "col", drop=FALSE] | data.frame |
| Lấy 1 element từ list | x[[i]] / x[["name"]] | element |
| Giữ list (sublist) | x[i] / x["name"] | list |
| Lấy 1 hàng matrix nhưng vẫn muốn matrix | m[i, , drop=FALSE] | matrix |