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)"),
)
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, size = (800, 1000)),
    color = "black",
    linewidth = 3,
    axis = (; xlabel = "Time (hr)", ylabel = "CTMX Concentration (μg/mL)"),
)

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)"),

    # Legend options
    legend = (; position = :bottom),
)
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; legend = (; position = :bottom))

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 ~ @. ProportionalNormal(cp, σ_p)
    end

end
Warning: Covariate Dose is not used in the model.
@ Pumas ~/run/_work/PumasTutorials.jl/PumasTutorials.jl/custom_julia_depot/packages/Pumas/GZeMg/src/dsl/model_macro.jl:3399
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, pkpain_noplb, init_params(pk_1cmp))

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
pkparam_np = (; init_params(pk_1cmp)..., Ω = Diagonal(zeros(3)))
pkfit_np = fit(pk_1cmp, pkpain_noplb, pkparam_np, NaivePooled(); constantcoef = (:Ω,))
[ 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.039103031158447266
     1     2.343899e+02     1.747348e+03
 * time: 2.720674991607666
     2     9.696232e+01     1.198088e+03
 * time: 2.7253801822662354
     3    -7.818699e+01     5.538151e+02
 * time: 2.728296995162964
     4    -1.234803e+02     2.462514e+02
 * time: 2.731283187866211
     5    -1.372888e+02     2.067458e+02
 * time: 2.734227180480957
     6    -1.410579e+02     1.162950e+02
 * time: 2.7370991706848145
     7    -1.434754e+02     5.632816e+01
 * time: 2.7400500774383545
     8    -1.453401e+02     7.859270e+01
 * time: 2.7428040504455566
     9    -1.498185e+02     1.455606e+02
 * time: 2.7457361221313477
    10    -1.534371e+02     1.303682e+02
 * time: 2.7485711574554443
    11    -1.563557e+02     5.975474e+01
 * time: 2.751595973968506
    12    -1.575052e+02     9.308611e+00
 * time: 2.7544591426849365
    13    -1.579357e+02     1.234484e+01
 * time: 2.7573649883270264
    14    -1.581874e+02     7.478196e+00
 * time: 2.7603371143341064
    15    -1.582981e+02     2.027162e+00
 * time: 2.7632901668548584
    16    -1.583375e+02     5.578262e+00
 * time: 2.7663002014160156
    17    -1.583556e+02     4.727050e+00
 * time: 2.7693071365356445
    18    -1.583644e+02     2.340173e+00
 * time: 2.772250175476074
    19    -1.583680e+02     7.738100e-01
 * time: 2.7750940322875977
    20    -1.583696e+02     3.300689e-01
 * time: 2.7778031826019287
    21    -1.583704e+02     3.641985e-01
 * time: 2.780508041381836
    22    -1.583707e+02     4.365901e-01
 * time: 2.783233165740967
    23    -1.583709e+02     3.887800e-01
 * time: 2.7858171463012695
    24    -1.583710e+02     2.766977e-01
 * time: 2.788438081741333
    25    -1.583710e+02     1.758029e-01
 * time: 2.791239023208618
    26    -1.583710e+02     1.133947e-01
 * time: 2.793800115585327
    27    -1.583710e+02     7.922544e-02
 * time: 2.7964460849761963
    28    -1.583710e+02     5.954998e-02
 * time: 2.799136161804199
    29    -1.583710e+02     4.157080e-02
 * time: 2.8019790649414062
    30    -1.583710e+02     4.295446e-02
 * time: 2.804753065109253
    31    -1.583710e+02     5.170752e-02
 * time: 2.807497978210449
    32    -1.583710e+02     2.644382e-02
 * time: 2.8117949962615967
    33    -1.583710e+02     4.548987e-03
 * time: 2.815988063812256
    34    -1.583710e+02     2.501806e-02
 * time: 2.8202590942382812
    35    -1.583710e+02     3.763439e-02
 * time: 2.822946071624756
    36    -1.583710e+02     3.206026e-02
 * time: 2.8257150650024414
    37    -1.583710e+02     1.003701e-02
 * time: 2.82863712310791
    38    -1.583710e+02     2.209083e-02
 * time: 2.8313980102539062
    39    -1.583710e+02     4.954121e-03
 * time: 2.8341851234436035
    40    -1.583710e+02     1.609362e-02
 * time: 2.838412046432495
    41    -1.583710e+02     1.579815e-02
 * time: 2.841245174407959
    42    -1.583710e+02     1.014181e-03
 * time: 2.8438470363616943
    43    -1.583710e+02     6.050877e-03
 * time: 2.848172187805176
    44    -1.583710e+02     1.354363e-02
 * time: 2.851065158843994
    45    -1.583710e+02     4.473198e-03
 * time: 2.8539271354675293
    46    -1.583710e+02     4.645873e-03
 * time: 2.8567140102386475
    47    -1.583710e+02     9.827001e-03
 * time: 2.8596880435943604
    48    -1.583710e+02     1.047016e-03
 * time: 2.862632989883423
    49    -1.583710e+02     8.378247e-03
 * time: 2.865457057952881
    50    -1.583710e+02     7.820745e-04
 * time: 2.868380069732666
FittedPumasModel

Dynamical system type:                 Closed form

Number of subjects:                            120

Observation records:         Active        Missing
    Conc:                      1320              0
    Total:                     1320              0

Number of parameters:      Constant      Optimized
                                  1              6

Likelihood approximation:              NaivePooled
Likelihood optimizer:                         BFGS

Termination Reason:                   GradientNorm
Log-likelihood value:                    158.37103

------------------
         Estimate
------------------
  tvcl    3.0054
  tvv    14.089
  tvka   44.227
† Ω₁,₁    0.0
† Ω₂,₂    0.0
† Ω₃,₃    0.0
  σ_p     0.32999
------------------
† indicates constant parameters
coefficients_table(pkfit_np)
7×4 DataFrame
Row Parameter Description Constant Estimate
String SubStrin… Bool Float64
1 tvcl Clearance (L/hr) false 3.005
2 tvv Volume (L) false 14.089
3 tvka Absorption rate constant (h-1) false 44.227
4 Ω₁,₁ ΩCL true 0.0
5 Ω₂,₂ ΩVc true 0.0
6 Ω₃,₃ ΩKa true 0.0
7 σ_p Proportional RUV false 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 = [loglikelihood(pk_1cmp, subj, pkparam, FOCE()) for subj in pkpain_noplb]
# the plot below is using native CairoMakie `hist`
hist(lls; bins = 10, normalization = :none, color = (:black, 0.5))

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::String, nll::Float64}}:
 (id = "148", nll = 16.659658856844782)
 (id = "135", nll = 16.648985190076324)
 (id = "156", nll = 15.9590695566075)
 (id = "159", nll = 15.441218240496484)
 (id = "149", nll = 14.715134644119514)
 (id = "88", nll = 13.09709837464614)
 (id = "16", nll = 12.982280521931417)
 (id = "61", nll = 12.652182902303675)
 (id = "71", nll = 12.500330088085486)
 (id = "59", nll = 12.241510254805224)
 ⋮
 (id = "57", nll = -22.797674232534305)
 (id = "93", nll = -22.836900711478222)
 (id = "12", nll = -23.007742339519236)
 (id = "123", nll = -23.292751843079227)
 (id = "41", nll = -23.425412534960515)
 (id = "99", nll = -23.53521484190102)
 (id = "29", nll = -24.025959868383097)
 (id = "52", nll = -24.164757842493685)
 (id = "24", nll = -25.572092325658446)
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,))
[ 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: 1.5974044799804688e-5
     1    -7.022088e+02     1.707063e+02
 * time: 0.8315911293029785
     2    -7.314067e+02     2.903269e+02
 * time: 1.5859360694885254
     3    -8.520591e+02     2.285888e+02
 * time: 1.685102939605713
     4    -1.120191e+03     3.795410e+02
 * time: 2.3231821060180664
     5    -1.178784e+03     2.323978e+02
 * time: 2.425240993499756
     6    -1.218320e+03     9.699907e+01
 * time: 2.589503049850464
     7    -1.223641e+03     5.862105e+01
 * time: 2.680711030960083
     8    -1.227620e+03     1.831402e+01
 * time: 2.7684690952301025
     9    -1.228381e+03     2.132323e+01
 * time: 2.8830349445343018
    10    -1.230098e+03     2.921228e+01
 * time: 2.9842469692230225
    11    -1.230854e+03     2.029661e+01
 * time: 3.0752429962158203
    12    -1.231116e+03     5.229104e+00
 * time: 3.1943600177764893
    13    -1.231179e+03     1.689231e+00
 * time: 3.274196147918701
    14    -1.231187e+03     1.215378e+00
 * time: 3.351107120513916
    15    -1.231188e+03     2.770380e-01
 * time: 3.4245400428771973
    16    -1.231188e+03     1.636651e-01
 * time: 3.5181760787963867
    17    -1.231188e+03     2.701104e-01
 * time: 3.5829989910125732
    18    -1.231188e+03     3.163369e-01
 * time: 3.6496591567993164
    19    -1.231188e+03     1.505174e-01
 * time: 3.744150161743164
    20    -1.231188e+03     2.484690e-02
 * time: 3.8059210777282715
    21    -1.231188e+03     8.387527e-04
 * time: 3.8611209392547607
FittedPumasModel

Dynamical system type:                 Closed form

Number of subjects:                            120

Observation records:         Active        Missing
    Conc:                      1320              0
    Total:                     1320              0

Number of parameters:      Constant      Optimized
                                  1              6

Likelihood approximation:                     FOCE
Likelihood optimizer:                         BFGS

Termination Reason:                   GradientNorm
Log-likelihood value:                     1231.188

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

Dynamical system type:                 Closed form

Number of subjects:                            120

Observation records:         Active        Missing
    Conc:                      1320              0
    Total:                     1320              0

Number of parameters:      Constant      Optimized
                                  1              6

Likelihood approximation:                     FOCE
Likelihood optimizer:                         BFGS

Termination Reason:                   GradientNorm
Log-likelihood value:                     1231.188

---------------------------------------------------------
         Estimate    SE          95.0% C.I.
---------------------------------------------------------
  tvcl    3.1642     0.08662     [  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.0063502   [  0.036122;  0.061014]
  Ω₃,₃    5.5811     1.2188      [  3.1922  ;  7.97    ]
  σ_p     0.10093    0.0057196   [  0.089718;  0.11214 ]
---------------------------------------------------------
† indicates constant parameters

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 ~ @. ProportionalNormal(cp, σ_p)
    end
end
Warning: Covariate Dose is not used in the model.
@ Pumas ~/run/_work/PumasTutorials.jl/PumasTutorials.jl/custom_julia_depot/packages/Pumas/GZeMg/src/dsl/model_macro.jl:3399
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)..., tvka = 2),
    FOCE();
    constantcoef = (:tvka,),
)
[ 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: 2.3126602172851562e-5
     1    -9.197817e+02     9.927951e+02
 * time: 1.744570016860962
     2    -1.372640e+03     2.054986e+02
 * time: 4.1191160678863525
     3    -1.446326e+03     1.543987e+02
 * time: 4.450785160064697
     4    -1.545570e+03     1.855028e+02
 * time: 4.668321132659912
     5    -1.581449e+03     1.713157e+02
 * time: 4.98983907699585
     6    -1.639433e+03     1.257382e+02
 * time: 5.28623104095459
     7    -1.695964e+03     7.450539e+01
 * time: 5.483325958251953
     8    -1.722243e+03     5.961044e+01
 * time: 5.742650032043457
     9    -1.736883e+03     7.320921e+01
 * time: 5.937930107116699
    10    -1.753547e+03     7.501938e+01
 * time: 6.175778150558472
    11    -1.764053e+03     6.185661e+01
 * time: 6.403334140777588
    12    -1.778991e+03     4.831033e+01
 * time: 6.619326114654541
    13    -1.791492e+03     4.943278e+01
 * time: 6.871178150177002
    14    -1.799847e+03     2.871410e+01
 * time: 7.12728214263916
    15    -1.805374e+03     7.520790e+01
 * time: 7.409208059310913
    16    -1.816260e+03     2.990621e+01
 * time: 7.6557090282440186
    17    -1.818252e+03     2.401915e+01
 * time: 7.884922027587891
    18    -1.822988e+03     2.587225e+01
 * time: 8.11568307876587
    19    -1.824653e+03     1.550517e+01
 * time: 8.34290599822998
    20    -1.826074e+03     1.788927e+01
 * time: 8.571897983551025
    21    -1.826821e+03     1.888389e+01
 * time: 8.797085046768188
    22    -1.827900e+03     1.432840e+01
 * time: 9.022336959838867
    23    -1.828511e+03     9.422040e+00
 * time: 9.254801034927368
    24    -1.828754e+03     5.363445e+00
 * time: 9.490007162094116
    25    -1.828862e+03     4.916168e+00
 * time: 9.716393947601318
    26    -1.829007e+03     4.695750e+00
 * time: 9.944612979888916
    27    -1.829358e+03     1.090244e+01
 * time: 10.17625117301941
    28    -1.829830e+03     1.451320e+01
 * time: 10.411141157150269
    29    -1.830201e+03     1.108695e+01
 * time: 10.647813081741333
    30    -1.830360e+03     2.892317e+00
 * time: 10.889543056488037
    31    -1.830390e+03     1.699266e+00
 * time: 11.115381956100464
    32    -1.830404e+03     1.602222e+00
 * time: 11.337216138839722
    33    -1.830432e+03     2.823465e+00
 * time: 11.561997175216675
    34    -1.830475e+03     4.119061e+00
 * time: 11.79040813446045
    35    -1.830527e+03     5.081976e+00
 * time: 12.046913146972656
    36    -1.830591e+03     2.669398e+00
 * time: 12.282770156860352
    37    -1.830615e+03     3.513273e+00
 * time: 12.513864994049072
    38    -1.830623e+03     2.267944e+00
 * time: 12.743221998214722
    39    -1.830625e+03     1.664224e+00
 * time: 12.961562156677246
    40    -1.830627e+03     9.587510e-01
 * time: 13.171951055526733
    41    -1.830628e+03     9.090960e-01
 * time: 13.389055967330933
    42    -1.830628e+03     3.474532e-01
 * time: 13.594627141952515
    43    -1.830629e+03     4.588056e-01
 * time: 13.801813125610352
    44    -1.830630e+03     6.648434e-01
 * time: 14.011648178100586
    45    -1.830630e+03     4.414323e-01
 * time: 14.22151517868042
    46    -1.830630e+03     7.962199e-02
 * time: 14.42474913597107
    47    -1.830630e+03     7.027626e-02
 * time: 14.64038896560669
    48    -1.830630e+03     2.999926e-02
 * time: 14.864964962005615
    49    -1.830630e+03     2.754764e-03
 * time: 15.046180009841919
    50    -1.830630e+03     2.754816e-03
 * time: 15.277672052383423
    51    -1.830630e+03     2.754867e-03
 * time: 15.511316061019897
    52    -1.830630e+03     2.754919e-03
 * time: 15.743077039718628
    53    -1.830630e+03     2.754970e-03
 * time: 15.995728015899658
    54    -1.830630e+03     2.755022e-03
 * time: 16.227960109710693
    55    -1.830630e+03     2.755073e-03
 * time: 16.45983600616455
    56    -1.830630e+03     2.755124e-03
 * time: 16.690906047821045
    57    -1.830630e+03     2.755129e-03
 * time: 16.950031995773315
    58    -1.830630e+03     2.755135e-03
 * time: 17.189418077468872
    59    -1.830630e+03     2.755140e-03
 * time: 17.428030014038086
    60    -1.830630e+03     2.755145e-03
 * time: 17.69140911102295
    61    -1.830630e+03     2.755145e-03
 * time: 17.942111015319824
    62    -1.830630e+03     2.755146e-03
 * time: 18.189216136932373
    63    -1.830630e+03     2.755146e-03
 * time: 18.458157062530518
    64    -1.830630e+03     2.755147e-03
 * time: 18.70750403404236
    65    -1.830630e+03     2.755147e-03
 * time: 18.983898162841797
    66    -1.830630e+03     2.755148e-03
 * time: 19.256494998931885
    67    -1.830630e+03     2.755149e-03
 * time: 19.508316040039062
    68    -1.830630e+03     2.755149e-03
 * time: 19.781512022018433
    69    -1.830630e+03     2.755150e-03
 * time: 20.030946969985962
    70    -1.830630e+03     2.755150e-03
 * time: 20.292317152023315
    71    -1.830630e+03     2.755150e-03
 * time: 20.5857150554657
    72    -1.830630e+03     2.755150e-03
 * time: 20.847697973251343
FittedPumasModel

Dynamical system type:                 Closed form

Number of subjects:                            120

Observation records:         Active        Missing
    Conc:                      1320              0
    Total:                     1320              0

Number of parameters:      Constant      Optimized
                                  1             10

Likelihood approximation:                     FOCE
Likelihood optimizer:                         BFGS

Termination Reason:              NoObjectiveChange
Log-likelihood value:                    1830.6305

-------------------
         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
-------------------
† indicates constant parameters

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.0607757
6 Ω₃,₃ 5.58107 1.20115
7 σ_p 0.100928 0.0484049
8 tvvp missing 5.53998
9 tvq missing 1.51591
10 Ω₄,₄ missing 0.423494
11 Ω₅,₅ missing 0.244731

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
WARNING: using deprecated binding Distributions.MatrixReshaped in Pumas.
, use Distributions.ReshapedDistribution{2, S, D} where D<:Distributions.Distribution{Distributions.ArrayLikeVariate{1}, S} where S<:Distributions.ValueSupport instead.
20×3 DataFrame
Row Metric pk2cmp pk1cmp
String Any Any
1 Successful true true
2 Estimation Time 20.848 3.861
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 dose control parameters.
[ Info: Evaluating individual parameters.
[ Info: Done.
FittedPumasModelInspection

Likelihood approximation used for weighted residuals: FOCE
res_inspect_2cmp = inspect(pkfit_2cmp)
[ Info: Calculating predictions.
[ Info: Calculating weighted residuals.
[ Info: Calculating empirical bayes.
[ Info: Evaluating dose control parameters.
[ Info: Evaluating individual parameters.
[ Info: Done.
FittedPumasModelInspection

Likelihood approximation used for weighted residuals: FOCE
gof_1cmp = goodness_of_fit(
    res_inspect_1cmp;
    figure = (; fontsize = 12),
    legend = (; position = :bottom),
)

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),
    legend = (; position = :bottom),
)

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,
    figure = (; fontsize = 18),
    axis = (; xlabel = "Time (hr)", ylabel = "CTMx Concentration (ng/mL)"),
)
fig_subject_fits[1]

Trend plot with observations for 9 individual subjects over time

Subject Fits for 9 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 = (; size = (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: 1.9073486328125e-5
     1    -8.682982e+02     1.000199e+03
 * time: 4.323143005371094
     2    -1.381870e+03     5.008081e+02
 * time: 4.609122037887573
     3    -1.551053e+03     6.833490e+02
 * time: 4.89163613319397
     4    -1.680887e+03     1.834586e+02
 * time: 5.252408981323242
     5    -1.726118e+03     8.870274e+01
 * time: 5.481750965118408
     6    -1.761023e+03     1.162036e+02
 * time: 5.732335090637207
     7    -1.786619e+03     1.114552e+02
 * time: 6.041699171066284
     8    -1.863556e+03     9.914305e+01
 * time: 6.287276029586792
     9    -1.882942e+03     5.342676e+01
 * time: 6.560899972915649
    10    -1.888020e+03     2.010181e+01
 * time: 6.840440034866333
    11    -1.889832e+03     1.867263e+01
 * time: 7.105010032653809
    12    -1.891649e+03     1.668512e+01
 * time: 7.363028049468994
    13    -1.892615e+03     1.820701e+01
 * time: 7.626281023025513
    14    -1.893453e+03     1.745195e+01
 * time: 7.883450984954834
    15    -1.894760e+03     1.850174e+01
 * time: 8.143324136734009
    16    -1.895647e+03     1.773939e+01
 * time: 8.401932001113892
    17    -1.896597e+03     1.143462e+01
 * time: 8.665438175201416
    18    -1.897114e+03     9.720097e+00
 * time: 8.926675081253052
    19    -1.897373e+03     6.054321e+00
 * time: 9.185259103775024
    20    -1.897498e+03     3.985954e+00
 * time: 9.441044092178345
    21    -1.897571e+03     4.262464e+00
 * time: 9.698110103607178
    22    -1.897633e+03     4.010234e+00
 * time: 9.977288007736206
    23    -1.897714e+03     4.805375e+00
 * time: 10.236560106277466
    24    -1.897802e+03     3.508706e+00
 * time: 10.507565021514893
    25    -1.897865e+03     3.691475e+00
 * time: 10.761420011520386
    26    -1.897900e+03     2.982721e+00
 * time: 11.006708145141602
    27    -1.897928e+03     2.563790e+00
 * time: 11.254144191741943
    28    -1.897968e+03     3.261485e+00
 * time: 11.502413034439087
    29    -1.898013e+03     3.064689e+00
 * time: 11.75778603553772
    30    -1.898040e+03     1.636525e+00
 * time: 11.99965214729309
    31    -1.898051e+03     1.439997e+00
 * time: 12.265926122665405
    32    -1.898057e+03     1.436504e+00
 * time: 12.541798114776611
    33    -1.898069e+03     1.881528e+00
 * time: 12.82759714126587
    34    -1.898095e+03     3.253164e+00
 * time: 13.076559066772461
    35    -1.898142e+03     4.257941e+00
 * time: 13.324912071228027
    36    -1.898199e+03     3.685241e+00
 * time: 13.581598043441772
    37    -1.898245e+03     2.567364e+00
 * time: 13.848958969116211
    38    -1.898246e+03     2.561569e+00
 * time: 14.231247186660767
    39    -1.898251e+03     2.530909e+00
 * time: 14.58387017250061
    40    -1.898298e+03     2.673535e+00
 * time: 14.857946157455444
    41    -1.898300e+03     2.796024e+00
 * time: 15.170075178146362
    42    -1.898337e+03     3.655512e+00
 * time: 15.512004137039185
    43    -1.898342e+03     3.773931e+00
 * time: 15.926733016967773
    44    -1.898433e+03     4.520984e+00
 * time: 16.285382986068726
    45    -1.898463e+03     3.636788e+00
 * time: 16.55614709854126
    46    -1.898477e+03     2.415944e+00
 * time: 16.830734968185425
    47    -1.898479e+03     1.827052e+00
 * time: 17.09283709526062
    48    -1.898479e+03     5.288379e-01
 * time: 17.407920122146606
    49    -1.898479e+03     4.636056e-01
 * time: 17.75364112854004
    50    -1.898480e+03     1.435316e+00
 * time: 18.024465084075928
    51    -1.898480e+03     2.491286e+00
 * time: 18.333590984344482
    52    -1.898480e+03     1.103596e-01
 * time: 18.665053129196167
    53    -1.898480e+03     4.098989e-02
 * time: 18.89751410484314
    54    -1.898480e+03     4.098989e-02
 * time: 19.22114109992981
    55    -1.898480e+03     4.098989e-02
 * time: 19.603698015213013
    56    -1.898480e+03     4.098989e-02
 * time: 19.96833610534668
    57    -1.898480e+03     5.127712e-03
 * time: 20.225841999053955
    58    -1.898480e+03     5.152593e-03
 * time: 20.458981037139893
    59    -1.898480e+03     5.152815e-03
 * time: 20.742069959640503
    60    -1.898480e+03     5.153036e-03
 * time: 21.001517057418823
    61    -1.898480e+03     5.153059e-03
 * time: 21.26834797859192
    62    -1.898480e+03     5.153081e-03
 * time: 21.54200315475464
    63    -1.898480e+03     5.153103e-03
 * time: 21.82597303390503
    64    -1.898480e+03     5.153125e-03
 * time: 22.15657901763916
    65    -1.898480e+03     5.153147e-03
 * time: 22.433865070343018
    66    -1.898480e+03     5.153169e-03
 * time: 22.70787215232849
    67    -1.898480e+03     5.153192e-03
 * time: 22.980709075927734
    68    -1.898480e+03     5.153194e-03
 * time: 23.29235315322876
    69    -1.898480e+03     5.153196e-03
 * time: 23.577898025512695
    70    -1.898480e+03     5.153198e-03
 * time: 23.86114501953125
    71    -1.898480e+03     5.153200e-03
 * time: 24.17035698890686
    72    -1.898480e+03     5.153203e-03
 * time: 24.45496702194214
    73    -1.898480e+03     5.153205e-03
 * time: 24.73299217224121
    74    -1.898480e+03     5.153207e-03
 * time: 25.012012004852295
    75    -1.898480e+03     5.153209e-03
 * time: 25.326637983322144
    76    -1.898480e+03     5.153212e-03
 * time: 25.619683027267456
    77    -1.898480e+03     5.153212e-03
 * time: 25.92441701889038
    78    -1.898480e+03     5.153212e-03
 * time: 26.277173042297363
    79    -1.898480e+03     5.153212e-03
 * time: 26.594150066375732
    80    -1.898480e+03     5.153212e-03
 * time: 26.93980312347412
    81    -1.898480e+03     5.153212e-03
 * time: 27.31635618209839
FittedPumasModel

Dynamical system type:                 Closed form

Number of subjects:                            120

Observation records:         Active        Missing
    Conc:                      1320              0
    Total:                     1320              0

Number of parameters:      Constant      Optimized
                                  0             11

Likelihood approximation:                     FOCE
Likelihood optimizer:                         BFGS

Termination Reason:              NoObjectiveChange
Log-likelihood value:                    1898.4797

-----------------
       Estimate
-----------------
tvcl    2.6191
tvv    11.378
tvvp    8.453
tvq     1.3164
tvka    4.8925
Ω₁,₁    0.13243
Ω₂,₂    0.059669
Ω₃,₃    0.41581
Ω₄,₄    0.080678
Ω₅,₅    0.24996
σ_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.61911
2 tvv 11.0046 11.3783
3 tvvp 5.53998 8.45298
4 tvq 1.51591 1.31636
5 tvka 2.0 4.89251
6 Ω₁,₁ 0.102669 0.132433
7 Ω₂,₂ 0.0607757 0.0596692
8 Ω₃,₃ 1.20115 0.415807
9 Ω₄,₄ 0.423494 0.080678
10 Ω₅,₅ 0.244731 0.249962
11 σ_p 0.0484049 0.0490975

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 dose control parameters.
[ Info: Evaluating individual parameters.
[ Info: Done.
FittedPumasModelInspection

Likelihood approximation used for weighted residuals: FOCE
goodness_of_fit(
    res_inspect_2cmp_unfix_ka;
    figure = (; fontsize = 12),
    legend = (; position = :bottom),
)

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 = (; size = (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 = (; linkyaxes = false),
    figure = (; fontsize = 18),
    axis = (; xlabel = "Time (hr)", ylabel = "CTMx Concentration (ng/mL)"),
)
fig_subject_fits2[6]

Trend plot with observations for 9 individual subjects over time

Subject Fits for 9 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])
[ Info: Continuous VPC
Visual Predictive Check
  Type of VPC: Continuous VPC
  Simulated populations: 200
  Subjects in data: 120
  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 = (; size = (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.