This vignette shows a realistic pattern for producing a mirror slope plot from an analysis dataset structured along ADaM conventions. It covers merging a population flag from a subject-level dataset, filtering to the analysis population, and supplying model-based estimates.
The package does not fit models. Estimates and their precision are computed upstream (for example from a mixed model for repeated measures) and passed in. This keeps the tool model-agnostic and the figure fully traceable to the values supplied.
We construct a basic-data-structure analysis dataset
(adqs) with one record per subject per visit, and a
subject-level dataset (adsl) carrying treatment assignment
and a safety population flag.
set.seed(7)
n_subj <- 60
adsl <- data.frame(
USUBJID = paste0("SUBJ-", sprintf("%03d", seq_len(n_subj))),
TRT01P = rep(c("Placebo", "Active"), each = n_subj / 2),
SAFFL = "Y",
ITTFL = "Y",
stringsAsFactors = FALSE
)
# A few subjects out of the safety population
adsl$SAFFL[c(3, 17, 42)] <- "N"
base_val <- rnorm(n_subj, 0, 4)
chg_val <- ifelse(adsl$TRT01P == "Placebo",
rnorm(n_subj, -11, 6),
rnorm(n_subj, -6, 6))
adqs <- data.frame(
USUBJID = rep(adsl$USUBJID, each = 2),
AVISIT = rep(c("Baseline", "Month 12"), times = n_subj),
PARAMCD = "FUNCSCORE",
CHG = as.numeric(rbind(rep(0, n_subj), chg_val)),
stringsAsFactors = FALSE
)
head(adqs, 4)
#> USUBJID AVISIT PARAMCD CHG
#> 1 SUBJ-001 Baseline FUNCSCORE 0.00000
#> 2 SUBJ-001 Month 12 FUNCSCORE -10.54177
#> 3 SUBJ-002 Baseline FUNCSCORE 0.00000
#> 4 SUBJ-002 Month 12 FUNCSCORE -10.04507Population flags and planned treatment usually live in ADSL. Merge
the needed columns into the analysis dataset before preparing the plot.
Base R merge() or a dplyr join both work; here
we use base R to avoid assumptions.
adqs <- merge(
adqs,
adsl[, c("USUBJID", "TRT01P", "SAFFL", "ITTFL")],
by = "USUBJID",
all.x = TRUE
)
head(adqs, 4)
#> USUBJID AVISIT PARAMCD CHG TRT01P SAFFL ITTFL
#> 1 SUBJ-001 Baseline FUNCSCORE 0.00000 Placebo Y Y
#> 2 SUBJ-001 Month 12 FUNCSCORE -10.54177 Placebo Y Y
#> 3 SUBJ-002 Baseline FUNCSCORE 0.00000 Placebo Y Y
#> 4 SUBJ-002 Month 12 FUNCSCORE -10.04507 Placebo Y Yprep_mirror_data() filters to the two named arms,
applies the population flags with AND logic, retains only the baseline
and endpoint visits, and counts the analysed N per arm. Any subject
lacking both timepoints is dropped with a message, so the reported N
reflects the subjects actually contributing.
md <- prep_mirror_data(
adqs,
subject_var = "USUBJID",
trt_var = "TRT01P",
trt_levels = c("Placebo", "Active"),
time_var = "AVISIT",
time_baseline = "Baseline",
time_endpoint = "Month 12",
value_var = "CHG",
pop_flags = list(SAFFL = "Y", ITTFL = "Y"),
population_label = "Safety / ITT",
value_label = "Change from Baseline (Functional Score)"
)
md
#> <mirror_data> schema 1.0
#> Reference (left) : Placebo (n=28)
#> Comparator (right): Active (n=29)
#> Timepoints : Baseline -> Month 12
#> Population : Safety / ITT
#> Filters applied : SAFFL=Y; ITTFL=Y
#> Observations : 114 OBS rows
#> Summary : no
#> Precision : no
#> Difference : noIn practice these come from your model output. If you use
emmeans, extract the estimates and confidence limits and
pass them as named vectors and lists. The named-vector interface shown
here is stable, and a future release may add a convenience method to
consume a model object directly.
md <- add_summary(
md,
estimates = c("Placebo" = -11.3, "Active" = -6.1),
stat_label = "LS-Mean"
)
md <- add_precision(
md,
ci = list("Placebo" = c(-14.0, -8.6),
"Active" = c(-8.7, -3.5))
)
md <- add_difference(
md,
estimate = 5.2,
ci_lower = 1.9,
ci_upper = 8.5,
p_value = 0.003
)
md
#> <mirror_data> schema 1.0
#> Reference (left) : Placebo (n=28)
#> Comparator (right): Active (n=29)
#> Timepoints : Baseline -> Month 12
#> Population : Safety / ITT
#> Filters applied : SAFFL=Y; ITTFL=Y
#> Observations : 114 OBS rows
#> Summary : yes (LS-Mean)
#> Precision : yes
#> Difference : yesmirror_slope_plot(
md,
trt_colors = mirror_palette("colorblind"),
show_difference = TRUE,
show_slowing = TRUE,
show_precision = TRUE,
title = "Functional Score: Change from Baseline at Month 12",
footnote = paste(
"LS-means and 95% CIs from a mixed model for repeated measures.",
"Safety/ITT population. Positive difference favours the active arm."
)
)
#> Warning in grid.Call(C_stringMetric, as.graphicsAnnot(x$label)): font family
#> not found in Windows font database
#> Warning in grid.Call(C_stringMetric, as.graphicsAnnot(x$label)): font family
#> not found in Windows font database
#> Warning in grid.Call(C_stringMetric, as.graphicsAnnot(x$label)): font family
#> not found in Windows font database
#> Warning in grid.Call(C_stringMetric, as.graphicsAnnot(x$label)): font family
#> not found in Windows font database
#> Warning in grid.Call(C_textBounds, as.graphicsAnnot(x$label), x$x, x$y, : font
#> family not found in Windows font database
#> Warning in grid.Call(C_textBounds, as.graphicsAnnot(x$label), x$x, x$y, : font
#> family not found in Windows font database
#> Warning in grid.Call(C_textBounds, as.graphicsAnnot(x$label), x$x, x$y, : font
#> family not found in Windows font database
#> Warning in grid.Call.graphics(C_text, as.graphicsAnnot(x$label), x$x, x$y, :
#> font family not found in Windows font database
#> Warning in grid.Call.graphics(C_text, as.graphicsAnnot(x$label), x$x, x$y, :
#> font family not found in Windows font database
#> Warning in grid.Call.graphics(C_text, as.graphicsAnnot(x$label), x$x, x$y, :
#> font family not found in Windows font database
#> Warning in grid.Call.graphics(C_text, as.graphicsAnnot(x$label), x$x, x$y, :
#> font family not found in Windows font database
#> Warning in grid.Call.graphics(C_text, as.graphicsAnnot(x$label), x$x, x$y, :
#> font family not found in Windows font database
#> Warning in grid.Call.graphics(C_text, as.graphicsAnnot(x$label), x$x, x$y, :
#> font family not found in Windows font database
#> Warning in grid.Call.graphics(C_text, as.graphicsAnnot(x$label), x$x, x$y, :
#> font family not found in Windows font database
#> Warning in grid.Call.graphics(C_text, as.graphicsAnnot(x$label), x$x, x$y, :
#> font family not found in Windows font database
#> Warning in grid.Call.graphics(C_text, as.graphicsAnnot(x$label), x$x, x$y, :
#> font family not found in Windows font database
#> Warning in grid.Call.graphics(C_text, as.graphicsAnnot(x$label), x$x, x$y, :
#> font family not found in Windows font database
#> Warning in grid.Call.graphics(C_text, as.graphicsAnnot(x$label), x$x, x$y, :
#> font family not found in Windows font database
#> Warning in grid.Call.graphics(C_text, as.graphicsAnnot(x$label), x$x, x$y, :
#> font family not found in Windows font database
#> Warning in grid.Call.graphics(C_text, as.graphicsAnnot(x$label), x$x, x$y, :
#> font family not found in Windows font database
#> Warning in grid.Call.graphics(C_text, as.graphicsAnnot(x$label), x$x, x$y, :
#> font family not found in Windows font database
#> Warning in grid.Call.graphics(C_text, as.graphicsAnnot(x$label), x$x, x$y, :
#> font family not found in Windows font database
#> Warning in grid.Call.graphics(C_text, as.graphicsAnnot(x$label), x$x, x$y, :
#> font family not found in Windows font databasedirection describes the direction of numeric movement,
derived by comparing the reference arm’s endpoint estimate to its
baseline. The default benefit wording follows from it (for example
\dQuote{% less decline} when values move down). For an outcome where the
favourable direction is unusual, set outcome_label and
benefit_label explicitly rather than relying on the
defaults.
The mirror_data object records provenance in its
metadata: the schema version, the population filters applied, the
analysed N per arm, a creation timestamp, and the originating call. This
supports reconstructing exactly which inputs produced a given figure,
which is useful in regulated settings.