Chapter 4: Covariate Model Building

MI-210 Essentials of Population PKPD M&S — Pumas Edition

Author

Adapted from Marc R. Gastonguay, Ph.D. (Metrum Institute)

Published

April 18, 2026

1 Overview

Covariate model building identifies measurable patient factors that explain inter-individual variability in PK/PD parameters:

  • Objectives of covariate model development
  • Common covariate model parameterizations
  • Data reduction before covariate building
  • Covariate screening methods (traditional and automated)
  • Inferences about covariate effects

2 Objectives of Covariate Model Development

  • Identify factors that explain variability in PK/PD parameters
  • Reduce unexplained inter-individual variability
  • Improve model predictions for individual patients
  • Support dose adjustment recommendations for special populations
Why Covariates Reduce Inter-Individual Variability

When a covariate genuinely explains inter-individual variability, incorporating it into the model splits the single population parameter into covariate-defined subgroups, each with its own typical value. The random effect \(\eta_i\) must then only account for the residual difference between the individual’s true parameter and the subgroup-specific population prediction, rather than the overall population mean. This forces \(\eta\) to do less work, producing a tighter distribution and a smaller \(\omega\).

Intuitive example: Suppose males have true CL \(\approx\) 10 L/h and females have true CL \(\approx\) 8 L/h. A model without a sex covariate estimates \(\text{tvCL} \approx 9\) L/h with a large \(\omega_{CL}\) that absorbs the male–female difference. A model that includes sex estimates \(\text{tvCL}_\text{male} = 10\) and \(\text{tvCL}_\text{female} = 8\), and \(\omega_{CL}\) shrinks because within each group the \(\eta\) values cluster more tightly around zero. The individual clearances have not changed — \(\eta\) simply shifts to accommodate the new, more precise population prediction.

3 Common Covariate Parameterizations

3.1 Continuous Covariates

Power model (centered on reference value):

\[TVCL = \theta_1 \cdot \left(\frac{CLCR}{80}\right)^{\theta_3}\]

Linear model:

\[TVCL = \theta_1 + \theta_3 \cdot (CLCR - 80)\]

Physiological Basis of Common Covariate-Parameter Relationships

Body weight on \(V\): The apparent volume of distribution is \(V = V_P + \sum_i K_{P,i} \cdot V_{T,i}\), where \(V_{T,i}\) are tissue volumes and \(K_{P,i}\) are tissue-to-plasma partition coefficients. A larger body has greater tissue mass and total body water, so \(V\) scales with body size. Allometric scaling formalizes this: \(V \propto \text{BW}^{1.0}\) and \(CL \propto \text{BW}^{0.75}\), reflecting biological scaling laws derived from fractal geometry and cross-species metabolic rate observations.

Creatinine clearance on \(CL\): For renally eliminated drugs, clearance is proportional to glomerular filtration rate (GFR). Filtration clearance is \(CL_F = f_u \cdot \text{GFR}\), where only unbound drug is filtered. The Cockcroft-Gault equation estimates \(\text{CLcr}\) as a surrogate for GFR, making it a natural covariate for renal clearance.

Hepatic function on \(CL\): For hepatically cleared drugs, the well-stirred model predicts \(CL_H = \frac{Q_H \cdot f_u \cdot CL_{\text{int}}}{Q_H + f_u \cdot CL_{\text{int}}}\), where \(Q_H\) is hepatic blood flow, \(f_u\) is fraction unbound, and \(CL_{\text{int}}\) is intrinsic clearance. Intrinsic clearance depends on hepatic enzyme activity (CYP genotype, enzyme induction/inhibition from DDIs), so covariates reflecting hepatic function or metabolizer status directly modulate \(CL\).

3.2 Categorical Covariates

Proportional shift:

\[TVCL = \theta_1 \cdot (1 + \theta_3 \cdot SEX)\]

Separate typical values:

\[TVCL = \theta_1 \cdot (1 - SEX) + \theta_2 \cdot SEX\]

3.3 Desirable Properties

  • Centering continuous covariates on a reference value (e.g., median)
  • Power model ensures positivity of parameters
  • Interpretable covariate effects (e.g., fold-change per unit)

4 Data and Base Model

