A Comprehensive Introduction to Pumas

Authors

Vijay Ivaturi

Jose Storopoli

This tutorial provides a comprehensive introduction to a modeling and simulation workflow in Pumas. The idea is not to get into the details of Pumas specifics, but instead provide a narrative on the lines of a regular workflow in our day-to-day work, with brevity where required to allow a broad overview. Wherever possible, cross-references will be provided to documentation and detailed examples that provide deeper insight into a particular topic.

As part of this workflow, you will be introduced to various aspects such as:

  1. Data wrangling in Julia
  2. Exploratory analysis in Julia
  3. Continuous data non-linear mixed effects modeling in Pumas
  4. Model comparison routines, post-processing, validation etc.

1 The Study and Design

CTMNopain is a novel anti-inflammatory agent under preliminary investigation. A dose-ranging trial was conducted comparing placebo with 3 doses of CTMNopain (5mg, 20mg and 80 mg QD). The maximum tolerated dose is 160 mg per day. Plasma concentrations (mg/L) of the drug were measured at 0, 0.5, 1, 1.5, 2, 2.5, 3-8 hours.

Pain score (0=no pain, 1=mild, 2=moderate, 3=severe) were obtained at time points when plasma concentration was collected. A pain score of 2 or more is considered as no pain relief.

The subjects can request for remedication if pain relief is not achieved after 2 hours post dose. Some subjects had remedication before 2 hours if they were not able to bear the pain. The time to remedication and the remedication status is available for subjects.

The pharmacokinetic dataset can be accessed using PharmaDatasets.jl.

2 Setup

2.1 Load libraries

These libraries provide the workhorse functionality in the Pumas ecosystem:

using Pumas
using PumasUtilities
using NCA
using NCAUtilities

In addition, libraries below are good add-on’s that provide ancillary functionality:

using GLM: lm, @formula
using Random
using CSV
using DataFramesMeta
using CairoMakie
using PharmaDatasets

2.2 Data Wrangling

We start by reading in the dataset and making some quick summaries.

Tip

If you want to learn more about data wrangling, don’t forget to check our Data Wrangling in Julia tutorials!

pkpain_df = dataset("pk_painrelief")
first(pkpain_df, 5)
5×7 DataFrame
Row Subject Time Conc PainRelief PainScore RemedStatus Dose
Int64 Float64 Float64 Int64 Int64 Int64 String7
1 1 0.0 0.0 0 3 1 20 mg
2 1 0.5 1.15578 1 1 0 20 mg
3 1 1.0 1.37211 1 0 0 20 mg
4 1 1.5 1.30058 1 0 0 20 mg
5 1 2.0 1.19195 1 1 0 20 mg

Let’s filter out the placebo data as we don’t need that for the PK analysis.

pkpain_noplb_df = @rsubset pkpain_df :Dose != "Placebo";
first(pkpain_noplb_df, 5)
5×7 DataFrame
Row Subject Time Conc PainRelief PainScore RemedStatus Dose
Int64 Float64 Float64 Int64 Int64 Int64 String7
1 1 0.0 0.0 0 3 1 20 mg
2 1 0.5 1.15578 1 1 0 20 mg
3 1 1.0 1.37211 1 0 0 20 mg
4 1 1.5 1.30058 1 0 0 20 mg
5 1 2.0 1.19195 1 1 0 20 mg

3 Analysis

3.1 Non-compartmental analysis

Let’s begin by performing a quick NCA of the concentration time profiles and view the exposure changes across doses. The input data specification for NCA analysis requires the presence of a :route column and an :amt column that specifies the dose. So, let’s add that in:

@rtransform! pkpain_noplb_df begin
    :route = "ev"
    :Dose = parse(Int, chop(:Dose; tail = 3))
end

We also need to create an :amt column:

@rtransform! pkpain_noplb_df :amt = :Time == 0 ? :Dose : missing

Now, we map the data variables to the read_nca function that prepares the data for NCA analysis.

pkpain_nca = read_nca(
    pkpain_noplb_df;
    id = :Subject,
    time = :Time,
    amt = :amt,
    observations = :Conc,
    group = [:Dose],
    route = :route,
)
NCAPopulation (120 subjects):
  Group: [["Dose" => 5], ["Dose" => 20], ["Dose" => 80]]
  Number of missing observations: 0
  Number of blq observations: 0

Now that we mapped the data in, let’s visualize the concentration vs time plots for a few individuals. When paginate is set to true, a vector of plots are returned and below we display the first element with 9 individuals.

f = observations_vs_time(
    pkpain_nca;
    paginate = true,
    axis = (; xlabel = "Time (hr)", ylabel = "CTMNoPain Concentration (ng/mL)"),
    facet = (; combinelabels = true),
)
f[1]

An observations versus time profile for all subjects

Observations versus Time

or you can view the summary curves by dose group as passed in to the group argument in read_nca

summary_observations_vs_time(
    pkpain_nca,
    figure = (; fontsize = 22, resolution = (800, 1000)),
    color = "black",
    linewidth = 3,
    axis = (; xlabel = "Time (hr)", ylabel = "CTMX Concentration (μg/mL)"),
    facet = (; combinelabels = true, linkaxes = true),
)

An observations versus time profile for all subjects in a summarized manner

Summary Observations versus Time

A full NCA Report is now obtained for completeness purposes using the run_nca function, but later we will only extract a couple of key metrics of interest.

pk_nca = run_nca(pkpain_nca; sigdigits = 3)

We can look at the NCA fits for some subjects. Here f is a vector or figures. We’ll showcase the first image by indexing f:

f = subject_fits(
    pk_nca,
    paginate = true,
    axis = (; xlabel = "Time (hr)", ylabel = "CTMX Concentration (μg/mL)"),
    facet = (; combinelabels = true, linkaxes = true),
)
f[1]

Trend plot with observations for all individual subjects over time

Subject Fits

As CTMNopain’s effect maybe mainly related to maximum concentration (cmax) or area under the curve (auc), we present some summary statistics using the summarize function from NCA.

strata = [:Dose]
1-element Vector{Symbol}:
 :Dose
params = [:cmax, :aucinf_obs]
2-element Vector{Symbol}:
 :cmax
 :aucinf_obs
output = summarize(pk_nca; stratify_by = strata, parameters = params)
6×10 DataFrame
Row Dose parameters numsamples minimum maximum mean std geomean geostd geomeanCV
Int64 String Int64 Float64 Float64 Float64 Float64 Float64 Float64 Float64
1 5 cmax 40 0.19 0.539 0.356075 0.0884129 0.345104 1.2932 26.1425
2 5 aucinf_obs 40 0.914 3.4 1.5979 0.490197 1.53373 1.32974 29.0868
3 20 cmax 40 0.933 2.7 1.4737 0.361871 1.43408 1.2633 23.6954
4 20 aucinf_obs 40 2.77 14.1 6.377 2.22239 6.02031 1.41363 35.6797
5 80 cmax 40 3.3 8.47 5.787 1.31957 5.64164 1.25757 23.2228
6 80 aucinf_obs 40 13.7 49.1 29.5 8.68984 28.2954 1.34152 30.0258

