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."
)
)direction 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.