From r-tidy-modeling
Comprehensive pharmacokinetic (PK) and pharmacodynamic (PD) modeling in R covering non-compartmental analysis (NCA), compartmental PK modeling, population PK with nonlinear mixed effects, bioequivalence assessment, PK/PD modeling, and drug-drug interaction evaluation.
npx claudepluginhub choxos/biostatagent --plugin r-tidy-modelingThis skill uses the workspace's default tool permissions.
Comprehensive pharmacokinetic (PK) and pharmacodynamic (PD) modeling in R covering non-compartmental analysis (NCA), compartmental PK modeling, population PK with nonlinear mixed effects, bioequivalence assessment, PK/PD modeling, and drug-drug interaction evaluation.
Creates new Angular apps using Angular CLI with flags for routing, SSR, SCSS, prefixes, and AI config. Follows best practices for modern TypeScript/Angular development. Use when starting Angular projects.
Generates Angular code and provides architectural guidance for projects, components, services, reactivity with signals, forms, dependency injection, routing, SSR, ARIA accessibility, animations, Tailwind styling, testing, and CLI tooling.
Provides UI/UX resources: 50+ styles, color palettes, font pairings, guidelines, charts for web/mobile across React, Next.js, Vue, Svelte, Tailwind, React Native, Flutter. Aids planning, building, reviewing interfaces.
Comprehensive pharmacokinetic (PK) and pharmacodynamic (PD) modeling in R covering non-compartmental analysis (NCA), compartmental PK modeling, population PK with nonlinear mixed effects, bioequivalence assessment, PK/PD modeling, and drug-drug interaction evaluation.
library(PKNCA)
# Prepare concentration data
conc_data <- data.frame(
subject = rep(1:3, each = 8),
time = rep(c(0, 0.5, 1, 2, 4, 8, 12, 24), 3),
concentration = c(
0, 450, 380, 290, 180, 85, 40, 12,
0, 520, 410, 320, 200, 95, 45, 15,
0, 480, 395, 305, 190, 90, 42, 13
)
)
# Prepare dose data
dose_data <- data.frame(
subject = 1:3,
time = 0,
dose = 100 # mg
)
# Create PKNCA objects
conc_obj <- PKNCAconc(concentration ~ time | subject, data = conc_data)
dose_obj <- PKNCAdose(dose ~ time | subject, data = dose_data)
# Combine into analysis object
data_obj <- PKNCAdata(conc_obj, dose_obj)
# Run NCA
results <- pk.nca(data_obj)
# View results
summary(results)
as.data.frame(results)
library(PKNCA)
# Specify intervals and parameters
intervals <- data.frame(
start = 0,
end = 24,
cmax = TRUE,
tmax = TRUE,
auclast = TRUE,
aucinf.obs = TRUE,
half.life = TRUE,
cl.obs = TRUE,
vss.obs = TRUE,
mrt.last = TRUE
)
# Create analysis with custom intervals
data_obj <- PKNCAdata(
conc_obj,
dose_obj,
intervals = intervals
)
results <- pk.nca(data_obj)
# Extract specific parameters
pk_summary <- summary(results)
# Individual subject parameters
individual_params <- as.data.frame(results) |>
tidyr::pivot_wider(
id_cols = subject,
names_from = PPTESTCD,
values_from = PPORRES
)
# Basic NCA calculations for a single subject
calculate_nca <- function(time, conc, dose) {
# Cmax and Tmax
cmax <- max(conc)
tmax <- time[which.max(conc)]
# AUC by linear trapezoidal rule
auc_last <- sum(diff(time) * (head(conc, -1) + tail(conc, -1)) / 2)
# Terminal phase (last 3 points for lambda_z)
n_terminal <- 3
terminal_idx <- (length(time) - n_terminal + 1):length(time)
terminal_time <- time[terminal_idx]
terminal_conc <- conc[terminal_idx]
# Lambda_z from log-linear regression
fit <- lm(log(terminal_conc) ~ terminal_time)
lambda_z <- -coef(fit)[2]
# Half-life
half_life <- log(2) / lambda_z
# AUC extrapolated to infinity
auc_extrap <- tail(conc, 1) / lambda_z
auc_inf <- auc_last + auc_extrap
# Clearance (CL/F for oral)
cl_f <- dose / auc_inf
# Volume of distribution (Vd/F)
vd_f <- cl_f / lambda_z
# Mean residence time
# AUMC (first moment)
aumc <- sum(diff(time) * (head(time * conc, -1) + tail(time * conc, -1)) / 2)
aumc_extrap <- tail(conc, 1) / lambda_z^2 + tail(time * conc, 1) / lambda_z
aumc_inf <- aumc + aumc_extrap
mrt <- aumc_inf / auc_inf
return(data.frame(
Cmax = cmax,
Tmax = tmax,
AUClast = auc_last,
AUCinf = auc_inf,
Lambda_z = lambda_z,
Half_life = half_life,
CL_F = cl_f,
Vd_F = vd_f,
MRT = mrt
))
}
library(ggplot2)
# Individual profiles (spaghetti plot)
ggplot(conc_data, aes(x = time, y = concentration, group = subject)) +
geom_line(alpha = 0.5) +
geom_point(alpha = 0.5) +
stat_summary(aes(group = 1), fun = mean, geom = "line",
color = "red", size = 1.5) +
stat_summary(aes(group = 1), fun = mean, geom = "point",
color = "red", size = 2) +
labs(x = "Time (hours)", y = "Concentration (ng/mL)",
title = "Concentration-Time Profile") +
theme_bw()
# Semi-log plot
ggplot(conc_data, aes(x = time, y = concentration, group = subject)) +
geom_line(alpha = 0.5) +
geom_point(alpha = 0.5) +
scale_y_log10() +
labs(x = "Time (hours)", y = "Concentration (ng/mL) - Log Scale",
title = "Semi-Logarithmic Concentration-Time Profile") +
theme_bw()
library(ggplot2)
library(dplyr)
# Calculate summary statistics
conc_summary <- conc_data |>
group_by(time) |>
summarise(
mean_conc = mean(concentration),
sd_conc = sd(concentration),
geom_mean = exp(mean(log(concentration + 0.001))),
n = n(),
se_conc = sd_conc / sqrt(n)
)
# Mean + SD plot
ggplot(conc_summary, aes(x = time, y = mean_conc)) +
geom_line(size = 1) +
geom_point(size = 2) +
geom_errorbar(aes(ymin = mean_conc - sd_conc,
ymax = mean_conc + sd_conc),
width = 0.5) +
labs(x = "Time (hours)",
y = "Concentration (ng/mL)",
title = "Mean (± SD) Concentration-Time Profile") +
theme_bw()
# Geometric mean plot
ggplot(conc_summary, aes(x = time, y = geom_mean)) +
geom_line(size = 1) +
geom_point(size = 2) +
scale_y_log10() +
labs(x = "Time (hours)",
y = "Geometric Mean Concentration (ng/mL)",
title = "Geometric Mean Concentration Profile") +
theme_bw()
library(mrgsolve)
# Define one-compartment model with first-order absorption
code_1cmt <- '
$PARAM CL = 10, V = 100, KA = 1.5
$CMT GUT CENT
$ODE
dxdt_GUT = -KA * GUT;
dxdt_CENT = KA * GUT - (CL/V) * CENT;
$TABLE
double CP = CENT / V;
$CAPTURE CP
'
# Compile model
mod_1cmt <- mcode("pk1cmt", code_1cmt)
# Single dose simulation
out <- mod_1cmt |>
ev(amt = 100, cmt = 1) |> # 100 mg oral dose
mrgsim(end = 24, delta = 0.1)
# Plot
plot(out, CP ~ time)
library(mrgsolve)
# Two-compartment model with central and peripheral
code_2cmt <- '
$PARAM CL = 10, V1 = 50, V2 = 100, Q = 15, KA = 1.2
$CMT GUT CENT PERIPH
$ODE
double K10 = CL / V1;
double K12 = Q / V1;
double K21 = Q / V2;
dxdt_GUT = -KA * GUT;
dxdt_CENT = KA * GUT - K10 * CENT - K12 * CENT + K21 * PERIPH;
dxdt_PERIPH = K12 * CENT - K21 * PERIPH;
$TABLE
double CP = CENT / V1;
$CAPTURE CP
'
mod_2cmt <- mcode("pk2cmt", code_2cmt)
# Simulate IV bolus
out_iv <- mod_2cmt |>
ev(amt = 100, cmt = 2) |> # IV bolus to central
mrgsim(end = 48, delta = 0.1)
plot(out_iv, CP ~ time, log = TRUE)
library(mrgsolve)
# Multiple dose regimen
dosing_regimen <- ev(
amt = 100,
ii = 12, # Dosing interval (hours)
addl = 6, # Additional doses
cmt = 1 # Oral
)
out_multi <- mod_1cmt |>
ev(dosing_regimen) |>
mrgsim(end = 96, delta = 0.1)
plot(out_multi, CP ~ time)
# Steady-state check
out_ss <- mod_1cmt |>
ev(amt = 100, ii = 12, addl = 100, cmt = 1, ss = 1) |>
mrgsim(end = 24, delta = 0.1)
library(nlmixr2)
# Define population PK model
one_cmt_pop <- function() {
ini({
# Fixed effects (thetas)
tka <- log(1) # Log Ka
tcl <- log(10) # Log CL
tv <- log(100) # Log V
# Random effects (omegas)
eta.ka ~ 0.6 # IIV on Ka
eta.cl ~ 0.3 # IIV on CL
eta.v ~ 0.1 # IIV on V
# Residual error
add.err <- 0.1 # Additive
prop.err <- 0.1 # Proportional
})
model({
# Individual parameters
ka <- exp(tka + eta.ka)
cl <- exp(tcl + eta.cl)
v <- exp(tv + eta.v)
# Differential equations
d/dt(depot) <- -ka * depot
d/dt(central) <- ka * depot - cl/v * central
# Concentration
cp <- central / v
# Combined error model
cp ~ add(add.err) + prop(prop.err)
})
}
# Prepare data (nlmixr2 format)
pk_data <- data.frame(
ID = rep(1:10, each = 8),
TIME = rep(c(0, 0.5, 1, 2, 4, 8, 12, 24), 10),
DV = rnorm(80, 100, 20), # Observed concentrations
AMT = c(rep(c(100, rep(0, 7)), 10)),
EVID = c(rep(c(1, rep(0, 7)), 10)),
CMT = 1
)
# Fit model using SAEM
fit <- nlmixr2(one_cmt_pop, pk_data, est = "saem",
control = saemControl(print = 50))
# Summary
summary(fit)
# Parameter estimates
fit$parFixed # Fixed effects
fit$omega # Random effects variance
fit$sigma # Residual error
# Individual predictions
fit$ipred
library(nlmixr2)
# Model with covariates (weight, age)
cov_model <- function() {
ini({
tka <- log(1)
tcl <- log(10)
tv <- log(100)
# Covariate effects
cl_wt <- 0.75 # Allometric on CL
v_wt <- 1 # Allometric on V
eta.ka ~ 0.6
eta.cl ~ 0.3
eta.v ~ 0.1
add.err <- 0.1
prop.err <- 0.1
})
model({
# Covariate-adjusted parameters
ka <- exp(tka + eta.ka)
cl <- exp(tcl + eta.cl) * (WT/70)^cl_wt
v <- exp(tv + eta.v) * (WT/70)^v_wt
d/dt(depot) <- -ka * depot
d/dt(central) <- ka * depot - cl/v * central
cp <- central / v
cp ~ add(add.err) + prop(prop.err)
})
}
# Fit with covariates
fit_cov <- nlmixr2(cov_model, pk_data_with_cov, est = "saem")
library(nlmixr2)
library(xpose.nlmixr2)
# Create xpose database
xpdb <- xpose_data_nlmixr2(fit)
# Goodness-of-fit plots
dv_vs_pred(xpdb) # DV vs PRED
dv_vs_ipred(xpdb) # DV vs IPRED
res_vs_pred(xpdb) # CWRES vs PRED
res_vs_idv(xpdb) # CWRES vs TIME
# Individual plots
ind_plots(xpdb, nrow = 3, ncol = 3)
# VPC (Visual Predictive Check)
vpc_result <- vpc(fit, n = 500)
plot(vpc_result)
library(BE)
# 2x2 crossover BE study data
be_data <- data.frame(
subj = rep(1:24, each = 2),
seq = rep(rep(c("TR", "RT"), each = 12), each = 2),
prd = rep(c(1, 2), 24),
trt = c(rep(c("T", "R"), 12), rep(c("R", "T"), 12)),
AUClast = rlnorm(48, log(100), 0.3),
Cmax = rlnorm(48, log(50), 0.3)
)
# BE analysis
be_result <- test2x2(be_data, "AUClast")
print(be_result)
# 90% CI for geometric mean ratio
be_result$ci # Should be within 80-125%
# Multiple endpoints
be_auc <- test2x2(be_data, "AUClast")
be_cmax <- test2x2(be_data, "Cmax")
library(nlme)
# ANOVA for 2x2 crossover
be_data$log_auc <- log(be_data$AUClast)
# Linear mixed effects model
fit_be <- lme(
log_auc ~ seq + prd + trt,
random = ~1 | subj,
data = be_data
)
# Treatment effect
trt_effect <- fixef(fit_be)["trtT"]
# 90% CI
se <- sqrt(vcov(fit_be)["trtT", "trtT"])
ci_lower <- exp(trt_effect - qt(0.95, df = 22) * se) * 100
ci_upper <- exp(trt_effect + qt(0.95, df = 22) * se) * 100
gmr <- exp(trt_effect) * 100
cat("GMR (T/R):", round(gmr, 2), "%\n")
cat("90% CI:", round(ci_lower, 2), "-", round(ci_upper, 2), "%\n")
# Bioequivalence conclusion
if (ci_lower >= 80 & ci_upper <= 125) {
cat("Conclusion: Bioequivalent\n")
} else {
cat("Conclusion: Not bioequivalent\n")
}
# Sample size calculation for BE study
be_sample_size <- function(cv, gmr = 1, alpha = 0.05, power = 0.80,
lower = 0.80, upper = 1.25) {
# Approximate formula for 2x2 crossover
se <- sqrt(2) * cv # Within-subject CV
delta <- log(gmr)
theta <- log(upper)
# Effect size
z_alpha <- qnorm(1 - alpha)
z_beta <- qnorm(power)
# Sample size per sequence
n <- 2 * ((z_alpha + z_beta) * se / (theta - abs(delta)))^2
return(ceiling(n) * 2) # Total subjects
}
# Example: CV = 30%, GMR = 1.0
n_total <- be_sample_size(cv = 0.30, gmr = 1.0)
cat("Required sample size:", n_total, "subjects\n")
# Using PowerTOST package
library(PowerTOST)
# Sample size for 2x2 crossover
sampleN.TOST(
CV = 0.30,
theta0 = 0.95, # Expected GMR
theta1 = 0.80, # Lower BE limit
theta2 = 1.25, # Upper BE limit
targetpower = 0.80,
design = "2x2"
)
library(mrgsolve)
# PK/PD with Emax effect
code_pkpd <- '
$PARAM CL = 10, V = 100, KA = 1.5
$PARAM EMAX = 100, EC50 = 50, GAMMA = 1, E0 = 10
$CMT GUT CENT
$ODE
dxdt_GUT = -KA * GUT;
dxdt_CENT = KA * GUT - (CL/V) * CENT;
$TABLE
double CP = CENT / V;
double EFFECT = E0 + EMAX * pow(CP, GAMMA) / (pow(EC50, GAMMA) + pow(CP, GAMMA));
$CAPTURE CP EFFECT
'
mod_pkpd <- mcode("pkpd_emax", code_pkpd)
out_pkpd <- mod_pkpd |>
ev(amt = 100, cmt = 1) |>
mrgsim(end = 24, delta = 0.1)
# Plot PK and PD
library(ggplot2)
out_df <- as.data.frame(out_pkpd)
p1 <- ggplot(out_df, aes(time, CP)) +
geom_line() +
labs(y = "Concentration", title = "PK")
p2 <- ggplot(out_df, aes(time, EFFECT)) +
geom_line() +
labs(y = "Effect", title = "PD")
library(patchwork)
p1 / p2
library(mrgsolve)
# Indirect response model (inhibition of production)
code_indirect <- '
$PARAM CL = 10, V = 100, KA = 1.5
$PARAM KIN = 10, KOUT = 0.1, IC50 = 50, IMAX = 1
$CMT GUT CENT RESP
$MAIN
RESP_0 = KIN / KOUT; // Baseline response
$ODE
double CP = CENT / V;
double INH = 1 - IMAX * CP / (IC50 + CP); // Inhibition function
dxdt_GUT = -KA * GUT;
dxdt_CENT = KA * GUT - (CL/V) * CENT;
dxdt_RESP = KIN * INH - KOUT * RESP;
$CAPTURE CP RESP
'
mod_indirect <- mcode("indirect", code_indirect)
out_indirect <- mod_indirect |>
ev(amt = 100, cmt = 1) |>
mrgsim(end = 72, delta = 0.1)
plot(out_indirect, RESP ~ time)
library(mrgsolve)
# DDI model with competitive inhibition
code_ddi <- '
$PARAM
// Victim drug parameters
CLv = 10, Vv = 100, KAv = 1
// Perpetrator parameters
CLp = 5, Vp = 50, KAp = 0.5
// Interaction parameters
KI = 20, FM = 0.8 // Ki and fraction metabolized by affected pathway
$CMT GUT_V CENT_V GUT_P CENT_P
$ODE
double CPv = CENT_V / Vv;
double CPp = CENT_P / Vp;
// Inhibition factor
double INH = 1 / (1 + CPp / KI);
// Effective clearance
double CL_eff = CLv * (1 - FM + FM * INH);
dxdt_GUT_V = -KAv * GUT_V;
dxdt_CENT_V = KAv * GUT_V - (CL_eff/Vv) * CENT_V;
dxdt_GUT_P = -KAp * GUT_P;
dxdt_CENT_P = KAp * GUT_P - (CLp/Vp) * CENT_P;
$TABLE
double CPvictim = CENT_V / Vv;
double CPperp = CENT_P / Vp;
$CAPTURE CPvictim CPperp
'
mod_ddi <- mcode("ddi", code_ddi)
# Victim alone
out_alone <- mod_ddi |>
ev(amt = 100, cmt = 1) |>
mrgsim(end = 24, delta = 0.1)
# Victim + perpetrator
out_ddi <- mod_ddi |>
ev(amt = 100, cmt = 1, time = 0) |>
ev(amt = 200, cmt = 3, time = 0) |>
mrgsim(end = 24, delta = 0.1)
# Calculate AUC ratio (DDI magnitude)
auc_alone <- sum(out_alone$CPvictim) * 0.1
auc_ddi <- sum(out_ddi$CPvictim) * 0.1
auc_ratio <- auc_ddi / auc_alone
cat("AUC ratio (DDI/alone):", round(auc_ratio, 2), "\n")
# Standard PK parameter summary table
create_pk_summary <- function(pk_params) {
pk_params |>
group_by(parameter) |>
summarise(
N = n(),
Mean = mean(value),
SD = sd(value),
CV_percent = SD / Mean * 100,
Median = median(value),
Min = min(value),
Max = max(value),
Geom_Mean = exp(mean(log(value))),
Geom_CV = sqrt(exp(var(log(value))) - 1) * 100
) |>
mutate(across(where(is.numeric), ~round(., 3)))
}
# Format for regulatory submission
format_pk_table <- function(summary_df) {
summary_df |>
mutate(
`Mean (SD)` = paste0(round(Mean, 2), " (", round(SD, 2), ")"),
`Median [Min, Max]` = paste0(round(Median, 2), " [",
round(Min, 2), ", ", round(Max, 2), "]"),
`Geom Mean (Geom CV%)` = paste0(round(Geom_Mean, 2), " (",
round(Geom_CV, 1), "%)")
) |>
select(parameter, N, `Mean (SD)`, `Median [Min, Max]`,
`Geom Mean (Geom CV%)`)
}
| Package | Purpose |
|---|---|
| PKNCA | Non-compartmental analysis |
| mrgsolve | ODE-based PK/PD simulation |
| nlmixr2 | Population PK modeling (NLME) |
| rxode2 | ODE solver, nlmixr2 backend |
| BE | Bioequivalence analysis |
| PowerTOST | BE sample size and power |
| pk | Classical PK calculations |
| NonCompart | NCA calculations |
| xpose.nlmixr2 | Diagnostic plots for nlmixr2 |
| vpc | Visual predictive checks |