Bài 1: Quy trình làm việc tái lập với R

Tổ chức một dự án mô hình sao cho mọi con số đều chạy lại được từ mã

Vì sao bạn nên quan tâm?

Cuối năm, Hội đồng thuốc và điều trị của một bệnh viện tỉnh họp để sửa phác đồ kháng sinh kinh nghiệm cho ICU, dựa trên bản tổng kết kháng sinh đồ do khoa vi sinh gửi lên. Một ngày colistin cho một người bệnh nhiễm CRE, tức nhóm vi khuẩn đường ruột kháng carbapenem, tốn khoảng 3,641,873 VNĐ theo giá trúng thầu quy về năm 2025 (Bệnh viện Trung ương Thái Nguyên 2023), gấp 23 lần một ngày meropenem generic (Bệnh viện Đa khoa khu vực Củ Chi 2024) và bằng 15 ngày lương tối thiểu vùng I theo mức từ 1/1/2026 (Chính phủ 2025). Nếu tỷ lệ K. pneumoniae kháng carbapenem trong báo cáo bị gõ tay sai, hội đồng có thể đẩy cả khoa sang phác đồ đắt gấp nhiều lần, hoặc giữ phác đồ đã mất tác dụng, mà không ai phát hiện. Bài này giúp bạn tổ chức dự án sao cho mọi con số trong báo cáo đều chạy lại được từ dữ liệu thô và truy được về nguồn, để hội đồng có thể tin và kiểm tra.

Mục tiêu học tập

Sau bài này, bạn có thể:

  1. Chạy một script R từ thư mục gốc của dự án, bằng IDE hoặc bằng Rscript.
  2. Sắp xếp một dự án mô hình thành các thư mục code/, params/, data/, figs/ và giải thích vai trò của từng thư mục.
  3. Lưu tham số trong bảng CSV có cột nguồn và cờ giá trị minh họa, đọc bằng read_params(), và truy một tham số về số liệu công bố gốc.
  4. Chọn đúng công cụ: script .R cho phần tính toán, Quarto .qmd cho phần báo cáo.
  5. Viết con số trong văn bản bằng mã R nội dòng thay vì gõ tay.

Ý chính

  • Tính tái lập (reproducibility) nghĩa là người khác, hoặc chính bạn sáu tháng sau, chạy lại mã trên cùng dữ liệu và ra đúng các con số đã báo cáo.
  • Thư mục dự án là đơn vị làm việc. Mọi đường dẫn đều tính tương đối, tức tính từ thư mục module (module dùng chung _shared/ đặt ở thư mục cha, xem bên dưới); không dùng setwd() với đường dẫn tuyệt đối, loại đường dẫn đầy đủ chỉ đúng trên máy bạn.
  • Tách ba lớp: đầu vào (params/, data/), mô hình (hàm trong code/model.R), phân tích (một script cho mỗi câu hỏi). Kết quả đi vào figs/ và báo cáo.
  • Dữ liệu thô chỉ để đọc, còn mọi bước làm sạch đều viết thành mã.
  • Mỗi tham số, tức mỗi con số đầu vào của mô hình, chiếm một dòng trong bảng tham số (parameter table), kèm nguồn, hoặc gắn cờ illustrative = TRUE nếu là giá trị minh họa. Tham số suy ra từ số liệu công bố thì phép suy ra cũng phải nằm trong mã.
  • Giá trị dùng chung cho cả khóa học (giá dịch vụ, giá thuốc, lương, chỉ số giá tiêu dùng CPI, bảng sống) chỉ nằm ở _shared/; module không chép lại.
  • Mọi con số trong văn bản sinh ra từ mã R nội dòng (inline R code). Số gõ tay sẽ lỗi thời ngay khi dữ liệu hoặc mã thay đổi.
  • Ghi lại môi trường chạy: phiên bản R và gói, cùng set.seed() cho mọi bước ngẫu nhiên.

Nội dung

Vì sao tái lập là yêu cầu bắt buộc

