[PK/PD] PK Table and Figure

Author

SEOYEON CHOI

Published

April 9, 2025

Project

1

[Project Title] Phase 1 임상시험 PK 분석

[Project Overview] 비구획 분석(Non-Compartmental Analysis, NCA)을 포함한 약동학(PK) 분석 수행, PK 파라미터 계산 및 임상시험 데이터 기반 통계 분석 수행

[My role] - 연구 계획서 기반 통계 분석 계획서/보고서(SAP/SAR) 작성 - SDTM datasets 기반 ADaM datasets 생성 - 기술 통계 및 추론 통계 분석 수행 - mixed-effects models 기반 PK 분석 수행 - 임상시험 결과 보고서(CSR)를 위한 TFLs 생성

[Analysis Workflow Diagram] - 분석 데이터 준비 → 통계 분석 → 시각화/TFLs 생성 → 통계 분석 보고서 작성

[Tools] SAS | R | Pheonix Winnonlin

2

[Project Title] 제 1상 임상시험에서 Drug–Drug Interaction (DDI) 분석

[Project Overview] Phase I DDI 임상시험에서 약물 간 상호작용이 PK 파라미터에 미치는 영향 분석, PK 기반 DDI 효과 추정 및 약력학(PD) 지표 평가

[My role] - 연구 계획서 기반 통계 분석 계획서/보고서(SAP/SAR) 작성 - SDTM datasets 기반 ADaM datasets 생성 - 기술 통계 및 추론 통계 분석 수행 - mixed-effects models 기반 PK 분석 수행 - PD 평가 지표 산출 - 임상시험 결과 보고서(CSR)를 위한 TFLs 생성

[Analysis Workflow Diagram] - 분석 데이터 준비 → 통계 분석 → 시각화/TFLs 생성 → 통계 분석 보고서 작성

[Tools] SAS | R | Pheonix Winnonlin

Data_Table

set.seed(1212)
n <- 24
compounded <- rlnorm(n, meanlog = log(100), sdlog = 0.1)
generic    <- rlnorm(n, meanlog = log(95),  sdlog = 0.1)
log_comp <- log(compounded)
log_gen  <- log(generic)
mean_diff <- mean(log_gen) - mean(log_comp)
se_diff <- sqrt(var(log_gen)/n + var(log_comp)/n)
gmr <- exp(mean_diff)

90% CI

(z = 1.645 for two-sided 90% CI)

z <- 1.645
ci_lower <- exp(mean_diff - z * se_diff)
ci_upper <- exp(mean_diff + z * se_diff)
diff_raw <- mean(generic) - mean(compounded)

Result

cat("GMR:", round(gmr, 2), "\n")
cat("90% CI:", round(ci_lower, 2), "~", round(ci_upper, 2), "\n")
cat("Difference (raw scale):", round(diff_raw, 2), "\n")
cat("SE (log scale):", round(se_diff, 4), "\n")
GMR: 0.93 
90% CI: 0.88 ~ 0.97 
Difference (raw scale): -7.56 
SE (log scale): 0.0311 

Data_Figure

library(dplyr)
library(ggplot2)
set.seed(12123)

n_subjects <- 10
time_points <- c(0.5, 1, 2, 4, 6, 8)

subjects <- 1:n_subjects
seq_A <- sample(subjects, 5) 
seq_B <- setdiff(subjects, seq_A) 

generate_subject_data <- function(id, seq_group) {
  if (seq_group == "A") {
    data.frame(
      subject_id = id,
      period = rep(c(1, 2), each = length(time_points)),
      treatment = rep(c("Compounded", "Generic"), each = length(time_points)),
      time = rep(time_points, times = 2)
    )
  } else {
    data.frame(
      subject_id = id,
      period = rep(c(1, 2), each = length(time_points)),
      treatment = rep(c("Generic", "Compounded"), each = length(time_points)),
      time = rep(time_points, times = 2)
    )
  }
}

pk_df <- do.call(rbind, lapply(seq_A, function(id) generate_subject_data(id, "A")))
pk_df <- rbind(pk_df, do.call(rbind, lapply(seq_B, function(id) generate_subject_data(id, "B"))))

