library(here)
library(mrgsolve)
library(dplyr)
library(tidyr)
library(bbr)
library(pmtables)
library(data.table)
library(yspec)
library(mrgmisc)
1 Introduction
This document provides an example of how mrgsolve can be used to simulate subject-specific exposures from empirical Bayes estimates (EBE) generated from a NONMEM
estimation run. We will simulate exposures at each individual’s nominal dose and summarize by DOSE
and renal function group.
2 Tools used
2.1 MetrumRG packages
mrgsolve Simulate from ODE-based population PK/PD and QSP models in R.
yspec Data specification, wrangling, and documentation for pharmacometrics.
pmtables Create summary tables commonly used in pharmacometrics and turn any R table into a highly customized tex table.
bbr Manage, track, and report modeling activities, through simple R objects, with a user-friendly interface between R and NONMEM®.
2.2 CRAN packages
dplyr A grammar of data manipulation.
3 Outline
First, we’ll prepare a model and data set for simulating subject-specific PK profiles from EBEs output from NONMEM®.
After simulating the data, we’ll create two summaries:
The first summary will look at exposures in subjects with normal renal function. We’ll create a table summarizing
Cmax
,Cmin
andAUC
at steady state by dose.The second summary will look at steady state exposures across doses and renal function groups (e.g. mild, moderate, or severe renal impairment)
The summaries will be saved in table format to include in the report.
Finally, we’ll demonstrate how to preview tables in an environment that mimics our report formatting.
4 Required packages and options
These options are set to direct outputs.
options(
mrgsolve.soloc = here("script/build"),
bbr.verbose = FALSE,
pmtables.dir = here("deliv/table")
)
We load the yspec object, which will provide information about the data set
<- ys_load(here("data/derived/pk.yml")) spec
5 EBE simulation model
The final model run (model/pk/106
) was a standard two-compartment model with first-order absorption. This means that we can simply use a model out of the internal library that ships with mrgsolve
<- modlib("pk2") mod
This model is parameterized with CL
, Vn
, Q
and KA
param(mod)
Model parameters (N=5):
name value . name value
CL 1 | V2 20
KA 1 | V3 10
Q 2 | . .
6 EBE simulation data set
Our strategy is to read in the output from run 106
where we tabled all of the subject-level EBEs and create a data set that will allow us to generate exposures using those parameter estimates.
We use nm_join()
to get an integrated data frame of input and output data
<- nm_join(here("model/pk/106"))
data head(data)
# A tibble: 6 × 49
C NUM ID TIME SEQ CMT EVID AMT DV.DATA AGE WT HT
<lgl> <dbl> <int> <dbl> <int> <int> <int> <int> <dbl> <dbl> <dbl> <dbl>
1 NA 1 1 0 0 1 1 5 0 28.0 55.2 160.
2 NA 2 1 0.61 1 2 0 NA 61.0 28.0 55.2 160.
3 NA 3 1 1.15 1 2 0 NA 91.0 28.0 55.2 160.
4 NA 4 1 1.73 1 2 0 NA 122. 28.0 55.2 160.
5 NA 5 1 2.15 1 2 0 NA 126. 28.0 55.2 160.
6 NA 6 1 3.19 1 2 0 NA 84.7 28.0 55.2 160.
# ℹ 37 more variables: EGFR <dbl>, ALB <dbl>, BMI <dbl>, SEX <int>, AAG <dbl>,
# SCR <dbl>, AST <dbl>, ALT <dbl>, CP <int>, TAFD <dbl>, TAD <dbl>,
# LDOS <int>, MDV <int>, BLQ <int>, PHASE <int>, STUDYN <int>, DOSE <int>,
# SUBJ <int>, USUBJID <chr>, STUDY <chr>, ACTARM <chr>, RF <chr>,
# IPRED <dbl>, NPDE <dbl>, CWRES <dbl>, CL <dbl>, V2 <dbl>, Q <dbl>,
# V3 <dbl>, KA <dbl>, ETA1 <dbl>, ETA2 <dbl>, ETA3 <dbl>, DV <dbl>,
# PRED <dbl>, RES <dbl>, WRES <dbl>
Next, we’ll subset that data frame down to doses
<- filter(data, EVID==1) dose_rec
and then generate a data frame of subject-level parameters
<- distinct(
pars
dose_rec,
ID, CL, V2, Q, V3, KA,
AMT, RF, ACTARM, DOSE
)
head(pars)
# A tibble: 6 × 10
ID CL V2 Q V3 KA AMT RF ACTARM DOSE
<int> <dbl> <dbl> <dbl> <dbl> <dbl> <int> <chr> <chr> <int>
1 1 2.62 39.8 3.02 53.0 1.39 5 norm DEMO 5 mg 5
2 2 1.88 34.2 2.88 49.8 1.77 5 norm DEMO 5 mg 5
3 3 3.27 50.9 3.01 52.7 1.41 5 norm DEMO 5 mg 5
4 4 7.34 129. 4.10 79.7 3.38 5 norm DEMO 5 mg 5
5 5 6.75 133. 4.62 93.5 2.91 5 norm DEMO 5 mg 5
6 6 3.69 62.2 3.61 67.3 1.46 10 norm DEMO 10 mg 10
This simulation is going to depend on the fact that our EBE parameter names (e.g. CL
and V3
) match up with the names of the parameters in the pk2
model from the internal library.
mrgsolve
comes with a function to check names in the data against names in the model to help confirm that the two match up
inventory(mod, pars)
Found all required parameters in 'pars'.
Great! We have accounted for all the model names in our data set.
We’ll simulate exposures at steady state for once-daily dosing
<- mutate(pars, II = 24, SS = 1, TIME = 0, EVID = 1, CMT = 1)
dose
head(dose)
# A tibble: 6 × 15
ID CL V2 Q V3 KA AMT RF ACTARM DOSE II SS TIME
<int> <dbl> <dbl> <dbl> <dbl> <dbl> <int> <chr> <chr> <int> <dbl> <dbl> <dbl>
1 1 2.62 39.8 3.02 53.0 1.39 5 norm DEMO … 5 24 1 0
2 2 1.88 34.2 2.88 49.8 1.77 5 norm DEMO … 5 24 1 0
3 3 3.27 50.9 3.01 52.7 1.41 5 norm DEMO … 5 24 1 0
4 4 7.34 129. 4.10 79.7 3.38 5 norm DEMO … 5 24 1 0
5 5 6.75 133. 4.62 93.5 2.91 5 norm DEMO … 5 24 1 0
6 6 3.69 62.2 3.61 67.3 1.46 10 norm DEMO … 10 24 1 0
# ℹ 2 more variables: EVID <dbl>, CMT <dbl>
7 Simulate PK in all subjects
We can control the precision of Cmax and AUC calculations by reducing the number of time steps in the simulated output. In this example, we’ll simulate one dosing interval (24 hours) with a simulation output time step of 0.1 hours. That is, we’ll have one simulated concentration every 0.1 hours in the output
<- update(mod, delta = 0.1, end = 24) mod
We would like to summarize the simulated data by certain data set items, so we’ll “recover” those from the input data to the simulated output
<- mrgsim_df(
out
mod,
dose, recover = "ACTARM,RF,CL,DOSE",
recsort = 3
)
8 Calculate exposure metrics
The data were simulated at steady state, so all exposure metric are steady state exposures.
8.1 Cmax and Cmin
This is a simple summary of the minimum and maximum concentration in the simulated time window by ID
and DOSE
<-
maxmin %>%
out group_by(ID, DOSE) %>%
summarise(Cmax = max(CP), Cmin = min(CP), .groups = "drop")
8.2 AUC
We use auc_partial()
from the mrgmisc
package to do a simple AUC calculation by trapezoidal rule over the 24 hour dosing interval
<-
auc %>%
out group_by(ID, DOSE) %>%
summarise(AUC = auc_partial(TIME, CP), .groups = "drop")
8.3 Create a single data set of exposures
Now, we’ll join the Cmin, Cmax, and AUC summaries together in a single data frame
<- left_join(maxmin, auc, by = c("ID", "DOSE"))
pk <- distinct(data, ID, USUBJID, STUDY, RF)
id <- left_join(pk, id, by = "ID")
pk <- select(pk, STUDY, USUBJID, everything())
pk head(pk, n=3)
# A tibble: 3 × 8
STUDY USUBJID ID DOSE Cmax Cmin AUC RF
<chr> <chr> <dbl> <int> <dbl> <dbl> <dbl> <chr>
1 101-DEMO-001 101-DEMO-0010001 1 5 0.141 0.0457 1.90 norm
2 101-DEMO-001 101-DEMO-0010002 2 5 0.186 0.0707 2.66 norm
3 101-DEMO-001 101-DEMO-0010003 3 5 0.112 0.0355 1.53 norm
9 Exposure comparison - normal renal function
First, make a table looking at normal renal function for doses between 10 and 100 mg daily at steady state
<- filter(pk, RF=="norm", DOSE >= 10 & DOSE <= 100)
normal
<- ys_add_factors(normal, spec, .suffix = "f") normal
To summarize exposures, we’ll tap the functionality provided by pt_cont_long()
from the pmtables
package. This will summarize continuous data in a table with long orientation.
<- pt_cont_long(
pmtab
normal, cols = "AUC, Cmax, Cmin",
panel = vars("Daily dose:" = "DOSEf"),
summarize_all = FALSE
)
Now, we’ll take the pmtable
object and annotate with some footnotes and input and output file names
<-
tab %>%
pmtab st_new() %>%
st_notes("AUC: ng$\\cdot$hr/mL, Cmax: ng/mL, Cmin: ng/mL") %>%
st_notes("All exposure metrics at steady state") %>%
st_notes("N: number of individuals") %>%
st_files(
r = "pk-ebe-simulate.Rmd",
output = "pk-ebe-normal.tex"
%>% stable() )
Preview the final table with st_as_image()
st_as_image(tab)
Save the table to the deliv/tables
directory
- Recall we set
output
in thest_files()
call to set the output file name (a.tex
file) - Recall we set the
pmtables.dir
option to direct the output to that directory
stable_save(tab)
10 Exposure comparison - renal function groups
In this section, we’ll create a table summarizing exposures by dose level and renal function group.
First, we’ll make the data long with respect to the exposure metrics
<- pivot_longer(pk, cols = c(Cmax, Cmin, AUC))
pk_long
head(pk_long)
# A tibble: 6 × 7
STUDY USUBJID ID DOSE RF name value
<chr> <chr> <dbl> <int> <chr> <chr> <dbl>
1 101-DEMO-001 101-DEMO-0010001 1 5 norm Cmax 0.141
2 101-DEMO-001 101-DEMO-0010001 1 5 norm Cmin 0.0457
3 101-DEMO-001 101-DEMO-0010001 1 5 norm AUC 1.90
4 101-DEMO-001 101-DEMO-0010002 2 5 norm Cmax 0.186
5 101-DEMO-001 101-DEMO-0010002 2 5 norm Cmin 0.0707
6 101-DEMO-001 101-DEMO-0010002 2 5 norm AUC 2.66
Now, group by dose level and renal function group and summarize
<-
pk_sum %>%
pk_long group_by(DOSE, RF, Metric = name) %>%
summarise(
N = n(),
Median = median(value),
Min = min(value),
Max = max(value),
.groups = "drop"
)
Subset the PK summary to the doses of interest
<- filter(pk_sum, DOSE >= 25 & DOSE <= 75) compare
We use the sig()
function from the pmtables
package to limit significant digits and render the summary as character data type
<- mutate(compare, across(c(Median, Min, Max), sig)) compare
Continue to get the data frame ready to generate a table.
We can convert the RF
column into a factor with labels coming from the yspec
object
<- ys_add_factors(compare, spec, RF, .suffix = "f")
compare count(compare, RF, RFf)
# A tibble: 4 × 3
RF RFf n
<chr> <fct> <int>
1 mild mild 3
2 mod moderate 3
3 norm normal 9
4 sev severe 3
<- select(compare, DOSE, Metric, Renal = RFf, everything())
compare <- arrange(compare, DOSE, Metric, Renal)
compare <- mutate(compare, DOSEf = paste0(DOSE, " mg daily")) compare
Using the compare
data frame, start styling the table
<-
tab2 %>%
compare select(-RF, -DOSE) %>%
st_new() %>%
st_right(.l = "Metric, Renal") %>%
st_clear_reps(Metric) %>%
st_panel("DOSEf", prefix = "Dose:") %>%
st_notes("AUC: ng$\\cdot$hr/mL, Cmax: ng/mL, Cmin: ng/mL") %>%
st_notes("All exposure metrics at steady state") %>%
st_notes("N: number of individuals") %>%
st_files(
r = "pk-ebe-simulate.R",
output = "pk-ebe-compare.tex"
%>% stable() )
In the above pipeline
st_right
justifies columns to the right and customizesMetric
andRenal
st_clear_reps
removes replicate values in theMetric
columnst_panel
creates groups of rows into panels with grouping titlest_notes
adds table footnotesst_files
attaches source code and output file names
To preview the TeX table as a png graphic, we’ll use st_as_image()
from the pmtables
package
st_as_image(tab2)
As above, we save the table object which already has the output file name embedded
stable_save(tab2)
11 Preview tables in report environment
On this page, we generated two tables using the pmtables package. Previews of those tables were shown in Figure 1 and Figure 2.
We might also be interested in seeing how the tables appear in our LaTex report template. This can be done with the st2report()
function from the pmtables
package. The code below won’t render in this website, but you can try it on the tables you create with pmtables
.
if(FALSE) {
st2report(list(tab, tab2), ntex = 2)
}
12 Other resources
12.1 Full script
The following script from the Github repository is discussed on this page. If you’re interested running this code, visit the About the Github Repo page first.