Mô hình kinh tế y tế là lập luận định lượng dùng để hỗ trợ ra quyết định, chẳng hạn có nên đưa một xét nghiệm nhanh vào danh mục chi trả hay không. Hội đồng đánh giá công nghệ y tế, người phản biện và đồng nghiệp đều cần kiểm tra lập luận đó. Mô hình dựng trong bảng tính thường khó kiểm tra: công thức nằm rải rác trong các ô, khó so sánh phiên bản, và khó chạy lại hàng nghìn lần với các bộ tham số khác nhau để phân tích bất định. Đó là nhận xét thực tế của người viết. Các tác giả đánh giá công nghệ y tế cũng nêu rằng bảng tính hạn chế tính minh bạch, khả năng tái lập và khả năng định lượng bất định, còn các ngôn ngữ lập trình như R cho phép xây mô hình minh bạch, tái lập, dùng lại được và biểu diễn được bất định (Incerti và c.s. 2019).

Trước khi bắt đầu, có hai bài viết ngắn đáng đọc, tóm tắt các thói quen tối thiểu: ghi lại cách tạo ra mọi kết quả, tránh thao tác tay, lưu dữ liệu gốc nguyên vẹn, đặt seed (giá trị khởi tạo bộ sinh số ngẫu nhiên) cho mọi bước ngẫu nhiên và quản lý phiên bản (Sandve và c.s. 2013; Wilson và c.s. 2017). Khóa học này áp dụng các thói quen đó ngay từ bài đầu.

R và trình soạn mã

R là môi trường tính toán thống kê miễn phí, chạy trên Windows, macOS và Linux (R Core Team không ngày). Bạn viết mã trong trình soạn mã tích hợp (IDE) như RStudio, Positron hoặc VS Code. Có hai cách chạy mã: muốn thử nhanh thì gõ từng lệnh vào console, khung mà R chạy ngay lệnh vừa gõ; muốn có kết quả chính thức thì lưu chuỗi lệnh vào một script .R rồi chạy từ đầu đến cuối. Kết quả chính thức chỉ do script tạo ra, chứ không phải những gì bạn đã gõ vào console.

Gói (package) là bộ hàm viết sẵn để mở rộng R; hàm ở đây là lệnh có tên, nhận đầu vào rồi trả về kết quả. Mỗi gói cài một lần bằng install.packages("ggplot2"), nhưng mỗi lần mở R lại phải nạp bằng library(ggplot2). Khóa học dùng base R, tức phần R có sẵn khi cài, cho cơ chế mô hình vì từng bước đều nhìn thấy được, và dùng ggplot2 để vẽ biểu đồ.

Cấu trúc một dự án mô hình

Mọi module của khóa học đều là dự án nhỏ, chung một cấu trúc:

