using Pumas,
CairoMakie,
Statistics,
Random,
PumasUtilities,
DeepPumas,
Flux,
Hyperopt,
Dates,
ValueHistories
const rng = Pumas.default_rng()
const seed = 500
Random.seed!(rng, seed)Scientific machine learning
1 Introduction
In this tutorial, two new approaches to fitting PKPD data will be presented, now that neural networks (NN) have been discussed in the previous course material. The first one is a purely data-driven method, namely a recurrent neural network (RNN), a type of NN architecture focused on sequential data. And the second method embeds a NN into the drug dynamics. This mix of scientific knowledge and AI defines the field of Scientific machine learning (SciML), of which the second approach presented here is an example.
As for the topic of the analysis, we will investigate neutropenia, a medical condition characterized by an abnormally low absolute neutrophil count (ANC). Neutrophils are a type of white cell produced in the bone marrow. They circulate through blood flow and fight bacterial and fungal infections. Neutropenia can be triggered as a side effect of cytotoxic anticancer drugs.
The classical pharmacokinetic-pharmacodynamic (PKPD) model describing the drug-ANC dynamics was proposed in (Friberg et al. 2002). However, researchers still debate the interaction between the proliferating (bone marrow) and circulating (blood) compartments. Therefore, (Soto et al. 2011) compares the original model to 5 alternatives proposed in the literature.
1.1 Environment
As usual, we start by setting up the environment with the required packages and definitions.
2 Data engineering
2.1 Data generation
A synthetic population will be used. First, the observation times are defined based on the dosage information. More information about representing dosing can be found in the Data Representation in Pumas tutorial. Afterwards, the population is increased by repeating subjects. And doses are determined by the original subject ID multiplied by 1.1e3. This keeps the dataset size divisible by the number of doses, which will be important for data splitting.
n_unique_subjects = 10
# Observations times
obs_times = vcat(range(0, 72, length = 4), range(120, 600, length = 6))
@show permutedims(obs_times)
@show length(obs_times)
repeated_indices = repeat(1:n_unique_subjects; inner = 10)
# Create population
population = map(enumerate(repeated_indices)) do (subject_ID, dose_base)
Subject(;
id = string(subject_ID),
events = DosageRegimen(dose_base * 1.1e3; time = 0.0, cmt = 1, evid = 1),
time = obs_times,
)
end;permutedims(obs_times) = [0.0 24.0 48.0 72.0 120.0 216.0 312.0 408.0 504.0 600.0]
length(obs_times) = 10
Now let’s define the alternative Friberg model. As discussed in the introduction, the drug-ANC interaction is a complex component in the scientific NLME model, being thus a great target for SciML. So, among the alternative Friberg models discussed in (Soto et al. 2011), the one labeled as 4A was chosen here to generate the synthetic data to be used for the remainder of the tutorial. As its defining characteristic, the growth rate of proliferating cells is described by a self-renewal mechanism that is affected by a feedback term, which is inversely proportional to the concentration of circulating neutrophils. According to the authors, the drug inhibited the rate of proliferation as a function of plasma concentration using an inhibitory EMAX model (Soto et al. 2011).
\[ E_{drug} = \frac{Conc}{IC_{50} + Conc} \]
alternative_model = @model begin
@param begin
CIRC0 ∈ RealDomain(init = 5.75)
Rmax ∈ RealDomain(init = 0.0714)
Rmin ∈ RealDomain(init = 0.0234)
CL ∈ RealDomain(init = 4)
VC ∈ RealDomain(init = 70)
VP ∈ RealDomain(init = 50)
Q ∈ RealDomain(init = 4)
KA ∈ RealDomain(init = 1)
ic50 ∈ RealDomain(init = 13.1)
km ∈ RealDomain(init = 1.65)
σ_add ∈ RealDomain(; lower = 1e-6, init = 0.41)
σ_prop ∈ RealDomain(; lower = 1e-6, init = 0.392)
σ_add_pd ∈ RealDomain(; lower = 1e-6, init = 0.46)
end
@pre begin
k_tr = Rmax - (Rmax - Rmin) * CIRC0 / (km + CIRC0)
end
@init begin
Prol = CIRC0
Transit1 = CIRC0
Transit2 = CIRC0
Circ = CIRC0
end
@vars begin
conc := Central / VC
e_drug := conc / (ic50 + conc)
end
@dynamics begin
# @Central1Periph1
Depot' = -KA * Depot
Central' = KA * Depot - (CL + Q) / VC * Central + Q / VP * Peripheral
Peripheral' = Q / VC * Central - Q / VP * Peripheral
Prol' =
(Rmax - (Rmax - Rmin) * Circ / (km + Circ)) * (1 - e_drug) * Prol - k_tr * Prol
Transit1' = k_tr * Prol - k_tr * Transit1
Transit2' = k_tr * Transit1 - k_tr * Transit2
Circ' = k_tr * Transit2 - k_tr * Circ
end
@derived begin
cp := @. Central / VC
pk ~ @. Normal(cp, sqrt(σ_add^2 + (cp * σ_prop)^2))
pd ~ @. Normal(Circ, σ_add_pd)
end
endPumasModel
Parameters: CIRC0, Rmax, Rmin, CL, VC, VP, Q, KA, ic50, km, σ_add, σ_prop, σ_add_pd
Random effects:
Covariates:
Dynamical system variables: Depot, Central, Peripheral, Prol, Transit1, Transit2, Circ
Dynamical system type: Nonlinear ODE
Derived: pk, pd
Observed: pk, pd
The alternative Friberg model is used to simulate PK and PD observations in the population just created.
const references = init_params(alternative_model)
# Simulate from alternative Friberg model
simulated_population =
Subject.(simobs(alternative_model, population, references; rng, obstimes = obs_times))Population
Subjects: 100
Observations: pk, pd
2.2 NLME diagnostic tools
To examine the model, Pumas provides many diagnostic utilities. For certain plots, fitting the model is required, so the next cell fits alternative_model to the data simulated from it. More information on diagnostic plots can be found in the Model Diagnostics and Evaluation in Pumas tutorial.
Note that these diagnostics are all “in-sample”; they say nothing about how well a Pumas model performs when compared with new clinical data not seen in training.
fpm_alternative = fit(
alternative_model,
simulated_population,
references,
NaivePooled();
optim_options = (; show_trace = false),
checkidentification = false,
)One option for diagnostics is to simulate from the fitted model at a finer time resolution with simobs and plot the predicted circulation ANC over time for each subject, color-coded by dose.
# Simulate with higher time resolution
fine_time = 0.0:0.1:obs_times[end]
plot_sims = simobs(
alternative_model,
Subject.(simulated_population),
coef(fpm_alternative);
obstimes = fine_time,
)
# Figure and axis
figure = Figure(; size = (800, 800), fontsize = 20)
axis = Axis(figure[1, 1]; xlabel = "Time", ylabel = "Circulation ANC (PD)")
# Pre-compute one distinct color per subject
subject_colors = cgrad(:oxy, length(simulated_population); categorical = true)
# Per subject, extract information and plot PD trend
(; Rmax, Rmin, km) = coef(fpm_alternative)
for (i, target_sub) in enumerate(plot_sims)
c = subject_colors[i]
circ = target_sub.dynamics.Circ
label = target_sub.subject.events[1].amt |> Int |> string
trend = @. (Rmax - (Rmax - Rmin) * circ / (km + circ))
lines!(axis, fine_time, trend; color = c, label)
end
axislegend(axis, "Doses"; position = :rt, unique = true)
display(figure);