cov_df = CSV.read(joinpath(data_dir, "poppk_wcovs.csv"), DataFrame)
rename!(cov_df,
    :ID => :id, :DV => :dv, :AMT => :amt, :TIME => :time,
    :II => :ii, :ADDL => :addl, :RATE => :rate,
)

cov_df.evid = ifelse.(cov_df.amt .> 0, 1, 0)
cov_df.cmt = fill(1, nrow(cov_df))
cov_df.dv = ifelse.(cov_df.evid .== 1, missing, cov_df.dv)

pop = read_pumas(cov_df; covariates = [:HT, :WT, :CLCR, :SEX, :AGE])
println("Subjects: ", length(pop))
Subjects: 40

4.1 EBE-Covariate Screening Plots

η-Shrinkage Limits EBE-Based Covariate Screening

When \(\eta\)-shrinkage exceeds approximately 30%, individual empirical Bayes estimates (EBEs) regress toward zero and lose their ability to reflect true inter-individual differences. This has two consequences for covariate screening: (1) genuine covariate–parameter relationships may be obscured because the \(\hat{\eta}\) values have collapsed toward the population mean, and (2) spurious patterns may emerge from the shrinkage structure itself rather than from biology. A flat EBE-vs-covariate plot under high shrinkage may reflect information poverty rather than the absence of a real relationship. Always compute and report \(\eta\)-shrinkage before interpreting EBE-covariate diagnostic plots; when shrinkage is substantial, consider alternative screening approaches such as simulation-based diagnostics.

Before formal covariate modeling, screen ETA estimates vs. covariates:

# Base model: 1-cpt IV, BLOCK(2) OMEGA, proportional error
# From 501.ctl
base_model = @model begin
    @metadata begin
        desc = "Base PopPK Model (no covariates)"
    end
    @param begin
        tvCL   ∈ RealDomain(lower = 0.0, init = 13.0)
        tvV    ∈ RealDomain(lower = 0.0, init = 75.0)
        Ω      ∈ PSDDomain(2)
        σ_prop ∈ RealDomain(lower = 0.0, init = 0.2)
    end
    @random begin
        η ~ MvNormal(Ω)
    end
    @covariates WT CLCR SEX AGE HT
    @pre begin
        CL = tvCL * exp(η[1])
        Vc = tvV * exp(η[2])
    end
    @dynamics Central1
    @derived begin
        cp := @. Central / Vc
        dv ~ @. Normal(cp, abs(cp) * σ_prop)
    end
end
┌ Warning: Covariates WT, CLCR, SEX, AGE and HT are not used in the model.
└ @ Pumas ~/.julia/packages/Pumas/GZeMg/src/dsl/model_macro.jl:3399
PumasModel
  Parameters: tvCL, tvV, Ω, σ_prop
  Random effects: η
  Covariates: WT, CLCR, SEX, AGE, HT
  Dynamical system variables: Central
  Dynamical system type: Closed form
  Derived: dv
  Observed: dv