The statistics printed above are the default, but you can pass in your own statistics using the stats = [] argument to the summarize function.

We can look at a few parameter distribution plots.

parameters_vs_group(
    pk_nca,
    parameter = :cmax,
    axis = (; xlabel = "Dose (mg)", ylabel = "Cₘₐₓ (ng/mL)"),
    figure = (; fontsize = 18),
)

A violin plot for the Cmax distribution for each dose group

Cmax for each Dose Group

Dose normalized PK parameters, cmax and aucinf were essentially dose proportional between for 5 mg, 20 mg and 80 mg doses. You can perform a simple regression to check the impact of dose on cmax:

dp = NCA.DoseLinearityPowerModel(pk_nca, :cmax; level = 0.9)
Dose Linearity Power Model
Variable: cmax
Model: log(cmax) ~ log(α) + β × log(dose)
────────────────────────────────────
   Estimate  low CI 90%  high CI 90%
────────────────────────────────────
β   1.00775     0.97571       1.0398
────────────────────────────────────

Here’s a visualization for the dose linearity using a power model for cmax:

power_model(dp)

A dose linearity power model plot for Cmax

Dose Linearity Plot

We can also visualize a dose proportionality results with respect to a specific endpoint in a NCA Report; for example cmax and aucinf_obs:

dose_vs_dose_normalized(pk_nca, :cmax)

A dose proportionality plot for Cmax

Dose Proportionality Plot
dose_vs_dose_normalized(pk_nca, :aucinf_obs)

A dose proportionality plot for AUC

Dose Proportionality Plot

Based on visual inspection of the concentration time profiles as seen earlier, CTMNopain exhibited monophasic decline, and perhaps a one compartment model best fits the PK data.

3.2 Pharmacokinetic modeling

As seen from the plots above, the concentrations decline monoexponentially. We will evaluate both one and two compartment structural models to assess best fit. Further, different residual error models will also be tested.

We will use the results from NCA to provide us good initial estimates.

3.2.1 Data preparation for modeling

PumasNDF requires the presence of :evid and :cmt columns in the dataset.

@rtransform! pkpain_noplb_df begin
    :evid = :Time == 0 ? 1 : 0
    :cmt = :Time == 0 ? 1 : 2
    :cmt2 = 1 # for zero order absorption
end

Further, observations at time of dosing, i.e., when evid = 1 have to be missing

@rtransform! pkpain_noplb_df :Conc = :evid == 1 ? missing : :Conc

The dataframe will now be converted to a Population using read_pumas. Note that both observations and covariates are required to be an array even if it is one element.

pkpain_noplb = read_pumas(
    pkpain_noplb_df;
    id = :Subject,
    time = :Time,
    amt = :amt,
    observations = [:Conc],
    covariates = [:Dose],
    evid = :evid,
    cmt = :cmt,
)
Population
  Subjects: 120
  Covariates: Dose
  Observations: Conc

Now that the data is transformed to a Population of subjects, we can explore different models.

3.2.2 One-compartment model

Note

If you are not familiar yet with the @model blocks and syntax, please check our documentation.

pk_1cmp = @model begin

    @metadata begin
        desc = "One Compartment Model"
        timeu = u"hr"
    end

    @param begin
        """
        Clearance (L/hr)
        """
        tvcl  RealDomain(; lower = 0, init = 3.2)
        """
        Volume (L)
        """
        tvv  RealDomain(; lower = 0, init = 16.4)
        """
        Absorption rate constant (h-1)
        """
        tvka  RealDomain(; lower = 0, init = 3.8)
        """
          - ΩCL
          - ΩVc
          - ΩKa
        """
        Ω  PDiagDomain(init = [0.04, 0.04, 0.04])
        """
        Proportional RUV
        """
        σ_p  RealDomain(; lower = 0.0001, init = 0.2)
    end

    @random begin
        η ~ MvNormal(Ω)
    end

    @covariates begin
        """
        Dose (mg)
        """
        Dose
    end

    @pre begin
        CL = tvcl * exp(η[1])
        Vc = tvv * exp(η[2])
        Ka = tvka * exp(η[3])
    end

    @dynamics Depots1Central1

    @derived begin
        cp := @. Central / Vc
        """
        CTMx Concentration (ng/mL)
        """
        Conc ~ @. Normal(cp, abs(cp) * σ_p)
    end

end
PumasModel
  Parameters: tvcl, tvv, tvka, Ω, σ_p
  Random effects: η
  Covariates: Dose
  Dynamical system variables: Depot, Central
  Dynamical system type: Closed form
  Derived: Conc
  Observed: Conc
Tip

Note that the local assignment := can be used to define intermediate statements that will not be carried outside of the block. This means that all the resulting data workflows from this model will not contain the intermediate variables defined with :=. We use this when we want to suppress the variable from any further output.

The idea behind := is for performance reasons. If you are not carrying the variable defined with := outside of the block, then it is not necessary to store it in the resulting data structures. Not only will your model run faster, but the resulting data structures will also be smaller.

Before going to fit the model, let’s evaluate some helpful steps via simulation to check appropriateness of data and model

# zero out the random effects
etas = zero_randeffs(pk_1cmp, init_params(pk_1cmp), pkpain_noplb)

Above, we are generating a vector of η’s of the same length as the number of subjects to zero out the random effects. We do this as we are evaluating the trajectories of the concentrations at the initial set of parameters at a population level. Other helper functions here are sample_randeffs and init_randeffs. Please refer to the documentation.

simpk_iparams = simobs(pk_1cmp, pkpain_noplb, init_params(pk_1cmp), etas)
Simulated population (Vector{<:Subject})
  Simulated subjects: 120
  Simulated variables: Conc
sim_plot(
    pk_1cmp,
    simpk_iparams;
    observations = [:Conc],
    figure = (; fontsize = 18),
    axis = (;
        xlabel = "Time (hr)",
        ylabel = "Observed/Predicted \n CTMx Concentration (ng/mL)",
    ),
)

A simulated observations versus time plot overlaid with the scatter plot of the observed observations

Simulated Observations Plot

Our NCA based initial guess on the parameters seem to work well.

Lets change the initial estimate of a couple of the parameters to evaluate the sensitivity.

pkparam = (; init_params(pk_1cmp)..., tvka = 2, tvv = 10)
(tvcl = 3.2,
 tvv = 10,
 tvka = 2,
 Ω = [0.04 0.0 0.0; 0.0 0.04 0.0; 0.0 0.0 0.04],
 σ_p = 0.2,)
simpk_changedpars = simobs(pk_1cmp, pkpain_noplb, pkparam, etas)
Simulated population (Vector{<:Subject})
  Simulated subjects: 120
  Simulated variables: Conc
sim_plot(
    pk_1cmp,
    simpk_changedpars;
    observations = [:Conc],
    figure = (; fontsize = 18),
    axis = (
        xlabel = "Time (hr)",
        ylabel = "Observed/Predicted \n CTMx Concentration (ng/mL)",
    ),
)