pk_df$conc <- with(pk_df, rlnorm(nrow(pk_df),
                                 meanlog = ifelse(treatment == "Compounded",
                                                  log(50 - 5 * time),
                                                  log(48 - 5 * time)),
                                 sdlog = 0.2))
options(repr.plot.width = 10, repr.plot.height = 6)
p <- ggplot(pk_df %>% filter(subject_id == 1), 
       aes(x = time, y = conc, color = treatment)) +
  geom_line(alpha = 0.8, linewidth = 1.2) +
  geom_point(size = 3) +
  labs(title = "Spaghetti Plot of PK Profile (Subject 1)",
       x = "Time (hr)", y = "Concentration (ng/mL)", color = "Treatment") +
  theme_minimal() +
  theme(
    legend.position = c(0.85, 0.9),
    legend.background = element_rect(fill = "white", color = "gray80"),
    legend.title = element_text(size = 10),
    legend.text = element_text(size = 9)
  )
p

# ggsave("subject1_pk_plot.png", plot = p, width = 10, height = 6, dpi = 300)
options(repr.plot.width = 8, repr.plot.height = 8)
compute_auc <- function(time, conc) {
  sum(diff(time) * (head(conc, -1) + tail(conc, -1)) / 2)
}

auc_df <- pk_df %>%
  group_by(subject_id, period, treatment) %>%
  summarise(
    AUC = compute_auc(time, conc),
    .groups = "drop"
  ) %>%
  mutate(log_AUC = log(AUC))
p <- ggplot(auc_df, aes(x = treatment, y = log_AUC, fill = treatment)) +
  geom_boxplot(alpha = 0.7) +
  labs(title = "Log-transformed AUC by Treatment",
       x = "Treatment", y = "log(AUC)") +
  theme_minimal() +
  theme(legend.position = "none")
p

# ggsave("auc_plot.png", plot = p, width = 8, height = 8, dpi = 300)

Reference

[1] PK Table Example

[2] AUC, Cmax Boxplot


library(ggplot2)
library(dplyr)
library(tidyr)
two_compartment_model <- function(t, ka, ke, Vc, Vp, dose) {
  Cp <- dose / (Vc + Vp) * (ka / (ka - ke)) * (exp(-ke * t) - exp(-ka * t))
  return(Cp)
}
# Time
t <- seq(0, 24, length.out = 20)
n_subjects <- 36
set.seed(1212)
seq <- sample(rep(c("1", "2"), each = 18))

ka1 <- runif(n_subjects, 0.2, 0.8)
ke1 <- runif(n_subjects, 0.05, 0.15)
Vc1 <- runif(n_subjects, 8, 12)
Vp1 <- runif(n_subjects, 3, 7)
dose <- 100
df1 <- data.frame()
for (i in 1:n_subjects) {
  Cp <- two_compartment_model(t, ka1[i], ke1[i], Vc1[i], Vp1[i], dose)
  temp <- data.frame(
    Subject = paste0("S", sprintf("%02d", i)),
    Time = t,
    Concentration = Cp,
    Period = "Period 1",
    Sequence = seq[i]
  )
  df1 <- rbind(df1, temp)
}
ka2 <- runif(n_subjects, 0.2, 0.8)
ke2 <- runif(n_subjects, 0.05, 0.15)
Vc2 <- runif(n_subjects, 8, 12)
Vp2 <- runif(n_subjects, 3, 7)
df2 <- data.frame()
for (i in 1:n_subjects) {
  Cp <- two_compartment_model(t, ka2[i], ke2[i], Vc2[i], Vp2[i], dose)
  temp <- data.frame(
    Subject = paste0("S", sprintf("%02d", i)),
    Time = t,
    Concentration = Cp,
    Period = "Period 2",
    Sequence = seq[i]
  )
  df2 <- rbind(df2, temp)
}
df_all <- bind_rows(df1, df2)
options(repr.plot.width = 12, repr.plot.height = 8)
p <- ggplot(df_all, aes(x = Time, y = Concentration, group = Subject)) +
  geom_line(alpha = 0.6) +
  facet_wrap(~ Period) +
  labs(title = "Pharmacokinetic Concentration Profile (36 Subjects)",
       x = "Time (hours)",
       y = "Concentration (mg/L)") +
  theme_minimal()
p

# ggsave("conc.png", plot = p, width = 12, height = 8, dpi = 300)