Bài 4: Đóng gói mô hình thành gói R

Từ một thư mục script đến gói mdrtbvn cài được, có tài liệu và vượt qua R CMD check

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

Một nhóm ở bệnh viện phổi tỉnh muốn chạy lại mô hình với giá khám của bệnh viện mình, thay cho mức 50,600 VNĐ một lượt khám theo bảng giá BHYT năm 2025 của bệnh viện hạng đặc biệt (Bộ Y tế 2024), tức khoảng 1.3 bát phở (Nhadautu.vn 2025). Mô hình M02 chỉ chạy khi có đúng thư mục khóa học bên cạnh: muốn dùng, họ phải chép nhiều tệp, sửa đường dẫn, và không chắc mình đang có bản nào. Trong khi đó, cùng mô hình còn được M03 chạy hàng nghìn lần và M10 dùng để lập ngân sách cho người bệnh lao kháng rifampicin, nhóm mà WHO ước tính có khoảng 9,400 người mắc mới mỗi năm ở Việt Nam (World Health Organization 2026). Bài này giúp bạn quyết định khi nào nên đóng gói một mô hình và phải đưa những gì vào gói, để ai cài gói cũng nhận được đúng mô hình và đúng bảng tham số đã tạo ra kết quả.

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

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

  1. Giải thích lợi ích của việc đóng gói mô hình thành một gói R (package) so với một thư mục script.
  2. Mô tả cấu trúc tối thiểu của một gói: DESCRIPTION, NAMESPACE, R/, man/, tests/, inst/.
  3. Viết tài liệu bằng chú thích roxygen2, và phân biệt hàm công khai (exported) với hàm nội bộ.
  4. Dựng, kiểm tra bằng R CMD check, cài và dùng gói; tăng số phiên bản đúng quy ước.

Ý chính

  • Một gói là một đơn vị cài được: có tên, phiên bản, phụ thuộc được khai báo, tài liệu và kiểm thử đi kèm. Ai cũng cài và gọi được nó theo cùng một cách.
  • R CMD check chạy hàng chục kiểm tra tự động, gồm cả bộ kiểm thử testthat. “Status: OK” là ngưỡng tối thiểu trước khi chia sẻ một mô hình.
  • NAMESPACE tách hàm công khai khỏi hàm nội bộ. Người dùng gọi run_model(); build_P() là chi tiết bên trong, đổi được mà không làm hỏng mã của người khác.
  • Chú thích #' ngay trên mỗi hàm công khai được roxygen2 biến thành trang trợ giúp và dòng export() trong NAMESPACE.
  • Mã trong gói không dựa vào môi trường làm việc: không source(), không library(), không đường dẫn tương đối, không phụ thuộc ẩn; hàm của gói khác gọi bằng pkg::fun() và khai báo trong trường Imports.
  • Dữ liệu đi kèm nằm trong inst/extdata/ và được đọc bằng system.file(). Gói đã cài không với tới kb/_shared, nên nó mang theo một ảnh chụp của bảng tham số đầy đủ (Bài 1).
  • Một nguồn sự thật: gói được dựng tự động từ code/model.R và tests/testthat/; số phiên bản lấy từ MODEL_VERSION.

Nội dung

Vì sao đóng gói một mô hình

Ở M02, muốn dùng mô hình thì phải source() đúng tệp, theo đúng thứ tự, từ đúng thư mục, và thư mục dữ liệu chung kb/_shared phải nằm ngay bên cạnh. Mỗi module dùng lại mô hình (M03, M10) lặp lại các đường dẫn đó. Gói R thay tất cả bằng một dòng library(mdrtbvn). Marwick và cộng sự đề xuất dùng chính cấu trúc gói R để tổ chức một công trình phân tích tái lập được, vì cấu trúc này đã quen thuộc với cộng đồng R và đi kèm sẵn công cụ kiểm tra tự động (Marwick, Boettiger, và Mullen 2018). Với mô hình kinh tế y tế còn thêm hai lợi ích: số phiên bản của gói chính là phiên bản của mô hình (Bài 3), và người duyệt có thể chạy R CMD check để biết mô hình ít nhất cài được, có tài liệu và vượt qua kiểm thử của chính nó.

