In this tutorial, the approach to both sequential and simultaneous PK/PD modeling in Pumas using the Warfarin example will be demonstrated. The tutorial will:
Define and fit the previously established Warfarin PK model.
Demonstrate how to set up and fit a PD (PCA endpoint) model sequentially using the empirical Bayes estimates (EBEs) from the PK model.
Demonstrate how to set up and fit a simultaneous PK/PD model.
Discuss advantages/disadvantages of each approach.
1 Introduction
When working on pharmacokinetic/pharmacodynamic (PK/PD) problems, there are two broad choices:
Sequential modeling: Fit the PK model first and generate individual PK parameters. Then pass these individual parameters (as if they were data) into the PD model. This is sometimes called the “two-stage” or “sequential” approach.
Simultaneous (or joint) modeling: Fit the PK and PD data within one model, estimating PK and PD parameters at the same time.
1.1 Advantages and Disadvantages
Sequential approach
Advantages:
Simpler, conceptually.
Faster,
because the PK fit is separated from the PD fit.
if PD is non-linear and PK is linear at which point one can use closed form or matrix exponential and investigate the PK form much faster than if everything was fitted together.
Easier to troubleshoot if the PD portion misbehaves (the PK portion is already finalized).
Disadvantages:
The uncertainty from the PK step is not propagated into the PD parameter estimates.
The PK model parameters are “frozen” once the PD step starts, which might overlook any interplay or correlation between PK and PD parameters.
Simultaneous approach
Advantages:
A single model handles PK and PD data together, ensuring that the correlation and covariance between PK parameters and PD parameters are accounted for.
Proper propagation of parameter uncertainty from PK into PD because the entire system is fit at once.
Disadvantages:
Model can become more complex and harder to fit.
Potentially heavier computational burden.
More complicated model diagnostics.
Note
Readers familiar with the PK model building from the previous tutorial can choose to skip the PK model fitting section and proceed to the PKPD modeling section (Section 3).
2 The Warfarin PK Model
Let us reuse the previously established Warfarin PK model.
PumasModel
Parameters: pop_CL, pop_Vc, pop_tabs, pop_lag, pk_Ω, σ_prop, σ_add
Random effects: pk_η
Covariates: FSZCL, FSZV
Dynamical system variables: Depot, Central
Dynamical system type: Closed form
Derived: conc
Observed: conc
# Source the example datasetwarfarin_data =dataset("paganz2024/warfarin_long")# Some subjects have duplicate time points for DVID = 1# For this dataset, the triple (ID, TIME, DVID) should define# a row uniquely, butnrow(warfarin_data)nrow(unique(warfarin_data, ["ID", "TIME", "DVID"]))# We can identify the problematic rows by grouping on the index variables@chain warfarin_data begin@groupby:ID :TIME :DVID@transform:tmp =length(:ID)@rsubset:tmp >1end# It is important to understand the reason for the duplicate values.# Sometimes the duplication is caused by recording errors, sometimes# it is a data processing error, e.g. when joining tables, or it can# be genuine records, e.g. when samples have been analyzed in multiple# labs. The next step depends on which of the causes are behind the# duplications.## In this case, we will assume that both values are informative and# we will therefore just adjust the time stamp a bit for the second# observation.warfarin_data_processed =@chain warfarin_data begin@groupby:ID :TIME :DVID@transform:tmp =1:length(:ID)@rtransform:TIME =:tmp ==1 ? :TIME ::TIME +1e-6@selectNot(:tmp)end# Transform the data in a single chain of operationswarfarin_data_wide =@chain warfarin_data_processed begin@rsubset !contains(:ID, "#")@rtransformbegin# Scaling factors:FSZV =:WEIGHT /70# volume scaling:FSZCL = (:WEIGHT /70)^0.75# clearance scaling (allometric)# Column name for the DV:DVNAME ="DV$(:DVID)"# Dosing indicator columns:CMT =ismissing(:AMOUNT) ? missing:1:EVID =ismissing(:AMOUNT) ? 0:1endunstack(Not([:DVID, :DVNAME, :DV]), :DVNAME, :DV)rename!(:DV1 =>:conc, :DV2 =>:pca)endpk_pop =read_pumas( warfarin_data_wide; id =:ID, time =:TIME, amt =:AMOUNT, cmt =:CMT, evid =:EVID, covariates = [:SEX, :WEIGHT, :FSZV, :FSZCL], observations = [:conc],)pkpd_pop =read_pumas( warfarin_data_wide; id =:ID, time =:TIME, amt =:AMOUNT, cmt =:CMT, evid =:EVID, covariates = [:SEX, :WEIGHT, :FSZV, :FSZCL], observations = [:conc, :pca],)
# A named tuple of parameter valuesparam_vals = ( pop_CL =0.134, pop_Vc =8.11, pop_tabs =0.523, pop_lag =0.1, pk_Ω =Diagonal([0.01, 0.01, 0.01]), σ_prop =0.00752, σ_add =0.0661,)pk_fit =fit(warfarin_pk_model, pk_pop, param_vals, FOCE(););
FittedPumasModel
Successful minimization: true
Likelihood approximation: FOCE
Likelihood Optimizer: BFGS
Dynamical system type: Closed form
Log-likelihood value: -319.93444
Number of subjects: 31
Number of parameters: Fixed Optimized
0 9
Observation records: Active Missing
conc: 239 47
Total: 239 47
-----------------------
Estimate
-----------------------
pop_CL 0.13065
pop_Vc 8.0713
pop_tabs 0.37143
pop_lag 0.92611
pk_Ω₁,₁ 0.050903
pk_Ω₂,₂ 0.019133
pk_Ω₃,₃ 1.1401
σ_prop 0.090622
σ_add 0.33356
-----------------------
3 Sequential PK/PD Modeling
In a sequential approach, the PD endpoint (e.g., PCA or clotting time, or INR) is modeled as a function of the Warfarin concentration using the indirect response model.
There are three ways to extract the individual EBEs:
Extract the individual coefficients (e.g., CL, Vc, tabs, etc.) from the fit and merge them with the original data using data wrangling tools.
Use the Subject constructor that augments the original PKPD data with the individual coefficients.
These methods require the full PKPD model to be defined and use the individual coefficients from the PK fit to predict the PK concentrations that get directly passed to the PD model. Below, the PKPD model is defined as warfarin_pkpd_model.
In this model above, the @pre block contains the expressions for the PK and PD parameters. The intention in a sequential approach is to let the individual coefficients of the PK model be fixed and known, and only estimate the PD parameters.
To facilitate this, the individual coefficients from the PK fit are introduced as @covariates in the @pre block. Hence, blocks representing the PK parameters such as @param, @random, @covariates, and the @pre blocks will be modified from the above specification to include the individual coefficients as seen below.
@parambegin# Notice how all PK related parameters are now removed from the @param block# PD parameters""" Baseline """ pop_e0 ∈RealDomain(lower =0.0, init =100.0)""" Emax """ pop_emax ∈RealDomain(init =-1.0)""" EC50 """ pop_c50 ∈RealDomain(lower =0.0, init =1.0)""" Turnover """ pop_tover ∈RealDomain(lower =0.0, init =14.0)# PD Inter-individual variability""" - Ωe0 - Ωemax - Ωec50 - Ωturn """ pd_Ω ∈PDiagDomain([0.01, 0.01, 0.01, 0.01])# Residual variability""" Proportional residual error for drug concentration """ σ_prop ∈RealDomain(lower =0.0, init =0.00752)""" Additive residual error for drug concentration (mg/L) """ σ_add ∈RealDomain(lower =0.0, init =0.0661)""" Additive error for PCA """ σ_fx ∈RealDomain(lower =0.0, init =0.01)end@randombegin# Notice how the random effects are now defined in terms of the PD parameters only# mean = 0, covariance = pd_Ω pd_η ~MvNormal(pd_Ω)end@covariatesbegin# Notice how the individual coefficients are now defined in terms of the covariates""" Individual Clearance """ CL""" Individual Volume """ Vc""" Individual Absorption time """ tabs""" Individual Lag time """ lags_Depot""" Scaling factor for Volume """ FSZV""" Scaling factor for Clearance """ FSZCLend@prebegin# PK Ka =log(2) / tabs# PD e0 = pop_e0 *exp(pd_η[1]) emax = pop_emax *exp(pd_η[2]) c50 = pop_c50 *exp(pd_η[3]) tover = pop_tover *exp(pd_η[4]) kout =log(2) / tover rin = e0 * koutend@dosecontrolbegin lags = (Depot = lags_Depot,)end
To bring the individual coefficients into the model, we need to extract the individual coefficients from the PK fit and then introduce them into the original PKPD dataset or in the PKPD population. Below is an example of how to do this using both methods.
Step 2: Merge the individual coefficients with the original PKPD dataset.
# Merge the individual PK parameters with the original dataset by ID and TIMEpd_dataframe =outerjoin(warfarin_data_wide, icoef_dataframe; on = [:ID, :TIME]);# Arrange the DataFrame by ID and TIMEsort!(pd_dataframe, [:ID, :TIME]);# Return the first 5 rowsfirst(pd_dataframe, 5)
5×17 DataFrame
Row
ID
TIME
WEIGHT
AGE
SEX
AMOUNT
FSZV
FSZCL
CMT
EVID
DV0
pca
conc
CL
Vc
tabs
lags_Depot
String
Float64
Float64?
Int64?
Int64?
Float64?
Float64?
Float64?
Int64?
Int64?
Float64?
Float64?
Float64?
Float64?
Float64?
Float64?
Float64?
1
1
0.0
66.7
50
1
100.0
0.952857
0.96443
1
1
missing
missing
missing
0.112039
7.67823
0.379683
0.926108
2
1
0.0
66.7
50
1
missing
0.952857
0.96443
missing
0
missing
100.0
missing
0.112039
7.67823
0.379683
0.926108
3
1
24.0
66.7
50
1
missing
0.952857
0.96443
missing
0
missing
49.0
9.2
missing
missing
missing
missing
4
1
36.0
66.7
50
1
missing
0.952857
0.96443
missing
0
missing
32.0
8.5
missing
missing
missing
missing
5
1
48.0
66.7
50
1
missing
0.952857
0.96443
missing
0
missing
26.0
6.4
missing
missing
missing
missing
Step 3: Read the merged dataframe into a Pumas Population object.
# Convert resulting DataFrame to a Pumas Population objectpdpop_dataframe =read_pumas( pd_dataframe; id =:ID, time =:TIME, amt =:AMOUNT, cmt =:CMT, evid =:EVID, covariates = [:CL, :Vc, :tabs, :lags_Depot], observations = [:conc, :pca],)
Step 2: Merge the individual coefficients with the original population.
pdpop_subject_constructor_icoef =map(zip(icoefs, pkpd_pop)) do (row, pdsubj)Subject(pdsubj, covariates = row)endpdpop_subject_constructor_icoef_dcs =map(zip(dcs, pdpop_subject_constructor_icoef)) do (dc, pdsubj)Subject(pdsubj, covariates = dc)end
Population
Subjects: 31
Covariates: SEX, WEIGHT, FSZV, FSZCL, CL, Vc, tabs, Ka, lags_Depot
Observations: conc, pca
First Step - Adding Individual PK Parameters:
The first step (pdpop_subject_constructor_icoef) adds the individual PK parameters (like clearance, volume, etc.) that were estimated from the PK model fit. These parameters come from icoef(pk_fit) which returns individual estimates for each subject.
Second Step - Adding Dose Control Parameters:
The second step (pdpop_subject_constructor_icoef_dcs) adds the dose control parameters (like lag times) that were also estimated from the PK model. These parameters come from dosecontrol(pk_fit) and are specifically related to how the drug dosing should be handled.
Step 1: Extract the individual coefficients from the PK fit (same as the DataFrames method).
Step 2: Extract the individual coefficients from the PK fit.
# Perform inspection on FittedPumasModelwarf_pk_insp =inspect(pk_fit);# Convert to a DataFramedf_inspect =DataFrame(warf_pk_insp);# Obtain the individual PK parametersicoef_dataframe =unique(df_inspect[!, [:id, :time, :CL, :Vc, :tabs, :lags_Depot]], :id)first(icoef_dataframe, 5)
Step 3: Use the Subject constructor to create a new dataset for the PD model.
# We need to convert the dataframe to a vector of named tuples that can be mapped over and passed to the `Subject` constructoricoef_nt = Tables.rowtable(icoef_dataframe)pdpop_subject_constructor_icoef_dataframe =map(zip(icoef_nt, pkpd_pop)) do (row, pdsubj)Subject( pdsubj, covariates = ( CL = row.CL, Vc = row.Vc, tabs = row.tabs, lags_Depot = row.lags_Depot, ), )end
The loglikehood for the PD model given the two datasets derived above can be compared to ensure that both ways of deriving the PKPD data are equivalent.
We will not explore the results further here, but the PD model fit using the Subject constructor is complete.
In summary, the sequential approach facilitates a simpler conceptual model, but the direct communication of uncertainty from the PK parameters into the PD model is lost.
4 Simultaneous PK/PD Modeling
For a simultaneous approach, a joint model that handles both PK and PD data at once is built. Conceptually, the concentration variable is shared between PK and PD within the same model code, so all parameters are estimated in a single fit.
4.1 Defining the Joint PK/PD Model
The joint PK/PD model for sequential fitting has already been defined above in warfarin_pkpd_model.
A single dataset that contains both PK and PD data, possibly at different times, is needed. Pumas can handle that by allowing the definition of multiple observations while constructing a Population:
pkpd_pop =read_pumas( warfarin_data_wide; id =:ID, time =:TIME, amt =:AMOUNT, cmt =:CMT, evid =:EVID, covariates = [:SEX, :WEIGHT, :FSZV, :FSZCL], observations = [:conc, :pca],)
Input predicted concentrations (as data) into the PD model.
Advantage: The PD model is simpler to interpret and fits quickly because it treats the Warfarin concentration as a known input. However, the direct communication of uncertainty from the PK parameters into the PD model is lost.
Simultaneous:
Fit PK and PD data in one step.
This approach provides the correct propagation of parameter uncertainty and potentially more robust estimates, but can be more complex to set up and can be slower or more sensitive to initial guesses.
Preparing the PKPD Data
Sequential approach: Both the DataFrame and the Subject constructor methods are easy to use, with the later being more flexible.
Simultaneous approach: The joint dataset can be prepared in a more flexible way, allowing for more complex data structures.
7 Conclusion
In this tutorial, the following was covered:
Defining a Warfarin PK model in Pumas.
Fitting that PK model to concentration data.
Performing sequential PK/PD modeling by using predicted PK concentrations as an input in a separate PD model.
Performing a simultaneous PK/PD approach by coding a joint model that handles PK and PD data in a single fit.