--- title: "Case study: an Lp(a) testing stepped-wedge trial" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Case study: an Lp(a) testing stepped-wedge trial} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set(collapse = TRUE, comment = "#>") set.seed(1) ``` This vignette works through the study that the package originally grew out of: a stepped-wedge trial of an intervention intended to raise lipoprotein(a) (Lp(a)) testing rates among physicians. It is worth being explicit about what is application-specific here and what is not. The *package* knows nothing about Lp(a), physicians, or specialties. It knows about clusters, sequences, periods, and aggregated binary outcomes. In this study the clusters happen to be physicians and the sequences happen to be specialties -- but a different trial could map schools onto clusters and districts onto sequences and use exactly the same functions. ```{r setup} library(stepwedgepower) ``` ## The applied vocabulary | Study concept | Package concept | |---|---| | Physician | cluster | | Specialty (Cardiology, ...) | sequence | | Study step | period | | Patients seen by a physician in a step | cluster-period sample size | | Patients with an Lp(a) test ordered | events | ## Baseline data The package ships a small synthetic physician-level dataset. Real analyses would substitute their own extract with the same shape. ```{r} physicians <- read_example_physician_data() str(physicians, max.level = 1) ``` `prepare_cluster_data()` is the generic filter: keep the groups of interest and trim clusters that are implausibly small or large. `prepare_physician_data()` is a thin application wrapper around it with the study's defaults. ```{r} analysis_set <- prepare_physician_data( physicians, min_patients = 100, max_patients = 10000 ) summarize_by_specialty(analysis_set, vars = "n_total_pat") ``` ## Baseline testing rates by specialty Baseline rates differ substantially across specialties, which matters because the power of the trial depends on where each sequence starts. `fit_specialty_rate_model()` wraps the generic `fit_grouped_rate_model()`. ```{r} rate_model <- fit_specialty_rate_model( analysis_set, successes = "n_lpa_pat", trials = "n_total_pat", specialty_var = "specialty", provider_var = "prov_id", random_intercept = TRUE ) baseline_rates <- estimate_specialty_rates(rate_model, specialty_var = "specialty") baseline_rates[, c("specialty", "probability")] ``` The physician-level random-effect variance from this model is what we will carry into the power calculation as the cluster random effect. Expressed as an ICC: ```{r} tau2 <- as.numeric(lme4::VarCorr(rate_model)$prov_id[1, 1]) cluster_sd_to_icc(sqrt(tau2)) ``` ## Powering the trial Now the design. Four specialties cross over one at a time after a single baseline period, giving five periods. ```{r} design <- sw_design( clusters_per_sequence = c(30, 30, 30, 30), crossover_period = c(2, 3, 4, 5), n_periods = 5, sequence_names = c("Cardiology", "Internal Medicine", "Family Medicine", "Neurology") ) design ``` The assumptions carry the specialty-specific baseline rates estimated above, the target effect, and the ICC. ```{r} assumptions <- sw_assumptions( baseline_prob = c(0.06, 0.04, 0.03, 0.02), treatment_or = 1.5, icc = 0.05, n_per_cluster_period = 20 ) assumptions ``` ```{r, eval = FALSE} power <- power_swcrt(design, assumptions, nsim = 1000, seed = 1, n_cores = 4) summary(power) ``` ## Sensitivity A single power number is rarely what a protocol needs. The effect size and the ICC are both assumptions, so vary them. ```{r, eval = FALSE} grid <- power_grid( design, assumptions, vary = list( treatment_or = c(1.3, 1.5, 1.75, 2.0), icc = c(0.02, 0.05, 0.10) ), nsim = 500, n_cores = 4, seed = 1 ) summary(grid) plot(grid) ``` ## A note on the analysis model `fit_stepwedge_model()` does not adjust for sequence by default. In a stepped-wedge trial each cluster sits in exactly one sequence for the whole study, so sequence is a cluster-level attribute that the cluster random intercept already absorbs. Adding `factor(sequence)` as a fixed effect is close to redundant, and in this design it measurably lowers power and raises the rate of singular fits. The baseline rates still differ by specialty -- that information enters through `baseline_prob`, where it belongs, rather than through an extra fixed effect in the analysis.