A simulated observations versus time plot overlaid with the scatter plot of the observed observations

Simulated Observations Plot

Changing the tvka and decreasing the tvv seemed to make an impact and observations go through the simulated lines.

To get a quick ballpark estimate of your PK parameters, we can do a NaivePooled analysis.

3.2.2.1 NaivePooled
pkfit_np = fit(pk_1cmp, pkpain_noplb, init_params(pk_1cmp), NaivePooled(); omegas = (:Ω,))
[ Info: Checking the initial parameter values.
[ Info: The initial negative log likelihood and its gradient are finite. Check passed.
Iter     Function value   Gradient norm 
     0     7.744356e+02     3.715711e+03
 * time: 0.023007869720458984
     1     2.343899e+02     1.747348e+03
 * time: 0.4593689441680908
     2     9.696232e+01     1.198088e+03
 * time: 0.4613628387451172
     3    -7.818699e+01     5.538151e+02
 * time: 0.4623379707336426
     4    -1.234803e+02     2.462514e+02
 * time: 0.4633939266204834
     5    -1.372888e+02     2.067458e+02
 * time: 0.4644429683685303
     6    -1.410579e+02     1.162950e+02
 * time: 0.4659128189086914
     7    -1.434754e+02     5.632816e+01
 * time: 0.5086348056793213
     8    -1.453401e+02     7.859270e+01
 * time: 0.5098237991333008
     9    -1.498185e+02     1.455606e+02
 * time: 0.5108017921447754
    10    -1.534371e+02     1.303682e+02
 * time: 0.5117688179016113
    11    -1.563557e+02     5.975474e+01
 * time: 0.5127229690551758
    12    -1.575052e+02     9.308611e+00
 * time: 0.5136549472808838
    13    -1.579357e+02     1.234484e+01
 * time: 0.5145819187164307
    14    -1.581874e+02     7.478196e+00
 * time: 0.5155220031738281
    15    -1.582981e+02     2.027162e+00
 * time: 0.5165379047393799
    16    -1.583375e+02     5.578262e+00
 * time: 0.5175778865814209
    17    -1.583556e+02     4.727050e+00
 * time: 0.5190989971160889
    18    -1.583644e+02     2.340173e+00
 * time: 0.5202310085296631
    19    -1.583680e+02     7.738100e-01
 * time: 0.5213217735290527
    20    -1.583696e+02     3.300689e-01
 * time: 0.5228948593139648
    21    -1.583704e+02     3.641985e-01
 * time: 0.5239639282226562
    22    -1.583707e+02     4.365901e-01
 * time: 0.5249907970428467
    23    -1.583709e+02     3.887800e-01
 * time: 0.5261509418487549
    24    -1.583710e+02     2.766977e-01
 * time: 0.5276229381561279
    25    -1.583710e+02     1.758029e-01
 * time: 0.528580904006958
    26    -1.583710e+02     1.133947e-01
 * time: 0.5295398235321045
    27    -1.583710e+02     7.922544e-02
 * time: 0.5309319496154785
    28    -1.583710e+02     5.954998e-02
 * time: 0.5318548679351807
    29    -1.583710e+02     4.157079e-02
 * time: 0.5328149795532227
    30    -1.583710e+02     4.295447e-02
 * time: 0.5342378616333008
    31    -1.583710e+02     5.170753e-02
 * time: 0.5352089405059814
    32    -1.583710e+02     2.644383e-02
 * time: 0.5365688800811768
    33    -1.583710e+02     4.548993e-03
 * time: 0.5383257865905762
    34    -1.583710e+02     2.501804e-02
 * time: 0.539543867111206
    35    -1.583710e+02     3.763440e-02
 * time: 0.5405049324035645
    36    -1.583710e+02     3.206026e-02
 * time: 0.5419049263000488
    37    -1.583710e+02     1.003698e-02
 * time: 0.5428338050842285
    38    -1.583710e+02     2.209089e-02
 * time: 0.5439188480377197
    39    -1.583710e+02     4.954172e-03
 * time: 0.5454268455505371
    40    -1.583710e+02     1.609373e-02
 * time: 0.5473079681396484
    41    -1.583710e+02     1.579802e-02
 * time: 0.5487899780273438
    42    -1.583710e+02     1.014113e-03
 * time: 0.5503959655761719
    43    -1.583710e+02     6.050644e-03
 * time: 0.5522170066833496
    44    -1.583710e+02     1.354412e-02
 * time: 0.5539968013763428
    45    -1.583710e+02     4.473248e-03
 * time: 0.5548789501190186
    46    -1.583710e+02     4.644735e-03
 * time: 0.588284969329834
    47    -1.583710e+02     9.829910e-03
 * time: 0.5896649360656738
    48    -1.583710e+02     1.047561e-03
 * time: 0.5906839370727539
    49    -1.583710e+02     8.366895e-03
 * time: 0.5916528701782227
    50    -1.583710e+02     7.879055e-04
 * time: 0.5926249027252197
FittedPumasModel

Successful minimization:                      true

Likelihood approximation:              NaivePooled
Likelihood Optimizer:                         BFGS
Dynamical system type:                 Closed form

Log-likelihood value:                    158.37103
Number of subjects:                            120
Number of parameters:         Fixed      Optimized
                                  1              6
Observation records:         Active        Missing
    Conc:                      1320              0
    Total:                     1320              0

------------------
         Estimate
------------------
tvcl      3.0054
tvv      14.089
tvka     44.228
Ω₁,₁      0.0
Ω₂,₂      0.0
Ω₃,₃      0.0
σ_p       0.32999
------------------
coefficients_table(pkfit_np)
7×3 DataFrame
Row Parameter Description Estimate
String Abstract… Float64
1 tvcl Clearance (L/hr)\n 3.005
2 tvv Volume (L)\n 14.089
3 tvka Absorption rate constant (h-1)\n 44.228
4 Ω₁,₁ ΩCL 0.0
5 Ω₂,₂ ΩVc 0.0
6 Ω₃,₃ ΩKa 0.0
7 σ_p Proportional RUV\n 0.33

The final estimates from the NaivePooled approach seem reasonably close to our initial guess from NCA, except for the tvka parameter. We will stick with our initial guess.

One way to be cautious before going into a complete fitting routine is to evaluate the likelihood of the individual subjects given the initial parameter values and see if any subject(s) pops out as unreasonable. There are a few ways of doing this:

  • check the loglikelihood subject wise
  • check if there any influential subjects

Below, we are basically checking if the initial estimates for any subject are way off that we are unable to compute the initial loglikelihood.

lls = []
for subj in pkpain_noplb
    push!(lls, loglikelihood(pk_1cmp, subj, pkparam, FOCE()))
end
# the plot below is using native CairoMakie `hist`
hist(lls; bins = 10, normalization = :none, color = (:black, 0.5), x_gap = 0)

A histogram of the individual loglikelihoods

Histogram of Loglikelihoods

The distribution of the loglikelihood’s suggest no extreme outliers.

A more convenient way is to use the findinfluential function that provides a list of k top influential subjects by showing the normalized (minus) loglikelihood for each subject. As you can see below, the minus loglikelihood in the range of 16 agrees with the histogram plotted above.

influential_subjects = findinfluential(pk_1cmp, pkpain_noplb, pkparam, FOCE())
120-element Vector{NamedTuple{(:id, :nll), Tuple{String, Float64}}}:
 (id = "148", nll = 16.65965885684477)
 (id = "135", nll = 16.64898519007633)
 (id = "156", nll = 15.959069556607496)
 (id = "159", nll = 15.441218240496486)
 (id = "149", nll = 14.715134644119509)
 (id = "88", nll = 13.09709837464614)
 (id = "16", nll = 12.98228052193144)
 (id = "61", nll = 12.652182902303679)
 (id = "71", nll = 12.500330088085505)
 (id = "59", nll = 12.241510254805235)
 ⋮
 (id = "57", nll = -22.79767423253431)
 (id = "93", nll = -22.836900711478208)
 (id = "12", nll = -23.007742339519247)
 (id = "123", nll = -23.292751843079234)
 (id = "41", nll = -23.425412534960515)
 (id = "99", nll = -23.535214841901112)
 (id = "29", nll = -24.025959868383083)
 (id = "52", nll = -24.164757842493685)
 (id = "24", nll = -25.57209232565845)
3.2.2.2 FOCE

Now that we have a good handle on our data, lets go ahead and fit a population model with FOCE:

pkfit_1cmp = fit(pk_1cmp, pkpain_noplb, pkparam, FOCE(); constantcoef = (; tvka = 2))
[ Info: Checking the initial parameter values.
[ Info: The initial negative log likelihood and its gradient are finite. Check passed.
Iter     Function value   Gradient norm 
     0    -5.935351e+02     5.597318e+02
 * time: 7.104873657226562e-5
     1    -7.022088e+02     1.707063e+02
 * time: 0.20298099517822266
     2    -7.314067e+02     2.903269e+02
 * time: 0.3285560607910156
     3    -8.520591e+02     2.285888e+02
 * time: 0.4285571575164795
     4    -1.120191e+03     3.795410e+02
 * time: 0.660750150680542
     5    -1.178784e+03     2.323978e+02
 * time: 0.7673070430755615
     6    -1.218320e+03     9.699907e+01
 * time: 0.8654439449310303
     7    -1.223641e+03     5.862105e+01
 * time: 0.9589660167694092
     8    -1.227620e+03     1.831403e+01
 * time: 1.0542960166931152
     9    -1.228381e+03     2.132323e+01
 * time: 1.1419241428375244
    10    -1.230098e+03     2.921228e+01
 * time: 1.2335381507873535
    11    -1.230854e+03     2.029662e+01
 * time: 1.3255980014801025
    12    -1.231116e+03     5.229097e+00
 * time: 1.4139759540557861
    13    -1.231179e+03     1.689232e+00
 * time: 1.485464096069336
    14    -1.231187e+03     1.215379e+00
 * time: 1.5669829845428467
    15    -1.231188e+03     2.770380e-01
 * time: 1.6419999599456787
    16    -1.231188e+03     1.636653e-01
 * time: 1.7093250751495361
    17    -1.231188e+03     2.701133e-01
 * time: 1.7767159938812256
    18    -1.231188e+03     3.163363e-01
 * time: 1.8306660652160645
    19    -1.231188e+03     1.505149e-01
 * time: 1.8950231075286865
    20    -1.231188e+03     2.484999e-02
 * time: 1.9575550556182861
    21    -1.231188e+03     8.446863e-04
 * time: 2.016414165496826
FittedPumasModel

Successful minimization:                      true

Likelihood approximation:                     FOCE
Likelihood Optimizer:                         BFGS
Dynamical system type:                 Closed form

Log-likelihood value:                     1231.188
Number of subjects:                            120
Number of parameters:         Fixed      Optimized
                                  1              6
Observation records:         Active        Missing
    Conc:                      1320              0
    Total:                     1320              0

-------------------
         Estimate
-------------------
tvcl      3.1642
tvv      13.288
tvka      2.0
Ω₁,₁      0.08494
Ω₂,₂      0.048568
Ω₃,₃      5.5811
σ_p       0.10093
-------------------
infer(pkfit_1cmp)
[ Info: Calculating: variance-covariance matrix.
[ Info: Done.
Asymptotic inference results using sandwich estimator

Successful minimization:                      true

Likelihood approximation:                     FOCE
Likelihood Optimizer:                         BFGS
Dynamical system type:                 Closed form

Log-likelihood value:                     1231.188
Number of subjects:                            120
Number of parameters:         Fixed      Optimized
                                  1              6
Observation records:         Active        Missing
    Conc:                      1320              0
    Total:                     1320              0

-------------------------------------------------------------------
        Estimate           SE                     95.0% C.I.
-------------------------------------------------------------------
tvcl     3.1642          0.086619         [ 2.9944  ;  3.334   ]
tvv     13.288           0.27481          [12.749   ; 13.827   ]
tvka     2.0             NaN              [  NaN    ;   NaN      ]
Ω₁,₁     0.08494         0.011022         [ 0.063338;  0.10654 ]
Ω₂,₂     0.048568        0.0063501        [ 0.036122;  0.061014]
Ω₃,₃     5.5811          1.2194           [ 3.1911  ;  7.9711  ]
σ_p      0.10093         0.0057196        [ 0.089718;  0.11214 ]
-------------------------------------------------------------------

Notice that tvka is fixed to 2 as we don’t have a lot of information before tmax. From the results above, we see that the parameter precision for this model is reasonable.

3.2.3 Two-compartment model

Just to be sure, let’s fit a 2-compartment model and evaluate:

pk_2cmp = @model begin

    @param begin
        """
        Clearance (L/hr)
        """
        tvcl  RealDomain(; lower = 0, init = 3.2)
        """
        Central Volume (L)
        """
        tvv  RealDomain(; lower = 0, init = 16.4)
        """
        Peripheral Volume (L)
        """
        tvvp  RealDomain(; lower = 0, init = 10)
        """
        Distributional Clearance (L/hr)
        """
        tvq  RealDomain(; lower = 0, init = 2)
        """
        Absorption rate constant (h-1)
        """
        tvka  RealDomain(; lower = 0, init = 1.3)
        """
          - ΩCL
          - ΩVc
          - ΩKa
          - ΩVp
          - ΩQ
        """
        Ω  PDiagDomain(init = [0.04, 0.04, 0.04, 0.04, 0.04])
        """
        Proportional RUV
        """
        σ_p  RealDomain(; lower = 0.0001, init = 0.2)
    end

    @random begin
        η ~ MvNormal(Ω)
    end

    @covariates begin
        """
        Dose (mg)
        """
        Dose
    end

    @pre begin
        CL = tvcl * exp(η[1])
        Vc = tvv * exp(η[2])
        Ka = tvka * exp(η[3])
        Vp = tvvp * exp(η[4])
        Q = tvq * exp(η[5])
    end

    @dynamics Depots1Central1Periph1

    @derived begin
        cp := @. Central / Vc
        """
        CTMx Concentration (ng/mL)
        """
        Conc ~ @. Normal(cp, cp * σ_p)
    end
end
PumasModel
  Parameters: tvcl, tvv, tvvp, tvq, tvka, Ω, σ_p
  Random effects: η
  Covariates: Dose
  Dynamical system variables: Depot, Central, Peripheral
  Dynamical system type: Closed form
  Derived: Conc
  Observed: Conc
3.2.3.1 FOCE
pkfit_2cmp =
    fit(pk_2cmp, pkpain_noplb, init_params(pk_2cmp), FOCE(); constantcoef = (; tvka = 2))
[ Info: Checking the initial parameter values.
[ Info: The initial negative log likelihood and its gradient are finite. Check passed.
Iter     Function value   Gradient norm 
     0    -6.302369e+02     1.021050e+03
 * time: 8.106231689453125e-5
     1    -9.197817e+02     9.927951e+02
 * time: 0.24565410614013672
     2    -1.372640e+03     2.054986e+02
 * time: 0.4350099563598633
     3    -1.446326e+03     1.543987e+02
 * time: 0.6477639675140381
     4    -1.545570e+03     1.855028e+02
 * time: 0.8503780364990234
     5    -1.581449e+03     1.713157e+02
 * time: 1.1719300746917725
     6    -1.639433e+03     1.257382e+02
 * time: 1.3612439632415771
     7    -1.695964e+03     7.450539e+01
 * time: 1.556236982345581
     8    -1.722243e+03     5.961044e+01
 * time: 1.7617769241333008
     9    -1.736883e+03     7.320921e+01
 * time: 1.9566221237182617
    10    -1.753547e+03     7.501938e+01
 * time: 2.1680891513824463
    11    -1.764053e+03     6.185661e+01
 * time: 2.3746581077575684
    12    -1.778991e+03     4.831033e+01
 * time: 2.5966410636901855
    13    -1.791492e+03     4.943278e+01
 * time: 2.8218400478363037
    14    -1.799847e+03     2.871410e+01
 * time: 3.073413133621216
    15    -1.805374e+03     7.520789e+01
 * time: 3.3226089477539062
    16    -1.816260e+03     2.990621e+01
 * time: 3.5513241291046143
    17    -1.818252e+03     2.401915e+01
 * time: 3.769308090209961
    18    -1.822988e+03     2.587225e+01
 * time: 3.9771780967712402
    19    -1.824653e+03     1.550517e+01
 * time: 4.188854932785034
    20    -1.826074e+03     1.788927e+01
 * time: 4.410035133361816
    21    -1.826821e+03     1.888389e+01
 * time: 4.616788148880005
    22    -1.827900e+03     1.432840e+01
 * time: 4.814429998397827
    23    -1.828511e+03     9.422040e+00
 * time: 5.027518033981323
    24    -1.828754e+03     5.363444e+00
 * time: 5.238345146179199
    25    -1.828862e+03     4.916167e+00
 * time: 5.444849967956543
    26    -1.829007e+03     4.695750e+00
 * time: 5.644551038742065
    27    -1.829358e+03     1.090245e+01
 * time: 5.863084077835083
    28    -1.829830e+03     1.451320e+01
 * time: 6.075309991836548
    29    -1.830201e+03     1.108694e+01
 * time: 6.297608137130737
    30    -1.830360e+03     2.892320e+00
 * time: 6.509427070617676
    31    -1.830390e+03     1.699267e+00
 * time: 6.7196900844573975
    32    -1.830404e+03     1.602159e+00
 * time: 6.908579111099243
    33    -1.830432e+03     2.822847e+00
 * time: 7.098942995071411
    34    -1.830475e+03     4.105639e+00
 * time: 7.31061315536499
    35    -1.830527e+03     5.093685e+00
 * time: 7.513188123703003
    36    -1.830592e+03     2.697353e+00
 * time: 7.736372947692871
    37    -1.830615e+03     3.468352e+00
 * time: 7.939083099365234
    38    -1.830623e+03     2.594581e+00
 * time: 8.155632019042969
    39    -1.830625e+03     1.770110e+00
 * time: 8.34770393371582
    40    -1.830627e+03     1.040041e+00
 * time: 8.538861989974976
    41    -1.830628e+03     1.124112e+00
 * time: 8.726274967193604
    42    -1.830628e+03     3.547258e-01
 * time: 8.90208911895752
    43    -1.830629e+03     4.070652e-01
 * time: 9.073261022567749
    44    -1.830630e+03     7.177755e-01
 * time: 9.265002965927124
    45    -1.830630e+03     5.121219e-01
 * time: 9.44118595123291
    46    -1.830630e+03     9.584921e-02
 * time: 9.6127450466156
    47    -1.830630e+03     1.039362e-02
 * time: 9.769767999649048
    48    -1.830630e+03     5.788836e-03
 * time: 9.924812078475952
    49    -1.830630e+03     6.037375e-03
 * time: 10.064406156539917
    50    -1.830630e+03     7.187933e-03
 * time: 10.204432010650635
    51    -1.830630e+03     7.187933e-03
 * time: 10.401621103286743
    52    -1.830630e+03     7.187933e-03
 * time: 10.585646152496338
    53    -1.830630e+03     7.212102e-03
 * time: 10.762042045593262
    54    -1.830630e+03     7.214659e-03
 * time: 10.93213701248169
    55    -1.830630e+03     7.217015e-03
 * time: 11.101531982421875
    56    -1.830630e+03     7.219581e-03
 * time: 11.282581090927124
    57    -1.830630e+03     7.222151e-03
 * time: 11.453551054000854
    58    -1.830630e+03     7.224724e-03
 * time: 11.631006002426147
    59    -1.830630e+03     7.227302e-03
 * time: 11.818910121917725
    60    -1.830630e+03     7.227561e-03
 * time: 12.003399133682251
    61    -1.830630e+03     7.227819e-03
 * time: 12.20097303390503
    62    -1.830630e+03     7.228077e-03
 * time: 12.382674932479858
    63    -1.830630e+03     7.228103e-03
 * time: 12.560586929321289
    64    -1.830630e+03     7.228129e-03
 * time: 12.746459007263184
    65    -1.830630e+03     7.228155e-03
 * time: 12.93652892112732
    66    -1.830630e+03     7.228180e-03
 * time: 13.117639064788818
    67    -1.830630e+03     7.228206e-03
 * time: 13.301226139068604
    68    -1.830630e+03     7.228209e-03
 * time: 13.482129096984863
    69    -1.830630e+03     7.228211e-03
 * time: 13.662544012069702
    70    -1.830630e+03     7.228214e-03
 * time: 13.837778091430664
    71    -1.830630e+03     7.228217e-03
 * time: 14.031696081161499
    72    -1.830630e+03     7.228217e-03
 * time: 14.214507102966309
    73    -1.830630e+03     7.228217e-03
 * time: 14.391113996505737
    74    -1.830630e+03     7.228217e-03
 * time: 14.577691078186035
    75    -1.830630e+03     7.228218e-03
 * time: 14.761384010314941
    76    -1.830630e+03     7.228218e-03
 * time: 14.93655014038086
    77    -1.830630e+03     7.228218e-03
 * time: 15.130031108856201
    78    -1.830630e+03     7.228218e-03
 * time: 15.34204912185669
    79    -1.830630e+03     7.228218e-03
 * time: 15.566895008087158
    80    -1.830630e+03     7.214381e-03
 * time: 15.713245153427124
    81    -1.830630e+03     7.214381e-03
 * time: 15.927448987960815
    82    -1.830630e+03     7.214381e-03
 * time: 16.15268301963806
    83    -1.830630e+03     7.214381e-03
 * time: 16.375805139541626
    84    -1.830630e+03     7.214381e-03
 * time: 16.597398042678833
FittedPumasModel

Successful minimization:                      true

Likelihood approximation:                     FOCE
Likelihood Optimizer:                         BFGS
Dynamical system type:                 Closed form

Log-likelihood value:                    1830.6304
Number of subjects:                            120
Number of parameters:         Fixed      Optimized
                                  1             10
Observation records:         Active        Missing
    Conc:                      1320              0
    Total:                     1320              0

-------------------
         Estimate
-------------------
tvcl      2.8138
tvv      11.005
tvvp      5.54
tvq       1.5159
tvka      2.0
Ω₁,₁      0.10267
Ω₂,₂      0.060776
Ω₃,₃      1.2012
Ω₄,₄      0.42349
Ω₅,₅      0.24473
σ_p       0.048405
-------------------

3.3 Comparing One- versus Two-compartment models

The 2-compartment model has a much lower objective function compared to the 1-compartment. Let’s compare the estimates from the 2 models using the compare_estimates function.

compare_estimates(; pkfit_1cmp, pkfit_2cmp)
11×3 DataFrame
Row parameter pkfit_1cmp pkfit_2cmp
String Float64? Float64?
1 tvcl 3.1642 2.81378
2 tvv 13.288 11.0046
3 tvka 2.0 2.0
4 Ω₁,₁ 0.0849405 0.102669
5 Ω₂,₂ 0.0485682 0.0607755
6 Ω₃,₃ 5.58107 1.20116
7 σ_p 0.100928 0.0484049
8 tvvp missing 5.53998
9 tvq missing 1.51591
10 Ω₄,₄ missing 0.423494
11 Ω₅,₅ missing 0.244732

We perform a likelihood ratio test to compare the two nested models. The test statistic and the \(p\)-value clearly indicate that a 2-compartment model should be preferred.

lrtest(pkfit_1cmp, pkfit_2cmp)
Statistic:          1200.0
Degrees of freedom:      4
P-value:               0.0

We should also compare the other metrics and statistics, such ηshrinkage, ϵshrinkage, aic, and bic using the metrics_table function.

@chain metrics_table(pkfit_2cmp) begin
    leftjoin(metrics_table(pkfit_1cmp); on = :Metric, makeunique = true)
    rename!(:Value => :pk2cmp, :Value_1 => :pk1cmp)
end
20×3 DataFrame
Row Metric pk2cmp pk1cmp
String Any Any
1 Successful true true
2 Estimation Time 16.598 2.017
3 Subjects 120 120
4 Fixed Parameters 1 1
5 Optimized Parameters 10 6
6 Conc Active Observations 1320 1320
7 Conc Missing Observations 0 0
8 Total Active Observations 1320 1320
9 Total Missing Observations 0 0
10 Likelihood Approximation Pumas.FOCE{Optim.NewtonTrustRegion{Float64}, Optim.Options{Float64, Nothing}} Pumas.FOCE{Optim.NewtonTrustRegion{Float64}, Optim.Options{Float64, Nothing}}
11 LogLikelihood (LL) 1830.63 1231.19
12 -2LL -3661.26 -2462.38
13 AIC -3641.26 -2450.38
14 BIC -3589.41 -2419.26
15 (η-shrinkage) η₁ 0.037 0.016
16 (η-shrinkage) η₂ 0.047 0.04
17 (η-shrinkage) η₃ 0.516 0.733
18 (ϵ-shrinkage) Conc 0.185 0.105
19 (η-shrinkage) η₄ 0.287 missing
20 (η-shrinkage) η₅ 0.154 missing

We next generate some goodness of fit plots to compare which model is performing better. To do this, we first inspect the diagnostics of our model fit.

res_inspect_1cmp = inspect(pkfit_1cmp)
[ Info: Calculating predictions.
[ Info: Calculating weighted residuals.
[ Info: Calculating empirical bayes.
[ Info: Evaluating individual parameters.
[ Info: Evaluating dose control parameters.
[ Info: Done.
FittedPumasModelInspection

Fitting was successful: true
Likehood approximation used for weighted residuals : Pumas.FOCE{Optim.NewtonTrustRegion{Float64}, Optim.Options{Float64, Nothing}}(Optim.NewtonTrustRegion{Float64}(1.0, 100.0, 1.4901161193847656e-8, 0.1, 0.25, 0.75, false), Optim.Options(x_abstol = 0.0, x_reltol = 0.0, f_abstol = 0.0, f_reltol = 0.0, g_abstol = 1.0e-5, g_reltol = 1.0e-8, outer_x_abstol = 0.0, outer_x_reltol = 0.0, outer_f_abstol = 0.0, outer_f_reltol = 0.0, outer_g_abstol = 1.0e-8, outer_g_reltol = 1.0e-8, f_calls_limit = 0, g_calls_limit = 0, h_calls_limit = 0, allow_f_increases = false, allow_outer_f_increases = true, successive_f_tol = 1, iterations = 1000, outer_iterations = 1000, store_trace = false, trace_simplex = false, show_trace = false, extended_trace = false, show_every = 1, time_limit = NaN, )
)
res_inspect_2cmp = inspect(pkfit_2cmp)
[ Info: Calculating predictions.
[ Info: Calculating weighted residuals.
[ Info: Calculating empirical bayes.
[ Info: Evaluating individual parameters.
[ Info: Evaluating dose control parameters.
[ Info: Done.
FittedPumasModelInspection

Fitting was successful: true
Likehood approximation used for weighted residuals : Pumas.FOCE{Optim.NewtonTrustRegion{Float64}, Optim.Options{Float64, Nothing}}(Optim.NewtonTrustRegion{Float64}(1.0, 100.0, 1.4901161193847656e-8, 0.1, 0.25, 0.75, false), Optim.Options(x_abstol = 0.0, x_reltol = 0.0, f_abstol = 0.0, f_reltol = 0.0, g_abstol = 1.0e-5, g_reltol = 1.0e-8, outer_x_abstol = 0.0, outer_x_reltol = 0.0, outer_f_abstol = 0.0, outer_f_reltol = 0.0, outer_g_abstol = 1.0e-8, outer_g_reltol = 1.0e-8, f_calls_limit = 0, g_calls_limit = 0, h_calls_limit = 0, allow_f_increases = false, allow_outer_f_increases = true, successive_f_tol = 1, iterations = 1000, outer_iterations = 1000, store_trace = false, trace_simplex = false, show_trace = false, extended_trace = false, show_every = 1, time_limit = NaN, )
)
gof_1cmp = goodness_of_fit(res_inspect_1cmp; figure = (; fontsize = 12))

A 4-mosaic goodness of fit plot showing the 1-compartment model

Goodness of Fit Plots
gof_2cmp = goodness_of_fit(res_inspect_2cmp; figure = (; fontsize = 12))

Trend plot with observations for all individual subjects over time

Subject Fits

These plots clearly indicate that the 2-compartment model is a better fit compared to the 1-compartment model.

We can look at selected sample of individual plots.

fig_subject_fits = subject_fits(
    res_inspect_2cmp;
    separate = true,
    paginate = true,
    facet = (; combinelabels = true),
    figure = (; fontsize = 18),
    axis = (; xlabel = "Time (hr)", ylabel = "CTMx Concentration (ng/mL)"),
)
fig_subject_fits[1]

Trend plot with observations for 4 individual subjects over time

Subject Fits for 4 Individuals

There a lot of important plotting functions you can use for your standard model diagnostics. Please make sure to read the documentation for plotting. Below, we are checking the distribution of the empirical Bayes estimates.

empirical_bayes_dist(res_inspect_2cmp; zeroline_color = :red)

A histogram for the empirical Bayes distribution of all subject-specific parameters

Empirical Bayes Distribution
empirical_bayes_vs_covariates(
    res_inspect_2cmp;
    categorical = [:Dose],
    figure = (; resolution = (600, 800)),
)

A histogram for the empirical Bayes distribution of all subject-specific parameters stratified by categorical covariates

Empirical Bayes Distribution Stratified by Covariates

Clearly, our guess at tvka seems off-target. Let’s try and estimate tvka instead of fixing it to 2:

pkfit_2cmp_unfix_ka = fit(pk_2cmp, pkpain_noplb, init_params(pk_2cmp), FOCE())
[ Info: Checking the initial parameter values.
[ Info: The initial negative log likelihood and its gradient are finite. Check passed.
Iter     Function value   Gradient norm 
     0    -3.200734e+02     1.272671e+03
 * time: 9.918212890625e-5
     1    -8.682982e+02     1.000199e+03
 * time: 0.31469011306762695
     2    -1.381870e+03     5.008081e+02
 * time: 0.5582900047302246
     3    -1.551053e+03     6.833490e+02
 * time: 0.8199670314788818
     4    -1.680887e+03     1.834586e+02
 * time: 1.071230173110962
     5    -1.726118e+03     8.870274e+01
 * time: 1.3435111045837402
     6    -1.761023e+03     1.162036e+02
 * time: 1.6267061233520508
     7    -1.786619e+03     1.114552e+02
 * time: 1.9000999927520752
     8    -1.863556e+03     9.914305e+01
 * time: 2.1820931434631348
     9    -1.882942e+03     5.342676e+01
 * time: 2.4329450130462646
    10    -1.888020e+03     2.010181e+01
 * time: 2.696493148803711
    11    -1.889832e+03     1.867263e+01
 * time: 2.9631760120391846
    12    -1.891649e+03     1.668512e+01
 * time: 3.2173070907592773
    13    -1.892615e+03     1.820701e+01
 * time: 3.474558115005493
    14    -1.893453e+03     1.745195e+01
 * time: 3.7264840602874756
    15    -1.894760e+03     1.850174e+01
 * time: 3.994093179702759
    16    -1.895647e+03     1.773939e+01
 * time: 4.230128049850464
    17    -1.896597e+03     1.143462e+01
 * time: 4.476845026016235
    18    -1.897114e+03     9.720097e+00
 * time: 4.728129148483276
    19    -1.897373e+03     6.054321e+00
 * time: 4.9801270961761475
    20    -1.897498e+03     3.985954e+00
 * time: 5.240940093994141
    21    -1.897571e+03     4.262464e+00
 * time: 5.503745079040527
    22    -1.897633e+03     4.010234e+00
 * time: 5.746827125549316
    23    -1.897714e+03     4.805375e+00
 * time: 5.984598159790039
    24    -1.897802e+03     3.508706e+00
 * time: 6.2309770584106445
    25    -1.897865e+03     3.691477e+00
 * time: 6.476570129394531
    26    -1.897900e+03     2.982720e+00
 * time: 6.715179204940796
    27    -1.897928e+03     2.563790e+00
 * time: 6.952192068099976
    28    -1.897968e+03     3.261485e+00
 * time: 7.184490203857422
    29    -1.898013e+03     3.064690e+00
 * time: 7.420509099960327
    30    -1.898040e+03     1.636525e+00
 * time: 7.674176216125488
    31    -1.898051e+03     1.439997e+00
 * time: 7.9276649951934814
    32    -1.898057e+03     1.436504e+00
 * time: 8.159252166748047
    33    -1.898069e+03     1.881529e+00
 * time: 8.410726070404053
    34    -1.898095e+03     3.253165e+00
 * time: 8.63944411277771
    35    -1.898142e+03     4.257942e+00
 * time: 8.883410215377808
    36    -1.898199e+03     3.685241e+00
 * time: 9.119894027709961
    37    -1.898245e+03     2.567364e+00
 * time: 9.38542103767395
    38    -1.898246e+03     2.561588e+00
 * time: 9.736809015274048
    39    -1.898251e+03     2.530898e+00
 * time: 10.035003185272217
    40    -1.898298e+03     2.673689e+00
 * time: 10.262229204177856
    41    -1.898300e+03     2.795097e+00
 * time: 10.562350034713745
    42    -1.898337e+03     3.699678e+00
 * time: 10.891691207885742
    43    -1.898435e+03     4.257964e+00
 * time: 11.136443138122559
    44    -1.898437e+03     4.246435e+00
 * time: 11.469350099563599
    45    -1.898448e+03     3.821693e+00
 * time: 11.776835203170776
    46    -1.898477e+03     2.785717e+00
 * time: 11.998000144958496
    47    -1.898477e+03     2.730327e+00
 * time: 12.299758195877075
    48    -1.898478e+03     2.516907e+00
 * time: 12.568533182144165
    49    -1.898479e+03     1.935688e+00
 * time: 12.861083030700684
    50    -1.898479e+03     1.258213e+00
 * time: 13.129094123840332
    51    -1.898480e+03     9.887950e-01
 * time: 13.39158010482788
    52    -1.898480e+03     1.281807e+01
 * time: 13.641710042953491
    53    -1.898480e+03     2.556060e-01
 * time: 13.932107210159302
    54    -1.898480e+03     1.118572e-01
 * time: 14.21106219291687
    55    -1.898480e+03     1.118585e-01
 * time: 14.549192190170288
    56    -1.898480e+03     1.118595e-01
 * time: 14.893805027008057
    57    -1.898480e+03     7.041556e-02
 * time: 15.146278142929077
    58    -1.898480e+03     6.966559e-02
 * time: 15.3181471824646
    59    -1.898480e+03     6.385055e-02
 * time: 15.507527112960815
    60    -1.898480e+03     7.499060e-02
 * time: 15.663819074630737
    61    -1.898480e+03     7.742415e-02
 * time: 15.876546144485474
    62    -1.898480e+03     7.742304e-02
 * time: 16.13555121421814
    63    -1.898480e+03     7.742255e-02
 * time: 16.382019996643066
    64    -1.898480e+03     7.742192e-02
 * time: 16.631980180740356
    65    -1.898480e+03     7.742191e-02
 * time: 16.89803719520569
    66    -1.898480e+03     7.742191e-02
 * time: 17.16580605506897
    67    -1.898480e+03     7.742191e-02
 * time: 17.40164804458618
    68    -1.898480e+03     7.979269e-02
 * time: 17.650196075439453
    69    -1.898480e+03     7.979256e-02
 * time: 17.929628133773804
    70    -1.898480e+03     7.979253e-02
 * time: 18.222684144973755
    71    -1.898480e+03     7.979253e-02
 * time: 18.56340718269348
    72    -1.898480e+03     7.979253e-02
 * time: 18.897646188735962
    73    -1.898480e+03     7.979253e-02
 * time: 19.111366033554077
FittedPumasModel

Successful minimization:                      true

Likelihood approximation:                     FOCE
Likelihood Optimizer:                         BFGS
Dynamical system type:                 Closed form

Log-likelihood value:                    1898.4797
Number of subjects:                            120
Number of parameters:         Fixed      Optimized
                                  0             11
Observation records:         Active        Missing
    Conc:                      1320              0
    Total:                     1320              0

-------------------
         Estimate
-------------------
tvcl      2.6192
tvv      11.378
tvvp      8.4522
tvq       1.3163
tvka      4.8925
Ω₁,₁      0.13243
Ω₂,₂      0.059668
Ω₃,₃      0.41582
Ω₄,₄      0.080661
Ω₅,₅      0.25001
σ_p       0.049098
-------------------
compare_estimates(; pkfit_2cmp, pkfit_2cmp_unfix_ka)
11×3 DataFrame
Row parameter pkfit_2cmp pkfit_2cmp_unfix_ka
String Float64? Float64?
1 tvcl 2.81378 2.61918
2 tvv 11.0046 11.3783
3 tvvp 5.53998 8.45221
4 tvq 1.51591 1.31633
5 tvka 2.0 4.89247
6 Ω₁,₁ 0.102669 0.13243
7 Ω₂,₂ 0.0607755 0.0596677
8 Ω₃,₃ 1.20116 0.415822
9 Ω₄,₄ 0.423494 0.0806615
10 Ω₅,₅ 0.244732 0.250008
11 σ_p 0.0484049 0.0490976

Let’s revaluate the goodness of fits and η distribution plots.

Not much change in the general gof plots

res_inspect_2cmp_unfix_ka = inspect(pkfit_2cmp_unfix_ka)
[ Info: Calculating predictions.
[ Info: Calculating weighted residuals.
[ Info: Calculating empirical bayes.
[ Info: Evaluating individual parameters.
[ Info: Evaluating dose control parameters.
[ Info: Done.
FittedPumasModelInspection

Fitting was successful: true
Likehood approximation used for weighted residuals : Pumas.FOCE{Optim.NewtonTrustRegion{Float64}, Optim.Options{Float64, Nothing}}(Optim.NewtonTrustRegion{Float64}(1.0, 100.0, 1.4901161193847656e-8, 0.1, 0.25, 0.75, false), Optim.Options(x_abstol = 0.0, x_reltol = 0.0, f_abstol = 0.0, f_reltol = 0.0, g_abstol = 1.0e-5, g_reltol = 1.0e-8, outer_x_abstol = 0.0, outer_x_reltol = 0.0, outer_f_abstol = 0.0, outer_f_reltol = 0.0, outer_g_abstol = 1.0e-8, outer_g_reltol = 1.0e-8, f_calls_limit = 0, g_calls_limit = 0, h_calls_limit = 0, allow_f_increases = false, allow_outer_f_increases = true, successive_f_tol = 1, iterations = 1000, outer_iterations = 1000, store_trace = false, trace_simplex = false, show_trace = false, extended_trace = false, show_every = 1, time_limit = NaN, )
)
goodness_of_fit(res_inspect_2cmp_unfix_ka; figure = (; fontsize = 12))

A 4-mosaic goodness of fit plot showing the 2-compartment model

Goodness of Fit Plots

But you can see a huge improvement in the ηka, (η₃) distribution which is now centered around zero

empirical_bayes_vs_covariates(
    res_inspect_2cmp_unfix_ka;
    categorical = [:Dose],
    ebes = [:η₃],
    figure = (; resolution = (600, 800)),
)

A histogram for the empirical Bayes distribution of all subject-specific parameters stratified by categorical covariates

Empirical Bayes Distribution Stratified by Covariates

Finally looking at some individual plots for the same subjects as earlier:

fig_subject_fits2 = subject_fits(
    res_inspect_2cmp_unfix_ka;
    separate = true,
    paginate = true,
    facet = (; combinelabels = true, linkyaxes = false),
    figure = (; fontsize = 18),
    axis = (; xlabel = "Time (hr)", ylabel = "CTMx Concentration (ng/mL)"),
)
fig_subject_fits2[6]

Trend plot with observations for 4 individual subjects over time

Subject Fits for 4 Individuals

The randomly sampled individual fits don’t seem good in some individuals, but we can evaluate this via a vpc to see how to go about.

3.4 Visual Predictive Checks (VPC)

We can now perform a vpc to check. The default plots provide a 80% prediction interval and a 95% simulated CI (shaded area) around each of the quantiles

pk_vpc = vpc(
    pkfit_2cmp_unfix_ka,
    200;
    observations = [:Conc],
    stratify_by = [:Dose],
    ensemblealg = EnsembleThreads(), # multi-threading
)
[ Info: Continuous VPC
Visual Predictive Check
  Type of VPC: Continuous VPC
  Simulated populations: 200
  Subjects in data: 40
  Stratification variable(s): [:Dose]
  Confidence level: 0.95
  VPC lines: quantiles ([0.1, 0.5, 0.9])
vpc_plot(
    pk_2cmp,
    pk_vpc;
    rows = 1,
    columns = 3,
    figure = (; resolution = (1400, 1000), fontsize = 22),
    axis = (;
        xlabel = "Time (hr)",
        ylabel = "Observed/Predicted\n CTMx Concentration (ng/mL)",
    ),
    facet = (; combinelabels = true),
)

A visual predictive plot stratified by dose group

Visual Predictive Plots

The visual predictive check suggests that the model captures the data well across all dose levels.

4 Additional Help

If you have questions regarding this tutorial, please post them on our discourse site.