using DataFramesMeta
using PharmaDatasets
using NCA
using Unitful
using Random
Random.seed!(123)
Introduction to NCA
Load the necessary libraries
1 Introduction
This tutorial will introduce the workflow of performing Non-Compartmental Analysis (NCA) in a command-line based workflow. A graphical user interface for NCA is also available for a more interactive experience. Please contact sales@pumas.ai for more information.
For the purpose of this tutorial, we will be using the following dataset:
Blood samples for pharmacokinetics were collected in 107 subjects who received 50 mg of DrugX q24 hours for 5 days, on Day 1 (first dose) and Day 5 (last dose at steady-state). We will analyze the profile of the first dose and the profile of the last dose, which was given at steady state.
In the given dataset
- Time = hrs
- Conc = mg/L
- Amt = mg
1.0.1 Load Data
Load the tutorial dataset using the dataset function from PharmaDatasets.
data = dataset(joinpath("nca", "NCA_tutorial"))
first(data, 5)| Row | ID | TIME | CONC | AMT | ROUTE | II |
|---|---|---|---|---|---|---|
| Int64 | Float64 | Float64 | Int64 | String3 | Int64 | |
| 1 | 2 | 0.0 | 0.0 | 50 | ev | 0 |
| 2 | 2 | 0.25 | 0.35 | 0 | ev | 0 |
| 3 | 2 | 0.5 | 0.33 | 0 | ev | 0 |
| 4 | 2 | 0.75 | 0.36 | 0 | ev | 0 |
| 5 | 2 | 1.0 | 0.37 | 0 | ev | 0 |
The dose amount and the dosing interval are given on the dosing row of each profile and are 0 on the observation rows. The reader expects missing on rows without a dose, so we convert the zeros first.
zero_to_missing(x) = x == 0 ? missing : x
data = @rtransform data :AMT = zero_to_missing(:AMT) :II = zero_to_missing(:II)1.0.2 The first-dose profile
Samples on Day 1 belong to the first dose, so we filter data for time <= 24 hrs
single_dose_data = @rsubset data :TIME <= 24We will specify the units of time, concentration and amount which will be used in the analysis.
timeu = u"hr"
concu = u"mg/L"
amtu = u"mg"Next, we map the columns in the dataset to the required arguments of the reading function. Our dataset has one dose row per profile and missing in the dose columns on all other rows, which is the layout that read_nca_pumas expects. Data in which every row of a profile repeats the same dose is read with read_nca instead.
- id
- concentration
- time
- tau - if the profile was measured at steady state
- dose
- route - Infusion “ivinfusion”, IV “ivbolus”, Oral “ev”
- duration - only for Infusion
- group - any other grouping variables that you require in the output
If your dataset has a specific style of header for a column then you will not need to map that column separately. Otherwise, columns are automatically determined by Pumas. eg: id, time, concentration, dose, route, duration(if needed), group(if needed).
Do not specify the dosing interval for these profiles, they were not measured at steady state.
pop = read_nca_pumas(
single_dose_data;
id = :ID,
time = :TIME,
concentration = :CONC,
dose = :AMT,
route = :ROUTE,
time_unit = timeu,
concentration_unit = concu,
dose_unit = amtu,
)| Row | id | profile | time | nominal_time | dose_time | nominal_dose_time | concentration | dose | route | duration | exclusion | exclusion_mask |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Int64 | Int64 | Quantity… | Quantity… | Quantity… | Quantity… | Quantity… | Quantity… | String | Missing | Missing | Bool | |
| 1 | 2 | 1 | 0.0 hr | 0.0 hr | 0.0 hr | 0.0 hr | 0.0 mg L^-1 | 50.0 mg | ev | missing | missing | false |
| 2 | 2 | 1 | 0.25 hr | 0.25 hr | 0.0 hr | 0.0 hr | 0.35 mg L^-1 | 50.0 mg | ev | missing | missing | false |
| 3 | 2 | 1 | 0.5 hr | 0.5 hr | 0.0 hr | 0.0 hr | 0.33 mg L^-1 | 50.0 mg | ev | missing | missing | false |
| 4 | 2 | 1 | 0.75 hr | 0.75 hr | 0.0 hr | 0.0 hr | 0.36 mg L^-1 | 50.0 mg | ev | missing | missing | false |
| 5 | 2 | 1 | 1.0 hr | 1.0 hr | 0.0 hr | 0.0 hr | 0.37 mg L^-1 | 50.0 mg | ev | missing | missing | false |
| 6 | 2 | 1 | 1.5 hr | 1.5 hr | 0.0 hr | 0.0 hr | 0.33 mg L^-1 | 50.0 mg | ev | missing | missing | false |
| 7 | 2 | 1 | 2.0 hr | 2.0 hr | 0.0 hr | 0.0 hr | 0.31 mg L^-1 | 50.0 mg | ev | missing | missing | false |
| 8 | 2 | 1 | 2.5 hr | 2.5 hr | 0.0 hr | 0.0 hr | 0.3 mg L^-1 | 50.0 mg | ev | missing | missing | false |
| 9 | 2 | 1 | 3.0 hr | 3.0 hr | 0.0 hr | 0.0 hr | 0.29 mg L^-1 | 50.0 mg | ev | missing | missing | false |
| 10 | 2 | 1 | 4.0 hr | 4.0 hr | 0.0 hr | 0.0 hr | 0.24 mg L^-1 | 50.0 mg | ev | missing | missing | false |
| 11 | 2 | 1 | 6.0 hr | 6.0 hr | 0.0 hr | 0.0 hr | 0.18 mg L^-1 | 50.0 mg | ev | missing | missing | false |
| 12 | 2 | 1 | 8.0 hr | 8.0 hr | 0.0 hr | 0.0 hr | 0.12 mg L^-1 | 50.0 mg | ev | missing | missing | false |
| 13 | 2 | 1 | 12.0 hr | 12.0 hr | 0.0 hr | 0.0 hr | 0.06 mg L^-1 | 50.0 mg | ev | missing | missing | false |
| ⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ |
| 1701 | 200 | 107 | 1.0 hr | 1.0 hr | 0.0 hr | 0.0 hr | 0.36 mg L^-1 | 50.0 mg | ev | missing | missing | false |
| 1702 | 200 | 107 | 1.5 hr | 1.5 hr | 0.0 hr | 0.0 hr | 0.3 mg L^-1 | 50.0 mg | ev | missing | missing | false |
| 1703 | 200 | 107 | 2.0 hr | 2.0 hr | 0.0 hr | 0.0 hr | 0.31 mg L^-1 | 50.0 mg | ev | missing | missing | false |
| 1704 | 200 | 107 | 2.5 hr | 2.5 hr | 0.0 hr | 0.0 hr | 0.29 mg L^-1 | 50.0 mg | ev | missing | missing | false |
| 1705 | 200 | 107 | 3.0 hr | 3.0 hr | 0.0 hr | 0.0 hr | 0.27 mg L^-1 | 50.0 mg | ev | missing | missing | false |
| 1706 | 200 | 107 | 4.0 hr | 4.0 hr | 0.0 hr | 0.0 hr | 0.23 mg L^-1 | 50.0 mg | ev | missing | missing | false |
| 1707 | 200 | 107 | 6.0 hr | 6.0 hr | 0.0 hr | 0.0 hr | 0.16 mg L^-1 | 50.0 mg | ev | missing | missing | false |
| 1708 | 200 | 107 | 8.0 hr | 8.0 hr | 0.0 hr | 0.0 hr | 0.08 mg L^-1 | 50.0 mg | ev | missing | missing | false |
| 1709 | 200 | 107 | 12.0 hr | 12.0 hr | 0.0 hr | 0.0 hr | 0.04 mg L^-1 | 50.0 mg | ev | missing | missing | false |
| 1710 | 200 | 107 | 16.0 hr | 16.0 hr | 0.0 hr | 0.0 hr | 0.02 mg L^-1 | 50.0 mg | ev | missing | missing | false |
| 1711 | 200 | 107 | 20.0 hr | 20.0 hr | 0.0 hr | 0.0 hr | 0.01 mg L^-1 | 50.0 mg | ev | missing | missing | false |
| 1712 | 200 | 107 | 24.0 hr | 24.0 hr | 0.0 hr | 0.0 hr | 0.0 mg L^-1 | 50.0 mg | ev | missing | missing | false |
Observations below the limit of quantification are handled after reading, with transform_nca:
pop = transform_nca(
pop;
llq = 0.001concu,
blq_rules = Dict(:first => :keep, :middle => :drop, :last => :keep),
)| Row | id | profile | time | nominal_time | dose_time | nominal_dose_time | concentration_before_blq | concentration | dose | route | duration | exclusion | exclusion_mask | blq |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Int64 | Int64 | Quantity… | Quantity… | Quantity… | Quantity… | Quantity… | Quantity… | Quantity… | String | Missing | Missing | Bool | Bool | |
| 1 | 2 | 1 | 0.0 hr | 0.0 hr | 0.0 hr | 0.0 hr | 0.0 mg L^-1 | 0.0 mg L^-1 | 50.0 mg | ev | missing | missing | false | true |
| 2 | 2 | 1 | 0.25 hr | 0.25 hr | 0.0 hr | 0.0 hr | 0.35 mg L^-1 | 0.35 mg L^-1 | 50.0 mg | ev | missing | missing | false | false |
| 3 | 2 | 1 | 0.5 hr | 0.5 hr | 0.0 hr | 0.0 hr | 0.33 mg L^-1 | 0.33 mg L^-1 | 50.0 mg | ev | missing | missing | false | false |
| 4 | 2 | 1 | 0.75 hr | 0.75 hr | 0.0 hr | 0.0 hr | 0.36 mg L^-1 | 0.36 mg L^-1 | 50.0 mg | ev | missing | missing | false | false |
| 5 | 2 | 1 | 1.0 hr | 1.0 hr | 0.0 hr | 0.0 hr | 0.37 mg L^-1 | 0.37 mg L^-1 | 50.0 mg | ev | missing | missing | false | false |
| 6 | 2 | 1 | 1.5 hr | 1.5 hr | 0.0 hr | 0.0 hr | 0.33 mg L^-1 | 0.33 mg L^-1 | 50.0 mg | ev | missing | missing | false | false |
| 7 | 2 | 1 | 2.0 hr | 2.0 hr | 0.0 hr | 0.0 hr | 0.31 mg L^-1 | 0.31 mg L^-1 | 50.0 mg | ev | missing | missing | false | false |
| 8 | 2 | 1 | 2.5 hr | 2.5 hr | 0.0 hr | 0.0 hr | 0.3 mg L^-1 | 0.3 mg L^-1 | 50.0 mg | ev | missing | missing | false | false |
| 9 | 2 | 1 | 3.0 hr | 3.0 hr | 0.0 hr | 0.0 hr | 0.29 mg L^-1 | 0.29 mg L^-1 | 50.0 mg | ev | missing | missing | false | false |
| 10 | 2 | 1 | 4.0 hr | 4.0 hr | 0.0 hr | 0.0 hr | 0.24 mg L^-1 | 0.24 mg L^-1 | 50.0 mg | ev | missing | missing | false | false |
| 11 | 2 | 1 | 6.0 hr | 6.0 hr | 0.0 hr | 0.0 hr | 0.18 mg L^-1 | 0.18 mg L^-1 | 50.0 mg | ev | missing | missing | false | false |
| 12 | 2 | 1 | 8.0 hr | 8.0 hr | 0.0 hr | 0.0 hr | 0.12 mg L^-1 | 0.12 mg L^-1 | 50.0 mg | ev | missing | missing | false | false |
| 13 | 2 | 1 | 12.0 hr | 12.0 hr | 0.0 hr | 0.0 hr | 0.06 mg L^-1 | 0.06 mg L^-1 | 50.0 mg | ev | missing | missing | false | false |
| ⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ |
| 1701 | 200 | 107 | 1.0 hr | 1.0 hr | 0.0 hr | 0.0 hr | 0.36 mg L^-1 | 0.36 mg L^-1 | 50.0 mg | ev | missing | missing | false | false |
| 1702 | 200 | 107 | 1.5 hr | 1.5 hr | 0.0 hr | 0.0 hr | 0.3 mg L^-1 | 0.3 mg L^-1 | 50.0 mg | ev | missing | missing | false | false |
| 1703 | 200 | 107 | 2.0 hr | 2.0 hr | 0.0 hr | 0.0 hr | 0.31 mg L^-1 | 0.31 mg L^-1 | 50.0 mg | ev | missing | missing | false | false |
| 1704 | 200 | 107 | 2.5 hr | 2.5 hr | 0.0 hr | 0.0 hr | 0.29 mg L^-1 | 0.29 mg L^-1 | 50.0 mg | ev | missing | missing | false | false |
| 1705 | 200 | 107 | 3.0 hr | 3.0 hr | 0.0 hr | 0.0 hr | 0.27 mg L^-1 | 0.27 mg L^-1 | 50.0 mg | ev | missing | missing | false | false |
| 1706 | 200 | 107 | 4.0 hr | 4.0 hr | 0.0 hr | 0.0 hr | 0.23 mg L^-1 | 0.23 mg L^-1 | 50.0 mg | ev | missing | missing | false | false |
| 1707 | 200 | 107 | 6.0 hr | 6.0 hr | 0.0 hr | 0.0 hr | 0.16 mg L^-1 | 0.16 mg L^-1 | 50.0 mg | ev | missing | missing | false | false |
| 1708 | 200 | 107 | 8.0 hr | 8.0 hr | 0.0 hr | 0.0 hr | 0.08 mg L^-1 | 0.08 mg L^-1 | 50.0 mg | ev | missing | missing | false | false |
| 1709 | 200 | 107 | 12.0 hr | 12.0 hr | 0.0 hr | 0.0 hr | 0.04 mg L^-1 | 0.04 mg L^-1 | 50.0 mg | ev | missing | missing | false | false |
| 1710 | 200 | 107 | 16.0 hr | 16.0 hr | 0.0 hr | 0.0 hr | 0.02 mg L^-1 | 0.02 mg L^-1 | 50.0 mg | ev | missing | missing | false | false |
| 1711 | 200 | 107 | 20.0 hr | 20.0 hr | 0.0 hr | 0.0 hr | 0.01 mg L^-1 | 0.01 mg L^-1 | 50.0 mg | ev | missing | missing | false | false |
| 1712 | 200 | 107 | 24.0 hr | 24.0 hr | 0.0 hr | 0.0 hr | 0.0 mg L^-1 | 0.0 mg L^-1 | 50.0 mg | ev | missing | missing | false | true |
1.0.3 Exploratory Analysis
We start by different ways of visualizing the concentration time profiles. Depending on how you are performing your analysis, plots can be either interactive or static. A detailed documentation on NCA related plotting is available here. There are two main plots that can be used:
observations_vs_timesummary_observations_vs_time
If you are working in VSCode, you could enjoy the interactive experience by calling interactive. Let’s look at the observations vs time plots for one subject, which is selected with the ids keyword argument.
ids = unique(pop.id)
observations_vs_time(pop; ids = [ids[1]])you can switch the axis to log scale by passing axis = (; yscale = log10) as a keyword argument (or use log instead of log10 for natural log). The pre-dose sample of this study was reported as a concentration of zero, which a logarithmic axis cannot show, and a single such value drops the whole profile from the plot. We therefore replace the zeros with missing, which leaves a gap in the curve instead of joining the samples on either side of it.
observations_vs_time(
@rtransform(pop, :concentration = iszero(:concentration) ? missing : :concentration);
ids = [ids[1]],
axis = (; yscale = log10),
)However, that can become cumbersome for many subjects. Here we show how to view many subjects at once in the non-interactive mode. Let’s plot the concentration vs time for a few subject picked randomly.
rand_subjs = rand(ids, 9)
ctplots = observations_vs_time(
pop;
ids = rand_subjs,
axis = (; xlabel = "Time hr", ylabel = "DrugX Concentration mg/L"),
paginate = true,
)
ctplots[1]You can also visualize the mean concentration vs time plot by using
summary_observations_vs_time(
pop;
axis = (; xlabel = "Time hr", ylabel = "DrugX Concentration mg/L"),
)1.0.4 NCA analysis
The workhorse function that performs the analysis is run_nca. It takes the data returned by the reader and gives back a DataFrame with one row per profile. Without a list of endpoints, all endpoints that apply to the data are computed; pass a vector of endpoint names as the second argument to restrict the output.
report_sd = run_nca(pop)You can view the individual subject fits in an interactive way, but for the tutorial we will showcase the non-interactive way:
individual_fits = subject_fits(
pop;
axis = (;
xlabel = "Time hr",
ylabel = "DrugX Concentration mg/L",
yscale = log10,
width = 180,
height = 180,
),
paginate = true,
limit = 16,
)Giving the axes a fixed width and height sizes the figure to its content, which keeps the per-subject subtitles from overlapping.
The above code generates a vector of plots with 16 subjects each (4 x 4). To view each set of plots, you can access via the vector index e.g.
individual_fits[1]1.0.4.1 Create Parameters Summaries
params = [:cmax, :aucinf_obs]2-element Vector{Symbol}:
:cmax
:aucinf_obs
param_summary_sd = summarize(report_sd; parameters = params)| Row | parameters | numsamples | nonmissing | minimum | maximum | mean | std | geomean | geostd | geomeanCV |
|---|---|---|---|---|---|---|---|---|---|---|
| String | Int64 | Int64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | |
| 1 | cmax | 107 | 107 | 0.34 | 0.77 | 0.515888 | 0.0956897 | 0.507222 | 1.20349 | 18.6827 |
| 2 | aucinf_obs | 107 | 107 | 1.6089 | 4.17295 | 2.61075 | 0.461858 | 2.56963 | 1.19822 | 18.2322 |
One can use some of the plotting functions to visualize the distribution of the parameters.
parameters_dist(report_sd; parameter = :aucinf_obs)1.0.4.2 Compute Specific Parameters
We will now compute the specific parameters that we require and merge all of them to a dataframe. Each parameter is requested from run_nca by name.
Volume of Distribution/F, in this case since the drug is given orally
vz = run_nca(pop, [:vz_f_obs])
first(vz, 5)| Row | id | profile | dose | route | duration | tau | assumed_tau | vz_f_obs |
|---|---|---|---|---|---|---|---|---|
| Int64 | Int64 | Quantity… | String | Missing | Missing | Missing | Quantity… | |
| 1 | 2 | 1 | 50.0 mg | ev | missing | missing | missing | 116.994 L |
| 2 | 3 | 2 | 50.0 mg | ev | missing | missing | missing | 90.694 L |
| 3 | 10 | 3 | 50.0 mg | ev | missing | missing | missing | 86.099 L |
| 4 | 14 | 4 | 50.0 mg | ev | missing | missing | missing | 95.2837 L |
| 5 | 15 | 5 | 50.0 mg | ev | missing | missing | missing | 102.629 L |
Clearance/F, in this case since the drug is given orally
cl = run_nca(pop, [:cl_f_obs])
first(cl, 5)| Row | id | profile | dose | route | duration | tau | assumed_tau | cl_f_obs |
|---|---|---|---|---|---|---|---|---|
| Int64 | Int64 | Quantity… | String | Missing | Missing | Missing | Quantity… | |
| 1 | 2 | 1 | 50.0 mg | ev | missing | missing | missing | 18.5931 L hr^-1 |
| 2 | 3 | 2 | 50.0 mg | ev | missing | missing | missing | 15.7161 L hr^-1 |
| 3 | 10 | 3 | 50.0 mg | ev | missing | missing | missing | 17.6125 L hr^-1 |
| 4 | 14 | 4 | 50.0 mg | ev | missing | missing | missing | 12.9591 L hr^-1 |
| 5 | 15 | 5 | 50.0 mg | ev | missing | missing | missing | 15.4735 L hr^-1 |
Terminal Elimination Rate Constant, threshold=3 specifies the max no. of time point used for calculation
lambdaz = run_nca(pop, [:lambdaz]; threshold = 3)
first(lambdaz, 5)| Row | id | profile | dose | route | duration | tau | assumed_tau | lambdaz |
|---|---|---|---|---|---|---|---|---|
| Int64 | Int64 | Quantity… | String | Missing | Missing | Missing | Quantity… | |
| 1 | 2 | 1 | 50.0 mg | ev | missing | missing | missing | 0.137327 hr^-1 |
| 2 | 3 | 2 | 50.0 mg | ev | missing | missing | missing | 0.173287 hr^-1 |
| 3 | 10 | 3 | 50.0 mg | ev | missing | missing | missing | 0.22397 hr^-1 |
| 4 | 14 | 4 | 50.0 mg | ev | missing | missing | missing | 0.156595 hr^-1 |
| 5 | 15 | 5 | 50.0 mg | ev | missing | missing | missing | 0.20118 hr^-1 |
The exact time points that the slope is calculated from are selected with a boolean lambdaz_mask column added to the data before it is read.
single_dose_data_slopetimes =
@rtransform single_dose_data :lambdaz_mask = :TIME in (8, 12, 16)
pop_slopetimes = read_nca_pumas(
single_dose_data_slopetimes;
id = :ID,
time = :TIME,
concentration = :CONC,
dose = :AMT,
route = :ROUTE,
time_unit = timeu,
concentration_unit = concu,
dose_unit = amtu,
)
lambdaz_1 = run_nca(pop_slopetimes, [:lambdaz => :lambdaz_specf])
first(lambdaz_1, 5)| Row | id | profile | dose | route | duration | tau | assumed_tau | lambdaz_specf | lambdaz_specf_err |
|---|---|---|---|---|---|---|---|---|---|
| Int64 | Int64 | Quantity… | String | Missing | Missing | Missing | Quantity…? | String? | |
| 1 | 2 | 1 | 50.0 mg | ev | missing | missing | missing | 0.173287 hr^-1 | missing |
| 2 | 3 | 2 | 50.0 mg | ev | missing | missing | missing | 0.22397 hr^-1 | missing |
| 3 | 10 | 3 | 50.0 mg | ev | missing | missing | missing | 0.20118 hr^-1 | missing |
| 4 | 14 | 4 | 50.0 mg | ev | missing | missing | missing | 0.103335 hr^-1 | missing |
| 5 | 15 | 5 | 50.0 mg | ev | missing | missing | missing | 0.128702 hr^-1 | missing |
Half-life calculation for the 4th individual
thalf = run_nca(pop, [:half_life])
@rsubset thalf :id == ids[4]| Row | id | profile | dose | route | duration | tau | assumed_tau | half_life |
|---|---|---|---|---|---|---|---|---|
| Int64 | Int64 | Quantity… | String | Missing | Missing | Missing | Quantity… | |
| 1 | 14 | 4 | 50.0 mg | ev | missing | missing | missing | 5.09648 hr |
Dose Normalized Cmax
cmax_d = run_nca(pop, [:cmax_dn])
first(cmax_d, 5)| Row | id | profile | dose | route | duration | tau | assumed_tau | cmax_dn |
|---|---|---|---|---|---|---|---|---|
| Int64 | Int64 | Quantity… | String | Missing | Missing | Missing | Quantity… | |
| 1 | 2 | 1 | 50.0 mg | ev | missing | missing | missing | 0.0074 L^-1 |
| 2 | 3 | 2 | 50.0 mg | ev | missing | missing | missing | 0.0146 L^-1 |
| 3 | 10 | 3 | 50.0 mg | ev | missing | missing | missing | 0.013 L^-1 |
| 4 | 14 | 4 | 50.0 mg | ev | missing | missing | missing | 0.01 L^-1 |
| 5 | 15 | 5 | 50.0 mg | ev | missing | missing | missing | 0.0096 L^-1 |
Mean residence time
mrt = run_nca(pop, [:mrtinf_obs])
first(mrt, 5)| Row | id | profile | dose | route | duration | tau | assumed_tau | mrtinf_obs |
|---|---|---|---|---|---|---|---|---|
| Int64 | Int64 | Quantity… | String | Missing | Missing | Missing | Quantity… | |
| 1 | 2 | 1 | 50.0 mg | ev | missing | missing | missing | 6.36645 hr |
| 2 | 3 | 2 | 50.0 mg | ev | missing | missing | missing | 4.53536 hr |
| 3 | 10 | 3 | 50.0 mg | ev | missing | missing | missing | 4.98956 hr |
| 4 | 14 | 4 | 50.0 mg | ev | missing | missing | missing | 7.53794 hr |
| 5 | 15 | 5 | 50.0 mg | ev | missing | missing | missing | 6.69361 hr |
AUMC calculation, using :linlog method
aumc = run_nca(pop, [:aumcinf_obs]; method = :linlog)
first(aumc, 5)| Row | id | profile | dose | route | duration | tau | assumed_tau | aumcinf_obs |
|---|---|---|---|---|---|---|---|---|
| Int64 | Int64 | Quantity… | String | Missing | Missing | Missing | Quantity… | |
| 1 | 2 | 1 | 50.0 mg | ev | missing | missing | missing | 17.1523 mg hr^2 L^-1 |
| 2 | 3 | 2 | 50.0 mg | ev | missing | missing | missing | 14.3832 mg hr^2 L^-1 |
| 3 | 10 | 3 | 50.0 mg | ev | missing | missing | missing | 14.1224 mg hr^2 L^-1 |
| 4 | 14 | 4 | 50.0 mg | ev | missing | missing | missing | 29.1743 mg hr^2 L^-1 |
| 5 | 15 | 5 | 50.0 mg | ev | missing | missing | missing | 21.6259 mg hr^2 L^-1 |
Every result carries the same dose metadata columns, so we select each one down to its endpoints before joining them on the profile identifiers.
endpoints_only(df) =
select(df, :id, :profile, Not([:dose, :route, :duration, :tau, :assumed_tau]))
individual_params = innerjoin(
endpoints_only.([vz, cl, lambdaz, lambdaz_1, cmax_d, mrt, aumc])...;
on = [:id, :profile],
)
first(individual_params, 5)| Row | id | profile | vz_f_obs | cl_f_obs | lambdaz | lambdaz_specf | lambdaz_specf_err | cmax_dn | mrtinf_obs | aumcinf_obs |
|---|---|---|---|---|---|---|---|---|---|---|
| Int64 | Int64 | Quantity… | Quantity… | Quantity… | Quantity…? | String? | Quantity… | Quantity… | Quantity… | |
| 1 | 2 | 1 | 116.994 L | 18.5931 L hr^-1 | 0.137327 hr^-1 | 0.173287 hr^-1 | missing | 0.0074 L^-1 | 6.36645 hr | 17.1523 mg hr^2 L^-1 |
| 2 | 3 | 2 | 90.694 L | 15.7161 L hr^-1 | 0.173287 hr^-1 | 0.22397 hr^-1 | missing | 0.0146 L^-1 | 4.53536 hr | 14.3832 mg hr^2 L^-1 |
| 3 | 10 | 3 | 86.099 L | 17.6125 L hr^-1 | 0.22397 hr^-1 | 0.20118 hr^-1 | missing | 0.013 L^-1 | 4.98956 hr | 14.1224 mg hr^2 L^-1 |
| 4 | 14 | 4 | 95.2837 L | 12.9591 L hr^-1 | 0.156595 hr^-1 | 0.103335 hr^-1 | missing | 0.01 L^-1 | 7.53794 hr | 29.1743 mg hr^2 L^-1 |
| 5 | 15 | 5 | 102.629 L | 15.4735 L hr^-1 | 0.20118 hr^-1 | 0.128702 hr^-1 | missing | 0.0096 L^-1 | 6.69361 hr | 21.6259 mg hr^2 L^-1 |
1.0.4.3 Calculate AUC at specific intervals
Calculation of AUC at specific time intervals is done with the auc_partial endpoint, where each interval gets its own output name:
# various other methods are :linear, :linlog
partial_aucs = run_nca(
pop,
[
:auc_partial =>
(; interval = (0timeu, 12timeu), method = :linuplogdown) => :auc0_12,
:auc_partial =>
(; interval = (12timeu, 24timeu), method = :linuplogdown) => :auc12_24,
],
)
first(partial_aucs, 5)| Row | id | profile | dose | route | duration | tau | assumed_tau | auc0_12 | auc12_24 |
|---|---|---|---|---|---|---|---|---|---|
| Int64 | Int64 | Quantity… | String | Missing | Missing | Missing | Quantity… | Quantity… | |
| 1 | 2 | 1 | 50.0 mg | ev | missing | missing | missing | 2.2645 mg hr L^-1 | 0.329483 mg hr L^-1 |
| 2 | 3 | 2 | 50.0 mg | ev | missing | missing | missing | 2.89846 mg hr L^-1 | 0.193123 mg hr L^-1 |
| 3 | 10 | 3 | 50.0 mg | ev | missing | missing | missing | 2.54614 mg hr L^-1 | 0.223346 mg hr L^-1 |
| 4 | 14 | 4 | 50.0 mg | ev | missing | missing | missing | 3.00976 mg hr L^-1 | 0.666289 mg hr L^-1 |
| 5 | 15 | 5 | 50.0 mg | ev | missing | missing | missing | 2.6432 mg hr L^-1 | 0.484746 mg hr L^-1 |
final = innerjoin(report_sd, endpoints_only(partial_aucs); on = [:id, :profile])
first(final, 5)| Row | id | profile | dose | route | duration | tau | assumed_tau | auc_extrap_percent_obs | auc_extrap_percent_pred | aucinf_dn_obs | aucinf_dn_pred | aucinf_obs | aucinf_pred | auclast | auclast_dn | aumc_extrap_percent_obs | aumc_extrap_percent_pred | aumcinf_obs | aumcinf_pred | aumclast | cl_f_obs | cl_f_pred | clast | clast_pred | cmax | cmax_dn | cmin | cmin_dn | half_life | lambdaz | lambdazadjr2 | lambdazintercept | lambdaznpoints | lambdazr2 | lambdaztimefirst | lambdaztimelast | mrtinf_obs | mrtinf_pred | mrtlast | n_samples | span | tfinal | tlag | tlast | tmax | tmin | vz_f_obs | vz_f_pred | auc0_12 | auc12_24 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Int64 | Int64 | Quantity… | String | Missing | Missing | Missing | Float64 | Float64 | Quantity… | Quantity… | Quantity… | Quantity… | Quantity… | Quantity… | Float64 | Float64 | Quantity… | Quantity… | Quantity… | Quantity… | Quantity… | Quantity… | Quantity… | Quantity… | Quantity… | Quantity… | Quantity… | Quantity… | Quantity… | Float64 | Float64 | Int64 | Float64 | Quantity… | Quantity… | Quantity… | Quantity… | Quantity… | Int64 | Float64 | Quantity… | Quantity… | Quantity… | Quantity… | Quantity… | Quantity… | Quantity… | Quantity… | Quantity… | |
| 1 | 2 | 1 | 50.0 mg | ev | missing | missing | missing | 2.33989 | 2.26095 | 0.0537835 hr L^-1 | 0.05374 hr L^-1 | 2.68917 mg hr L^-1 | 2.687 mg hr L^-1 | 2.62625 mg hr L^-1 | 0.052525 hr L^-1 | 11.1335 | 10.7907 | 17.1205 mg hr^2 L^-1 | 17.0547 mg hr^2 L^-1 | 15.2144 mg hr^2 L^-1 | 18.5931 L hr^-1 | 18.6081 L hr^-1 | 0.01 mg L^-1 | 0.00965482 mg L^-1 | 0.37 mg L^-1 | 0.0074 L^-1 | 0.0 mg L^-1 | 0.0 L^-1 | 4.36154 hr | 0.158923 hr^-1 | 0.996589 | -0.826153 | 11 | 0.99693 | 1.5 hr | 24.0 hr | 6.36645 hr | 6.34711 hr | 5.79319 hr | 16 | 5.15873 | 24.0 hr | 0.0 hr | 24.0 hr | 1.0 hr | 0.0 hr | 116.994 L | 117.089 L | 2.2645 mg hr L^-1 | 0.329483 mg hr L^-1 |
| 2 | 3 | 2 | 50.0 mg | ev | missing | missing | missing | 1.81388 | 1.81388 | 0.0636292 hr L^-1 | 0.0636292 hr L^-1 | 3.18146 mg hr L^-1 | 3.18146 mg hr L^-1 | 3.12375 mg hr L^-1 | 0.062475 hr L^-1 | 10.3068 | 10.3068 | 14.4291 mg hr^2 L^-1 | 14.4291 mg hr^2 L^-1 | 12.9419 mg hr^2 L^-1 | 15.7161 L hr^-1 | 15.7161 L hr^-1 | 0.01 mg L^-1 | 0.01 mg L^-1 | 0.73 mg L^-1 | 0.0146 L^-1 | 0.0 mg L^-1 | 0.0 L^-1 | 4.0 hr | 0.173287 hr^-1 | 1.0 | -1.13943 | 3 | 1.0 | 12.0 hr | 20.0 hr | 4.53536 hr | 4.53536 hr | 4.14306 hr | 16 | 2.0 | 24.0 hr | 0.0 hr | 20.0 hr | 1.0 hr | 0.0 hr | 90.694 L | 90.694 L | 2.89846 mg hr L^-1 | 0.193123 mg hr L^-1 |
| 3 | 10 | 3 | 50.0 mg | ev | missing | missing | missing | 1.72198 | 1.69357 | 0.0567777 hr L^-1 | 0.0567613 hr L^-1 | 2.83889 mg hr L^-1 | 2.83806 mg hr L^-1 | 2.79 mg hr L^-1 | 0.0558 hr L^-1 | 8.58943 | 8.45747 | 14.1648 mg hr^2 L^-1 | 14.1444 mg hr^2 L^-1 | 12.9481 mg hr^2 L^-1 | 17.6125 L hr^-1 | 17.6176 L hr^-1 | 0.01 mg L^-1 | 0.00983218 mg L^-1 | 0.65 mg L^-1 | 0.013 L^-1 | 0.0 mg L^-1 | 0.0 L^-1 | 3.38845 hr | 0.204562 hr^-1 | 0.993176 | -0.530861 | 8 | 0.994151 | 2.5 hr | 20.0 hr | 4.98956 hr | 4.98381 hr | 4.64091 hr | 16 | 5.1646 | 24.0 hr | 0.0 hr | 20.0 hr | 0.5 hr | 0.0 hr | 86.099 L | 86.1239 L | 2.54614 mg hr L^-1 | 0.223346 mg hr L^-1 |
| 4 | 14 | 4 | 50.0 mg | ev | missing | missing | missing | 3.81135 | 4.02956 | 0.0771661 hr L^-1 | 0.0773415 hr L^-1 | 3.8583 mg hr L^-1 | 3.86708 mg hr L^-1 | 3.71125 mg hr L^-1 | 0.074225 hr L^-1 | 15.8526 | 16.6409 | 29.0836 mg hr^2 L^-1 | 29.3587 mg hr^2 L^-1 | 24.4731 mg hr^2 L^-1 | 12.9591 L hr^-1 | 12.9297 L hr^-1 | 0.02 mg L^-1 | 0.0211931 mg L^-1 | 0.5 mg L^-1 | 0.01 L^-1 | 0.0 mg L^-1 | 0.0 L^-1 | 5.09648 hr | 0.136005 hr^-1 | 0.992565 | -0.589958 | 10 | 0.993391 | 2.0 hr | 24.0 hr | 7.53794 hr | 7.59196 hr | 6.59431 hr | 16 | 4.3167 | 24.0 hr | 0.0 hr | 24.0 hr | 0.75 hr | 0.0 hr | 95.2837 L | 95.0675 L | 3.00976 mg hr L^-1 | 0.666289 mg hr L^-1 |
| 5 | 15 | 5 | 50.0 mg | ev | missing | missing | missing | 2.05258 | 2.6782 | 0.0646265 hr L^-1 | 0.065042 hr L^-1 | 3.23133 mg hr L^-1 | 3.2521 mg hr L^-1 | 3.165 mg hr L^-1 | 0.0633 hr L^-1 | 9.3934 | 11.9828 | 21.6292 mg hr^2 L^-1 | 22.2655 mg hr^2 L^-1 | 19.5975 mg hr^2 L^-1 | 15.4735 L hr^-1 | 15.3747 L hr^-1 | 0.01 mg L^-1 | 0.0131319 mg L^-1 | 0.48 mg L^-1 | 0.0096 L^-1 | 0.0 mg L^-1 | 0.0 L^-1 | 4.59733 hr | 0.150772 hr^-1 | 0.98643 | -0.714195 | 11 | 0.987787 | 1.5 hr | 24.0 hr | 6.69361 hr | 6.84651 hr | 6.19194 hr | 16 | 4.89414 | 24.0 hr | 0.0 hr | 24.0 hr | 1.0 hr | 0.0 hr | 102.629 L | 101.973 L | 2.6432 mg hr L^-1 | 0.484746 mg hr L^-1 |
1.0.5 The steady-state profile
An NCA profile always belongs to a single dose, so the day 5 samples are read as their own profiles. Filter the data for time > 24 hrs to get them:
multiple_dose_data = @rsubset data :TIME > 24
first(multiple_dose_data, 5)| Row | ID | TIME | CONC | AMT | ROUTE | II |
|---|---|---|---|---|---|---|
| Int64 | Float64 | Float64 | Int64? | String3 | Int64? | |
| 1 | 2 | 96.0 | 0.01 | 50 | ev | 24 |
| 2 | 2 | 96.25 | 0.43 | missing | ev | missing |
| 3 | 2 | 96.5 | 0.46 | missing | ev | missing |
| 4 | 2 | 96.75 | 0.4 | missing | ev | missing |
| 5 | 2 | 97.0 | 0.37 | missing | ev | missing |
These profiles were measured at steady state, so the dosing interval is passed as tau and the parameters that are calculated over the dosing interval become available. You can select the NCA parameters that you wish to see in the output with the second argument of run_nca.
pop_md = read_nca_pumas(
multiple_dose_data;
id = :ID,
time = :TIME,
concentration = :CONC,
dose = :AMT,
route = :ROUTE,
tau = :II, # the dosing interval of the steady state profile
time_unit = timeu,
concentration_unit = concu,
dose_unit = amtu,
)
pop_md = transform_nca(
pop_md;
llq = 0.001concu,
blq_rules = Dict(:first => :keep, :middle => :drop, :last => :keep),
)| Row | id | profile | time | nominal_time | dose_time | nominal_dose_time | concentration_before_blq | concentration | dose | route | duration | tau | exclusion | exclusion_mask | blq |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Int64 | Int64 | Quantity… | Quantity… | Quantity… | Quantity… | Quantity… | Quantity… | Quantity… | String | Missing | Quantity… | Missing | Bool | Bool | |
| 1 | 2 | 1 | 0.0 hr | 0.0 hr | 96.0 hr | 96.0 hr | 0.01 mg L^-1 | 0.01 mg L^-1 | 50.0 mg | ev | missing | 24.0 hr | missing | false | false |
| 2 | 2 | 1 | 0.25 hr | 0.25 hr | 96.0 hr | 96.0 hr | 0.43 mg L^-1 | 0.43 mg L^-1 | 50.0 mg | ev | missing | 24.0 hr | missing | false | false |
| 3 | 2 | 1 | 0.5 hr | 0.5 hr | 96.0 hr | 96.0 hr | 0.46 mg L^-1 | 0.46 mg L^-1 | 50.0 mg | ev | missing | 24.0 hr | missing | false | false |
| 4 | 2 | 1 | 0.75 hr | 0.75 hr | 96.0 hr | 96.0 hr | 0.4 mg L^-1 | 0.4 mg L^-1 | 50.0 mg | ev | missing | 24.0 hr | missing | false | false |
| 5 | 2 | 1 | 1.0 hr | 1.0 hr | 96.0 hr | 96.0 hr | 0.37 mg L^-1 | 0.37 mg L^-1 | 50.0 mg | ev | missing | 24.0 hr | missing | false | false |
| 6 | 2 | 1 | 1.5 hr | 1.5 hr | 96.0 hr | 96.0 hr | 0.33 mg L^-1 | 0.33 mg L^-1 | 50.0 mg | ev | missing | 24.0 hr | missing | false | false |
| 7 | 2 | 1 | 2.0 hr | 2.0 hr | 96.0 hr | 96.0 hr | 0.31 mg L^-1 | 0.31 mg L^-1 | 50.0 mg | ev | missing | 24.0 hr | missing | false | false |
| 8 | 2 | 1 | 2.5 hr | 2.5 hr | 96.0 hr | 96.0 hr | 0.27 mg L^-1 | 0.27 mg L^-1 | 50.0 mg | ev | missing | 24.0 hr | missing | false | false |
| 9 | 2 | 1 | 3.0 hr | 3.0 hr | 96.0 hr | 96.0 hr | 0.23 mg L^-1 | 0.23 mg L^-1 | 50.0 mg | ev | missing | 24.0 hr | missing | false | false |
| 10 | 2 | 1 | 4.0 hr | 4.0 hr | 96.0 hr | 96.0 hr | 0.23 mg L^-1 | 0.23 mg L^-1 | 50.0 mg | ev | missing | 24.0 hr | missing | false | false |
| 11 | 2 | 1 | 6.0 hr | 6.0 hr | 96.0 hr | 96.0 hr | 0.16 mg L^-1 | 0.16 mg L^-1 | 50.0 mg | ev | missing | 24.0 hr | missing | false | false |
| 12 | 2 | 1 | 8.0 hr | 8.0 hr | 96.0 hr | 96.0 hr | 0.13 mg L^-1 | 0.13 mg L^-1 | 50.0 mg | ev | missing | 24.0 hr | missing | false | false |
| 13 | 2 | 1 | 12.0 hr | 12.0 hr | 96.0 hr | 96.0 hr | 0.06 mg L^-1 | 0.06 mg L^-1 | 50.0 mg | ev | missing | 24.0 hr | missing | false | false |
| ⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ |
| 1701 | 200 | 107 | 1.0 hr | 1.0 hr | 96.0 hr | 96.0 hr | 0.37 mg L^-1 | 0.37 mg L^-1 | 50.0 mg | ev | missing | 24.0 hr | missing | false | false |
| 1702 | 200 | 107 | 1.5 hr | 1.5 hr | 96.0 hr | 96.0 hr | 0.37 mg L^-1 | 0.37 mg L^-1 | 50.0 mg | ev | missing | 24.0 hr | missing | false | false |
| 1703 | 200 | 107 | 2.0 hr | 2.0 hr | 96.0 hr | 96.0 hr | 0.34 mg L^-1 | 0.34 mg L^-1 | 50.0 mg | ev | missing | 24.0 hr | missing | false | false |
| 1704 | 200 | 107 | 2.5 hr | 2.5 hr | 96.0 hr | 96.0 hr | 0.29 mg L^-1 | 0.29 mg L^-1 | 50.0 mg | ev | missing | 24.0 hr | missing | false | false |
| 1705 | 200 | 107 | 3.0 hr | 3.0 hr | 96.0 hr | 96.0 hr | 0.27 mg L^-1 | 0.27 mg L^-1 | 50.0 mg | ev | missing | 24.0 hr | missing | false | false |
| 1706 | 200 | 107 | 4.0 hr | 4.0 hr | 96.0 hr | 96.0 hr | 0.2 mg L^-1 | 0.2 mg L^-1 | 50.0 mg | ev | missing | 24.0 hr | missing | false | false |
| 1707 | 200 | 107 | 6.0 hr | 6.0 hr | 96.0 hr | 96.0 hr | 0.14 mg L^-1 | 0.14 mg L^-1 | 50.0 mg | ev | missing | 24.0 hr | missing | false | false |
| 1708 | 200 | 107 | 8.0 hr | 8.0 hr | 96.0 hr | 96.0 hr | 0.09 mg L^-1 | 0.09 mg L^-1 | 50.0 mg | ev | missing | 24.0 hr | missing | false | false |
| 1709 | 200 | 107 | 12.0 hr | 12.0 hr | 96.0 hr | 96.0 hr | 0.05 mg L^-1 | 0.05 mg L^-1 | 50.0 mg | ev | missing | 24.0 hr | missing | false | false |
| 1710 | 200 | 107 | 16.0 hr | 16.0 hr | 96.0 hr | 96.0 hr | 0.02 mg L^-1 | 0.02 mg L^-1 | 50.0 mg | ev | missing | 24.0 hr | missing | false | false |
| 1711 | 200 | 107 | 20.0 hr | 20.0 hr | 96.0 hr | 96.0 hr | 0.01 mg L^-1 | 0.01 mg L^-1 | 50.0 mg | ev | missing | 24.0 hr | missing | false | false |
| 1712 | 200 | 107 | 24.0 hr | 24.0 hr | 96.0 hr | 96.0 hr | 0.0 mg L^-1 | 0.0 mg L^-1 | 50.0 mg | ev | missing | 24.0 hr | missing | false | true |
md_params = [
:half_life,
:tmax,
:cmax,
:auclast,
:auctau_obs,
:vz_f_pred,
:cl_f_pred,
:aumcinf_obs,
:cavg,
]
report_md = run_nca(pop_md, md_params)At steady state, clearance and volume of distribution are derived from the AUC over the dosing interval, so cl_f_pred and vz_f_pred are requested here instead of cl_f_obs and vz_f_obs. The average concentration over the interval is cavg.
Just like for the first-dose profiles you can compute individual parameters and merge them to the final report.
Compute the summary statistics of the selected variables that you require:
param_summary_md = summarize(report_md; parameters = md_params)| Row | parameters | numsamples | nonmissing | minimum | maximum | mean | std | geomean | geostd | geomeanCV |
|---|---|---|---|---|---|---|---|---|---|---|
| String | Int64 | Int64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | |
| 1 | half_life | 107 | 107 | 1.9074 | 5.93739 | 3.51682 | 0.741075 | 3.43644 | 1.24597 | 22.2599 |
| 2 | tmax | 107 | 107 | 0.25 | 1.5 | 0.570093 | 0.232385 | 0.523904 | 1.52739 | 44.3287 |
| 3 | cmax | 107 | 107 | 0.32 | 0.84 | 0.521215 | 0.100599 | 0.51204 | 1.20711 | 18.9908 |
| 4 | auclast | 107 | 107 | 1.47125 | 4.11375 | 2.58982 | 0.498424 | 2.54104 | 1.21959 | 20.049 |
| 5 | auctau_obs | 107 | 107 | 1.49125 | 4.11375 | 2.60254 | 0.492372 | 2.55521 | 1.21524 | 19.6807 |
| 6 | vz_f_pred | 107 | 107 | 58.0001 | 153.212 | 98.9164 | 19.3711 | 97.0123 | 1.22153 | 20.2124 |
| 7 | cl_f_pred | 107 | 107 | 12.1544 | 33.5289 | 19.9489 | 4.06361 | 19.5679 | 1.21524 | 19.6807 |
| 8 | aumcinf_obs | 107 | 107 | 4.99686 | 29.689 | 13.809 | 4.96719 | 12.9386 | 1.44561 | 38.1404 |
| 9 | cavg | 107 | 107 | 0.0621354 | 0.171406 | 0.108439 | 0.0205155 | 0.106467 | 1.21524 | 19.6807 |
1.0.6 Documentation
Please refer to the NCA Documentation for any further help.
1.0.7 Save NCA Report
Earlier versions of NCA had a report function which took the run_nca and summarize output and generated a PDF report from it, with a fixed layout and the study metadata that was passed to run_nca. It has been removed in favor of the Quarto workflow: what goes into the report, and in which order, is now up to the document, tables and figures can come from anywhere in the Pumas ecosystem, and the same source renders to HTML, PDF or Word.
The results and summaries above are plain DataFrames and figures, so a report is written as a Quarto document, like this one, that runs the analysis and renders its tables and figures. The study metadata goes into the document header and the tables and figures are the values that the cells return.
A report usually shows a figure per subject, which is a number that is only known at runtime. QuartoTools provides objects for that: Expand inserts a vector of values into the document one after another, and a Div whose id starts with fig- becomes a numbered figure whose caption is its trailing MarkdownCell, so it can be cross-referenced as @fig-fit-2.
using QuartoTools: Div, Expand, MarkdownCell
Expand([
Div(
[
subject_fits(
pop;
ids = [id],
axis = (; xlabel = "Time hr", ylabel = "DrugX Concentration mg/L"),
),
MarkdownCell("Individual fit for subject $id"),
],
id = "fig-fit-$id",
) for id in ids[1:4]
])The reporting tutorials cover these building blocks in detail.