Perform Global Sensitivity Analysis using Morris Method
This RMarkdown file creates and executes PSA run with user-defined set of variables data points. The script utilizes the gastroPlusAPI package to communicate with the GastroPlus X 10.2 service. It demonstrates how to perform a global sensitivity analysis (GSA) using the Morris method (also known as Elementary Effects method) with GastroPlus. The script identifies which input variables have the greatest influence on model outputs, helping to prioritize further investigations and optimize model development.
The Morris method is particularly effective for PBPK models with numerous input parameters and non-linear behaviors. It systematically perturbs one input at a time across multiple trajectories, calculating elementary effects to estimate:
μ*: the mean absolute effect, indicating overall parameter influence.
σ: the standard deviation, reflecting non-linearity and interactions.
This approach requires fewer model evaluations than variance-based methods, making it suitable for complex PBPK models.Studies have shown that the Morris method can effectively identify influential parameters, aiding in model simplification and optimization ([@ROBINS2024106881, @McNally_2011]) .
To customize your study with this script for a different simulation / project / variables, please make changes to the “Set Input Information” section.
Configure required packages
Load other necessary packages required to execute the script (tidyverse, plotly,corrplot and sensitivity)
Load gastroPlusAPI package
library(tidyverse)
library(gastroPlusAPI)
library(plotly)
library(corrplot)
library(sensitivity)
Set working directory
Set working directory as the current source editor context
if (rstudioapi::isAvailable()){
current_working_directory <- dirname(rstudioapi::getSourceEditorContext()$path)
setwd(current_working_directory)
}
Start GPX Service
Establishes a connection to the GastroPlus service, which allows R to communicate with GastroPlus through its API
gpx_service <- start_service(verbose=FALSE)
✔ Configured the GastroPlus Service
gpx_service$is_alive()
[1] TRUE
Setup Input Information
Open an existing GastroPlus project,create a PSA run named global_sensitivity_analysis. Specify variables to include in the sensitivity analysis (variable_names). Defines upper and lower bounds for each parameter based on domain knowledge or default scaling. Prepare a data frame (psa_parameters_df) used to sample parameter space via Morris method.
Make modification to the variables in this chunk to customize your analysis.
project_path: Location of the project where PSA run will be created
simulation_name: Name of the simulation for the PSA run
run_name: Name of the PSA run that will be created
Create a run in GPX to utilize the values provided by morris method and retrieve corresponding output for further analysis (next section)
Variables required for Morris method (refer morris method from sensitivity package):
num_of_repetition_per_factor: Number of elementary effects computed per factor / variable
num_of_levels: Number of levels of the OAT (One-At-a-Time) design. Higher values give finer resolution but increase computational cost
variable_names: Vector/list containing the variable information required to setup the PSA run in GPX project. Variable names can be obtained after opening the project (open_project function) and then calling the get_variables function
variable_information: Vector/list containing the variable information where the variable_names of the parameters on which the GSA is defined
psa_parameters_df: define the upper and lower bound of the GSA, in the case the bounds are specific pass them directly to the variable name as seen for “Stomach | Lumen | pH” otherwise, set the lower and uper bound to the baseline values of the parameters as seen for other parameters. The variable bounds will be used to generate the set of points to be sampled in the design space
output_variable: The model output variable whose sensitivity to input is to be calculated (possible values are “cmax”, “tmax”, absorbed”, “bioavailable”, “portal_vein” “infinite_exposure”, “total_exposure”, liver_cmax” )
project_path = "../../ProjectFiles/GPX Library.gpproject"
simulation_name = "Atenolol 100mg PO tablet"
run_name = "global_sensitivity_analysis"
open_project(project_path)
#Create new PSA run
create_run(run_name, RunType$ParameterSensitivityAnalysis)
#Add simulations to the run
add_simulations_to_run(run_name, c(simulation_name))
# confirm that the simulation was added to the run.
get_simulations_in_run(run_name)
[[1]]
[1] "Atenolol 100mg PO tablet"
variable_names = c("Stomach | Lumen | pH",
"Small Intestine Transit Time",
"Ileum1 | Surface Area Enhancement Factor",
"Stomach | Transit Time",
"Small Intestine Length",
"Solubility",
"Particle | Density")
variable_information <- get_variable_information(
run_name = run_name,
simulation_name = simulation_name,
variable_names = variable_names
)
glimpse(variable_information)
Rows: 7
Columns: 4
$ input_variable_name <chr> "Ileum1 | Surface Area Enhancement Factor", "Parti…
$ scalar_type <chr> "Unitless", "Concentration", "Length", "Time", "Co…
$ variable_name <chr> "pharma::PhysiologyRegimen-78kg HumanFasted Physio…
$ variable_value <df[,2]> <data.frame[7 x 2]>
variable_information <- variable_information %>%
unnest(cols = variable_value)
#define the upper and lower bound of the GSA, in the case the bounds are specific pass them directly to the variable name as seen for "Stomach | Lumen | pH" otherwise, set the lower and upper bound to the baseline values of the parameters as seen for other parameters.
psa_parameters_df <- variable_information %>%
transmute(
variable_name = input_variable_name,
baseline_value = value,
lower_bound = case_when(
variable_name == "Stomach | Lumen | pH" ~ 0.1 ,
TRUE ~ 0.25 * baseline_value
),
upper_bound = case_when(
variable_name == "Stomach | Lumen | pH" ~ 3,
TRUE ~ 1.75 * baseline_value
),
unit = unit,
scalar_type = scalar_type
)
#Define parameters for morris GSA
num_of_repetition_per_factor <- 20
num_of_levels <- 25
output_variable <- "absorbed"
cli::cli_alert_info("Specified input information is as follows")
ℹ Specified input information is as follows
glimpse(variable_information)
Rows: 7
Columns: 5
$ input_variable_name <chr> "Ileum1 | Surface Area Enhancement Factor", "Parti…
$ scalar_type <chr> "Unitless", "Concentration", "Length", "Time", "Co…
$ variable_name <chr> "pharma::PhysiologyRegimen-78kg HumanFasted Physio…
$ unit <chr> "[no units]", "g/mL", "m", "s", "g/m³", "[no units…
$ value <dbl> 3.029, 1.200, 3.250, 11880.000, 27000.000, 1.300, …
Configure Global Sensitvity Analysis to generate design matrix
Based on the variable input information provided, morris method will sample the datapoints for each variable specified. The code uses the morris() function from the R sensitivity package to sample the input space with One-At-a-Time (OAT) perturbations, create a design matrix containing parameter values for each parameter in the simulation. The design follows an OAT approach where one parameter changes at a time along trajectory.
# Morris method will generate the design matrix
morris_results <- morris(
model = NULL,
factors = psa_parameters_df$variable_name,
r = num_of_repetition_per_factor, # of trajectories
design = list(type = "oat", levels = num_of_levels, grid.jump = floor(num_of_levels/2)),
binf = psa_parameters_df$lower_bound,
bsup = psa_parameters_df$upper_bound,
scale = TRUE
)
design_matrix <- as_tibble(morris_results$X)
#Determine number of user defined simulations
number_of_user_defined_simulations <- nrow(design_matrix)
cli::cli_alert_info(
"{number_of_user_defined_simulations} simulations will be executed as part of the PSA run in GPX to determine the elementary effects of input variables on output variable")
ℹ 160 simulations will be executed as part of the PSA run in GPX to determine the elementary effects of input variables on output variable
Run the simulations in GPX
Utilize the values provided by morris method to the PSA configuration using user-defined values, and link the design matrix to GastroPlus input variables.Configure and execute the run to retrieve corresponding output for further analysis.
#Setup PSA Parameters List
psa_parameter_list <- list()
count = 1
for(var in psa_parameters_df$variable_name){
var_info <- variable_information %>% filter(input_variable_name == var)
input_info <- psa_parameters_df %>% filter(variable_name == var)
scalar_values_list <- list()
design_matrix_variable_values <- design_matrix %>% pull(var)
i=1
for(value in design_matrix_variable_values) {
scalar_value <- ScalarValue$new(value, input_info$unit)
scalar_values_list[[i]] <- scalar_value
i = i+1
}
psa_parameter_list[[count]] <- PSAParameter$new(
variable_name = var,
baseline_value = var_info$value,
unit = input_info$unit,
scalar_type = var_info$scalar_type,
number_of_tests = number_of_user_defined_simulations,
user_defined_values = scalar_values_list
)
count = count + 1
}
#Setup PSA Setting
psa_setting <- PSASetting$new(
analysis_mode = ParameterSensitivityAnalysisMode$UserDefined,
simulate_baseline = FALSE,
update_Kp = FALSE,
number_of_user_defined_simulations = number_of_user_defined_simulations)
#Create PSA Configuration object
PSA_configuration <- PSAConfiguration$new(PSA_Setting = psa_setting,
PSA_Parameters = psa_parameter_list)
#Create Run Configuration object
run_configuration <- RunConfiguration$new(PSA_Configuration = PSA_configuration)
#Configure the run in GPX
configure_run(run_name, run_configuration, use_full_variable_name = FALSE)
#Execute Run
execute_run(run_name)
Compile Run Output for analysis
Retrieve simulation output summaries from GastroPlus (get_simulation_keys() and get_summary_output()). Get the values for the variables in simulation iterations present in the run (get_iteration_records()). Produce a unified data frame for GSA analysis.
simulation_keys <- get_simulation_keys(run_name)
summary_output <- get_summary_output(run_name, simulation_keys$simulation_iteration_key_name)
summary_output_variable_names <- colnames(summary_output)
summary_output$simulation_iteration_key <- simulation_keys$simulation_iteration_key_name
iteration_records <- get_iteration_records(run_name, unlist(simulation_keys$simulation_iteration_key_name), variable_information$input_variable_name)
#Re-structure iteration records so that it can be merged with summary output
iteration_records <- iteration_records %>%
tidyr::unnest(cols = where(is.list)) %>%
tidyr::pivot_wider(id_cols=simulation_iteration_key,
names_from=variable,
values_from=variable_value)
#Merge the two tibbles
input_output_merged <- dplyr::full_join(iteration_records, summary_output, by="simulation_iteration_key") %>%
unnest(cols=all_of(summary_output_variable_names), names_sep= " ") %>%
unnest(cols=all_of(unlist(variable_information$input_variable_name)), names_sep=" ")
glimpse(input_output_merged)
Rows: 160
Columns: 37
$ simulation_iteration_key <chr> "Atenolol 100mg PO ta…
$ `Ileum1 | Surface Area Enhancement Factor unit` <chr> "[no units]", "[no un…
$ `Ileum1 | Surface Area Enhancement Factor value` <dbl> 0.9466, 0.9466, 3.218…
$ `Particle | Density unit` <chr> "g/m³", "g/m³", "g/m³…
$ `Particle | Density value` <dbl> 1350000, 1350000, 135…
$ `Small Intestine Length unit` <chr> "m", "m", "m", "m", "…
$ `Small Intestine Length value` <dbl> 3.4531, 3.4531, 3.453…
$ `Small Intestine Transit Time unit` <chr> "s", "s", "s", "s", "…
$ `Small Intestine Transit Time value` <dbl> 8910.0, 17820.0, 1782…
$ `Solubility unit` <chr> "g/m³", "g/m³", "g/m³…
$ `Solubility value` <dbl> 27000.0, 27000.0, 270…
$ `Stomach | Lumen | pH unit` <chr> "[no units]", "[no un…
$ `Stomach | Lumen | pH value` <dbl> 0.3417, 0.3417, 0.341…
$ `Stomach | Transit Time unit` <chr> "s", "s", "s", "s", "…
$ `Stomach | Transit Time value` <dbl> 1575, 1575, 1575, 157…
$ absorbed <dbl> 0.3094681, 0.5026083,…
$ bioavailable <dbl> 0.2983507, 0.4920607,…
$ `clearance unit` <chr> "L/h", "L/h", "L/h", …
$ `clearance value` <dbl> 37.01235, 22.50756, 2…
$ `cmax unit` <chr> "ng/mL", "ng/mL", "ng…
$ `cmax value` <dbl> 256.7865, 374.1147, 4…
$ compound <chr> "Atenolol", "Atenolol…
$ `infinite_exposure unit` <chr> "(ng/mL)*h", "(ng/mL)…
$ `infinite_exposure value` <dbl> 2701.801, 4442.952, 4…
$ iteration <int> 0, 1, 2, 3, 4, 5, 6, …
$ `liver_cmax unit` <chr> "ng/mL", "ng/mL", "ng…
$ `liver_cmax value` <dbl> 296.2553, 423.8012, 4…
$ portal_vein <dbl> 0.2983507, 0.4920607,…
$ run_simulation_iteration_key <chr> "global_sensitivity_a…
$ simulation_iteration_key_name <chr> "Atenolol 100mg PO ta…
$ simulation_iteration_key_name_2 <chr> "Atenolol 100mg PO ta…
$ `tmax unit` <chr> "h", "h", "h", "h", "…
$ `tmax value` <dbl> 3.709168, 4.517222, 4…
$ `total_dose unit` <chr> "mg", "mg", "mg", "mg…
$ `total_dose value` <dbl> 100, 100, 100, 100, 1…
$ `total_exposure unit` <chr> "(ng/mL)*h", "(ng/mL)…
$ `total_exposure value` <dbl> 2581.972, 4229.401, 4…
Get Morris sensitivity analysis results
Get and plot GSA results from Morris method.
A higher mean (mu.star) indicates that the parameter has a higher influence on the output of the model, while a higher standard deviation (sigma) indicates higher interaction of the parameter with other parameters
if (output_variable %in% colnames(input_output_merged)) {
output_values <- input_output_merged %>% pull(output_variable)
} else {
column_of_interest <- paste(output_variable, "value", sep = " ")
output_values <- input_output_merged %>% pull(column_of_interest)
}
morris_results <- tell(morris_results, output_values)
mu <- apply(morris_results$ee, 2, mean)
mu.star <- apply(morris_results$ee, 2, function(x) mean(abs(x)))
sigma <- apply(morris_results$ee, 2, sd)
# If you have bootstrap replicates or multiple runs:
mu.star.se <- apply(morris_results$ee, 2, function(x) sd(abs(x))/sqrt(length(x)))
sigma.se <- apply(morris_results$ee, 2, sd)/sqrt(nrow(morris_results$ee))
df <- data.frame(mu.star = mu.star, sigma = sigma, mu = mu,mu.star.se=mu.star.se, sigma.se=sigma.se, factors = variable_information$input_variable_name)
#to help mapping the annotations for sigma/mu
x_range <- range(df$mu.star, na.rm = TRUE)
y_range <- range(df$sigma, na.rm = TRUE)
max_coord <- max(c(x_range[2], y_range[2]))
# Define parameters of abline at once at the top
ratio_slopes <- c(0.1, 0.5, 1)
ratio_colors <- c("grey60", "grey50", "grey40")
ratio_linetypes <- c("dotted", "dashed", "solid")
ratio_sizes <- c(0.8, 0.8, 0.9)
#plot
ggplot(df, aes(x = mu.star, y = sigma)) +
geom_point(aes(fill = factors),
shape = 21,
size = 4,
alpha = 0.8,
stroke = 0.8,
color = "white") +
# Error bars
geom_errorbar(aes(ymin = pmax(0, sigma - sigma.se),
ymax = sigma + sigma.se),
width = max(df$mu.star) * 0.015,
color = "grey30",
alpha = 0.7,
size = 0.5) +
geom_errorbarh(aes(xmin = pmax(0, mu.star - mu.star.se),
xmax = mu.star + mu.star.se),
height = max(df$sigma) * 0.015,
color = "grey30",
alpha = 0.7,
size = 0.5) + #using imap to mapp at the predefined ablines
imap(ratio_slopes, ~geom_abline(slope = .x, intercept = 0,
color = ratio_colors[.y],
linetype = ratio_linetypes[.y],
size = ratio_sizes[.y],
alpha = 0.8)) +
scale_fill_discrete(name = "Variable name") +
{
x_positions <- max_coord * c(0.85, 0.75, 0.65)
y_positions <- x_positions * ratio_slopes + max_coord * c(0.02, 0.03, 0.04)
angles <- c(atan(0.1) * 180/pi, atan(0.5) * 180/pi, 45)
labels <- paste0("σ/μ* = ", ratio_slopes)
imap(ratio_slopes, ~annotate("text",
x = x_positions[.y],
y = y_positions[.y],
label = labels[.y],
color = ratio_colors[.y],
fontface = "bold",
size = 3.2,
angle = angles[.y],
hjust = 0.5))
} + # Labels and styling
labs(title = paste("Morris Sensitivity Analysis:", output_variable),
subtitle = "Reference lines show σ/μ* ratios • Higher ratios indicate more non-linear effects",
x = "μ* (Mean of |Elementary Effects|)",
y = "σ (Standard Deviation of Elementary Effects)",
caption = "Dotted: σ/μ* = 0.1 • Dashed: σ/μ* = 0.5 • Solid: σ/μ* = 1") +
theme_minimal() +
theme(
plot.title = element_text(size = 16, face = "bold", hjust = 0.5),
plot.subtitle = element_text(size = 11, hjust = 0.5, color = "grey40"),
plot.caption = element_text(size = 9, hjust = 0.5, color = "grey50"),
axis.title = element_text(size = 12, face = "bold"),
axis.text = element_text(size = 10),
panel.grid.minor = element_blank(),
panel.grid.major = element_line(color = "grey90", size = 0.3),
legend.position = "right",
legend.title = element_text(size = 10, face = "bold"),
legend.text = element_text(size = 9),
legend.box = "vertical",
aspect.ratio = 1
) +
# Set equal axis limits
coord_cartesian(xlim = c(0, max_coord * 1.05), ylim = c(0, max_coord * 1.05))
Warning: Using `size` aesthetic for lines was deprecated in ggplot2 3.4.0.
ℹ Please use `linewidth` instead.
Warning: The `size` argument of `element_line()` is deprecated as of ggplot2 3.4.0.
ℹ Please use the `linewidth` argument instead.

Kill GPX Service
Delete the assets created (delete_run()) or aternatively persist the changes in the project (save_project()) and then kill the GPX service after the script is executed
#delete_run(run_name)
#save_project()
gpx_service$kill()
[1] TRUE
References