Thư mục/tệp Chứa gì Quy tắc
params/*.csv Tham số: giá trị, khoảng, phân phối, nguồn Không có số gõ cứng trong mã
data/ Dữ liệu đầu vào Chỉ đọc; tạo ra bằng mã hoặc nhận nguyên trạng
code/model.R Định nghĩa hàm Nạp vào không gây tác dụng phụ (không chạy phân tích, không ghi tệp)
code/lNN-*.R Một phân tích hoàn chỉnh Chạy độc lập từ thư mục gốc
figs/ Hình do mã tạo ra Không sửa tay
lNN-*.qmd Bài viết, kéo mã từ script Mọi con số là mã R nội dòng

Cách tách này giống khung viết mã đề xuất cho mô hình quyết định trong R, vốn chia công việc thành đầu vào, triển khai mô hình, hiệu chuẩn, thẩm định và phân tích (Alarid-Escudero và c.s. 2019). Lợi ích rất thực tế: muốn đổi một giá thuốc, bạn sửa một dòng CSV và chạy lại, không phải lục tìm trong mã.

Mọi module cùng dùng hai tệp đặt ở thư mục cha: _shared/R/helpers.R (hàm định dạng số, hàm kiểm tra, giao diện chung cho biểu đồ) và _shared/R/vn_data.R (giá trị dùng chung của Việt Nam: giá dịch vụ BHYT, giá thuốc trúng thầu, lương tối thiểu, CPI, bảng sống). Nhờ vậy, khi Bộ Y tế đổi giá ngày giường, ta sửa đúng một dòng trong _shared/ là mọi module cập nhật ở lần chạy sau. “Dự án” của khóa học là thư mục gốc kb/, trong đó các module nằm cạnh _shared/. Khi tải về, hãy tải cả _shared/ lẫn module bạn học; mỗi script chạy với thư mục module làm thư mục làm việc, tức nơi R tìm và ghi tệp.

flowchart LR
  A["params/*.csv<br/>data/ (chỉ đọc)"] --> B["code/model.R<br/>chỉ định nghĩa hàm"]
  B --> C["code/lNN-*.R<br/>phân tích"]
  A --> C
  C --> D["figs/*.png<br/>con số"]
  D --> E["lNN-*.qmd<br/>mã R nội dòng"]
  E --> F["HTML / DOCX"]

flowchart LR
  A["params/*.csv<br/>data/ (chỉ đọc)"] --> B["code/model.R<br/>chỉ định nghĩa hàm"]
  B --> C["code/lNN-*.R<br/>phân tích"]
  A --> C
  C --> D["figs/*.png<br/>con số"]
  D --> E["lNN-*.qmd<br/>mã R nội dòng"]
  E --> F["HTML / DOCX"]

Script hay Quarto?

Script .R là nơi tính toán: chạy từ trên xuống, không phụ thuộc thứ tự bạn bấm nút. Quarto (Posit không ngày) trộn văn bản với mã và xuất ra HTML, Word hoặc PDF, mà mỗi lần xuất thì toàn bộ mã chạy lại từ đầu. Khóa học dùng cả hai mà không chép mã hai lần: script đánh dấu từng đoạn bằng ## ---- nhãn ----, và bài học kéo đoạn đó vào bằng knitr::read_chunk(). Mã chỉ nằm ở một nơi, nên bài viết không bao giờ lệch khỏi mã.

Mọi con số do mã tạo ra

Đây là nguyên tắc 9 trong mười quy tắc của Sandve và cộng sự: mỗi câu nêu kết quả phải truy được về kết quả nền bên dưới (Sandve và c.s. 2013). Trong tệp .qmd, câu nêu tỷ lệ đề kháng không chứa con số gõ tay mà chứa `r fmt_pct(res_2025)` (biểu thức R đặt giữa hai dấu backtick, bắt đầu bằng chữ r). Khi xuất, Quarto chạy biểu thức này và chèn kết quả vào câu. Nếu dữ liệu, tham số hoặc seed thay đổi, con số tự cập nhật. Các hàm fmt_num(), fmt_pct(), fmt_vnd() và fmt_ci() trong _shared/R/helpers.R thống nhất cách viết số cho cả khóa học: dấu chấm thập phân, dấu phẩy phân cách hàng nghìn.

Ghi lại môi trường chạy

Kết quả có thể đổi khi phiên bản R hoặc phiên bản gói thay đổi. Tối thiểu, hãy in phiên bản R và các gói chính ở cuối báo cáo. Với dự án dài hạn, gói renv lưu đúng phiên bản của từng gói vào một tệp khóa để người khác cài lại y hệt (Ushey và Wickham không ngày). Mọi bước ngẫu nhiên đều bắt đầu bằng set.seed(), để mỗi lần chạy lại ra đúng dãy số cũ (xem Bài 6).

Thực hành

Mọi script trong khóa học mở đầu như nhau: dừng ngay nếu chạy sai thư mục, bật mã hóa UTF-8 để hiển thị tiếng Việt, nạp các hàm dùng chung (helpers.R, vn_data.R) và hàm của module, rồi bật cảnh báo khi $, phép lấy phần tử theo tên, tự khớp với tên viết tắt (xem Bài 2). Kiểm tra: R không báo lỗi nào.

# Fail early, before anything is sourced, if the script is not run from the module folder
if (!file.exists("code/model.R") || !file.exists("../_shared/R/helpers.R")) {
  stop("Run this script from the module folder (the one that contains code/ and params/); ",
       "the course root must also contain _shared/.")
}
# Vietnamese text needs a UTF-8 locale (Rscript may start in the "C" locale)
if (!isTRUE(l10n_info()[["UTF-8"]])) invisible(Sys.setlocale("LC_CTYPE", "en_US.UTF-8"))
source("../_shared/R/helpers.R")   # shared helpers: formatting, checks, plot theme
source("../_shared/R/vn_data.R")   # shared Vietnamese inputs: prices, wages, life table, CPI
source("code/model.R")             # this module's functions (definitions only)
options(warnPartialMatchDollar = TRUE)   # warn when `$` silently matches a shortened name (Lesson 2)

Lỗi hay gặp nhất của người mới là chạy script từ sai thư mục; các dòng đầu của đoạn mã trên đã chặn lỗi đó. Đoạn mã sau liệt kê thư mục hiện hành và các thư mục của dự án, tạo data/ và figs/ nếu chưa có. Kiểm tra: tên thư mục hiện hành là m00-r-foundations và cả bốn thư mục đều tồn tại.

# Where are we? code/ and params/ must exist; data/ and figs/ are created when missing
stopifnot(dir.exists("code"), dir.exists("params"))
for (f in c("data", "figs")) dir.create(f, showWarnings = FALSE)
basename(getwd())
[1] "m00-r-foundations"
folders <- c("code", "params", "data", "figs")
data.frame(folder = folders, exists = dir.exists(folders))
  folder exists
1   code   TRUE
2 params   TRUE
3   data   TRUE
4   figs   TRUE

Tiếp theo, ta đọc bảng tham số của bộ dữ liệu mô phỏng. read_params() trả về một danh sách có tên, nên ta gọi mỗi tham số bằng p_sim$ten_tham_so. Bảng trộn hai loại dòng: mốc đã công bố, có khóa tài liệu trong cột source (tỷ lệ đề kháng, tỷ lệ loài, xu hướng), và giá trị minh họa cho những gì không có số liệu công khai (lỗi nhập liệu, số lần cấy lại). Kiểm tra: bảng đếm theo nguồn cho thấy mỗi dòng có nguồn trỏ tới một khóa trong refs.bib, tệp tài liệu tham khảo.

p_sim <- read_params("params/amr_sim.csv")
tab_sim <- attr(p_sim, "table")
tab_sim[tab_sim$name %in% c("n_pat_a", "target_kpn_mem", "trend_kpn"),
        c("name", "value", "source", "illustrative")]
             name   value              source illustrative
3         n_pat_a 300.000                             TRUE
21 target_kpn_mem   0.370   larsson2025burden        FALSE
24      trend_kpn   0.146 vu2021antimicrobial        FALSE
n_params <- nrow(tab_sim)
n_sourced <- sum(!as.logical(tab_sim$illustrative))
n_illustrative <- n_params - n_sourced
c(n_params = n_params, n_sourced = n_sourced, n_illustrative = n_illustrative)
      n_params      n_sourced n_illustrative 
            41             24             17 
table(source = ifelse(as.logical(tab_sim$illustrative), "(minh họa)", tab_sim$source))
source
         (minh họa)   larsson2025burden vu2021antimicrobial 
                 17                   3                  21 
stopifnot(n_sourced + n_illustrative == n_params, n_sourced > 0)
p_sim$target_kpn_mem   # access one value by name
[1] 0.37

Bảng có 41 tham số: 24 có nguồn và 17 là giá trị minh họa.

read_params() còn làm hàng rào an toàn: nếu bảng khai một tham số là số liệu thật (illustrative = FALSE) mà không ghi nguồn, hàm dừng lại và nêu tên tham số đó. Đoạn mã sau cố ý tạo một bảng lỗi như vậy để xem thông báo. Kiểm tra: thông báo nêu đúng tên tham số thiếu nguồn.

# A "real" parameter without a source is refused by read_params()
bad <- tab_sim[1:3, ]
bad$illustrative[2] <- FALSE
tmp <- tempfile(fileext = ".csv")
write.csv(bad, tmp, row.names = FALSE)
msg <- tryCatch(read_params(tmp), error = function(e) conditionMessage(e))
msg
[1] "Non-illustrative parameters need a source: n_years"

Có nguồn chưa đủ: phải truy được con số về số liệu gốc. Xác suất mắc mới CRE mỗi ngày trong params/icu_markov.csv không có sẵn trong bài báo nào, mà suy ra từ hai mốc công bố (Tran và c.s. 2019). Gọi \(\pi_0\) và \(\pi_T\) là tỷ lệ mang khuẩn vào ngày nhập viện và ngày thứ \(T\). Nếu mỗi ngày có tỷ lệ \(p\) người chưa mang khuẩn bị lây, phần chưa mang khuẩn giảm theo cấp số nhân: \((1 - p)^T = (1 - \pi_T)/(1 - \pi_0)\). Phép suy ra này là xấp xỉ: điều tra gốc là cắt ngang, gộp cả sơ sinh, trẻ em và người lớn, và ta đọc đường cong theo ngày nằm viện như thể đó là đường theo dõi một đoàn hệ. Các mốc nằm trong params/reference_data.csv; đoạn mã sau dùng hàm icu_probs_from_anchors() làm lại mọi phép suy ra rồi so với CSV. Kiểm tra: hai cột khớp nhau đến sai số làm tròn.

# Every number must trace back to its source. The daily probabilities of the ICU model
# (params/icu_markov.csv) were derived from published figures kept in params/reference_data.csv.
# Recompute them in code and compare with the CSV.
ref <- read_params("params/reference_data.csv")
q_csv <- read_params("params/icu_markov.csv")
derived <- icu_probs_from_anchors(ref)
in_csv <- c(unlist(q_csv[setdiff(names(derived), "p_i_alive_exit")]),
            p_i_alive_exit = q_csv$p_i_c + q_csv$p_i_x)
trace_tab <- cbind(csv = in_csv, derived = derived[names(in_csv)])
round(trace_tab, 5)
                  csv derived
p_u_c          0.1288 0.12879
p_u_x          0.0897 0.08966
p_u_d          0.0227 0.02270
p_c_u          0.0088 0.00880
p_c_i          0.0197 0.01972
p_c_x          0.0897 0.08966
p_c_d          0.0227 0.02270
p_i_d          0.0305 0.03048
p_i_alive_exit 0.0657 0.06567
stopifnot(all(abs(trace_tab[, "csv"] - trace_tab[, "derived"]) < 1e-4))   # equal up to rounding

Với \(\pi_0\) = 13%, \(\pi_T\) = 89% và \(T\) = 15 ngày, xác suất mắc mới là 0.1288 mỗi ngày, đúng giá trị 0.1288 trong CSV. Ai đọc báo cáo mà muốn kiểm tra con số này thì mở bài báo gốc và chạy lại một dòng mã là đủ.

Bây giờ ta tạo bộ dữ liệu của module. simulate_isolates() sinh từng chủng phân lập (isolate) theo các tham số trên, quanh các mốc đã công bố; str() cho thấy mỗi cột có kiểu gì. Kiểm tra: bảng chéo bệnh viện × năm không có ô trống, và ba điều kiện trong stopifnot() đều đúng (hàm này dừng script khi có điều kiện sai).

iso <- simulate_isolates(p_sim, seed = 2025)
str(iso)
'data.frame':   4080 obs. of  12 variables:
 $ isolate_id: chr  "I00001" "I00002" "I00003" "I00004" ...
 $ patient_id: chr  "P00005" "P00073" "P00190" "P00218" ...
 $ hospital  : chr  "A" "A" "A" "A" ...
 $ ward      : chr  "non-ICU" "ICU" "non-ICU" "ICU" ...
 $ date      : Date, format: "2021-01-01" "2021-01-01" ...
 $ year      : int  2021 2021 2021 2021 2021 2021 2021 2021 2021 2021 ...
 $ organism  : chr  "A. baumannii" "E. coli" "E. coli" "E. coli" ...
 $ CRO       : chr  NA "R" "R" "R" ...
 $ CIP       : chr  "R" "R" "R" "R" ...
 $ GEN       : chr  "R" "R" "R" "R" ...
 $ SXT       : chr  "R" "R" "R" "R" ...
 $ MEM       : chr  "R" "S" "S" "R" ...
table(iso$hospital, iso$year)
   
    2021 2022 2023 2024 2025
  A  426  412  414  383  383
  B  260  264  250  222  252
  C  174  160  150  176  154
# Sanity checks: unique IDs, every antibiotic column present, years as planned
stopifnot(!anyDuplicated(iso$isolate_id),
          all(names(ANTIBIOTICS) %in% names(iso)),
          all(iso$year %in% (p_sim$year_start + 0:(p_sim$n_years - 1))))
n_iso <- nrow(iso)
n_pat <- length(unique(iso$patient_id))

Bộ dữ liệu có 4,080 chủng từ 3,094 người bệnh. Số chủng nhiều hơn số người bệnh vì một số người được cấy nhiều lần.

Phòng xét nghiệm không gửi dữ liệu sạch. make_messy() biến dữ liệu sạch thành một tệp trích xuất từ phòng xét nghiệm (laboratory export) có lỗi nhập liệu điển hình, rồi ta ghi tệp ấy vào data/. Tệp này do mã tạo ra hoàn toàn, nên xóa đi rồi chạy lại vẫn ra đúng tệp cũ. Kiểm tra: số dòng đọc lại phải khớp với số dòng đã ghi.

# The "laboratory export": written by code, never edited by hand
raw <- make_messy(iso, p_sim, seed = 2026)
dir.create("data", showWarnings = FALSE)
write.csv(raw, "data/amr_isolates_raw.csv", row.names = FALSE, na = "")
raw_back <- read.csv("data/amr_isolates_raw.csv", na.strings = "", encoding = "UTF-8")
stopifnot(nrow(raw_back) == nrow(raw), ncol(raw_back) == ncol(raw))
dim(raw_back)
[1] 4100   11

Tệp data/amr_isolates_raw.csv có 4,100 dòng, nhiều hơn dữ liệu sạch 20 dòng do các dòng bị nhập trùng.

Đoạn mã sau cho thấy vì sao không được gõ số bằng tay: ta tính tỷ lệ A. baumannii đề kháng meropenem hai lần, trên hai bộ dữ liệu sinh từ hai seed khác nhau. Tỷ lệ này tính trên mọi chủng, kể cả chủng lặp lại; Bài 4 sẽ loại các chủng lặp lại ấy. Kiểm tra: hai kết quả khác nhau.

# One result computed twice: with the seed used in the lesson and with another seed.
# (Computed on all isolates, repeats included; Lesson 4 removes the repeats.)
aba_mem_r <- function(d) mean(d$MEM[d$organism == "A. baumannii"] == "R", na.rm = TRUE)
res_2025 <- aba_mem_r(iso)
res_2026 <- aba_mem_r(simulate_isolates(p_sim, seed = 2026))
c(seed_2025 = fmt_pct(res_2025), seed_2026 = fmt_pct(res_2026))
seed_2025 seed_2026 
  "72.4%"   "73.8%" 
stopifnot(res_2025 != res_2026)   # the lesson says the two results differ

Với seed của bài học, tỷ lệ là 72.4%; với seed khác, 73.8%. Nếu bạn gõ tay con số thứ nhất vào báo cáo rồi sau đó đổi seed hay sửa dữ liệu, báo cáo sẽ sai mà không ai biết. Câu văn bạn vừa đọc dùng mã R nội dòng, nên luôn đúng.

Cuối cùng, ta ghi lại môi trường đã tạo ra kết quả: phiên bản R, phiên bản gói vẽ biểu đồ và nền tảng máy.

c(R = R.version.string,
  ggplot2 = as.character(utils::packageVersion("ggplot2")),
  platform = R.version$platform)
                             R                        ggplot2 
"R version 4.5.3 (2026-03-11)"                        "4.0.2" 
                      platform 
      "aarch64-apple-darwin20" 
Mẹo thực hành
  • Trong RStudio hoặc Positron, tạo một Project cho mỗi dự án. Khi mở project, thư mục làm việc tự động là thư mục gốc, nên mọi đường dẫn tương đối đều đúng.
  • Thỉnh thoảng khởi động lại R (Session → Restart R) rồi chạy script từ đầu. Nếu script chỉ chạy được khi console còn “nhớ” một biến cũ, đó là lỗi cần sửa ngay.
  • Đặt tên tham số ngắn, không dấu, có quy ước (p_ cho xác suất, c_ cho chi phí); dùng cột label_vi để mô tả bằng tiếng Việt.
  • Chạy cả script bằng Rscript code/ten-script.R trong terminal, tức cửa sổ dòng lệnh của máy. Lệnh này khởi động R từ đầu, nên chạy được nghĩa là script không phụ thuộc vào bất cứ thứ gì bạn đã làm bằng tay.
Lỗi thường gặp
  • Dùng setwd("C:/Users/..."). Đường dẫn tuyệt đối chỉ đúng trên máy bạn. Hãy dùng đường dẫn tương đối từ thư mục gốc của project.
  • Sửa dữ liệu thô bằng Excel. Thao tác tay không để lại dấu vết. Mọi chỉnh sửa phải nằm trong mã; tệp gốc giữ nguyên.
  • Gõ số vào văn bản. Dấu hiệu nhận biết: trong tệp .qmd có một con số kết quả không nằm trong mã R nội dòng.
  • Nhầm thứ tự ưu tiên toán tử. R hiểu x %in% a + 0:4 là (x %in% a) + 0:4. Khi viết kiểm tra, hãy thêm ngoặc: x %in% (a + 0:4), rồi chạy thử kiểm tra trên một ví dụ đã biết đáp án để chắc rằng stopifnot() thực sự bắt được lỗi.

Bài tập

Bài tập 1 (dễ). Đếm số chủng theo loài vi khuẩn và bệnh viện. Ở mỗi bệnh viện, bao nhiêu phần trăm chủng lấy từ khoa hồi sức tích cực (ICU)?

table() đếm theo hai chiều. Trung bình của một vectơ logic, tức dãy giá trị đúng/sai, chính là tỷ lệ phần tử TRUE, vì R coi đúng là một, sai là không; tapply() tính trung bình đó cho từng bệnh viện.

# Exercise 1: isolates per organism and hospital, and share of isolates from ICU
table(iso$organism, iso$hospital)
               
                   A    B    C
  A. baumannii   539  304  201
  E. coli       1047  655  410
  K. pneumoniae  432  289  203
round(tapply(iso$ward == "ICU", iso$hospital, mean), 3)
    A     B     C 
0.200 0.194 0.216 

Bài tập 2 (vừa). Tăng gấp đôi quy mô bệnh viện C bằng cách sửa tham số trong R, không sửa CSV. Chạy lại mô phỏng với cùng seed và so sánh số chủng của bệnh viện C.

Sao chép danh sách tham số, đổi một phần tử, rồi chạy lại. Tệp CSV không thay đổi, nên phân tích cơ sở, tức kết quả với bộ tham số gốc, vẫn nguyên vẹn.

# Exercise 2: double the size of hospital C, re-simulate, compare total isolates
p_big <- p_sim
p_big$n_pat_c <- 2 * p_sim$n_pat_c
iso_big <- simulate_isolates(p_big, seed = 2025)
c(base = sum(iso$hospital == "C"), doubled = sum(iso_big$hospital == "C"))
   base doubled 
    814    1548 

Bài tập 3 (khó hơn). Nạp tham số của mô hình ICU bằng load_icu_params(). Có bao nhiêu tham số được gắn một phân phối xác suất, bao nhiêu có nguồn và bao nhiêu là giá trị minh họa? Hai chi phí lấy từ bảng giá dùng chung nằm ở đâu, và vì sao không được chép chúng vào params/icu_markov.csv?

Bảng đầy đủ nằm trong thuộc tính "table" của kết quả.

# Exercise 3: which ICU parameters are sourced and which illustrative, and which have a
# probability distribution attached? load_icu_params() adds the two shared prices.
tab_icu <- attr(load_icu_params(), "table")
table(dist = tab_icu$dist, illustrative = tab_icu$illustrative)
       illustrative
dist    FALSE TRUE
  beta      8    2
  fixed     0    5
  gamma     2    1
tab_icu[tab_icu$name %in% c("c_icu_day", "c_infection"), c("name", "value", "source")]
                   name   value                            source
c_icu_day     c_icu_day 1017300                     byt2024qd3220
c_infection c_infection 3801137 thainguyen2023thau; cuchi2024thau
n_icu_sourced <- sum(!as.logical(tab_icu$illustrative))
n_icu_illustrative <- sum(as.logical(tab_icu$illustrative))
# The lesson text lists the illustrative ones: check the list is still right
stopifnot(setequal(tab_icu$name[as.logical(tab_icu$illustrative)],
                   c("n_days", "outbreak_rr", "outbreak_start", "outbreak_end", "c_contact",
                     "cost_gamma_shape", "p_i_c", "p_i_x")))

Mô hình ICU có 10 tham số có nguồn và 8 giá trị minh họa (số ngày theo dõi, ba tham số của đợt bùng phát, chi phí phòng ngừa tiếp xúc, độ rộng phân phối của chi phí, và hai xác suất chia lối ra của người đang nhiễm khuẩn). Giá ngày giường và giá thuốc nằm trong _shared/params/vn_prices.csv; load_icu_params() đọc chúng qua vn_prices(). Nếu chép giá vào CSV của module thì khi bảng giá chung cập nhật, module này sẽ âm thầm dùng giá cũ, và hai module có thể báo hai giá khác nhau cho cùng một ngày giường. Ngay cả khi mọi tham số có nguồn, mô hình vẫn đơn giản hóa thực tế (một ICU chung, không phân tầng bệnh nặng), nên kết quả là con số gần đúng để học, chưa đủ để ra quyết định cho một bệnh viện cụ thể.

Tài liệu tham khảo

# Slide Lời giảng chính Hình/Code
1 Vì sao tái lập Mô hình là lập luận hỗ trợ quyết định; ai cũng phải chạy lại được; bảng tính khó kiểm tra Trích Incerti 2019
2 R và IDE Console để thử, script để chạy thật; cài gói một lần, nạp mỗi phiên Màn hình RStudio/Positron
3 Cấu trúc dự án Bốn thư mục; ba lớp đầu vào, mô hình, phân tích Bảng thư mục, sơ đồ Mermaid
4 Chạy đúng thư mục stop() nếu thiếu code/model.R; _shared/ đặt cạnh module; Project trong IDE l01-setup, l01-check-wd
5 Bảng tham số Một dòng một tham số; nguồn hoặc cờ minh họa; _shared/ cho giá trị chung l01-params
6 Hàng rào an toàn và truy nguồn read_params() từ chối số liệu “thật” không có nguồn; tính lại xác suất mắc mới CRE từ 13% và 89% l01-guardrail, l01-trace-source
7 Dữ liệu do mã tạo Mô phỏng rồi tạo tệp trích xuất có lỗi; dữ liệu thô chỉ đọc l01-simulate, l01-save-raw
8 Script và Quarto Một nơi chứa mã; read_chunk() kéo đoạn mã vào bài Nhãn ## ---- l01-setup ----
9 Mọi con số do mã tạo ra Hai seed, hai con số khác nhau; số gõ tay sẽ lỗi thời l01-every-number
10 Ghi lại môi trường Phiên bản R và gói; renv; seed l01-session
11 Lỗi thường gặp setwd() tuyệt đối; sửa dữ liệu bằng Excel; thứ tự toán tử Callout cảnh báo