Cấu trúc tối thiểu

flowchart TB
  P["mdrtbvn/"] --> D["DESCRIPTION<br/>tên, phiên bản, tác giả,<br/>giấy phép, phụ thuộc"]
  P --> N["NAMESPACE<br/>hàm công khai<br/>(roxygen2 viết)"]
  P --> R["R/model.R<br/>= code/model.R"]
  P --> M["man/*.Rd<br/>trang trợ giúp<br/>(roxygen2 viết)"]
  P --> T["tests/testthat/<br/>= tests/testthat/ của module"]
  P --> I["inst/extdata/mdrtbvn-params.csv<br/>ảnh chụp bảng tham số đầy đủ"]
  P --> L["LICENSE, NEWS.md"]

flowchart TB
  P["mdrtbvn/"] --> D["DESCRIPTION<br/>tên, phiên bản, tác giả,<br/>giấy phép, phụ thuộc"]
  P --> N["NAMESPACE<br/>hàm công khai<br/>(roxygen2 viết)"]
  P --> R["R/model.R<br/>= code/model.R"]
  P --> M["man/*.Rd<br/>trang trợ giúp<br/>(roxygen2 viết)"]
  P --> T["tests/testthat/<br/>= tests/testthat/ của module"]
  P --> I["inst/extdata/mdrtbvn-params.csv<br/>ảnh chụp bảng tham số đầy đủ"]
  P --> L["LICENSE, NEWS.md"]

DESCRIPTION là tệp duy nhất bắt buộc phải tự viết: tên gói, tiêu đề, phiên bản, tác giả và người duy trì, mô tả, giấy phép, và các gói phụ thuộc (Imports cho gói bắt buộc, Suggests cho gói chỉ dùng khi kiểm thử). Chi tiết đầy đủ của mọi tệp nằm trong tài liệu chính thức Writing R Extensions (R Core Team 2026); sách R Packages trình bày cùng nội dung theo lối thực hành (Wickham và Bryan 2023).

Tài liệu bằng roxygen2

roxygen2 đọc các dòng chú thích bắt đầu bằng #' ngay trên một hàm: dòng đầu là tiêu đề, @param mô tả đối số, @return mô tả giá trị trả về, @export đưa hàm vào NAMESPACE (Wickham và c.s. 2025). Tài liệu nằm cạnh mã nên dễ sửa cùng lúc; R CMD check báo lỗi nếu một đối số có trong hàm mà không có trong tài liệu, hoặc ngược lại. Chỉ các hàm công khai cần khối roxygen; hàm nội bộ dùng chú thích # thường.

Những gì phải đổi khi đưa mô hình vào gói

Phần lớn đã làm ở Bài 1: mô hình chỉ dùng base R, không có phụ thuộc ẩn, mã chỉ có ký tự ASCII. Hai điểm còn lại là dữ liệu và phiên bản. Gói đã cài nằm trong thư viện R của người dùng, không biết thư mục khóa học ở đâu, nên các giá trị chung (ngưỡng, giá, bảng sống) phải đi theo gói. Ta ghi bảng tham số đầy đủ do assemble_params() ghép vào inst/extdata/mdrtbvn-params.csv; khi cài, nội dung inst/ được chép vào thư mục gốc của gói, và default_params() tìm tệp bằng system.file(). Đây là một ảnh chụp có chủ đích: kết quả của gói không đổi khi kb/_shared đổi; muốn dùng giá mới, người dùng gọi assemble_params() trên tệp mới hoặc chờ phiên bản gói mới, và mã tham số cho biết họ đang dùng bảng nào. Trong module (chưa cài gói), tùy chọn mdrtbvn.params trỏ tới bảng ghép ở thư mục tạm. Số phiên bản trong DESCRIPTION được lấy từ hằng MODEL_VERSION trong code/model.R, để không bao giờ có hai số phiên bản mâu thuẫn.

Trong thực tế, người ta thường dùng usethis và devtools để làm các bước này trong một phiên R tương tác:

