graphicalMCP
Graphical Multiple Comparison Procedures
Guide users through graphical multiple comparison procedures using the graphicalMCP R package. Use this skill when the user asks about: multiplicity graphs, Bonferroni-based procedures, graph_create, graph_test_shortcut, graph_update, transition matrices, alpha reallocation, or closed testing with graphs.
Graphical Multiple Comparison Procedures with graphicalMCP
Note: This skill targets graphicalMCP >= 0.2.9 (CRAN, github.com/openpharma/graphicalMCP). Version 0.2.9 fixed precision issues in parametric tests (#90).
API reference
- Full function docs:
references/llms.txt(source: https://openpharma.github.io/graphicalMCP/) - Workflow patterns:
references/code_patterns.md
Key functions
Graph creation and manipulation
graph_create()- Create a multiplicity graph (hypotheses, weights, transitions)graph_update()- Update graph by deleting rejected hypothesesas_graph()- Convert from gMCP, igraph, or matrix objects
Testing
graph_test_shortcut()- Shortcut (Bonferroni-based) graphical testinggraph_test_closure()- Full closure-based testing (supports Simes, parametric)graph_rejection_orderings()- Enumerate all valid rejection orderings
Adjusted p-values and weights
adjust_p()- Adjusted p-values (Bonferroni, Simes, parametric)adjust_weights()- Adjusted significance levels (Bonferroni, Simes, parametric)graph_generate_weights()- Generate weights for all intersection hypotheses
Power
graph_calculate_power()- Power simulation via multivariate normal
Plotting
plot.initial_graph()- Plot multiplicity graphplot.updated_graph()- Plot updated graph sequence
Example graphs
example_graphs()- Pre-built example graphs (Bonferroni-Holm, fixed sequence, etc.)
Workflow patterns
For detailed code templates, read references/code_patterns.md.
Topics covered: - Creating graphs with graph_create() (weights, transitions) - Common graph structures (fixed sequence, Bonferroni-Holm, fallback, successive) - Shortcut (Bonferroni) testing with graph_test_shortcut() - Closure testing with graph_test_closure() (Simes, parametric, Hochberg, mixed) - Updating graphs manually with graph_update() - Generating intersection weights with graph_generate_weights() - Rejection orderings with graph_rejection_orderings() - Power simulation with graph_calculate_power() (marginal power, correlation) - Custom success criteria for power (e.g., “reject primary + secondary”) - Plotting graphs (layout, edge curves, epsilon edges, colors) - Built-in example graphs (20+ pre-built procedures) - Converting between graphicalMCP, gMCPLite, and igraph formats - Parametric tests with known correlation (nested populations, shared controls) - Multi-population multi-endpoint designs (oncology PFS + OS in subgroup + overall)
Important design considerations
graph_test_shortcut()vsgraph_test_closure(): Shortcut is Bonferroni-only and faster; closure supports Simes, parametric, and Hochberg tests for more power- Simes test: Valid when test statistics are positively correlated (PRDS condition); more powerful than Bonferroni for correlated hypotheses
- Parametric test: Requires known
test_corrbetween test statistics; most powerful when correlation is high test_groupsandtest_types: Allow different test types for different groups of hypotheses (e.g., parametric for co-primary, Simes for secondary)test_corr: Must matchtest_typesin length; useNAfor non-parametric tests (Bonferroni, Simes, Hochberg)sim_corrvstest_corr:sim_corris for generating correlated p-values in power simulation;test_corris the known correlation used in the parametric test itselfpower_marginal: Marginal power for each hypothesis at full alpha (not at allocated alpha); higher values = stronger signal- Transition matrix rows must sum to ≤ 1: Each row represents how alpha propagates when that hypothesis is rejected
- Sequential p-values with multiplicity: Sequential p-values can be passed directly to
graph_test_shortcut()to test multiple hypotheses in group sequential trials while controlling FWER.- For gsDesign objects (from
gsSurv(),gsSurvCalendar(),gsSurvPower()): usegsDesign::sequentialPValue() - For gsDesign2 objects (from
gs_design_ahr()): usegsDesign2::sequential_pval() - See Maurer & Bretz (2013) and the graphicalMCP-gsDesign2 skill.
- For gsDesign objects (from
Code Patterns
Code Patterns for graphicalMCP
Note: These patterns target graphicalMCP (CRAN). See https://openpharma.github.io/graphicalMCP/ for full documentation.
Table of Contents
- Creating graphs with graph_create
- Common graph structures
- Shortcut (Bonferroni) testing
- Closure testing (Simes, parametric, Hochberg)
- Updating graphs manually
- Generating intersection weights
- Rejection orderings
- Power simulation
- Custom success criteria
- Plotting graphs
- Example graphs (built-in)
- Converting between graph formats
- Parametric tests with correlation
- Multi-population multi-endpoint designs
Creating graphs with graph_create
graph_create() defines a multiplicity graph from hypothesis weights and a transition matrix.
Two-hypothesis graph
library(graphicalMCP)
# Simple Bonferroni split: equal weights, pass all alpha on rejection
g <- graph_create(
hypotheses = c(H1 = 0.5, H2 = 0.5),
transitions = matrix(c(
0, 1,
1, 0
), nrow = 2, byrow = TRUE)
)
g
plot(g)Four-hypothesis graph (two endpoints, two populations)
g <- graph_create(
hypotheses = c(H1 = 0.25, H2 = 0.25, H3 = 0.25, H4 = 0.25),
transitions = matrix(c(
0, 0.5, 0.5, 0,
0.5, 0, 0, 0.5,
0, 0.5, 0, 0.5,
0.5, 0, 0.5, 0
), nrow = 4, byrow = TRUE)
)Weighted graph with asymmetric transitions
# Primary endpoint gets more alpha, secondary gets remainder
g <- graph_create(
hypotheses = c(H1 = 0.5, H2 = 0.5, H3 = 0, H4 = 0),
transitions = matrix(c(
0, 0, 1, 0,
0, 0, 0, 1,
0, 1, 0, 0,
1, 0, 0, 0
), nrow = 4, byrow = TRUE)
)Common graph structures
Fixed sequence (gatekeeping)
# H1 must be rejected before testing H2, etc.
g <- graph_create(
hypotheses = c(H1 = 1, H2 = 0, H3 = 0),
transitions = matrix(c(
0, 1, 0,
0, 0, 1,
0, 0, 0
), nrow = 3, byrow = TRUE)
)
# Or use the built-in:
g <- fixed_sequence(3)Bonferroni-Holm
# Equal weights, full propagation
g <- bonferroni_holm(3)
# Equivalent to:
g <- graph_create(
hypotheses = c(H1 = 1/3, H2 = 1/3, H3 = 1/3),
transitions = matrix(c(
0, 0.5, 0.5,
0.5, 0, 0.5,
0.5, 0.5, 0
), nrow = 3, byrow = TRUE)
)Fallback procedure
# Ordered but with propagation back
g <- fallback(hypotheses = c(H1 = 0.5, H2 = 0.5))Simple successive (two families)
# Two primary, two secondary with gatekeeping
g <- simple_successive_1()
plot(g)Two doses, two primary, two secondary
g <- two_doses_two_primary_two_secondary()
plot(g)Shortcut (Bonferroni) testing
graph_test_shortcut() performs the sequentially rejective Bonferroni-based graphical MCP. This is the standard Maurer-Bretz procedure.
Basic testing
g <- graph_create(
hypotheses = c(H1 = 0.5, H2 = 0.5),
transitions = matrix(c(0, 1, 1, 0), nrow = 2, byrow = TRUE)
)
p <- c(H1 = 0.012, H2 = 0.030)
result <- graph_test_shortcut(g, p = p, alpha = 0.025)
result
# Access results
result$outputs$rejected # Logical vector of rejections
result$outputs$adjusted_p # Adjusted p-valuesWith verbose output (intermediate graphs)
result <- graph_test_shortcut(g, p = p, alpha = 0.025, verbose = TRUE)
# See each step of the sequential procedure
result$detailsWith test values (adjusted significance levels)
result <- graph_test_shortcut(g, p = p, alpha = 0.025, test_values = TRUE)
# Adjusted significance levels at each step
result$test_valuesClosure testing (Simes, parametric, Hochberg)
graph_test_closure() performs the full closure principle, allowing more powerful tests than Bonferroni for correlated endpoints.
Parametric test (known correlation)
# Correlation between test statistics
corr <- matrix(c(1, 0.5, 0.5, 1), nrow = 2)
result <- graph_test_closure(
g, p = p, alpha = 0.025,
test_groups = list(1:2),
test_types = c("parametric"),
test_corr = list(corr)
)
result$outputs$rejectedMixed test types (different tests for different groups)
# 4 hypotheses: parametric for H1-H2, Simes for H3-H4
g <- graph_create(
hypotheses = c(H1 = 0.25, H2 = 0.25, H3 = 0.25, H4 = 0.25),
transitions = matrix(c(
0, 0.5, 0.5, 0,
0.5, 0, 0, 0.5,
0, 0.5, 0, 0.5,
0.5, 0, 0.5, 0
), nrow = 4, byrow = TRUE)
)
p <- c(H1 = 0.01, H2 = 0.02, H3 = 0.03, H4 = 0.04)
# Correlation for the parametric group
corr12 <- matrix(c(1, 0.5, 0.5, 1), nrow = 2)
result <- graph_test_closure(
g, p = p, alpha = 0.025,
test_groups = list(1:2, 3:4),
test_types = c("parametric", "simes"),
test_corr = list(corr12, NA) # NA for non-parametric tests
)
result$outputs$rejectedHochberg test
result <- graph_test_closure(
g, p = p, alpha = 0.025,
test_groups = list(1:4),
test_types = c("hochberg")
)Verbose closure output
result <- graph_test_closure(
g, p = p, alpha = 0.025,
test_groups = list(1:4),
test_types = c("bonferroni"),
verbose = TRUE
)
# Adjusted p-values for every intersection hypothesis
result$details$adjusted_pUpdating graphs manually
graph_update() shows what the graph looks like after rejecting specific hypotheses. Useful for visualizing alpha propagation.
g <- graph_create(
hypotheses = c(H1 = 0.5, H2 = 0.5, H3 = 0, H4 = 0),
transitions = matrix(c(
0, 0, 1, 0,
0, 0, 0, 1,
0, 1, 0, 0,
1, 0, 0, 0
), nrow = 4, byrow = TRUE)
)
# Update after rejecting H1
updated <- graph_update(g, delete = 1)
updated$updated_graph
# Update after rejecting H1 and H3
updated <- graph_update(g, delete = c(1, 3))
updated$updated_graph
# Using logical indexing
updated <- graph_update(g, delete = c(TRUE, FALSE, TRUE, FALSE))
# Plot the update sequence
plot(updated)Generating intersection weights
graph_generate_weights() generates the weighting strategy for all 2^m - 1 intersection hypotheses in the closure.
g <- graph_create(
hypotheses = c(H1 = 0.5, H2 = 0.5),
transitions = matrix(c(0, 1, 1, 0), nrow = 2, byrow = TRUE)
)
weights <- graph_generate_weights(g)
weights
# Returns matrix: first m columns = membership, last m columns = weights
# H1 H2 H1 H2
# 1 1 1 0.50 0.50 # {H1, H2}
# 2 1 0 1.00 0.00 # {H1}
# 3 0 1 0.00 1.00 # {H2}Rejection orderings
graph_rejection_orderings() enumerates all valid orderings in which hypotheses can be rejected, given the same p-values and graph.
g <- bonferroni_holm(3)
p <- c(H1 = 0.01, H2 = 0.005, H3 = 0.03)
result <- graph_test_shortcut(g, p = p, alpha = 0.025)
orderings <- graph_rejection_orderings(result)
orderings
# Shows all valid sequences of rejectionsPower simulation
graph_calculate_power() simulates power using multivariate normal test statistics. Requires marginal power for each hypothesis and a correlation matrix.
Basic power simulation
g <- graph_create(
hypotheses = c(H1 = 0.5, H2 = 0.5),
transitions = matrix(c(0, 1, 1, 0), nrow = 2, byrow = TRUE)
)
# Marginal power (what each test achieves individually at full alpha)
power_marginal <- c(H1 = 0.9, H2 = 0.8)
# Correlation between test statistics
sim_corr <- matrix(c(1, 0.5, 0.5, 1), nrow = 2)
pwr <- graph_calculate_power(
graph = g,
alpha = 0.025,
power_marginal = power_marginal,
sim_corr = sim_corr,
sim_n = 1e5
)
pwr
# Key outputs
pwr$power$power_local # Local power for each hypothesis
pwr$power$power_at_least_1 # Power to reject at least one
pwr$power$power_all # Power to reject all
pwr$power$rejection_expected # Expected number of rejectionsPower with Simes test
pwr <- graph_calculate_power(
graph = g,
alpha = 0.025,
power_marginal = power_marginal,
test_groups = list(1:2),
test_types = c("simes"),
sim_corr = sim_corr,
sim_n = 1e5
)Power with parametric test
# Test correlation (for the parametric test itself)
test_corr <- matrix(c(1, 0.5, 0.5, 1), nrow = 2)
pwr <- graph_calculate_power(
graph = g,
alpha = 0.025,
power_marginal = power_marginal,
test_groups = list(1:2),
test_types = c("parametric"),
test_corr = list(test_corr),
sim_corr = sim_corr,
sim_n = 1e5
)Power with mixed test types
g4 <- graph_create(
hypotheses = c(H1 = 0.25, H2 = 0.25, H3 = 0.25, H4 = 0.25),
transitions = matrix(c(
0, 0.5, 0.5, 0,
0.5, 0, 0, 0.5,
0, 0.5, 0, 0.5,
0.5, 0, 0.5, 0
), nrow = 4, byrow = TRUE)
)
power_marginal <- c(0.9, 0.8, 0.7, 0.6)
sim_corr <- matrix(0.3, 4, 4)
diag(sim_corr) <- 1
test_corr_12 <- matrix(c(1, 0.5, 0.5, 1), 2, 2)
pwr <- graph_calculate_power(
graph = g4,
alpha = 0.025,
power_marginal = power_marginal,
test_groups = list(1:2, 3:4),
test_types = c("parametric", "simes"),
test_corr = list(test_corr_12, NA),
sim_corr = sim_corr,
sim_n = 1e5
)Custom success criteria
Define what “success” means beyond rejecting individual hypotheses.
g <- graph_create(
hypotheses = c(H1 = 0.5, H2 = 0.5, H3 = 0, H4 = 0),
transitions = matrix(c(
0, 0, 1, 0,
0, 0, 0, 1,
0, 1, 0, 0,
1, 0, 0, 0
), nrow = 4, byrow = TRUE)
)
# Success = reject at least one primary AND at least one secondary
success_criteria <- list(
"At least 1 primary + 1 secondary" = function(x) {
(x[1] | x[2]) & (x[3] | x[4])
},
"Both primaries" = function(x) {
x[1] & x[2]
}
)
pwr <- graph_calculate_power(
graph = g,
alpha = 0.025,
power_marginal = c(0.9, 0.8, 0.7, 0.6),
sim_corr = diag(4),
sim_n = 1e5,
sim_success = success_criteria
)
pwr$power$power_successPlotting graphs
Basic plot
g <- simple_successive_1()
plot(g)Customizing layout
# Grid layout with specified dimensions
plot(g, layout = "grid", nrow = 2, ncol = 2)
# Custom vertex positions (igraph-style layout matrix)
layout_matrix <- matrix(c(
0, 1, # H1 position (x, y)
1, 1, # H2
0, 0, # H3
1, 0 # H4
), ncol = 2, byrow = TRUE)
plot(g, layout = layout_matrix)Customizing appearance
plot(g,
v_palette = c("#6baed6", "#cccccc"), # Active, deleted colors
precision = 4, # Decimal places
background_color = "white",
margins = c(1, 1, 1, 1)
)Controlling edge curvature
# Curve all paired edges
plot(g, edge_curves = c("pairs" = 0.8))
# Curve specific edges
plot(g, edge_curves = c("H1|H3" = 0.5, "H3|H1" = 0.5))Plotting updated graphs
updated <- graph_update(g, delete = 1)
plot(updated) # Shows the update sequenceEpsilon edges
# Display very small transition weights as epsilon
g_eps <- graph_create(
hypotheses = c(H1 = 0.5, H2 = 0.5),
transitions = matrix(c(0, 1e-5, 1, 0), nrow = 2, byrow = TRUE)
)
plot(g_eps, eps = 1e-4) # Edges with weight < eps shown as εExample graphs (built-in)
graphicalMCP provides many pre-built example graphs.
# Simple procedures
bonferroni(3) # Equal-weight Bonferroni
bonferroni_holm(4) # Bonferroni-Holm step-down
fixed_sequence(3) # Fixed sequence (gatekeeping)
fallback(c(0.5, 0.5)) # Fallback procedure
# Weighted variants
bonferroni_weighted(c(0.6, 0.4))
bonferroni_holm_weighted(c(0.6, 0.3, 0.1))
# Simes-based
hochberg(3) # Hochberg step-up
hommel(3) # Hommel procedure
sidak(3) # Sidak procedure
# Dunnett-type
dunnett_single_step(3)
dunnett_single_step_weighted(c(0.5, 0.3, 0.2))
dunnett_closure_weighted(c(0.5, 0.3, 0.2))
# Complex clinical trial graphs
simple_successive_1() # 2 primary + 2 secondary
simple_successive_2() # 2 primary + 2 secondary (variant)
huque_etal() # Huque et al. procedure
two_doses_two_primary_two_secondary()
three_doses_two_primary_two_secondary()
# Improved fallback
fallback_improved_1(c(0.5, 0.5))
fallback_improved_2(c(0.5, 0.5), epsilon = 1e-4)
# Random (for testing)
random_graph(4)Converting between graph formats
# graphicalMCP → gMCPLite (legacy)
g <- bonferroni_holm(3)
g_gmcp <- as_graphMCP(g)
# gMCPLite → graphicalMCP
g_back <- as_initial_graph(g_gmcp)
# graphicalMCP → igraph (for network analysis)
g_ig <- as_igraph(g)
# igraph → graphicalMCP
g_back2 <- as_initial_graph(g_ig)Parametric tests with correlation
When test statistics have known correlation (e.g., nested populations, shared control arm), parametric tests can be more powerful than Bonferroni.
Two endpoints with known correlation
g <- graph_create(
hypotheses = c(H1 = 0.5, H2 = 0.5),
transitions = matrix(c(0, 1, 1, 0), nrow = 2, byrow = TRUE)
)
p <- c(H1 = 0.015, H2 = 0.020)
# Correlation from shared patients (e.g., co-primary endpoints)
test_corr <- matrix(c(1, 0.7, 0.7, 1), nrow = 2)
# Parametric closure test
result <- graph_test_closure(
g, p = p, alpha = 0.025,
test_groups = list(1:2),
test_types = c("parametric"),
test_corr = list(test_corr)
)
result$outputs$rejectedNested populations (subgroup + overall)
# H1 = subgroup PFS, H2 = overall PFS
# H3 = subgroup OS, H4 = overall OS
g <- graph_create(
hypotheses = c(H1 = 0.25, H2 = 0.25, H3 = 0.25, H4 = 0.25),
transitions = matrix(c(
0, 0.5, 0.5, 0,
0.5, 0, 0, 0.5,
0, 0.5, 0, 0.5,
0.5, 0, 0.5, 0
), nrow = 4, byrow = TRUE)
)
p <- c(H1 = 0.008, H2 = 0.012, H3 = 0.02, H4 = 0.04)
# Correlation structure (nested populations are correlated)
prevalence <- 0.4
corr_sub_full <- sqrt(prevalence) # ~0.632
test_corr <- matrix(c(
1, corr_sub_full, 0.5, 0.3,
corr_sub_full, 1, 0.3, 0.5,
0.5, 0.3, 1, corr_sub_full,
0.3, 0.5, corr_sub_full, 1
), nrow = 4, byrow = TRUE)
result <- graph_test_closure(
g, p = p, alpha = 0.025,
test_groups = list(1:4),
test_types = c("parametric"),
test_corr = list(test_corr)
)
result$outputs$rejectedMulti-population multi-endpoint designs
Oncology trial: PFS + OS in subgroup + overall
# Typical oncology multiplicity graph
# H1: PFS in BM+, H2: PFS in ITT
# H3: OS in BM+, H4: OS in ITT
g <- graph_create(
hypotheses = c(H1 = 0.5, H2 = 0.5, H3 = 0, H4 = 0),
transitions = matrix(c(
0, 0, 1, 0,
0, 0, 0, 1,
0, 0.5, 0, 0.5,
0.5, 0, 0.5, 0
), nrow = 4, byrow = TRUE)
)
plot(g, layout = matrix(c(0, 1, 1, 1, 0, 0, 1, 0), ncol = 2, byrow = TRUE))Testing with sequential p-values from group sequential designs
# Sequential p-values from gsDesign2 (one per hypothesis)
p <- c(H1 = 0.008, H2 = 0.015, H3 = 0.020, H4 = 0.045)
result <- graph_test_shortcut(g, p = p, alpha = 0.025)
result$outputs$rejected
result$outputs$adjusted_pPower for the full design
# Marginal power for each hypothesis
power_marginal <- c(H1 = 0.90, H2 = 0.85, H3 = 0.60, H4 = 0.55)
# Simulation correlation (from information fraction and overlap)
sim_corr <- matrix(c(
1.0, 0.6, 0.3, 0.2,
0.6, 1.0, 0.2, 0.3,
0.3, 0.2, 1.0, 0.6,
0.2, 0.3, 0.6, 1.0
), nrow = 4, byrow = TRUE)
pwr <- graph_calculate_power(
graph = g,
alpha = 0.025,
power_marginal = power_marginal,
sim_corr = sim_corr,
sim_n = 1e5,
sim_success = list(
"Reject H1 or H2" = function(x) x[1] | x[2],
"Reject any OS" = function(x) x[3] | x[4],
"Reject H1 and H3" = function(x) x[1] & x[3]
)
)
pwr$power$power_local
pwr$power$power_success