base_params = (
    tvCL = 13.0, tvV = 75.0,
    Ω = [0.04 0.02; 0.02 0.04],
    σ_prop = 0.2,
)
base_fit = fit(base_model, pop, base_params, Pumas.FOCEI())
base_fit
[ 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.329348e+02     2.137954e+02
 * time: 0.012467145919799805
     1     7.311461e+02     1.247906e+02
 * time: 0.9712700843811035
     2     7.307777e+02     1.862206e+02
 * time: 1.0094261169433594
     3     7.116558e+02     5.785995e+01
 * time: 1.0139260292053223
     4     7.089067e+02     1.076801e+02
 * time: 1.0181691646575928
     5     7.073020e+02     8.481440e+01
 * time: 1.0223591327667236
     6     7.044493e+02     5.135452e+01
 * time: 1.0264620780944824
     7     7.004105e+02     9.413825e+00
 * time: 1.0909039974212646
     8     6.997037e+02     6.836002e+00
 * time: 1.0949652194976807
     9     6.994446e+02     5.826943e+00
 * time: 1.0987060070037842
    10     6.993923e+02     8.408957e-01
 * time: 1.102431058883667
    11     6.993907e+02     3.426738e-01
 * time: 1.1056702136993408
    12     6.993900e+02     1.631341e-01
 * time: 1.1091980934143066
    13     6.993897e+02     1.129596e-01
 * time: 1.1124200820922852
    14     6.993896e+02     3.428446e-02
 * time: 1.503749132156372
    15     6.993896e+02     4.000807e-03
 * time: 1.506422996520996
    16     6.993896e+02     5.317351e-04
 * time: 1.5086700916290283
FittedPumasModel

Dynamical system type:                 Closed form

Number of subjects:                             40

Observation records:         Active        Missing
    dv:                         560              0
    Total:                      560              0

Number of parameters:      Constant      Optimized
                                  0              6

Likelihood approximation:                     FOCE
Likelihood optimizer:                         BFGS

Termination Reason:                   GradientNorm
Log-likelihood value:                   -699.38964

-------------------
         Estimate
-------------------
tvCL     11.846
tvV      85.0
Ω₁,₁      0.057303
Ω₂,₁      0.010948
Ω₂,₂      0.061453
σ_prop    0.21302
-------------------
base_insp = inspect(base_fit)
base_df = DataFrame(base_insp)
# Get one row per subject (first obs row)
subj_df = combine(groupby(@rsubset(base_df, !ismissing(:dv)), :id), first)

fig = Figure(size = (900, 600))

ax1 = Axis(fig[1, 1], xlabel = "CLCR", ylabel = "ETA(CL)")
scatter!(ax1, subj_df.CLCR, subj_df.η₁, color = :navy, markersize = 6)
hlines!(ax1, 0, color = :black)

ax2 = Axis(fig[1, 2], xlabel = "WT", ylabel = "ETA(V)")
scatter!(ax2, subj_df.WT, subj_df.η₂, color = :navy, markersize = 6)
hlines!(ax2, 0, color = :black)

ax3 = Axis(fig[1, 3], xlabel = "AGE", ylabel = "ETA(CL)")
scatter!(ax3, subj_df.AGE, subj_df.η₁, color = :navy, markersize = 6)
hlines!(ax3, 0, color = :black)

ax4 = Axis(fig[2, 1], xlabel = "AGE", ylabel = "ETA(V)")
scatter!(ax4, subj_df.AGE, subj_df.η₂, color = :navy, markersize = 6)
hlines!(ax4, 0, color = :black)

ax5 = Axis(fig[2, 2], xlabel = "SEX", ylabel = "ETA(CL)")
scatter!(ax5, subj_df.SEX, subj_df.η₁, color = :navy, markersize = 6)
hlines!(ax5, 0, color = :black)

ax6 = Axis(fig[2, 3], xlabel = "SEX", ylabel = "ETA(V)")
scatter!(ax6, subj_df.SEX, subj_df.η₂, color = :navy, markersize = 6)
hlines!(ax6, 0, color = :black)

fig
[ Info: Calculating predictions.
[ Info: Calculating weighted residuals.
[ Info: Calculating empirical bayes.
[ Info: Evaluating dose control parameters.
[ Info: Evaluating individual parameters.
[ Info: Done.
Figure 1: EBE vs Covariate Screening Plots

5 Full Covariate Model

From 510.ctl — power model parameterization with CLCR and AGE on CL, WT and AGE on V:

\[TVCL = \theta_1 \cdot \left(\frac{CLCR}{80}\right)^{\theta_3} \cdot \left(\frac{AGE}{45}\right)^{\theta_4}\]

\[TVV = \theta_2 \cdot \left(\frac{WT}{70}\right)^{\theta_5} \cdot \left(\frac{AGE}{45}\right)^{\theta_6}\]

full_cov_model = @model begin
    @metadata begin
        desc = "Full Covariate Model (power parameterization)"
    end
    @param begin
        tvCL      ∈ RealDomain(lower = 0.0, init = 13.0)
        tvV       ∈ RealDomain(lower = 0.0, init = 75.0)
        dCL_CLCR  ∈ RealDomain(init = 0.5)      # THETA(3): CL~CLCR power
        dCL_AGE   ∈ RealDomain(init = -0.3)      # THETA(4): CL~AGE power
        dV_WT     ∈ RealDomain(init = 0.5)        # THETA(5): V~WT power
        dV_AGE    ∈ RealDomain(init = 0.1)         # THETA(6): V~AGE power
        Ω         ∈ PSDDomain(2)
        σ_prop    ∈ RealDomain(lower = 0.0, init = 0.2)
    end
    @random begin
        η ~ MvNormal(Ω)
    end
    @covariates WT CLCR AGE SEX HT
    @pre begin
        TVCL = tvCL * (CLCR / 80)^dCL_CLCR * (AGE / 45)^dCL_AGE
        CL   = TVCL * exp(η[1])
        TVV  = tvV * (WT / 70)^dV_WT * (AGE / 45)^dV_AGE
        Vc   = TVV * exp(η[2])
    end
    @dynamics Central1
    @derived begin
        cp := @. Central / Vc
        dv ~ @. Normal(cp, abs(cp) * σ_prop)
    end
end
┌ Warning: Covariates SEX and HT are not used in the model.
└ @ Pumas ~/.julia/packages/Pumas/GZeMg/src/dsl/model_macro.jl:3399
PumasModel
  Parameters: tvCL, tvV, dCL_CLCR, dCL_AGE, dV_WT, dV_AGE, Ω, σ_prop
  Random effects: η
  Covariates: WT, CLCR, AGE, SEX, HT
  Dynamical system variables: Central
  Dynamical system type: Closed form
  Derived: dv
  Observed: dv
full_params = (
    tvCL = 13.0, tvV = 75.0,
    dCL_CLCR = 0.5, dCL_AGE = -0.3,
    dV_WT = 0.5, dV_AGE = 0.1,
    Ω = [0.04 0.02; 0.02 0.04],
    σ_prop = 0.2,
)
full_fit = fit(full_cov_model, pop, full_params, Pumas.FOCEI())
full_fit
[ 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.343467e+02     2.109806e+02
 * time: 2.3126602172851562e-5
     1     7.305659e+02     1.085502e+02
 * time: 0.26302218437194824
     2     7.205771e+02     1.374909e+02
 * time: 0.47431302070617676
     3     7.052267e+02     5.024212e+01
 * time: 0.48009514808654785
     4     7.038430e+02     1.009489e+02
 * time: 0.5226230621337891
     5     7.007879e+02     5.859850e+01
 * time: 0.5270190238952637
     6     6.998238e+02     4.166342e+01
 * time: 0.531620979309082
     7     6.963667e+02     1.073305e+01
 * time: 0.5356230735778809
     8     6.957381e+02     1.217252e+01
 * time: 0.5398869514465332
     9     6.954567e+02     4.410505e+00
 * time: 0.5589900016784668
    10     6.952999e+02     2.106663e+00
 * time: 0.5629520416259766
    11     6.952374e+02     2.678294e+00
 * time: 0.5666561126708984
    12     6.951089e+02     2.496807e+00
 * time: 0.570357084274292
    13     6.949554e+02     3.026525e+00
 * time: 0.5740900039672852
    14     6.946986e+02     3.345164e+00
 * time: 0.581773042678833
    15     6.944501e+02     3.343745e+00
 * time: 0.5855240821838379
    16     6.942799e+02     3.183658e+00
 * time: 0.5891799926757812
    17     6.941981e+02     2.412127e+00
 * time: 0.592799186706543
    18     6.941469e+02     1.591244e+00
 * time: 0.5993170738220215
    19     6.941207e+02     9.072599e-01
 * time: 0.6028611660003662
    20     6.941138e+02     5.684698e-01
 * time: 0.6062710285186768
    21     6.941127e+02     3.146168e-01
 * time: 0.6095640659332275
    22     6.941125e+02     1.474094e-01
 * time: 0.6152021884918213
    23     6.941123e+02     5.936196e-02
 * time: 0.6183240413665771
    24     6.941123e+02     3.644993e-02
 * time: 0.6212880611419678
    25     6.941123e+02     1.119091e-02
 * time: 0.6266829967498779
    26     6.941123e+02     9.393166e-03
 * time: 0.6295230388641357
    27     6.941123e+02     1.301718e-02
 * time: 0.6324219703674316
    28     6.941123e+02     3.038450e-02
 * time: 0.6376190185546875
    29     6.941123e+02     4.816357e-02
 * time: 0.6406090259552002
    30     6.941122e+02     4.946959e-02
 * time: 0.6434280872344971
    31     6.941122e+02     2.676456e-02
 * time: 0.6464600563049316
    32     6.941122e+02     5.802006e-03
 * time: 0.6518580913543701
    33     6.941122e+02     2.525897e-04
 * time: 0.6545531749725342
FittedPumasModel

Dynamical system type:                 Closed form

Number of subjects:                             40

Observation records:         Active        Missing
    dv:                         560              0
    Total:                      560              0

Number of parameters:      Constant      Optimized
                                  0             10

Likelihood approximation:                     FOCE
Likelihood optimizer:                         BFGS

Termination Reason:                   GradientNorm
Log-likelihood value:                   -694.11224

---------------------
           Estimate
---------------------
tvCL       12.462
tvV        91.672
dCL_CLCR    0.21568
dCL_AGE     0.55065
dV_WT       0.40743
dV_AGE     -0.45537
Ω₁,₁        0.049235
Ω₂,₁        0.003531
Ω₂,₂        0.050735
σ_prop      0.21309
---------------------
full_infer = infer(full_fit)
full_infer
[ Info: Calculating: variance-covariance matrix.
[ Info: Done.
Asymptotic inference results using sandwich estimator

Dynamical system type:                 Closed form

Number of subjects:                             40

Observation records:         Active        Missing
    dv:                         560              0
    Total:                      560              0

Number of parameters:      Constant      Optimized
                                  0             10

Likelihood approximation:                     FOCE
Likelihood optimizer:                         BFGS

Termination Reason:                   GradientNorm
Log-likelihood value:                   -694.11224

------------------------------------------------------------
           Estimate    SE          95.0% C.I.
------------------------------------------------------------
tvCL       12.462      0.51434     [ 11.454   ;  13.47    ]
tvV        91.672      4.4503      [ 82.949   ; 100.39    ]
dCL_CLCR    0.21568    0.092126    [  0.035111;   0.39624 ]
dCL_AGE     0.55065    0.49402     [ -0.41761 ;   1.5189  ]
dV_WT       0.40743    0.15347     [  0.10663 ;   0.70824 ]
dV_AGE     -0.45537    0.73577     [ -1.8975  ;   0.9867  ]
Ω₁,₁        0.049235   0.011635    [  0.026431;   0.072038]
Ω₂,₁        0.003531   0.0070028   [ -0.010194;   0.017256]
Ω₂,₂        0.050735   0.012472    [  0.026289;   0.07518 ]
σ_prop      0.21309    0.0093414   [  0.19478 ;   0.2314  ]
------------------------------------------------------------

6 Model Comparison

println("Covariate Model Comparison")
println("=" ^ 50)
println("Base Model    AIC: ", round(aic(base_fit), digits = 1))
println("Full Cov      AIC: ", round(aic(full_fit), digits = 1))

delta_ofv = -2 * loglikelihood(base_fit) - (-2 * loglikelihood(full_fit))
println("\nDelta OFV: ", round(delta_ofv, digits = 2), " (4 df)")
println("Chi-sq critical (p=0.05, 4df): 9.49")
println("Chi-sq critical (p=0.005, 4df): 14.86")
Covariate Model Comparison
==================================================
Base Model    AIC: 1410.8
Full Cov      AIC: 1408.2

Delta OFV: 10.55 (4 df)
Chi-sq critical (p=0.05, 4df): 9.49
Chi-sq critical (p=0.005, 4df): 14.86

7 Covariate Modeling Methods in Pumas

7.1 Automated Covariate Selection

Pumas provides covariate_select() for automated forward addition / backward elimination. The key design principle: define all covariate effects in the model upfront, then use control_param to specify which parameters “turn on/off” each effect. Setting a control parameter to zero removes its covariate effect.

7.1.1 Forward Selection

Starts with base model (all covariate effects zeroed out), iteratively adds the single most impactful covariate:

# Step 1: Define model with ALL candidate covariate effects pre-coded
full_cov_model = @model begin
    @param begin
        tvCL     ∈ RealDomain(lower = 0.0, init = 4.0)
        tvV      ∈ RealDomain(lower = 0.0, init = 70.0)
        dWTCL    ∈ RealDomain(init = 0.0)     # WT effect on CL (power)
        dWTV     ∈ RealDomain(init = 0.0)     # WT effect on V (power)
        dCLCR_CL ∈ RealDomain(init = 0.0)     # CLCR effect on CL (power)
        dAGE_CL  ∈ RealDomain(init = 0.0)     # AGE effect on CL (power)
        dSEX_CL  ∈ RealDomain(init = 0.0)     # SEX effect on CL (proportional shift)
        Ω        ∈ PSDDomain(2)
        σ_prop   ∈ RealDomain(lower = 0.0, init = 0.2)
    end
    @random begin
        η ~ MvNormal(Ω)
    end
    @covariates WT CLCR AGE SEX
    @pre begin
        CL = tvCL * (WT / 70)^dWTCL * (CLCR / 80)^dCLCR_CL *
             (AGE / 45)^dAGE_CL * (1 + dSEX_CL * (SEX == 1)) * exp(η[1])
        Vc = tvV * (WT / 70)^dWTV * exp(η[2])
    end
    @dynamics Central1
    @derived begin
        cp := @. Central / Vc
        dv ~ @. Normal(cp, abs(cp) * σ_prop)
    end
end

# Step 2: Forward selection — control_param lists the covariate effect parameters
covar_result = covariate_select(
    full_cov_model, pop, init_params, Pumas.FOCEI();
    control_param = (:dWTCL, :dWTV, :dCLCR_CL, :dAGE_CL, :dSEX_CL),
    method = CovariateSelection.Forward,
    criterion = aic,
)

# Access results
covar_result.best_model    # final selected model
covar_result.fits          # all intermediate fits

7.1.2 Backward Elimination

Starts with full model (all effects active), iteratively removes the least impactful:

covar_result_be = covariate_select(
    full_cov_model, pop, full_params, Pumas.FOCEI();
    control_param = (:dWTCL, :dWTV, :dCLCR_CL, :dAGE_CL, :dSEX_CL),
    method = CovariateSelection.Backward,
    criterion = bic,   # BIC penalizes complexity more than AIC
)
Covariate Selection Methodologies: SCM, GAM, and FFM

Stepwise Covariate Modeling (SCM): The most widely used approach, consisting of forward addition followed by backward elimination within the full nonlinear mixed-effects model. Forward steps add covariates using a liberal threshold (e.g., \(p < 0.05\)); backward steps remove covariates using a stricter threshold (e.g., \(p < 0.001\)). Each step constitutes a hypothesis test, raising concerns about alpha spending from multiple comparisons — in practice, corrections such as Bonferroni are rarely applied, which is a known limitation.

Generalized Additive Modeling (GAM): A non-parametric, EBE-based screening method that operates outside the PK model. Individual \(\hat{\eta}\) values are regressed against candidate covariates using simple linear models, and the best-fitting covariate is selected iteratively. GAM is computationally efficient but inherits the shrinkage bias of EBEs: when \(\eta\)-shrinkage is substantial, GAM results may be unreliable.

Full Fixed-Effects Model (FFM): All biologically plausible covariates are included simultaneously from the outset, and no stepwise hypothesis testing is performed. Covariate importance is judged by the clinical significance of effect magnitudes (e.g., whether a covariate shifts exposure by \(\geq\) 20%) rather than by \(p\)-values. FFM avoids the multiple-testing problem inherent in SCM but requires careful prior identification and de-correlation of candidate covariates using domain knowledge.

7.1.3 AIC vs BIC vs LRT

Criterion Formula Favors
AIC \(-2LL + 2k\) Predictive accuracy; may overfit with large n
BIC \(-2LL + k \cdot \log(n)\) Parsimony; stronger penalty for large datasets
LRT \(\Delta OFV \sim \chi^2(\Delta df)\) Nested model comparison only; requires p-value cutoff
  • Forward selection: typically use AIC (p ≈ 0.05 equivalent) or LRT with α = 0.05
  • Backward elimination: typically use BIC or LRT with stricter α = 0.01 (or 0.001)
  • AIC and BIC work for both nested and non-nested models
Tip

NONMEM vs Pumas Covariate Workflow:

  • NONMEM: Manual control stream editing + SCM in PsN
  • Pumas: covariate_select() automates forward/backward selection, or use manual AIC/LRT comparison as shown above

7.2 Time-Varying Covariates

Covariates that change during the study (e.g., weight, creatinine clearance, concomitant medications) require special handling:

# In the dataset: include covariate values at each observation/event time
# Pumas interpolates between recorded values using piece-wise constant interpolation:
#   :left  → LOCF (Last Observation Carried Forward) — Pumas default
#   :right → NOCB (Next Observation Carried Backward) — NONMEM default

pop = read_pumas(df; covariates = [:WT, :CLCR], covariates_direction = :left)

# The covariate is automatically available as a time-varying quantity in @pre
Warning

NONMEM defaults to NOCB (right-continuous interpolation), while Pumas defaults to LOCF (left-continuous). When comparing results between the two, explicitly set covariates_direction = :right in Pumas for NONMEM equivalence.

7.3 Missing Covariates in Population Models

Strategy When to Use Pumas Implementation
Median imputation Standard approach for sporadic missingness @rtransform :WT = coalesce(:WT, 70.0) before read_pumas
Group-wise imputation When covariate differs by subgroup groupby(:SEX) → @transform :WT = coalesce.(:WT, median(skipmissing(:WT)))
Complete case When missingness is minimal dropmissing(df, [:WT, :CLCR])
Flag + sensitivity When imputation impact is uncertain Add :WT_IMPUTED column, run with/without imputed subjects

8 GOF Diagnostics: Full Model

full_insp = inspect(full_fit)
full_df = DataFrame(full_insp)
obs = @rsubset(full_df, !ismissing(:dv))

fig = Figure(size = (800, 600))

ax1 = Axis(fig[1, 1], xlabel = "PRED", ylabel = "DV", title = "DV vs PRED")
scatter!(ax1, obs.dv_pred, obs.dv, color = :navy, markersize = 4, alpha = 0.5)
ablines!(ax1, 0, 1, color = :black)

ax2 = Axis(fig[1, 2], xlabel = "IPRED", ylabel = "DV", title = "DV vs IPRED")
scatter!(ax2, obs.dv_ipred, obs.dv, color = :navy, markersize = 4, alpha = 0.5)
ablines!(ax2, 0, 1, color = :black)

ax3 = Axis(fig[2, 1], xlabel = "PRED", ylabel = "CWRES", title = "CWRES vs PRED")
scatter!(ax3, obs.dv_pred, obs.dv_wres, color = :navy, markersize = 4, alpha = 0.5)
hlines!(ax3, 0, color = :black)

ax4 = Axis(fig[2, 2], xlabel = "Time", ylabel = "CWRES", title = "CWRES vs TIME")
scatter!(ax4, obs.time, obs.dv_wres, color = :navy, markersize = 4, alpha = 0.5)
hlines!(ax4, 0, color = :black)

fig
[ Info: Calculating predictions.
[ Info: Calculating weighted residuals.
[ Info: Calculating empirical bayes.
[ Info: Evaluating dose control parameters.
[ Info: Evaluating individual parameters.
[ Info: Done.
Figure 2: Full Covariate Model GOF Diagnostics

9 Study Guide Questions

  1. What are desirable properties of covariate-parameter models?
  2. How does centering a continuous covariate on its median help interpretation?
  3. What is the difference between forward addition and backward elimination?
  4. When might a statistically significant covariate NOT be clinically relevant?
  5. What are the limitations of using EBE-based covariate screening?
  6. How would you calculate the initial estimate for OMEGA assuming 30% CV with exponential IIV?

10 Supplementary Material

Topic Tutorial Key Additions
Lecture: Covariate Fundamentals PKPD W7.5 Covariate types, functional forms, parameterization strategies
Lecture: Covariate Diagnostics & Methodology PKPD W8 EBE screening, η-shrinkage, SCM/GAM/FFM comparison
Lecture: Volume of Distribution PK W4 Why body weight affects V — tissue mass, body water, protein binding
Lecture: Renal Elimination PK W7b Why CLcr predicts CL — GFR, filtration, secretion, reabsorption
Covariate model introduction Covariates Allometric scaling, hepatic/renal CL breakdown, time-varying covariates, categorical effects
AIC/BIC/LRT theory Covariate Selection Intro Mathematical derivations, penalty comparison, custom OFV functions
Forward selection workflow Forward Selection covariate_select() API, control_param patterns, custom criteria
Backward elimination workflow Backward Elimination Full backward workflow, BIC criterion, result interpretation
Mixed stepwise (SCM) Mixed Selection Combined forward + backward in one pipeline
Cloud batch covariate search Batch Job Running covariate_select on JuliaHub for large populations