Exposure-response and population PK analysis datasets, built to a documented structure instead of a new one every study.
Open-source clinical R has a standard for almost everything except this. admiral will build you an ADaM dataset, rtables will summarise it — but exposure-response and population PK analysis datasets have no CDISC structure, so every study invents one, and the pharmacometrician receives something different each time: a different name for time after dose, a different rule for values below the limit of quantification, a different guess at whether the dose record comes before or after the sample taken at the same hour.
eradam produces those datasets to one structure, documents it, and returns the documentation as data.
PC + EX + ADSL -> ADPC (concentrations, explicit BLQ policy)
-> PopPK (dosing/observation records interleaved, NONMEM items)
-> exposure (CMAX, TMAX, CMIN, CTROUGH, AUCLST)
-> terminal (LAMZ, LAMZHL, AUCIFO/AUCIFP, %extrapolated + fit diagnostics)
-> partial (AUCINT over a stated window, edge rule documented)
-> ADER (exposure joined to response, quantile groups)
install.packages(
"https://clincoder.cloud/eradam/eradam_0.0.0.9001.tar.gz",
repos = NULL, type = "source"
)Everything below runs on the fixtures shipped with the package — no network, no private data.
library(eradam)
pc <- eradam_example("pc") # 576 concentrations, 24 subjects, 2 analytes
ex <- eradam_example("ex") # 2 doses per subject, at 0 h and 24 h
adsl <- eradam_example("adsl")
resp <- eradam_example("resp") # percent change from baseline in a biomarker
# 1. concentration analysis dataset: BLQ imputed at half the limit, and said so
adpc <- make_adpc(pc, adsl, ex, blq = "half")
adpc[1:3, c("USUBJID", "PARAMCD", "NFRLT", "AFRLT", "ARRLT", "AVAL", "BLQFL", "DTYPE")]
#> # A tibble: 3 x 8
#> USUBJID PARAMCD NFRLT AFRLT ARRLT AVAL BLQFL DTYPE
#> <chr> <chr> <dbl> <dbl> <dbl> <dbl> <chr> <chr>
#> 1 ERADAM01-001 DRUGX 0 0 0 0.25 Y LOQ
#> 2 ERADAM01-001 DRUGX 0.5 0.58 0.58 230. N NA
#> 3 ERADAM01-001 DRUGX 1 1.01 1.01 348. N NA
# 2. population PK dataset: dose records first when a dose and a sample share a time
pk <- make_poppk(adpc, ex, adsl, covariates = c("AGE", "SEX", "WTBL", "CRCL"))
pk[pk$ID == 1, c("ID", "TIME", "TAD", "AMT", "DV", "MDV", "EVID", "CMT")][1:4, ]
#> # A tibble: 4 x 8
#> ID TIME TAD AMT DV MDV EVID CMT
#> <int> <dbl> <dbl> <dbl> <dbl> <int> <int> <int>
#> 1 1 0 0 25 NA 1 1 1
#> 2 1 0 0 NA 0.25 0 0 2
#> 3 1 0 0 NA 0.1 0 0 3
#> 4 1 0.497 0.497 NA 79.6 0 0 3
# 3. exposure metrics, AUC by the linear trapezoidal rule
expo <- derive_exposure_metrics(adpc[adpc$PARAMCD == "DRUGX", ], ex)
expo[1, c("USUBJID", "CMAX", "TMAX", "CTROUGH", "AUCLST", "AUCLSTD")]
#> # A tibble: 1 x 6
#> USUBJID CMAX TMAX CTROUGH AUCLST AUCLSTD
#> <chr> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 ERADAM01-001 378. 2.03 14.8 6686. 134.
# 4. terminal phase: lambda z with its fit diagnostics, half-life, AUC to infinity
tp <- derive_terminal_phase(adpc[adpc$PARAMCD == "DRUGX", ], method = "lin_up_log_down")
tp[1:2, c("USUBJID", "LAMZ", "LAMZHL", "LAMZNPT", "LAMZLL", "LAMZUL", "R2ADJ", "AUCPEO", "AUCPEFL")]
#> # A tibble: 2 x 9
#> USUBJID LAMZ LAMZHL LAMZNPT LAMZLL LAMZUL R2ADJ AUCPEO AUCPEFL
#> <chr> <dbl> <dbl> <int> <dbl> <dbl> <dbl> <dbl> <chr>
#> 1 ERADAM01-001 0.148 4.69 3 28.0 48.0 0.999 1.67 N
#> 2 ERADAM01-002 0.101 6.85 3 28.1 48.0 0.996 5.48 N
# 10 of the 24 profiles get NA, and say why rather than returning a number
sum(is.na(tp$LAMZ))
#> [1] 10
unique(tp$LAMZNRS[!is.na(tp$LAMZNRS)])[1]
#> [1] "Only 2 quantifiable points after TMAX (28); 3 required."
# 5. partial AUC over a stated window, with the edge rule documented
derive_partial_auc(adpc[adpc$PARAMCD == "DRUGX", ], 0, 24)[1:2, ]
#> # A tibble: 2 x 8
#> USUBJID PARAMCD AUCINTLL AUCINTUL AUCINT NOBSINT INTRPFL AUCINTRS
#> <chr> <chr> <dbl> <dbl> <dbl> <int> <chr> <chr>
#> 1 ERADAM01-001 DRUGX 0 24 3304. 9 Y NA
#> 2 ERADAM01-002 DRUGX 0 24 5514. 9 Y NA
# 6. exposure-response dataset with labelled exposure quartiles
ader <- make_ader(expo, resp)
round(tapply(ader$AVAL, ader$EXPQ, mean), 1)
#> Q1 (6680-11900) Q2 (11900-22400) Q3 (22400-32200) Q4 (32200-53000)
#> -17.9 -27.9 -32.5 -41.7Every variable the package produces, its meaning and where it came from, returned as data so it can go straight into a define document or a data specification:
eradam_structure("poppk")
#> # A tibble: 13 x 4
#> dataset variable label source
#> <chr> <chr> <chr> <chr>
#> 1 poppk ID Sequential integer subject identifier (1..n)… Derived from USUBJID
#> 2 poppk USUBJID Unique subject identifier, retained for trac… `pc` / `ex`
#> 3 poppk TIME Actual time from first dose (h) `pc$AFRLT` / `ex$AFRLT`
#> …The same table is rendered into each function’s help page, so the code and the documentation cannot drift apart.
Where a real standard exists, eradam follows it and says which:
NFRLT, AFRLT, NRRLT, ARRLT and the negative pre-dose ARRLT come from the CDISC ADaM Implementation Guide for Non-compartmental Analysis Input Data (ADNCA v1.0).DTYPE = "LOQ" is the ADaM controlled-terminology value for a value imputed from the limit of quantification. AVAL, AVALC, AVALU, PARAMCD, PARAM, ANL01FL are ADaM BDS variables.CMAX, TMAX, CMIN, CTROUGH, AUCLST are CDISC PPTESTCD parameter codes. So are the terminal-phase and partial-area codes: LAMZ, LAMZHL, LAMZLL, LAMZUL, LAMZNPT, R2, R2ADJ, AUCIFO, AUCIFP, AUCPEO, AUCPEP, CLST, TLST and AUCINT, used with their CDISC meanings.ID, TIME, AMT, DV, MDV, EVID, CMT are NONMEM data items with their NONMEM meanings.TMAX and everything before it, the three-point minimum, t½ = ln(2) / λz, AUCIFO = AUCLST + CLST / λz, AUCIFP = AUCLST + CLSTP / λz, and the 20% extrapolated-fraction acceptance threshold.method = "linear" and logarithmically on a descending segment under "lin_up_log_down" is what non-compartmental analysis software does.Where no standard exists, the package says so rather than implying otherwise. These are eradam’s own conventions:
PP/ADPP;CTROUGH definition, the BLQFL/LLOQ names, the compartment numbering, and every EXP* variable in the exposure-response dataset;CLSTP for the fitted concentration at TLST, AUCPEFL for the 20% flag, LAMZNRS for the reason λz is missing, and AUCINTLL/AUCINTUL/NOBSINT/ INTRPFL/AUCINTRS around AUCINT — the parameters are standard, these wrapper names are not;NA with a written reason instead of a number when a λz fit fails, rather than the alternative practice of reporting the fit anyway and leaving the reviewer to spot it;C = 0 at time zero and forward-extrapolating the tail with λz are both real conventions used elsewhere; picking one silently is how two “AUC0-24” columns end up disagreeing, so this package returns NA and the reason instead. Use derive_terminal_phase() when you want the tail, because it says how much it extrapolated;AUCLST in derive_terminal_phase() runs to the last positive concentration (the non-compartmental convention), while derive_exposure_metrics() runs its AUCLST over every non-missing record. The two agree unless a profile has trailing zeros — which it will after make_adpc(blq = "zero").All of it is documented in ?derive_exposure_metrics, ?derive_terminal_phase, ?derive_partial_auc, ?make_adpc, ?make_poppk and ?make_ader.
What this package does not do, stated so it does not have to be discovered:
AUCTAU, CAVG, CLSS, accumulation ratio and fluctuation are absent. derive_partial_auc() over a dosing interval gives the same number as AUCTAU, under the name AUCINT, and nothing else.CL, CLF, VZ, VZF, MRT, AUMC and VSS are absent, so derive_terminal_phase() stops at λz, t½ and AUCINF.by group. derive_terminal_phase() treats everything in a group as a single profile. On the shipped two-dose fixture that is visible: TMAX for 10 of the 24 subjects falls after the second dose, leaving only two terminal points, and those 10 correctly return NA with the reason. For a multiple-dose study, subset to the interval you mean before calling.make_adpc(), on purpose, so it is visible in the delivered dataset — which is why derive_partial_auc(pc, 0, 24) on the raw fixture returns NA: the 0 h sample is below the limit of quantification and still missing until make_adpc() has decided what to do with it.?derive_exposure_metrics, ?derive_terminal_phase and ?derive_partial_auc each repeat the limits that apply to them.
testthat suite asserting on values, including the AUC of a hand-computed curve (0, 10, 20, 10 at 0, 1, 2, 3 h: 35 linear, 34.42695 linear up / log down) and the record ordering and event flags of the population PK dataset.C = 100 exp(-0.1 t) where every answer has a closed form. Measured on that curve: λz recovers 0.1 to 1.4e-17 absolute, t½ to 8.9e-16 h, and AUCIFO/AUCIFP reproduce the analytic 1000 exactly under lin_up_log_down (the log trapezoid is exact on an exponential). Under the linear trapezoid the same AUCIFO reads +2.144% high, which the tests pin as a number rather than hide behind a loose tolerance.AUCLST exactly, and AUC[2, 5] on the same exponential matches the analytic value to 0 relative error under lin_up_log_down (3.3e-3 under the linear rule, from the chord interpolation at the 5 h edge).admiral::derive_vars_merged(), and every return value is a plain tibble that admiral, rtables and xportr accept unchanged.