using DataFramesMeta
using CSV
using NCA
using NCAUtilities
using NCA.Unitful
using Dates
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 compute Single Dose and Multiple Dose NCA parameters.
In the given dataset
- Time = hrs
- Conc = mg/L
- Amt = mg
1.0.1 Load Data
Load the NCA dataset using the CSV.read
function and convert it to a DataFrame.
# You may need to navigate to the correct path for NCA_tutorial.csv
= CSV.read("NCA_tutorial.csv", DataFrame, missingstring = ["NA", ".", ""])
data 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 |
1.0.2 Single-dose NCA
Samples on Day 1 will be used for single dose NCA. So, we filter data for time <= 24 hrs
= @rsubset data :TIME <= 24 single_dose_data
We will specify the units of time, concentration and amount which will be used in the analysis.
= u"hr"
timeu = u"mg/L"
concu = u"mg" amtu
Next, we map the columns in the dataset to the required arguments in the read_nca
function
- id
- observations
- time
- ii - if you have multiple doses
- amt
- route - Infusion “inf”, IV “iv”, 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, observations, amt, route, duration(if needed), group(if needed).
Do not specify the Inter Dose Interval in this case because we are computing Single-Dose NCA parameters.
= read_nca(
pop
single_dose_data;= :ID,
id = :TIME,
time = :CONC,
observations = :AMT,
amt = :ROUTE,
route = 0.001,
llq )
NCAPopulation (107 subjects):
Number of missing observations: 0
Number of blq observations: 100
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_time
summary_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 (you can toggle through the subject index).
observations_vs_time(pop[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).
observations_vs_time(pop[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(1:length(pop), 9)
rand_subjs = observations_vs_time(
ctplots
pop[rand_subjs];= (; xlabel = "Time hr", ylabel = "DrugX Concentration mg/L"),
axis = true,
paginate = 3,
columns = 3,
rows = (; combinelabels = true),
facet
)1] ctplots[
You can also visualize the mean concentration vs time plot by using
summary_observations_vs_time(
pop;= (; xlabel = "Time hr", ylabel = "DrugX Concentration mg/L"),
axis )
1.0.4 NCA analysis
The workhorse function that performs the analysis is run_nca
(previously called NCAReport
). While it is sufficient to use run_nca(pop)
, there are a lot of arguments that the function accepts. These are mostly metadata information that is used to generate an automated report that will be covered later.
= run_nca(
report_sd
pop;= 3,
sigdigits = "STUDY-001",
studyid = "Phase 1 SAD of DrugX", # required
studytitle = [("Vijay", "Pumas-AI")], # required
author = "PumasAI",
sponsor = Dates.now(),
date = "DrugX Concentration (mg/L)",
conclabel = "Time hr",
timelabel = v"0.1",
versionnumber )
You can view the individual subject fits in an interactive way, but for the tutorial we will showcase the non-interactive way:
= subject_fits(
individual_fits
report_sd;= (; xlabel = "Time hr", ylabel = "DrugX Concentration mg/L", yscale = log10),
axis = true,
separate = true,
paginate = 16,
limit = 4,
columns = 4,
rows = (; combinelabels = true),
facet )
The above code generates a vector of 16 plots (4 x 4). To view each set of plots, you can access via the vector index of 1:16 e.g.
1] individual_fits[
1.0.4.1 Create Parameters Summaries
= [:cmax, :aucinf_obs] params
2-element Vector{Symbol}:
:cmax
:aucinf_obs
= summarize(report_sd; parameters = params) param_summary_sd
Row | parameters | numsamples | minimum | maximum | mean | std | geomean | geostd | geomeanCV |
---|---|---|---|---|---|---|---|---|---|
String | Int64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | |
1 | cmax | 107 | 0.34 | 0.77 | 0.515888 | 0.0956897 | 0.507222 | 1.20349 | 18.6827 |
2 | aucinf_obs | 107 | 1.61 | 4.17 | 2.61065 | 0.461849 | 2.56954 | 1.19816 | 18.2274 |
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. Note how you have to qualify each function with NCA.
to avoid variables named as those parameters.
Volume of Distribution/F, in this case since the drug is given orally
= NCA.vz(pop)
vz first(vz, 5)
Row | id | vz |
---|---|---|
String | Float64 | |
1 | 2 | 116.994 |
2 | 3 | 90.694 |
3 | 10 | 86.099 |
4 | 14 | 95.2837 |
5 | 15 | 102.629 |
Clearance/F, in this case since the drug is given orally
= NCA.cl(pop)
cl first(cl, 5)
Row | id | cl |
---|---|---|
String | Float64 | |
1 | 2 | 18.5931 |
2 | 3 | 15.7161 |
3 | 10 | 17.6125 |
4 | 14 | 12.9591 |
5 | 15 | 15.4735 |
Terminal Elimination Rate Constant, threshold=3
specifies the max no. of time point used for calculation
= NCA.lambdaz(pop; threshold = 3)
lambdaz first(lambdaz, 5)
Row | id | lambdaz |
---|---|---|
String | Float64 | |
1 | 2 | 0.137327 |
2 | 3 | 0.173287 |
3 | 10 | 0.22397 |
4 | 14 | 0.156595 |
5 | 15 | 0.20118 |
slopetimes
in this case specifies the exact time point you want for the calculation
= NCA.lambdaz(pop; slopetimes = [8, 12, 16])
lambdaz_1 first(lambdaz_1, 5)
[ Info: ID 38 errored: lambdaz calculation failed because of non-positive concentration
[ Info: ID 40 errored: lambdaz calculation failed because of non-positive concentration
[ Info: ID 76 errored: lambdaz calculation failed because of non-positive concentration
[ Info: ID 107 errored: lambdaz calculation failed because of non-positive concentration
[ Info: ID 116 errored: lambdaz calculation failed because of non-positive concentration
[ Info: ID 192 errored: lambdaz calculation failed because of non-positive concentration
Row | id | lambdaz |
---|---|---|
String | Float64? | |
1 | 2 | 0.173287 |
2 | 3 | 0.22397 |
3 | 10 | 0.20118 |
4 | 14 | 0.103335 |
5 | 15 | 0.128702 |
Half-life calculation for 4th individual
= NCA.thalf(pop[4])
thalf first(thalf, 5)
1-element Vector{Float64}:
5.096481600264849
Dose Normalized Cmax
= NCA.cmax(pop, normalize = true)
cmax_d first(cmax_d, 5)
Row | id | cmax |
---|---|---|
String | Float64 | |
1 | 2 | 0.0074 |
2 | 3 | 0.0146 |
3 | 10 | 0.013 |
4 | 14 | 0.01 |
5 | 15 | 0.0096 |
Mean residence time
= NCA.mrt(pop)
mrt first(mrt, 5)
Row | id | mrt |
---|---|---|
String | Float64 | |
1 | 2 | 6.36645 |
2 | 3 | 4.53536 |
3 | 10 | 4.98956 |
4 | 14 | 7.53794 |
5 | 15 | 6.69361 |
AUMC calculation, using :linlog method
= NCA.aumc(pop; method = :linlog)
aumc first(aumc, 5)
Row | id | aumc |
---|---|---|
String | Float64 | |
1 | 2 | 17.1523 |
2 | 3 | 14.3832 |
3 | 10 | 14.1224 |
4 | 14 | 29.1743 |
5 | 15 | 21.6259 |
# since we have two lambdaz calculations rename one column to merge in a dataframe
rename!(lambdaz_1, Dict(:lambdaz => "lambdaz_specf"))
=
individual_params innerjoin(vz, cl, lambdaz, lambdaz_1, cmax_d, mrt, aumc; on = [:id], makeunique = true)
first(individual_params, 5)
Row | id | vz | cl | lambdaz | lambdaz_specf | cmax | mrt | aumc |
---|---|---|---|---|---|---|---|---|
String | Float64 | Float64 | Float64 | Float64? | Float64 | Float64 | Float64 | |
1 | 2 | 116.994 | 18.5931 | 0.137327 | 0.173287 | 0.0074 | 6.36645 | 17.1523 |
2 | 3 | 90.694 | 15.7161 | 0.173287 | 0.22397 | 0.0146 | 4.53536 | 14.3832 |
3 | 10 | 86.099 | 17.6125 | 0.22397 | 0.20118 | 0.013 | 4.98956 | 14.1224 |
4 | 14 | 95.2837 | 12.9591 | 0.156595 | 0.103335 | 0.01 | 7.53794 | 29.1743 |
5 | 15 | 102.629 | 15.4735 | 0.20118 | 0.128702 | 0.0096 | 6.69361 | 21.6259 |
1.0.4.3 Calculate AUC at specific intervals
Calculation of AUC at specific time intervals and merge to the final report dataframe:
# various other methods are :linear, :linlog
= NCA.auc(pop; interval = (0, 12), method = :linuplogdown)
auc0_12 first(auc0_12, 5)
Row | id | auc0_12 |
---|---|---|
String | Float64 | |
1 | 2 | 2.2645 |
2 | 3 | 2.89846 |
3 | 10 | 2.54614 |
4 | 14 | 3.00976 |
5 | 15 | 2.6432 |
= NCA.auc(pop; interval = (12, 24), method = :linuplogdown)
auc12_24 first(auc12_24, 5)
Row | id | auc12_24 |
---|---|---|
String | Float64 | |
1 | 2 | 0.329483 |
2 | 3 | 0.193123 |
3 | 10 | 0.223346 |
4 | 14 | 0.666289 |
5 | 15 | 0.484746 |
= innerjoin(report_sd.reportdf, auc0_12, auc12_24; on = [:id], makeunique = true)
final first(final, 5)
Row | id | dose | tlag | tmax | cmax | tlast | clast | clast_pred | auclast | kel | half_life | aucinf_obs | aucinf_pred | vz_f_obs | cl_f_obs | vz_f_pred | cl_f_pred | n_samples | cmax_dn | auclast_dn | aucinf_dn_obs | auc_extrap_obs | aucinf_dn_pred | auc_extrap_pred | aumclast | aumcinf_obs | aumc_extrap_obs | aumcinf_pred | aumc_extrap_pred | n_samples_kel | rsq_kel | rsq_adj_kel | corr_kel | intercept_kel | kel_t_low | kel_t_high | span | route | auc0_12 | auc12_24 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
String | Int64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Int64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Int64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | String | Float64 | Float64 | |
1 | 2 | 50 | 0.0 | 1.0 | 0.37 | 24.0 | 0.01 | 0.00965 | 2.63 | 0.159 | 4.36 | 2.69 | 2.69 | 117.0 | 18.6 | 117.0 | 18.6 | 16 | 0.0074 | 0.0525 | 0.0538 | 2.34 | 0.0537 | 2.26 | 15.2 | 17.1 | 11.1 | 17.1 | 10.8 | 11 | 0.997 | 0.997 | 0.998 | -0.826 | 1.5 | 24.0 | 5.16 | EV | 2.2645 | 0.329483 |
2 | 3 | 50 | 0.0 | 1.0 | 0.73 | 20.0 | 0.01 | 0.01 | 3.12 | 0.173 | 4.0 | 3.18 | 3.18 | 90.7 | 15.7 | 90.7 | 15.7 | 16 | 0.0146 | 0.0625 | 0.0636 | 1.81 | 0.0636 | 1.81 | 12.9 | 14.4 | 10.3 | 14.4 | 10.3 | 3 | 1.0 | 1.0 | 1.0 | -1.14 | 12.0 | 20.0 | 2.0 | EV | 2.89846 | 0.193123 |
3 | 10 | 50 | 0.0 | 0.5 | 0.65 | 20.0 | 0.01 | 0.00983 | 2.79 | 0.205 | 3.39 | 2.84 | 2.84 | 86.1 | 17.6 | 86.1 | 17.6 | 16 | 0.013 | 0.0558 | 0.0568 | 1.72 | 0.0568 | 1.69 | 12.9 | 14.2 | 8.59 | 14.1 | 8.46 | 8 | 0.994 | 0.993 | 0.997 | -0.531 | 2.5 | 20.0 | 5.16 | EV | 2.54614 | 0.223346 |
4 | 14 | 50 | 0.0 | 0.75 | 0.5 | 24.0 | 0.02 | 0.0212 | 3.71 | 0.136 | 5.1 | 3.86 | 3.87 | 95.3 | 13.0 | 95.1 | 12.9 | 16 | 0.01 | 0.0742 | 0.0772 | 3.81 | 0.0773 | 4.03 | 24.5 | 29.1 | 15.9 | 29.4 | 16.6 | 10 | 0.993 | 0.993 | 0.997 | -0.59 | 2.0 | 24.0 | 4.32 | EV | 3.00976 | 0.666289 |
5 | 15 | 50 | 0.0 | 1.0 | 0.48 | 24.0 | 0.01 | 0.0131 | 3.16 | 0.151 | 4.6 | 3.23 | 3.25 | 103.0 | 15.5 | 102.0 | 15.4 | 16 | 0.0096 | 0.0633 | 0.0646 | 2.05 | 0.065 | 2.68 | 19.6 | 21.6 | 9.39 | 22.3 | 12.0 | 11 | 0.988 | 0.986 | 0.994 | -0.714 | 1.5 | 24.0 | 4.89 | EV | 2.6432 | 0.484746 |
One can also pass in a vector of partial AUC’s directly as seen below.
= NCA.auc(pop; interval = [(0, 12), (12, 24)], method = :linuplogdown)
partial_aucs first(partial_aucs, 5)
Row | id | auc0_12 | auc12_24 |
---|---|---|---|
String | Float64 | Float64 | |
1 | 2 | 2.2645 | 0.329483 |
2 | 3 | 2.89846 | 0.193123 |
3 | 10 | 2.54614 | 0.223346 |
4 | 14 | 3.00976 | 0.666289 |
5 | 15 | 2.6432 | 0.484746 |
1.0.5 Multiple-dose NCA
Filter data for time >= 24 hrs to perform the Multiple-dose NCA:
= @rsubset data :TIME > 24
multiple_dose_data 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 | 0 | ev | 0 |
3 | 2 | 96.5 | 0.46 | 0 | ev | 0 |
4 | 2 | 96.75 | 0.4 | 0 | ev | 0 |
5 | 2 | 97.0 | 0.37 | 0 | ev | 0 |
In the case of multiple-dose NCA the extra parameters which are calculated based on the dosing interval (τ) will be included in the output. You can select the NCA parameters that you wish to see in the output with the parameters
keyword argument to run_nca
.
= read_nca(
pop_md
multiple_dose_data;= :ID,
id = :TIME,
time = :CONC,
observations = :AMT,
amt = :ROUTE,
route = :II, # please specify ii for Multiple-dose NCA
ii = 0.001,
llq )
NCAPopulation (107 subjects):
Number of missing observations: 0
Number of blq observations: 168
= run_nca(
report_md
pop_md;= 3,
sigdigits = "STUDY-002",
studyid = "Phase 1 MAD of DrugX", # required
studytitle = [("Vijay", "Pumas-AI")], # required
author = "PumasAI",
sponsor = Dates.now(),
date = "DrugX Concentration (mg/L)",
conclabel = "Time hr",
timelabel = v"0.1",
versionnumber )
Similar to the Single-NCA case you can compute individual parameters and merge them to the final report.
Compute the summary statistics of the selected variables that you require:
= summarize(
param_summary_md
report_md;= [
parameters :half_life,
:tmax,
:cmax,
:auclast,
:auc_tau_obs,
:vz_f_obs,
:cl_f_obs,
:aumcinf_obs,
:tau,
:cavgss,
], )
Row | parameters | numsamples | minimum | maximum | mean | std | geomean | geostd | geomeanCV |
---|---|---|---|---|---|---|---|---|---|
String | Int64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | |
1 | half_life | 107 | 1.91 | 5.94 | 3.5172 | 0.741081 | 3.43684 | 1.24592 | 22.2562 |
2 | tmax | 107 | 0.25 | 1.5 | 0.570093 | 0.232385 | 0.523904 | 1.52739 | 44.3287 |
3 | cmax | 107 | 0.32 | 0.84 | 0.521215 | 0.100599 | 0.51204 | 1.20711 | 18.9908 |
4 | auclast | 107 | 1.47 | 4.11 | 2.58972 | 0.498504 | 2.54092 | 1.21963 | 20.0518 |
5 | auc_tau_obs | 107 | 1.49 | 4.11 | 2.60243 | 0.492456 | 2.55509 | 1.21527 | 19.6837 |
6 | vz_f_obs | 107 | 57.7 | 150.0 | 97.3551 | 18.6422 | 95.5629 | 1.21604 | 19.7489 |
7 | cl_f_obs | 107 | 12.0 | 33.0 | 19.6561 | 4.05638 | 19.2703 | 1.2186 | 19.9654 |
8 | aumcinf_obs | 107 | 5.0 | 29.7 | 13.8078 | 4.9646 | 12.9381 | 1.44538 | 38.1231 |
9 | tau | 107 | 24.0 | 24.0 | 24.0 | 0.0 | 24.0 | 1.0 | 4.46179e-14 |
10 | cavgss | 107 | 0.0631 | 0.174 | 0.110213 | 0.021162 | 0.108151 | 1.21862 | 19.9671 |
1.0.6 Documentation
Please refer to the NCA Documentation for any further help.
1.0.7 Save NCA Report
The NCA report can be generated with the report
function on the output of run_nca
, this generates a pdf report as discussed here.
# Single-NCA final report
report(report_sd, param_summary_sd)
[ Info: Generating Tables
[ Info: Generating Plots
[ Info: Gathering System Information
[ Info: Ordering Report Sections
"output/Phase 1 SAD of DrugX.pdf"
# Multiple-NCA final report
report(report_md, param_summary_md)
[ Info: Generating Tables
[ Info: Generating Plots
[ Info: Gathering System Information
[ Info: Ordering Report Sections
"output/Phase 1 MAD of DrugX.pdf"