usethis::create_package("mdrtbvn")   # skeleton: DESCRIPTION, R/, NAMESPACE
usethis::use_mit_license()
usethis::use_testthat()               # tests/testthat/ and the testthat edition
devtools::document()                  # roxygen2: man/ and NAMESPACE
devtools::check()                     # R CMD build + R CMD check
devtools::install()

Bài này làm từng bước bằng base R và dòng lệnh R CMD để bạn thấy rõ mỗi lệnh trên làm gì; kết quả giống nhau.

Thực hành

Chuẩn bị thư mục dựng gói (trong thư mục tạm) và hai hàm trợ giúp: r_cmd() chạy R CMD ...; document() chạy roxygen2 trong một tiến trình R riêng để phiên làm việc hiện tại không bị nạp bản phát triển của gói. Kiểm tra: không có lỗi.

# Vietnamese text in tables needs a UTF-8 locale (Rscript may start in "C")
if (!isTRUE(l10n_info()[["UTF-8"]])) invisible(Sys.setlocale("LC_CTYPE", "en_US.UTF-8"))
source("../_shared/R/helpers.R")
source("code/model.R")
# The complete parameter table, written once; the module and the package read the same file
param_file <- write_param_table(assemble_params("../m02-cohort-markov/params/mdrtb.csv", "../_shared"),
                                file.path(tempdir(), "mdrtbvn-params.csv"))
pkg_name <- "mdrtbvn"
work <- file.path(tempdir(), "pkg-build")
unlink(work, recursive = TRUE)
dir.create(work)
pkg <- file.path(work, pkg_name)
R_bin <- file.path(R.home("bin"), "R")
# Run `R CMD <args>` inside the build folder and return its output lines
r_cmd <- function(args) {
  old <- setwd(work)
  on.exit(setwd(old))
  system2(R_bin, c("CMD", args), stdout = TRUE, stderr = TRUE)
}
# Run roxygen2 in a separate R process, so this session stays clean (roxygen2 loads a
# development copy of the package while it reads the code)
document <- function(path) {
  rscript <- file.path(R.home("bin"), "Rscript")
  system2(rscript, c("-e", shQuote(paste0("roxygen2::roxygenise('", path, "')"))),
          stdout = TRUE, stderr = TRUE)
}

Dựng khung gói từ các tệp nguồn của module: mã, kiểm thử, ảnh chụp bảng tham số, DESCRIPTION, LICENSE, NEWS.md (ghi cả mã bảng tham số). Tác giả trong DESCRIPTION là tên giữ chỗ, không phải người thật. Kiểm tra: cây thư mục khớp với sơ đồ ở trên.

# The minimal anatomy: DESCRIPTION, R/, tests/, inst/extdata/ (+ LICENSE, NEWS.md)
for (d in c("R", "tests/testthat", "inst/extdata")) dir.create(file.path(pkg, d), recursive = TRUE)
invisible(file.copy("code/model.R", file.path(pkg, "R", "model.R")))
invisible(file.copy(list.files("tests/testthat", full.names = TRUE), file.path(pkg, "tests", "testthat")))
# The shared inputs travel WITH the package as a frozen snapshot: an installed package cannot
# reach kb/_shared, and its results must not change when that folder changes
invisible(file.copy(param_file, file.path(pkg, "inst", "extdata", "mdrtbvn-params.csv")))
writeLines(c("library(testthat)", "library(mdrtbvn)", "", "test_check(\"mdrtbvn\")"),
           file.path(pkg, "tests", "testthat.R"))
