using NCA
using NCAUtilities
using PumasUtilities
Plot customization for Noncompartmental analysis - Single Ascending Dose
1 Libraries
We will load the NCA
, NCAUtilities
, and PumasUtilities
packages in order to have access to Pumas’ core and utilities functions for Noncompartmental analysis:
We will also use Makie.jl
as the main plotting library and also AlgebraOfGraphics.jl
.
CairoMakie.jl
will act as our plotting backend and will also make possible the various customization options that we will go over. On the other hand, AlgebraOfGraphics.jl
will provide a syntax inspired by the grammar of graphics and similar to R’s ggplot2
to create complex plots.
using CairoMakie
using AlgebraOfGraphics
Lastly, we will use the CSV.jl
package to read external data and DataFramesMeta.jl
for data wrangling.
using CSV
using DataFrames
using DataFramesMeta
2 Introduction
This tutorial will show you how to customize some of the plots available in the Pumas NCA workflow and create unique plots and layouts using Pumas
and Makie.jl
.
We will be focusing on plotting and customization. Please refer to the tutorial Introduction to Noncompartmental Analysis (NCA) if anything feels unfamiliar or if you wish to explore the NCA capabilities that Pumas has to offer.
3 Motivation
Data visualization plays a central role in communicating NCA results. However, effective communication depends strongly on having control over how your data visualization tools will look like. By customizing the look and feel of your plots, you will be able to play emphasis on what you consider to be most the important results, create unique plots and layouts for your presentations, and follow the appropriate format for your tables, listings, and figures (TLF).
Luckily, Pumas and Makie.jl support a wide variety of plotting and customization options in order to fulfill your data visualization needs.
3.1 🎨 Color palette
In order to showcase the extensive support for customization in Pumas, we will be trying to apply a consistent color palette across the plots generated in this tutorial. As an example, consider the following two colors to be used as the main ones in the palette.
We will also use these three additional colors for highlighting and more complex plots.
These are the names and hex codes for the colors in the palette:
- Navy Blue:
#0B1E3F
- Emerald Green:
#50C878
- Coral:
#FF7F50
- Mustard yellow:
#FFDB58
- Deep purple:
#6A5ACD
Makie
uses Colors.jl
to define colors. This means that you can pass a string representing any [CSS color] (https://developer.mozilla.org/en-US/docs/Web/CSS/color) to Pumas
and Makie
’s plotting functions.
For instance, you could define the color red with "rgb(255,0,0)"
, "#FF0000"
, or "red"
. You can also use one of the named colors from Colors.jl
by passing them as a Symbol
(e.g :navyblue
)
We will store the colors in a Vector
so that we can access them later more easily. Let’s begin by defining the main and complementary colors. Notice that we are creating strings with the hex codes from above:
= ["#0B1E3F", "#50C878"];
main_colors = ["#FF7F50", "#FFDB58", "#6A5ACD"]; complementary
We can join both lists of colors using vcat
, which will concatenate main_colors
and complementary_colors
to create a single color palette vector.
= vcat(complementary, main_colors); palette
We would also like to use a shade of gray for the background color instead of the default white color
= "#FAF9F6"; background
4 Load Data
We will be using simulated data for an orally administered drug through a single dose ranging from 100 to 1,600 mg.
This tutorial is based on Alison Margolskee and Andy Stein’s PK - Single Ascending Dose workflow. You can get the dataset used in this tutorial here.
Read the data using CSV.jl
and convert it to a DataFrame
= CSV.read("Single_Ascending_Dose_Dataset2.csv", DataFrame; missingstring = ["NA"])
df first(df, 5)
Row | ID | TIME | NOMTIME | TIMEUNIT | AMT | LIDV | CMT | NAME | EVENTU | CENS | EVID | WEIGHTB | SEX | TRTACT | DOSE |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Int64 | Float64 | Float64 | String7 | Int64 | Float64? | Int64 | String31 | String7 | Int64 | Int64 | Float64 | String7 | String7 | Int64 | |
1 | 1 | -0.089 | -0.1 | Hours | 0 | missing | 2 | PK Concentration | ng/mL | 0 | 0 | 77.9 | Male | 100 mg | 100 |
2 | 1 | 0.0 | 0.0 | Hours | 100 | missing | 1 | Dosing | mg | 0 | 1 | 77.9 | Male | 100 mg | 100 |
3 | 1 | 0.17 | 0.1 | Hours | 0 | 0.665 | 2 | PK Concentration | ng/mL | 0 | 0 | 77.9 | Male | 100 mg | 100 |
4 | 1 | 0.565 | 0.5 | Hours | 0 | 0.997 | 2 | PK Concentration | ng/mL | 0 | 0 | 77.9 | Male | 100 mg | 100 |
5 | 1 | 1.145 | 1.0 | Hours | 0 | 1.35 | 2 | PK Concentration | ng/mL | 0 | 0 | 77.9 | Male | 100 mg | 100 |
4.1 Data wrangling
We have a series of tutorials on Data Wrangling in Julia, where you can learn more about reading and manipulating data.
Before defining a population with read_nca
function, we need to do some transformations on the dataset. First, we will switch the time units to days and create a column for the route of administration as extravascular ("ev"
).
@rtransform! df begin
:NOMTIME = :NOMTIME / 24
:ROUTE = "ev"
end;
We also have to get rid of the rows where the time is negative:
@rsubset! df begin
:NOMTIME >= 0
end;
Finally, we have to define the units for the time (days), concentration (ng/mL), and dose amount (mg)
= u"d";
timeu = u"ng/mL";
concu = u"mg"; amtu
Notice that we have defined units using the u
string literal (as in u"..."
). This syntax corresponds to Unitful.jl
, which is the Julia package from which Pumas obtains its unit support.
Now we are ready to run read_nca
. Notice that we are setting the keyword argument group
as [:DOSE]
because we want to be able to account for different dosing levels in our analysis.
= read_nca(
nca_data
df;= :ID,
id = :NOMTIME,
time = :LIDV,
observations = :AMT,
amt = :ROUTE,
route = [:DOSE],
group = timeu,
timeu = concu,
concu = amtu,
amtu );
5 Exploring concentration time profiles
In order to get the concentration time profile for each subject in the population, we use the function observations_vs_time
.
We recommend setting the keyword argument paginate
to true
for datasets containing many subjects (more than 9). By doing this, observations_vs_time
will return a vector of plots instead of trying to cram all of them into the limited space of a single figure.
This is the default look for the plots.
= observations_vs_time(nca_data; paginate = true);
obs_default 1] obs_default[
Let’s try to customize it to follow our color palette. We will start by changing the color of the markers and the lines, which can be done using the keyword arguments markercolor
and color
.
= observations_vs_time(
obs_color
nca_data;= true,
paginate = main_colors[1],
markercolor = main_colors[2],
color
);1] obs_color[
Now let’s change the background to our color of choice and customize the axis labels. In order to do this, we have to use the axis
and figure
keyword arguments. We will also use the facet
keyword to combine labels where possible and avoid cluttering the plots:
= observations_vs_time(
obs_final
nca_data;= true,
paginate = main_colors[1],
markercolor = main_colors[2],
color = (;
axis = background,
backgroundcolor = "Time (days)",
xlabel = "Concentration (ng/mL)",
ylabel
),= (; backgroundcolor = background, resolution = (1000, 800), fontsize = 18),
figure = (; combinelabels = true),
facet
);1] obs_final[
We have also changed the figure’s resolution
and fontsize
. Makie.jl
offers a very wide range of customization options, so please make sure to check our tutorial on Customization of AlgebraOfGraphics.jl Plots and Makie’s tutorials for more information.
Lastly, Pumas’ documentation on plotting is also a great place to learn more about the customization options available for functions like observations_vs_time
.
With just a few keyword arguments, we have changed look and feel of our plot, and we have made it our own by applying our custom color palette.
6 Exploring the effect of dosing
To explore the effect of dosing on drug concentration we will generate the average concentration time profile for each dose level. This can be accomplished by using the function summary_observations_vs_time
.
Once again, we will use the axis
, figure
, and color
keyword arguments to make the result look the way we want.
= summary_observations_vs_time(
f1, ax1, p1
nca_data;= false,
separate = [:DOSE],
group = (; backgroundcolor = background, resolution = (900, 500), fontsize = 16),
figure = (;
axis = background,
backgroundcolor = "Time (days)",
xlabel = "Concentration (ng/mL)",
ylabel
),= :Set1_5,
color
)
axislegend(ax1; bgcolor = background)
f1
Notice that we had to run the function axislegend
which accepts an Axis
(in our case ax1
) from Makie.jl
in order to get a legend for the plot. We also had to store the figure, axis, and plot objects when calling the function summary_observations_vs_time
in order to use the axislegend
function and display the result.
You probably noticed that we assigned Set1_5
to the color keyword argument. Currently, summary_observations_vs_time
only accepts a single color or a predefined colormap from ColorSchemes.jl
. We are currently working to add support for a custom colormap
and expect this feature to be available soon.
We recommend using a colormap
if you are setting separate
to false
since this will allow you to get a different color for each line.
The last plot is very useful to observe the behavior of the concentration and determine the maximum concentration for each dosage regimen, but other interesting information such as the number of compartments that would be needed for a PK model, half-life, and nonlinearity of clearance is better observed on a logarithmic scale. To do this, simply add yscale=log10
to axis
.
= summary_observations_vs_time(
f2, ax2, p2
nca_data;= false,
separate = [:DOSE],
group = (; backgroundcolor = background, resolution = (900, 500), fontsize = 16),
figure = (;
axis = background,
backgroundcolor = "Time (days)",
xlabel = "Concentration (ng/mL)",
ylabel = log10,
yscale
),= :Set1_5,
color
)
axislegend(ax2; bgcolor = background)
f2
You can set yscale
(or xscale
) to be any function you like. For instance, you could set the scale to be the natural logarithm (log
) or the logarithm to the base 2 (log2
). Finally, you can also pass any custom user-defined function and not be restricted by Julia’s or Pumas’ existing functions.
6.1 Dose normalized concentration
In addition to the concentration time profile, it could also be interesting to analyze the dose-normalized concentration profile. Normalizing the concentration will allow us to assess the dose linearity of exposure and we will use the normalized concentration for the subsequent analysis.
We begin by creating a new column, NLIDV
, which will be the concentration (LIDV
) divided by the corresponding dose (DOSE
):
@rtransform! df begin
:NLIDV = :LIDV / :DOSE
end;
Now we create another NCAPopulation
, but this time the observations will be the normalized concentrations.
= read_nca(
nca_data_norm
df;= :ID,
id = :NOMTIME,
time = :NLIDV,
observations = :AMT,
amt = :ROUTE,
route = [:DOSE],
group );
In order to get the normalized concentration profile for each dose, we use the same code as before, just changing the NCAPopulation
for the one that we just computed (this time we only show you the plot on a logarithmic scale):
= summary_observations_vs_time(
f3, ax3, p3
nca_data_norm;= false,
separate = [:DOSE],
group = (; backgroundcolor = background, resolution = (900, 500), fontsize = 16),
figure = (;
axis = background,
backgroundcolor = "Time (days)",
xlabel = "Normalized Concentration (ng/mL)/mg",
ylabel = log10,
yscale
),= :Set1_5,
color
)axislegend(ax3; bgcolor = background)
f3
Now that we have normalized the concentration, we will calculate the AUC and Cₘₐₓ metrics. To do this, we must first run the analysis on the population by using the function run_nca
.
= run_nca(nca_data_norm); nca_results_norm
In order to visualize these results, we will generate violin plots for both the AUC and the Cₘₐₓ at each dose level. Also, we would like to observe them side by side.
To do this, use the function parameters_vs_group
to generate each plot, and we will put them together using the Figure
object from CairoMakie.jl
.
Let’s start by creating the figure:
= Figure(; backgroundcolor = background, resolution = (1000, 500)); fAUC_Cmax
Now we will add the plots to the figure by indexing it and passing it as the first argument of the parameters_vs_group
function. We will start with the AUC.
parameters_vs_group(
1, 1],
fAUC_Cmax[
nca_results_norm,= "DOSE",
group = :aucinf_obs;
parameter = (;
axis = background,
backgroundcolor = "Dose (mg)",
xlabel = "AUC (ng/mL)*day",
ylabel = "AUC",
title
),= main_colors[1],
color );
Notice that this will not return the plot. If you ever want to check how the plot is looking, inspect the variable associated with the figure (in this case AUC_Cmax
).
Now let’s add the plot corresponding to Cₘₐₓ. In this case, we want to place it on the right of the AUC plot, so we index the figure at [1, 2]:
parameters_vs_group(
1, 2],
fAUC_Cmax[
nca_results_norm;= "DOSE",
group = :cmax,
parameter = (;
axis = background,
backgroundcolor = "Dose (mg)",
xlabel = "Cₘₐₓ (ng/mL)/mg",
ylabel = "Cₘₐₓ",
title
),= main_colors[2],
color );
And now we have completed our custom layout. Again, we can see our results by inspecting the figure.
fAUC_Cmax
This procedure can be generalized to be used with any other plotting functions, just pass the axis in which you want to place the plot (e.g figure[2, 1]
) as the first argument of the function. Make sure to check our documentation section on customizing plots for a more detailed explanation.
We would also like to explore the effect of dosing on these metrics. One way in which we might go about this is by performing a linear regression with respect to dose levels. This can be achieved by using the function power_model
:
First, we have to create a DoseLinearityPowerModel
object. We will create one for both Cₘₐₓ and AUC, which we will name dpcmax
and dpauc
.
= NCA.DoseLinearityPowerModel(nca_results_norm, :cmax); dpcmax
= NCA.DoseLinearityPowerModel(nca_results_norm, :aucinf_obs); dpauc
Now we will apply the same procedure as with the violin plots in order to put the two visualizations next to each other:
=
fAUC_Cmax_reg Figure(resolution = (1500, 800), backgroundcolor = background, fontsize = 20);
Let’s determine the unique dose levels in order to use them as our x-axis ticks:
= unique(df.DOSE); doses
Now we can add our plot to the Figure
:
power_model(
1, 1],
fAUC_Cmax_reg[
dpauc;= (;
axis = background,
backgroundcolor = "Dose (mg)",
xlabel = "Dose-normalized AUC (ng/mL)*day",
ylabel = "AUC",
title = doses,
xticks = log10,
xscale
),= main_colors[1],
color = main_colors[2],
markercolor );
Now we can add our plot to the Figure
:
power_model(
1, 2],
fAUC_Cmax_reg[
dpcmax;= (;
axis = background,
backgroundcolor = "Dose (mg)",
xlabel = "Dose-normalized Cₘₐₓ (ng/mL)",
ylabel = "Cₘₐₓ",
title = doses,
xticks = log10,
xscale
),= main_colors[1],
color = main_colors[2],
markercolor );
We could stop here, but let’s add a figure legend with figurelegend
:
figurelegend(
fAUC_Cmax_reg;= background,
bgcolor = :b,
position = 2,
nbanks = true,
tellheight );
Again, we can see our results by inspecting our Figure
(fAUC_Cmax_reg
):
fAUC_Cmax_reg
7 Exploring the effects of covariates
As you may have noticed, the dataset that we are using includes information about the subject’s sex and weight. We will explore the effect of these two covariates on the concentration profiles, AUC and Cₘₐₓ
7.1 Sex
We will generate the mean concentration profile for each sex. In order to do this, we will have to create a new NCAPopulation
with read_nca
. However, this time we’ll specify SEX
as the group variable
= read_nca(
nca_data_sex
df;= :ID,
id = :NOMTIME,
time = :NLIDV,
observations = :AMT,
amt = :ROUTE,
route = [:SEX],
group );
We could add any number of columns to the group
keyword argument. For instance, we could have done group = [:SEX, :DOSE]
to let Pumas know that we want to group subjects both by sex and by dose level. However, this will result in 10 groups (cartesian product for a set of 2 categories of sex and 5 dose levels), and we want to limit this analysis to only the sex covariate.
Now let’s generate the concentration profile on a logarithmic scale following the same procedure as described above.
= summary_observations_vs_time(
f4, ax4, p4
nca_data_sex;= false,
separate = "SEX",
group = (; backgroundcolor = background, resolution = (900, 500), fontsize = 16),
figure = (;
axis = background,
backgroundcolor = "Time (days)",
xlabel = "Normalized Concentration (ng/mL)/mg",
ylabel = log10,
yscale
),= :Set1_5,
color
)
axislegend(ax4; bgcolor = background)
f4
Here we notice that, on average, female subjects exhibit a higher concentration profile. Let’s also compute the values for AUC and Cₘₐₓ in order to examine more closely this difference. Just like before, we must use the function run_nca
on our new NCA population.
= run_nca(nca_data_sex); nca_results_sex
Now we can generate violin plots for each of these metrics by using the same method as when we were exploring the effect of dosing:
= Figure(; backgroundcolor = background, resolution = (1000, 500))
fAUC_Cmax_sex
parameters_vs_group(
1, 1],
fAUC_Cmax_sex[
nca_results_sex;= "SEX",
group = :aucinf_obs,
parameter = (;
axis = "Sex",
xlabel = "AUC (ng/mL)*day",
ylabel = "AUC",
title = background,
backgroundcolor
),= main_colors[1],
color
)
parameters_vs_group(
1, 2],
fAUC_Cmax_sex[
nca_results_sex;= "SEX",
group = :cmax,
parameter = (;
axis = background,
backgroundcolor = "Sex",
xlabel = "Cₘₐₓ (ng/mL)/mg",
ylabel = "Cₘₐₓ",
title
),= main_colors[2],
color
)
fAUC_Cmax_sex
7.2 Weight
To explore the effects of weight, we will divide subjects into those under 70 kg and those over. This means that we have to create the following column in our dataset.
@rtransform! df begin
:WEIGHT = :WEIGHTB > 70 ? "Over 70 kg" : "Under 70 kg"
end;
Having done that, we can create an NCAPopulation
setting the group
as :WEIGHT
.
= read_nca(
nca_data_weight
df;= :ID,
id = :NOMTIME,
time = :NLIDV,
observations = :AMT,
amt = :ROUTE,
route = [:WEIGHT],
group );
= summary_observations_vs_time(
f5, ax5, p5
nca_data_weight;= false,
separate = [:WEIGHT],
group = (; backgroundcolor = background, resolution = (900, 500), fontsize = 16),
figure = (;
axis = background,
backgroundcolor = "Time (days)",
xlabel = "Normalized Concentration (ng/mL)/mg",
ylabel = log10,
yscale
),= :Set1_5,
color
)
axislegend(ax5; bgcolor = background)
f5
This indicates that drug concentration is higher for individuals weighing less than 70 kg. We could also create the violin plots for AUC and Cₘₐₓ by following the same procedure as before, but we’ll leave it here since we already went over the procedure
8 Exploring both covariates simultaneously
For our last visualization, let’s combine both covariates in a single analysis. To do this, we will plot AUC and Cₘₐₓ against body weight, and we will also examine if there are differences related to sex.
Let’s start by creating an NCA population specifying the two groups we are interested in: SEX
and WEIGHT
.
= read_nca(
nca_data_sex_weight
df;= :ID,
id = :NOMTIME,
time = :NLIDV,
observations = :AMT,
amt = :ROUTE,
route = [:SEX, :WEIGHTB],
group );
Now that we have created the population, we can run the analysis:
= run_nca(nca_data_sex_weight).reportdf; nca_results_sex_weight
You can get the results of run_nca
as a DataFrame
by inspecting the field reportdf
.
Let’s begin by generating a scatter plot of AUC vs weight and perform a linear regression on top of it.
We are going to be using AlgebraOfGraphics.jl
for plotting. If you are unfamiliar with its syntax and use, feel free to refer to our series of tutorials on data visualization in Julia, starting with Introduction to AlgebraOfGraphics.jl.
=
auc_plot data(nca_results_sex_weight) *
mapping(:WEIGHTB, :aucinf_obs; color = :SEX => "Sex") *
visual(Scatter) + AlgebraOfGraphics.linear()); (
We can check our mapping and visuals by using the draw
function
draw(auc_plot)
It seems like we got the result we wanted. Let’s do the same for Cₘₐₓ:
=
cmax_plot data(nca_results_sex_weight) *
mapping(:WEIGHTB, :cmax; color = :SEX => "Sex") *
visual(Scatter) + AlgebraOfGraphics.linear()); (
We can check our mapping and visuals by using the draw
function
draw(cmax_plot)
This time we can trust it will look the way we want. Let’s now create a figure where both plots are shown next to each other and we apply our custom color palette:
= Figure(; backgroundcolor = background, resolution = (1000, 600));
fig_two_cov = fig_two_cov[1, 1];
axs_auc = fig_two_cov[1, 2]; axs_cmax
= draw!(
auc
axs_auc,
auc_plot,= (;
axis = background,
backgroundcolor = "Weight (kg)",
xlabel = "AUC (ng/mL)*day",
ylabel = "AUC",
title
),= (; color = main_colors),
palettes
)= draw!(
cmax
axs_cmax,
cmax_plot,= (;
axis = background,
backgroundcolor = "Weight (kg)",
xlabel = "Cₘₐₓ (ng/mL)/mg",
ylabel = "Cₘₐₓ",
title
),= (; color = main_colors),
palettes
)legend!(
end+1, 1:2],
fig_two_cov[
cmax;= :horizontal,
orientation = true,
tellheight = background,
bgcolor
) fig_two_cov
This plot lets us see that, on average, AUC
and Cₘₐₓ
decrease as the person’s weight increases, and that these metrics generally have a higher value for female subjects than for male subjects of the same weight.
9 Conclusions
We have given you a very broad overlook of Pumas’ plot customization capabilities for Noncompartmental analysis. We hope that you feel more comfortable with applying your custom color palette to your data visualization outputs and creating your custom layouts by mixing different plotting functions and creating your own with AlgebraOfGraphics.jl
and Makie.jl
.
We would like to end with an invitation to check our documentation on plotting. You’ll find that this is the most complete source of information for Pumas’ plotting capabilities. Don’t forget to check the documentation for Makie.jl
and our tutorials on data visualization in Julia to get more information on the available customization options for things like figures and axes.