library(boinet)
library(dplyr)
# Load additional packages for reporting
if (requireNamespace("gt", quietly = TRUE)) library(gt)
if (requireNamespace("ggplot2", quietly = TRUE)) library(ggplot2)
This vignette demonstrates how to create complete, reproducible clinical trial reports using the boinet package with Quarto. The workflow covers everything from simulation to final report generation suitable for regulatory submissions, academic publications, and internal documentation.
# Extract operating characteristics data
<- function(boinet_result) {
extract_oc_data <- names(boinet_result$n.patient)
dose_levels
data.frame(
dose_level = dose_levels,
toxicity_prob = as.numeric(boinet_result$toxprob),
efficacy_prob = as.numeric(boinet_result$effprob),
n_patients = as.numeric(boinet_result$n.patient),
selection_prob = as.numeric(boinet_result$prop.select),
stringsAsFactors = FALSE
)
}
# Create summary statistics
<- function(boinet_result) {
create_summary_stats <- extract_oc_data(boinet_result)
oc_data <- oc_data$dose_level[which.max(oc_data$selection_prob)]
optimal_dose
list(
optimal_dose = optimal_dose,
max_selection_prob = max(oc_data$selection_prob),
early_stop_rate = as.numeric(boinet_result$prop.stop),
avg_duration = as.numeric(boinet_result$duration),
total_patients = sum(oc_data$n_patients),
design_type = class(boinet_result)[1]
)
}
# Create formatted text summaries
<- function(boinet_result) {
create_text_summary <- create_summary_stats(boinet_result)
stats
sprintf(
"The %s design selected dose level %s in %.1f%% of trials, with an average trial duration of %.0f days and early stopping rate of %.1f%%.",
toupper(gsub("\\.", "-", stats$design_type)),
$optimal_dose,
stats$max_selection_prob,
stats$avg_duration,
stats$early_stop_rate
stats
)
}
# Create design parameters table
<- function(result) {
create_design_table <- data.frame(
design_params Parameter = c("Target Toxicity Rate (φ)", "Target Efficacy Rate (δ)",
"Lower Toxicity Boundary (λ₁)", "Upper Toxicity Boundary (λ₂)",
"Efficacy Boundary (η₁)", "Early Stop Rate (%)",
"Average Duration (days)", "Toxicity Assessment Window (days)",
"Efficacy Assessment Window (days)", "Accrual Rate (days)"),
Value = c(result$phi, result$delta, result$lambda1, result$lambda2,
$eta1, result$prop.stop, result$duration,
result$tau.T, result$tau.E, result$accrual),
resultstringsAsFactors = FALSE
)
# Format values
$Value <- round(as.numeric(design_params$Value), 3)
design_params
if (gt_available) {
%>%
design_params gt() %>%
tab_header(
title = "TITE-BOIN-ET Design Parameters",
subtitle = paste("Based on", result$n.sim, "simulated trials")
%>%
) fmt_number(columns = "Value", decimals = 3) %>%
cols_align(align = "left", columns = "Parameter") %>%
cols_align(align = "center", columns = "Value")
else {
} # Fallback to basic table
print(design_params)
}
}
# Create operating characteristics table
<- function(result) {
create_oc_table <- names(result$n.patient)
dose_levels
<- data.frame(
oc_data `Dose Level` = dose_levels,
`True Toxicity Probability` = round(as.numeric(result$toxprob), 3),
`True Efficacy Probability` = round(as.numeric(result$effprob), 3),
`Average N Treated` = round(as.numeric(result$n.patient), 1),
`Selection Probability (%)` = round(as.numeric(result$prop.select), 1),
check.names = FALSE
)
if (gt_available) {
%>%
oc_data gt() %>%
tab_header(
title = "Operating Characteristics",
subtitle = "TITE-BOIN-ET Design Simulation Results"
%>%
) fmt_number(columns = c("True Toxicity Probability", "True Efficacy Probability"), decimals = 3) %>%
fmt_number(columns = "Average N Treated", decimals = 1) %>%
fmt_number(columns = "Selection Probability (%)", decimals = 1) %>%
cols_align(align = "center", columns = everything()) %>%
cols_align(align = "left", columns = "Dose Level") %>%
tab_style(
style = cell_fill(color = "lightblue"),
locations = cells_body(
rows = `Selection Probability (%)` == max(`Selection Probability (%)`)
)
)else {
} # Fallback to basic table
print(oc_data)
} }
# Create mock result for demonstration
<- list(
boinet_result toxprob = c("1" = 0.02, "2" = 0.08, "3" = 0.15, "4" = 0.25, "5" = 0.40),
effprob = c("1" = 0.10, "2" = 0.20, "3" = 0.35, "4" = 0.50, "5" = 0.65),
n.patient = c("1" = 8.2, "2" = 12.5, "3" = 15.8, "4" = 10.3, "5" = 7.2),
prop.select = c("1" = 5.2, "2" = 18.7, "3" = 42.1, "4" = 28.3, "5" = 5.7),
phi = 0.30, delta = 0.60, lambda1 = 0.03, lambda2 = 0.42, eta1 = 0.36,
tau.T = 28, tau.E = 42, accrual = 7, duration = 156.3, prop.stop = 3.2, n.sim = 1000
)class(boinet_result) <- "tite.boinet"
Here’s a complete Quarto document template for BOIN-ET analysis that
you can save as a separate .qmd
file:
---
title: "BOIN-ET Clinical Trial Design Analysis"
subtitle: "Protocol ABC-2024-001: Novel Kinase Inhibitor"
author: "Biostatistics Team"
date: "2025-06-05"
format:
html:
theme: cosmo
toc: true
toc-depth: 3
code-fold: true
fig-width: 8
fig-height: 6
pdf:
toc: true
number-sections: true
fig-width: 7
fig-height: 5
docx:
toc: true
fig-width: 6.5
fig-height: 4.5
execute:
echo: false
warning: false
message: false
---
The following demonstrates what would go in your actual Quarto report:
# Design specifications table
create_design_table(boinet_result)
TITE-BOIN-ET Design Parameters | |
Based on 1000 simulated trials | |
Parameter | Value |
---|---|
Target Toxicity Rate (φ) | 0.300 |
Target Efficacy Rate (δ) | 0.600 |
Lower Toxicity Boundary (λ₁) | 0.030 |
Upper Toxicity Boundary (λ₂) | 0.420 |
Efficacy Boundary (η₁) | 0.360 |
Early Stop Rate (%) | 3.200 |
Average Duration (days) | 156.300 |
Toxicity Assessment Window (days) | 28.000 |
Efficacy Assessment Window (days) | 42.000 |
Accrual Rate (days) | 7.000 |
# Operating characteristics table
create_oc_table(boinet_result)
Operating Characteristics | ||||
TITE-BOIN-ET Design Simulation Results | ||||
Dose Level | True Toxicity Probability | True Efficacy Probability | Average N Treated | Selection Probability (%) |
---|---|---|---|---|
1 | 0.020 | 0.100 | 8.2 | 5.2 |
2 | 0.080 | 0.200 | 12.5 | 18.7 |
3 | 0.150 | 0.350 | 15.8 | 42.1 |
4 | 0.250 | 0.500 | 10.3 | 28.3 |
5 | 0.400 | 0.650 | 7.2 | 5.7 |
# Extract key statistics for inline reporting
<- names(boinet_result$n.patient)
dose_levels <- as.numeric(boinet_result$prop.select)
selection_probs <- which.max(selection_probs)
best_dose_idx <- dose_levels[best_dose_idx]
best_dose <- max(selection_probs)
max_selection <- as.numeric(boinet_result$duration)
avg_duration <- as.numeric(boinet_result$prop.stop)
early_stop
cat("Key findings:\n")
#> Key findings:
cat("- Optimal dose level:", best_dose, "\n")
#> - Optimal dose level: 3
cat("- Selection probability:", round(max_selection, 1), "%\n")
#> - Selection probability: 42.1 %
cat("- Average trial duration:", round(avg_duration, 0), "days\n")
#> - Average trial duration: 156 days
cat("- Early stopping rate:", round(early_stop, 1), "%\n")
#> - Early stopping rate: 3.2 %
if (ggplot2_available) {
# Create data frame for plotting
<- data.frame(
plot_data dose_level = names(boinet_result$n.patient),
selection_prob = as.numeric(boinet_result$prop.select)
)
%>%
plot_data ggplot(aes(x = dose_level, y = selection_prob)) +
geom_col(fill = "steelblue", alpha = 0.7) +
geom_text(aes(label = paste0(round(selection_prob, 1), "%")),
vjust = -0.3) +
labs(
x = "Dose Level",
y = "Selection Probability (%)",
title = "TITE-BOIN-ET Dose Selection Performance"
+
) theme_minimal()
else {
} cat("ggplot2 package not available for visualization.\n")
}
Dose selection probabilities
if (ggplot2_available) {
# Create risk-benefit plot data
<- data.frame(
rb_data dose_level = names(boinet_result$n.patient),
toxicity_prob = as.numeric(boinet_result$toxprob),
efficacy_prob = as.numeric(boinet_result$effprob),
selection_prob = as.numeric(boinet_result$prop.select)
)
%>%
rb_data ggplot(aes(x = toxicity_prob, y = efficacy_prob)) +
geom_point(aes(size = selection_prob), alpha = 0.7, color = "darkred") +
geom_text(aes(label = dose_level), vjust = -1.5) +
scale_size_continuous(name = "Selection\nProbability (%)", range = c(3, 12)) +
labs(
x = "True Toxicity Probability",
y = "True Efficacy Probability",
title = "Risk-Benefit Profile"
+
) theme_minimal()
else {
} cat("ggplot2 package not available for risk-benefit visualization.\n")
}
Efficacy-toxicity profile
Quarto supports parameterized reports where you can pass values to customize the analysis.
Step 1: Add parameters to your YAML header:
---
title: "BOIN-ET Analysis Report"
params:
target_tox: 0.30
target_eff: 0.60
protocol_id: "ABC-001"
compound_name: "XYZ-123"
---
Step 2: Use parameters in your analysis code (in a real parameterized document):
# Access parameters like this:
# target_toxicity <- params$target_tox
# protocol_name <- params$protocol_id
Step 3: Render with custom parameters:
# quarto::quarto_render(
# "your_template.qmd",
# execute_params = list(
# target_tox = 0.25,
# target_eff = 0.65,
# protocol_id = "XYZ-002",
# compound_name = "New Drug"
# )
# )
# Demonstration of the concept using mock parameters
<- list(
mock_params target_tox = 0.30,
target_eff = 0.60,
protocol_id = "ABC-001",
compound_name = "XYZ-123"
)
cat("Parameterized report demonstration:\n")
#> Parameterized report demonstration:
cat("Protocol:", mock_params$protocol_id, "\n")
#> Protocol: ABC-001
cat("Compound:", mock_params$compound_name, "\n")
#> Compound: XYZ-123
cat("Target toxicity:", mock_params$target_tox, "\n")
#> Target toxicity: 0.3
cat("Target efficacy:", mock_params$target_eff, "\n")
#> Target efficacy: 0.6
# This shows how you could customize analysis based on parameters
cat("\nCustomized analysis settings:\n")
#>
#> Customized analysis settings:
if (mock_params$target_tox < 0.25) {
cat("- Conservative toxicity approach\n")
else if (mock_params$target_tox > 0.35) {
} cat("- Aggressive toxicity approach\n")
else {
} cat("- Standard toxicity approach\n")
}#> - Standard toxicity approach
# Demonstrate the concept with executable code
<- list(
scenarios conservative = list(phi = 0.25, delta = 0.50, name = "Conservative"),
standard = list(phi = 0.30, delta = 0.60, name = "Standard"),
aggressive = list(phi = 0.35, delta = 0.70, name = "Aggressive")
)
# Function to create scenario-specific summaries
<- function(scenario) {
create_scenario_summary sprintf(
"Scenario: %s (φ=%.2f, δ=%.2f)",
$name, scenario$phi, scenario$delta
scenario
)
}
# Generate summaries for all scenarios
<- lapply(scenarios, create_scenario_summary)
scenario_summaries cat("Available scenarios:\n")
#> Available scenarios:
for(summary in scenario_summaries) {
cat("-", summary, "\n")
}#> - Scenario: Conservative (φ=0.25, δ=0.50)
#> - Scenario: Standard (φ=0.30, δ=0.60)
#> - Scenario: Aggressive (φ=0.35, δ=0.70)
For batch processing, you would typically:
quarto::quarto_render()
with different
execute_params
for each scenarioShow different content based on results:
# Extract results for conditional logic
<- extract_oc_data(boinet_result)
oc_data <- max(oc_data$selection_prob)
max_selection_prob <- oc_data$dose_level[which.max(oc_data$selection_prob)]
optimal_dose
# Conditional content based on results
if (max_selection_prob > 40) {
<- "Strong evidence for optimal dose identification"
recommendation <- "High"
confidence_level else if (max_selection_prob > 25) {
} <- "Moderate evidence for dose selection"
recommendation <- "Moderate"
confidence_level else {
} <- "Weak evidence - consider design modifications"
recommendation <- "Low"
confidence_level
}
cat("Recommendation Confidence:", confidence_level, "\n")
#> Recommendation Confidence: High
cat("Analysis Conclusion:", recommendation, "\n")
#> Analysis Conclusion: Strong evidence for optimal dose identification
cat(sprintf("The optimal dose level %s was selected in %.1f%% of simulations.\n",
optimal_dose, max_selection_prob))#> The optimal dose level 3 was selected in 42.1% of simulations.
Use meaningful YAML headers with appropriate output formats for your audience.
cache: true
for expensive computationsecho: false
to hide code in final reportseval: false
for demonstration code# Include session information
cat("R version:", R.version.string, "\n")
#> R version: R version 4.3.3 (2024-02-29 ucrt)
cat("boinet version:", as.character(packageVersion("boinet")), "\n")
#> boinet version: 1.2.0
if (gt_available) {
cat("gt version:", as.character(packageVersion("gt")), "\n")
}#> gt version: 1.0.0
if (ggplot2_available) {
cat("ggplot2 version:", as.character(packageVersion("ggplot2")), "\n")
}#> ggplot2 version: 3.5.1
# Document analysis timestamp
cat("Analysis completed:", format(Sys.time(), "%Y-%m-%d %H:%M:%S"), "\n")
#> Analysis completed: 2025-06-05 16:01:14
Consider your target audience and output format when designing tables and figures.
# Function to create standardized clinical report content
<- function(boinet_result, protocol_info) {
create_clinical_report_content
# Extract key information
<- extract_oc_data(boinet_result)
oc_data <- create_summary_stats(boinet_result)
stats
# Create content structure
<- list(
content title = sprintf("Protocol %s: %s Analysis",
$id, protocol_info$compound),
protocol_infosummary = create_text_summary(boinet_result),
key_findings = list(
optimal_dose = stats$optimal_dose,
selection_prob = round(stats$max_selection_prob, 1),
duration = round(stats$avg_duration, 0),
early_stop = round(stats$early_stop_rate, 1)
),oc_data = oc_data
)
return(content)
}
# Example usage
<- list(id = "XYZ-2024-001", compound = "Novel Kinase Inhibitor")
protocol_info <- boinet_result
demo_result <- create_clinical_report_content(demo_result, protocol_info)
report_content
cat("Report title:", report_content$title, "\n")
#> Report title: Protocol XYZ-2024-001: Novel Kinase Inhibitor Analysis
cat("Summary:", report_content$summary, "\n")
#> Summary: The TITE-BOINET design selected dose level 3 in 42.1% of trials, with an average trial duration of 156 days and early stopping rate of 3.2%.
# Function to create regulatory-style formatting
<- function(boinet_result, submission_info) {
create_regulatory_content
<- create_summary_stats(boinet_result)
stats
# Regulatory-style summary
<- sprintf(
reg_summary "Study %s evaluated %d dose levels using a %s design. The recommended Phase II dose is %s, selected in %s%% of %d simulated trials.",
$study_id,
submission_infolength(names(boinet_result$n.patient)),
toupper(gsub("\\.", "-", stats$design_type)),
$optimal_dose,
statsformat(stats$max_selection_prob, digits = 3),
$n.sim
boinet_result
)
return(list(
summary = reg_summary,
table_title = sprintf("Table %s: Operating Characteristics", submission_info$table_number),
stats = stats
))
}
# Example usage
<- list(study_id = "ABC-123-001", table_number = "14.2.1")
submission_info <- boinet_result
demo_result <- create_regulatory_content(demo_result, submission_info)
reg_content
cat("Regulatory summary:", reg_content$summary, "\n")
#> Regulatory summary: Study ABC-123-001 evaluated 5 dose levels using a TITE-BOINET design. The recommended Phase II dose is 3, selected in 42.1% of 1000 simulated trials.
# Check required packages
<- c("boinet", "dplyr")
required_packages <- c("gt", "ggplot2", "knitr", "rmarkdown")
optional_packages
<- function(packages, required = TRUE) {
check_package_status <- sapply(packages, function(pkg) {
status requireNamespace(pkg, quietly = TRUE)
})
<- names(status)[!status]
missing if (length(missing) > 0) {
cat(ifelse(required, "Missing required", "Missing optional"), "packages:",
paste(missing, collapse = ", "), "\n")
cat("Install with: install.packages(c('", paste(missing, collapse = "', '"), "'))\n")
else {
} cat("All", ifelse(required, "required", "optional"), "packages available\n")
}
}
check_package_status(required_packages, TRUE)
#> All required packages available
check_package_status(optional_packages, FALSE)
#> All optional packages available
# Check Quarto availability
if (quarto_available) {
cat("✓ Quarto CLI available\n")
else {
} cat("⚠ Quarto CLI not found. Install from: https://quarto.org\n")
}#> ✓ Quarto CLI available
# Tips for handling large simulations
<- function(result) {
check_result_size <- object.size(result) / (1024^2)
size_mb cat(sprintf("Result object size: %.1f MB\n", size_mb))
if (size_mb > 50) {
cat("⚠ Large result object detected. Consider:\n")
cat(" - Using cache: true in chunks\n")
cat(" - Reducing n.sim for development\n")
cat(" - Extracting only needed data\n")
else {
} cat("✓ Result size is manageable\n")
}
}
# Use the demo result we created earlier
<- boinet_result
demo_result check_result_size(demo_result)
#> Result object size: 0.0 MB
#> ✓ Result size is manageable
# Memory-efficient data extraction
<- function(result) {
extract_minimal_data list(
oc_summary = data.frame(
dose = names(result$n.patient),
selection_prob = as.numeric(result$prop.select),
stringsAsFactors = FALSE
),key_stats = list(
optimal_dose = names(result$prop.select)[which.max(result$prop.select)],
duration = result$duration,
early_stop = result$prop.stop
)
)
}
<- extract_minimal_data(demo_result)
minimal_data cat("Minimal data extracted successfully\n")
#> Minimal data extracted successfully
The boinet package provides comprehensive support for Quarto-based reporting workflows, enabling:
This workflow ensures that BOIN-ET analysis results can be effectively communicated to clinical teams, regulatory agencies, and academic audiences while maintaining full reproducibility and professional presentation standards.
For basic result formatting, see the result-formatting
vignette. For publication-ready tables, see the
gt-integration
vignette.