desc <- c(
  Package = pkg_name,
  Title = "Cohort Model of MDR/RR-TB Treatment Regimens in Viet Nam",
  Version = MODEL_VERSION,                        # single source of truth: code/model.R
  `Authors@R` = 'person("HEM", "Course", email = "hem-course@example.org", role = c("aut", "cre"))',
  Description = paste("A cohort state-transition (Markov) model comparing the longer regimen,",
                      "the 9-month short regimen and the 6-month BPaLM regimen for MDR/RR-TB",
                      "in Viet Nam, with retreatment after failure or loss to follow-up.",
                      "Ships a snapshot of its complete parameter table (model parameters and",
                      "shared Vietnamese course inputs). Teaching package of course module M11."),
  License = "MIT + file LICENSE",
  Encoding = "UTF-8",
  Imports = "stats, tools, utils",
  Suggests = "testthat (>= 3.0.0)",
  `Config/testthat/edition` = "3",
  Roxygen = "list(markdown = TRUE)",
  RoxygenNote = as.character(utils::packageVersion("roxygen2"))
)
write.dcf(t(desc), file.path(pkg, "DESCRIPTION"), keep.white = names(desc))
writeLines(c("YEAR: 2026", "COPYRIGHT HOLDER: mdrtbvn authors"), file.path(pkg, "LICENSE"))
n_tests <- sum(vapply(list.files("tests/testthat", "^test-", full.names = TRUE),
                      function(f) sum(grepl("^test_that\\(", readLines(f))), numeric(1)))
p <- read_param_file(param_file)
writeLines(c(paste("# mdrtbvn", MODEL_VERSION), "",
             paste0("* First release: the M02 model refactored into inputs, model and outputs, ",
                    "with ", n_tests, " automated tests."),
             paste0("* Parameter table ", param_fingerprint(p), " (inst/extdata/mdrtbvn-params.csv).")),
           file.path(pkg, "NEWS.md"))
cat(paste0(pkg_name, "/", sort(list.files(pkg, recursive = TRUE))), sep = "\n")
mdrtbvn/DESCRIPTION
mdrtbvn/inst/extdata/mdrtbvn-params.csv
mdrtbvn/LICENSE
mdrtbvn/NEWS.md
mdrtbvn/R/model.R
mdrtbvn/tests/testthat.R
mdrtbvn/tests/testthat/golden-m02.rds
mdrtbvn/tests/testthat/helper-model.R
mdrtbvn/tests/testthat/test-golden-master.R
mdrtbvn/tests/testthat/test-inputs.R
mdrtbvn/tests/testthat/test-results.R
mdrtbvn/tests/testthat/test-scenarios.R
mdrtbvn/tests/testthat/test-trace.R

Nội dung DESCRIPTION. Kiểm tra: Version trùng MODEL_VERSION; chỉ stats, tools và utils (đều đi kèm R) nằm trong Imports.

cat(readLines(file.path(pkg, "DESCRIPTION")), sep = "\n")
Package: mdrtbvn
Title: Cohort Model of MDR/RR-TB Treatment Regimens in Viet Nam
Version: 1.0.0
Authors@R: person("HEM", "Course", email = "hem-course@example.org", role = c("aut", "cre"))
Description: A cohort state-transition (Markov) model comparing the longer regimen, the 9-month short regimen and the 6-month BPaLM regimen for MDR/RR-TB in Viet Nam, with retreatment after failure or loss to follow-up. Ships a snapshot of its complete parameter table (model parameters and shared Vietnamese course inputs). Teaching package of course module M11.
License: MIT + file LICENSE
Encoding: UTF-8
Imports: stats, tools, utils
Suggests: testthat (>= 3.0.0)
Config/testthat/edition: 3
Roxygen: list(markdown = TRUE)
RoxygenNote: 7.3.3

Một khối roxygen2 trong code/model.R, ngay trên run_model(). Kiểm tra: có tiêu đề, @param cho mọi đối số, @return và @export.

# A roxygen2 block: #' comments right above the function they document
src <- readLines("code/model.R")
fun_line <- grep("^run_model <- function", src)
doc_start <- max(grep("^#' Run the whole model", src))
cat(src[doc_start:(fun_line + 1)], "  ...", sep = "\n")
#' Run the whole model: validate inputs, simulate, summarise, compare
#'
#' @param p Named list of parameter values (see `default_params()`).
#' @param new,ref Regimens of the pairwise comparison; by default BPaLM versus the 9-month
#'   short regimen, the regimen it would replace in Viet Nam.
#' @return List with `version` (model version), `param_id` (identifier of the parameter
#'   set, see `param_fingerprint()`), `results` (see `run_cea()`), `frontier` (see
#'   `cea_frontier()`) and `comparison` (see `compare_regimens()`), all at `p$wtp`.
#' @export
run_model <- function(p, new = "bpalm", ref = "short") {
  results <- run_cea(p)
  ...

Chạy roxygen2 để viết NAMESPACE và các trang trợ giúp. Kiểm tra: mỗi hàm công khai có đúng một tệp .Rd.

# roxygen2 turns the #' comments in R/model.R into help pages (man/*.Rd) and NAMESPACE
doc_out <- document(pkg)
cat(readLines(file.path(pkg, "NAMESPACE")), sep = "\n")
# Generated by roxygen2: do not edit by hand

export(apply_updates)
export(assemble_params)
export(cea_frontier)
export(compare_regimens)
export(default_params)
export(nmb_by_wtp)
export(param_fingerprint)
export(params_from_table)
export(read_param_file)
export(run_cea)
export(run_model)
export(validate_params)
export(write_param_table)
man_files <- list.files(file.path(pkg, "man"))
exports <- sub("^export\\((.*)\\)$", "\\1", grep("^export", readLines(file.path(pkg, "NAMESPACE")), value = TRUE))
stopifnot(length(man_files) == length(exports))  # every exported function is documented

Gói công khai 13 hàm. Các hàm còn lại của model.R (dựng ma trận, chạy vết, tóm tắt) là hàm nội bộ: kiểm thử vẫn gọi được chúng, người dùng gói thì không cần biết đến.

Dựng tệp nguồn .tar.gz và chạy R CMD check. Kiểm tra: dòng cuối là “Status: OK”.

# R CMD build makes the source tarball; R CMD check runs dozens of automatic checks,
# including our tests, exactly as a reviewer or CRAN would
build_out <- r_cmd(c("build", "--no-build-vignettes", "--no-manual", pkg_name))
tarball <- list.files(work, pattern = "\\.tar\\.gz$")
Sys.setenv("_R_CHECK_SYSTEM_CLOCK_" = "0")        # do not ask an internet time server
t_check <- system.time(check_out <- r_cmd(c("check", "--no-manual", tarball)))[["elapsed"]]
n_checks <- sum(grepl("^\\* checking", check_out))
check_status <- sub("^Status: ", "", check_out[grepl("^Status:", check_out)])
show <- grepl("R code for possible problems|missing documentation|code/documentation mismatches|for non-ASCII|Running .testthat|^Status", check_out)
cat(check_out[show], sep = "\n")
* checking code files for non-ASCII characters ... OK
* checking R code for possible problems ... OK
* checking for missing documentation entries ... OK
* checking for code/documentation mismatches ... OK
  Running ‘testthat.R’
Status: OK
stopifnot(length(tarball) == 1, check_status == "OK")

R CMD check chạy 41 nhóm kiểm tra trong khoảng 11 giây, gồm cả 54 kiểm thử của Bài 2, và kết luận “OK”. Kiểm tra “R code for possible problems” chính là phép tìm phụ thuộc ẩn bằng codetools ở Bài 1: codetools::checkUsageEnv(env) duyệt mọi hàm trong môi trường env và in các lời gọi tới hàm không nhìn thấy được từ đó (bài tập 3 dùng nó). Kiểm tra “non-ASCII characters” là lý do mã mô hình chỉ dùng ký tự ASCII.

Cài gói vào một thư viện tạm và dùng như mọi gói khác. Kiểm tra: kết quả của gói đúng bằng kết quả của module, và cùng mã bảng tham số.

# Install into a temporary library and use the package like any other
lib <- file.path(work, "lib")
dir.create(lib)
install_out <- r_cmd(c("INSTALL", "-l", shQuote(lib), tarball))
invisible(loadNamespace(pkg_name, lib.loc = lib))
sort(getNamespaceExports(pkg_name))
 [1] "apply_updates"     "assemble_params"   "cea_frontier"     
 [4] "compare_regimens"  "default_params"    "nmb_by_wtp"       
 [7] "param_fingerprint" "params_from_table" "read_param_file"  
[10] "run_cea"           "run_model"         "validate_params"  
[13] "write_param_table"
out_pkg <- mdrtbvn::run_model(mdrtbvn::default_params())    # reads inst/extdata/mdrtbvn-params.csv
out_mod <- run_model(p)
c(package_d_nmb_million = out_pkg$comparison$d_nmb / 1e6,
  module_d_nmb_million = out_mod$comparison$d_nmb / 1e6)
package_d_nmb_million  module_d_nmb_million 
             66.80761              66.80761 
stopifnot(identical(out_pkg$results, out_mod$results), out_pkg$version == MODEL_VERSION,
          out_pkg$param_id == out_mod$param_id)

Trang trợ giúp mà roxygen2 viết cho run_model(), giống như khi gõ ?run_model. Kiểm tra: phần Arguments và Value lấy đúng từ khối #' ở trên.

# The help page that roxygen2 wrote from the #' comments above run_model()
help_txt <- capture.output(tools::Rd2txt(file.path(pkg, "man", "run_model.Rd"),
                                         options = list(underline_titles = FALSE)))
cat(help_txt[nzchar(trimws(help_txt))], sep = "\n")
Run the whole model: validate inputs, simulate, summarise, compare
Description:
     Run the whole model: validate inputs, simulate, summarise, compare
Usage:
     run_model(p, new = "bpalm", ref = "short")
Arguments:
       p: Named list of parameter values (see 'default_params()').
new, ref: Regimens of the pairwise comparison; by default BPaLM versus
          the 9-month short regimen, the regimen it would replace in
          Viet Nam.
Value:
     List with 'version' (model version), 'param_id' (identifier of the
     parameter set, see 'param_fingerprint()'), 'results' (see
     'run_cea()'), 'frontier' (see 'cea_frontier()') and 'comparison'
     (see 'compare_regimens()'), all at 'p$wtp'.
Mẹo thực hành
  • Chạy R CMD check (hoặc devtools::check()) trước mỗi lần gắn nhãn phiên bản. Một gói “Status: OK” trên máy bạn là điều kiện cần, chưa phải điều kiện đủ.
  • Giữ ít hàm công khai. Mỗi hàm công khai là một lời hứa với người dùng; đổi đối số của nó là một thay đổi MAJOR.
  • Viết NEWS.md mỗi lần đổi phiên bản: một dòng cho mỗi thay đổi người dùng cần biết, đặc biệt là thay đổi làm kết quả khác đi.
  • Một gói mô hình không cần lên CRAN. Cài trực tiếp từ GitHub (ví dụ bằng remotes::install_github()) là đủ cho một nhóm nghiên cứu.
Lỗi thường gặp
  • Gọi library() hay source() trong R/: gói phải khai báo phụ thuộc trong DESCRIPTION và gọi pkg::fun(). Dấu hiệu: R CMD check báo NOTE hoặc WARNING ở phần mã.
  • Đường dẫn tương đối tới dữ liệu như "../params/mdrtb.csv": chạy được trong thư mục của bạn, hỏng khi gói đã cài. Dùng system.file().
  • Sửa tay NAMESPACE hay man/ khi đã dùng roxygen2: lần chạy roxygen2 sau sẽ ghi đè. Sửa khối #' rồi chạy lại.
  • Quên tăng phiên bản khi thay đổi mô hình: hai bản khác nhau mang cùng số phiên bản, và không ai biết kết quả nào do bản nào tạo ra.

Bài tập

  1. (Dễ) Bạn sửa một lỗi nhỏ không đổi thiết kế mô hình. Phiên bản mới là gì? Viết hàm tăng số phiên bản theo quy ước MAJOR.MINOR.PATCH, sửa MODEL_VERSION trong R/model.R của gói và ghi lại Version của DESCRIPTION từ đó.
# Exercise 1: semantic versioning. A bug fix bumps PATCH, a new function MINOR,
# a change that breaks existing code MAJOR (and resets the parts to its right)
bump_version <- function(v, part = c("patch", "minor", "major")) {
  part <- match.arg(part)
  x <- as.integer(strsplit(v, ".", fixed = TRUE)[[1]])
  if (part == "major") x <- c(x[1] + 1, 0, 0)
  if (part == "minor") x <- c(x[1], x[2] + 1, 0)
  if (part == "patch") x <- c(x[1], x[2], x[3] + 1)
  paste(x, collapse = ".")
}
c(bug_fix = bump_version("1.0.0", "patch"), new_function = bump_version("1.0.0", "minor"),
  breaking = bump_version("1.2.3", "major"))
     bug_fix new_function     breaking 
     "1.0.1"      "1.1.0"      "2.0.0" 
# The number lives in ONE place, MODEL_VERSION in the package's R/model.R; DESCRIPTION
# is regenerated from it, so the two can never disagree
model_file <- file.path(pkg, "R", "model.R")
model_lines <- readLines(model_file)
i_ver <- grep("^MODEL_VERSION <- ", model_lines)
old_version <- gsub("^MODEL_VERSION <- \"|\"$", "", model_lines[i_ver])
new_version <- bump_version(old_version, "patch")
model_lines[i_ver] <- sprintf("MODEL_VERSION <- \"%s\"", new_version)
writeLines(model_lines, model_file)
d <- read.dcf(file.path(pkg, "DESCRIPTION"))
d[, "Version"] <- new_version
write.dcf(d, file.path(pkg, "DESCRIPTION"), keep.white = colnames(d))
env_ver <- new.env()
sys.source(model_file, envir = env_ver)
c(model = env_ver$MODEL_VERSION, description = read.dcf(file.path(pkg, "DESCRIPTION"), fields = "Version")[1, 1])
              model description.Version 
            "1.0.1"             "1.0.1" 
stopifnot(env_ver$MODEL_VERSION == new_version,
          read.dcf(file.path(pkg, "DESCRIPTION"), fields = "Version")[1, 1] == new_version)
# Restore the original number, so that the exercises below build on the version of Lesson 4
model_lines[i_ver] <- sprintf("MODEL_VERSION <- \"%s\"", old_version)
writeLines(model_lines, model_file)
d[, "Version"] <- old_version
write.dcf(d, file.path(pkg, "DESCRIPTION"), keep.white = colnames(d))

Sửa lỗi tăng PATCH (1.0.0 → 1.0.1); thêm hàm mới tăng MINOR; thay đổi làm mã cũ không chạy được tăng MAJOR và đưa hai số sau về 0 (Preston-Werner 2013). Con số chỉ có một nơi gốc, MODEL_VERSION; DESCRIPTION được ghi từ đó nên hai nơi không thể lệch nhau (script trả gói về 1.0.0 sau bài tập).

  1. (Vừa) Thêm vào gói một hàm công khai threshold_price(p) tìm giá thuốc BPaLM mỗi tháng tại đó lợi ích ròng quy ra tiền gia tăng (ΔNMB) so với phác đồ ngắn bằng 0. Đặt nó trong một tệp riêng ở R/, có khối roxygen, chạy lại roxygen2 và kiểm tra NAMESPACE.
# Exercise 2: add and export a new function. Write it with a roxygen block in its own
# file under R/, run roxygen again, and check that NAMESPACE now exports it.
writeLines(c(
  "#' Monthly BPaLM drug price at which BPaLM stops paying off against the short regimen",
  "#'",
  "#' @param p Named list of parameter values (see `default_params()`).",
  "#' @param upper Upper end of the search interval, in VND per month.",
  "#' @return Threshold price in VND per month (incremental NMB = 0).",
  "#' @export",
  "threshold_price <- function(p, upper = 100 * p$c_drug_bpalm) {",
  "  gap <- function(price) {",
  "    p$c_drug_bpalm <- price",
  "    run_model(p)$comparison$d_nmb",
  "  }",
  "  stats::uniroot(gap, lower = 0, upper = upper)$root",
  "}"), file.path(pkg, "R", "threshold.R"))
doc_out <- document(pkg)
stopifnot("export(threshold_price)" %in% readLines(file.path(pkg, "NAMESPACE")))
source(file.path(pkg, "R", "threshold.R"))
price_zero <- threshold_price(p)
q <- p
q$c_drug_bpalm <- price_zero
# At the threshold price the incremental NMB is (numerically) zero, and above the base price
c(d_nmb_at_threshold = run_model(q)$comparison$d_nmb)
d_nmb_at_threshold 
      2.235174e-08 
stopifnot(abs(run_model(q)$comparison$d_nmb) < 1e3, price_zero > p$c_drug_bpalm)

Giá ngưỡng là 12.87 triệu VNĐ mỗi tháng, gấp 10.9 lần giá GDF hiện tại; tại giá đó ΔNMB bằng 0 đến sai số làm tròn. Đồ thị đầy đủ nằm ở Bài 5. Hàm mới chỉ gọi run_model(), một hàm công khai, nên không phụ thuộc vào chi tiết bên trong mô hình. Thêm một hàm công khai là thay đổi MINOR.

  1. (Khó) Cho summarise_regimen() gọi discount_weights() của tệp tiện ích khóa học thay vì discount_factor(). Trong phiên của bạn mô hình vẫn chạy (tệp tiện ích đã nạp). Hãy cho thấy R CMD check sẽ báo lỗi này: nạp mã vào một môi trường chỉ thấy base R (new.env(parent = baseenv())) rồi chạy codetools::checkUsageEnv() trên môi trường đó.
# Exercise 3: why R CMD check catches hidden dependencies. Make summarise_regimen() call
# the course helper discount_weights(), then check the code the way R CMD check does:
# against base R only, not against whatever happens to be in your workspace.
src <- readLines("code/model.R")
target <- "disc <- discount_factor((0:n) / 12, p$disc_rate)"
stopifnot(sum(grepl(target, src, fixed = TRUE)) == 1)
src_bad <- sub(target, "disc <- discount_weights((0:n) / 12, p$disc_rate)", src, fixed = TRUE)
check_usage <- function(lines) {
  env <- new.env(parent = baseenv())                 # sees base R, not the workspace
  eval(parse(text = lines), envir = env)
  capture.output(codetools::checkUsageEnv(env))
}
check_usage(src)        # the real model: no complaints
character(0)
check_usage(src_bad)    # the hidden dependency is reported
[1] "summarise_regimen: no visible global function definition for 'discount_weights'"
stopifnot(length(check_usage(src)) == 0, any(grepl("discount_weights", check_usage(src_bad))))

codetools::checkUsageEnv() kiểm tra mỗi hàm so với những gì nhìn thấy được từ môi trường của nó. Đặt mã vào một môi trường chỉ thấy base R, ta có đúng thông báo “no visible global function definition” mà R CMD check sẽ in ra. Đây là phụ thuộc ẩn của Bài 1, được phát hiện tự động.

Tài liệu tham khảo

# Slide Lời giảng chính Hình/Code
1 Vì sao đóng gói một dòng library() thay cho các source(); phiên bản của gói là phiên bản mô hình —
2 Cấu trúc tối thiểu DESCRIPTION, NAMESPACE, R/, man/, tests/, inst/ sơ đồ Mermaid
3 Dựng từ nguồn của module một nguồn sự thật; ảnh chụp bảng tham số đầy đủ chunk l04-skeleton
4 DESCRIPTION phiên bản, giấy phép, Imports, Suggests chunk l04-description
5 roxygen2 #' → trang trợ giúp và export() chunk l04-roxygen-block
6 Công khai và nội bộ ít hàm công khai; nội bộ đổi tự do chunk l04-document
7 R CMD check hàng chục kiểm tra, gồm testthat; “Status: OK” chunk l04-build-check
8 Cài và dùng kết quả của gói = kết quả của module chunk l04-install
9 Phiên bản PATCH, MINOR, MAJOR; NEWS.md chunk l04-ex1
10 usethis/devtools cùng các bước, tự động hơn khối lệnh
11 Chuyển tiếp gói cho người viết mã; ứng dụng cho người ra quyết định → Bài 5 —