About the simulated experiment This module follows a fixed-seed, simulated 2 × 2 factorial trial in 12 community clinics. Within every clinic, four participants receive each combination of health coaching and home blood-pressure monitoring, for 192 participants total. The outcome is 12-week reduction in systolic blood pressure (mmHg), where a larger value is better. No real people or clinical records are represented.
Define the causal question and experimental unit → choose factors, levels, controls, and blocks → randomize reproducibly → preserve the assignment structure in analysis → estimate factorial contrasts → diagnose assumptions and attrition → report estimands, uncertainty, and design limitations.
After completing this tutorial, you should be able to:
lm(bp_reduction ~ clinic + coaching * home_monitoring);An experiment deliberately assigns one or more interventions and observes their consequences. Design begins before a statistical test is selected.
| Term | Meaning in this trial | Question to ask |
|---|---|---|
| Experimental unit | Participant receiving an assigned combination | What entity was independently randomized? |
| Observational unit | Participant whose 12-week blood-pressure change is measured | At what level is the response recorded? |
| Factor | Coaching; home monitoring | What intervention dimension is manipulated? |
| Level | No or Yes for each factor | Which versions are compared? |
| Treatment combination | Control, coaching only, monitoring only, or both | Which joint factor levels can be assigned? |
| Block | Clinic | Which known nuisance source is controlled locally? |
| Replicate | A distinct randomized participant at a combination | Is there independent information about experimental error? |
| Response | Systolic BP reduction at 12 weeks, in mmHg | Is direction, timing, and measurement protocol prespecified? |
| Estimand | For example, marginal effect of coaching averaged over monitoring levels and clinics | Exactly which contrast in potential outcomes is targeted? |
The experimental unit follows randomization, not the row count. Here, participants are randomized within clinics, so 192 participants are experimental units and clinics are blocks. If a whole clinic received one policy, the clinic—not each patient—would be the experimental unit.
Let be participant ’s potential BP reduction under coaching level and monitoring level . One finite-sample marginal coaching estimand is
The difference-in-differences interaction estimand is
These definitions specify how factor levels are averaged. They are clearer than saying only “the treatment effect.” Generalization from these trial participants to another population is a separate question from internal causal identification.
A nonsignificant result is not evidence of no effect A large -value can coexist with effects that are clinically important in either direction. Report the prespecified effect estimate, confidence interval, outcome scale, and compatibility with clinically meaningful values. If equivalence or noninferiority is the goal, design and analyze that question explicitly.
Randomization does not guarantee perfect baseline balance in one realized trial, cure attrition, ensure adherence, prevent measurement bias, or make an unrepresentative sample representative. Those problems need their own design and analysis protections.
Each clinic contains all four combinations, with four independently randomized participants per combination. Before looking at outcomes, audit unique identifiers, factor levels, allocation counts, and missing values.
design_audit <- data.frame(
Item = c(
"Participants", "Clinics", "Participants per clinic",
"Participants per clinic-combination", "Missing outcomes",
"Duplicate clinic-participant identifiers"
),
Value = c(
nrow(doe_data), nlevels(doe_data[["clinic"]]),
paste(range(as.vector(table(doe_data[["clinic"]]))), collapse = " to "),
paste(range(as.vector(xtabs(~ clinic + treatment_combination, doe_data))),
collapse = " to "),
sum(is.na(doe_data[["bp_reduction"]])),
sum(duplicated(doe_data[c("clinic", "participant_in_clinic")]))
)
)
knitr::kable(design_audit, caption = "Pre-outcome structural audit of the simulated experiment")| Item | Value |
|---|---|
| Participants | 192 |
| Clinics | 12 |
| Participants per clinic | 16 to 16 |
| Participants per clinic-combination | 4 to 4 |
| Missing outcomes | 0 |
| Duplicate clinic-participant identifiers | 0 |
The simulation has true clinic effects with standard deviation 2.2 mmHg and individual errors with standard deviation 4.5 mmHg. Its four population cell means, before clinic shifts, are 4.0, 7.0, 6.2, and 12.0 mmHg. Therefore:
These are simulation truths, not estimates available in a real study.
A randomization list is part of the design record. It should be generated by an authorized reproducible procedure, secured before enrollment, and paired with identifiers only through the allocation system. Never reconstruct a sequence after seeing outcomes.
schedule_preview <- doe_data[
doe_data[["clinic"]] %in% clinic_levels[1:2],
c("clinic", "participant_in_clinic", "treatment_combination")
]
knitr::kable(schedule_preview,
caption = "First two clinic-specific assignment schedules (illustrative only)")| clinic | participant_in_clinic | treatment_combination |
|---|---|---|
| Clinic 01 | 1 | Both |
| Clinic 01 | 2 | Monitoring only |
| Clinic 01 | 3 | Coaching only |
| Clinic 01 | 4 | Monitoring only |
| Clinic 01 | 5 | Control |
| Clinic 01 | 6 | Monitoring only |
| Clinic 01 | 7 | Coaching only |
| Clinic 01 | 8 | Both |
| Clinic 01 | 9 | Coaching only |
| Clinic 01 | 10 | Coaching only |
| Clinic 01 | 11 | Control |
| Clinic 01 | 12 | Control |
| Clinic 01 | 13 | Both |
| Clinic 01 | 14 | Both |
| Clinic 01 | 15 | Control |
| Clinic 01 | 16 | Monitoring only |
| Clinic 02 | 1 | Coaching only |
| Clinic 02 | 2 | Coaching only |
| Clinic 02 | 3 | Coaching only |
| Clinic 02 | 4 | Both |
| Clinic 02 | 5 | Monitoring only |
| Clinic 02 | 6 | Coaching only |
| Clinic 02 | 7 | Both |
| Clinic 02 | 8 | Control |
| Clinic 02 | 9 | Both |
| Clinic 02 | 10 | Monitoring only |
| Clinic 02 | 11 | Control |
| Clinic 02 | 12 | Control |
| Clinic 02 | 13 | Control |
| Clinic 02 | 14 | Both |
| Clinic 02 | 15 | Monitoring only |
| Clinic 02 | 16 | Monitoring only |
allocation_matrix <- matrix(doe_data[["combination"]], nrow = n_clinics, byrow = TRUE)
old_par <- par(mar = c(5, 6, 4, 1))
image(
x = seq_len(16), y = seq_len(n_clinics), z = t(allocation_matrix),
col = c(palette_doe["gray"], palette_doe["blue"],
palette_doe["orange"], palette_doe["teal"]),
breaks = seq(0.5, 4.5, by = 1), axes = FALSE,
xlab = "Enrollment position within clinic", ylab = "Clinic",
main = "Randomized complete block allocation"
)
axis(1, at = c(1, 4, 8, 12, 16))
axis(2, at = seq_len(n_clinics), labels = clinic_levels, las = 1, cex.axis = 0.7)
legend(
"bottom", inset = 0.01, horiz = TRUE, bty = "n",
legend = levels(doe_data[["treatment_combination"]]),
fill = c(palette_doe["gray"], palette_doe["blue"],
palette_doe["orange"], palette_doe["teal"]), cex = 0.8
)Clinic-specific randomized treatment schedules. Every row is a clinic, every column is an enrollment position, and color indicates one of the four treatment combinations. The order varies while each clinic retains four assignments to every combination.
In a completely randomized design, treatment combinations are assigned across all eligible experimental units without a blocking restriction. With 192 participants and four arms, a balanced CRD could be generated as follows.
set.seed(314159)
crd_assignment <- sample(rep(
c("Control", "Coaching only", "Monitoring only", "Both"), each = 48
))
knitr::kable(as.data.frame(table(crd_assignment)),
col.names = c("Treatment combination", "Assigned participants"),
caption = "Balanced completely randomized allocation example")| Treatment combination | Assigned participants |
|---|---|
| Both | 48 |
| Coaching only | 48 |
| Control | 48 |
| Monitoring only | 48 |
A CRD is simple and flexible. It can be efficient when units are homogeneous or no strong preassignment prognostic factor is available. In a multiclinic trial, however, chance might place disproportionate combinations in clinics with systematically different populations or measurement practices.
In an RCBD, each block contains every treatment combination. Here, each clinic independently randomizes four participants to each combination. The analysis compares combinations after accounting for clinic, and the randomization test must permute only within clinics.
allocation_by_clinic <- xtabs(~ clinic + treatment_combination, data = doe_data)
knitr::kable(allocation_by_clinic,
caption = "Treatment-combination counts within all 12 complete clinic blocks")| Control | Coaching only | Monitoring only | Both | |
|---|---|---|---|---|
| Clinic 01 | 4 | 4 | 4 | 4 |
| Clinic 02 | 4 | 4 | 4 | 4 |
| Clinic 03 | 4 | 4 | 4 | 4 |
| Clinic 04 | 4 | 4 | 4 | 4 |
| Clinic 05 | 4 | 4 | 4 | 4 |
| Clinic 06 | 4 | 4 | 4 | 4 |
| Clinic 07 | 4 | 4 | 4 | 4 |
| Clinic 08 | 4 | 4 | 4 | 4 |
| Clinic 09 | 4 | 4 | 4 | 4 |
| Clinic 10 | 4 | 4 | 4 | 4 |
| Clinic 11 | 4 | 4 | 4 | 4 |
| Clinic 12 | 4 | 4 | 4 | 4 |
Blocking is most useful when blocks explain outcome variation and every planned treatment comparison occurs within blocks. Blocks should be defined by preassignment information. Excessively many tiny strata can make implementation fragile; dynamic allocation or covariate-adaptive procedures require their own documented inferential methods.
A 2 × 2 factorial design learns about two factors and their interaction in the same experiment. With equal allocation, each coaching main-effect comparison uses all 192 participants: coaching is present in half and absent in half, averaged across monitoring. This can be much more efficient than running two unrelated trials—provided the joint interventions are feasible and the interaction is scientifically meaningful.
For cell means in the order control, coaching only, monitoring only, and both:
An interaction means the effect of one factor differs across levels of the other. It does not automatically imply a biological mechanism, and its scale matters: additivity on the mmHg scale is a different assumption from additivity on a log-risk or log-rate scale.
Coding determines coefficient labels, not
the estimand With R’s default 0/1 treatment coding, the
coachingYes coefficient is coaching’s simple effect when
monitoring is “No,” and home_monitoringYes is monitoring’s
simple effect when coaching is “No.” The interaction coefficient is the
difference in differences. With
effect coding in
,
the marginal effects are
and
,
while the interaction difference in differences is
.
Always state the contrast itself.
Descriptive statistics should preserve the randomized combinations and blocks. They are not substitutes for uncertainty estimates, and observed baseline or outcome imbalances should not trigger undocumented changes to a prespecified analysis.
cell_n <- aggregate(bp_reduction ~ coaching + home_monitoring, doe_data, length)
cell_mean <- aggregate(bp_reduction ~ coaching + home_monitoring, doe_data, mean)
cell_sd <- aggregate(bp_reduction ~ coaching + home_monitoring, doe_data, sd)
names(cell_n)[3] <- "n"
names(cell_mean)[3] <- "Mean"
names(cell_sd)[3] <- "SD"
cell_summary <- Reduce(
function(x, y) merge(x, y, by = c("coaching", "home_monitoring")),
list(cell_n, cell_mean, cell_sd)
)
knitr::kable(cell_summary, digits = 2,
caption = "Observed BP-reduction summaries by randomized factorial cell")| coaching | home_monitoring | n | Mean | SD |
|---|---|---|---|---|
| No | No | 48 | 2.96 | 4.72 |
| No | Yes | 48 | 6.18 | 5.55 |
| Yes | No | 48 | 5.77 | 6.23 |
| Yes | Yes | 48 | 11.24 | 5.08 |
boxplot(
bp_reduction ~ treatment_combination, data = doe_data,
col = c("#E8ECEF", "#C9E1F2", "#F8E2B5", "#B9DFDB"), border = palette_doe["navy"],
ylab = "12-week systolic BP reduction (mmHg)", xlab = "",
main = "Outcome distributions by treatment combination", las = 1
)
stripchart(
bp_reduction ~ treatment_combination, data = doe_data,
vertical = TRUE, method = "jitter", add = TRUE, pch = 16,
col = grDevices::adjustcolor(palette_doe["navy"], alpha.f = 0.35)
)
abline(h = 0, lty = 3, col = palette_doe["gray"])Observed 12-week systolic blood-pressure reduction by randomized treatment combination. Each box summarizes 48 participants across all 12 clinics; points show individual outcomes and should not be mistaken for independent clinic-level assignments.
Parallel traces suggest additivity on the plotted scale; nonparallel traces suggest interaction. Sampling noise can also create nonparallel lines, so the plot should accompany a contrast estimate and interval.
with(doe_data, interaction.plot(
x.factor = home_monitoring, trace.factor = coaching,
response = bp_reduction, fun = mean, type = "b", pch = c(1, 19), lwd = 2,
col = c(palette_doe["orange"], palette_doe["teal"]),
xlab = "Home monitoring", ylab = "Mean BP reduction (mmHg)",
trace.label = "Coaching", main = "Observed factorial pattern"
))Mean BP reduction for coaching and no coaching across home-monitoring levels. Nonparallel lines visualize the positive interaction: the coaching difference is larger when home monitoring is also provided.
For this balanced design with a continuous outcome, use fixed clinic-block indicators plus both factor main terms and their interaction:
The clinic coefficients remove between-clinic level shifts. The model assumes independent experimental-unit errors with a common variance and an adequate additive clinic structure. Randomization supplies the causal comparison; the outcome model supplies a convenient estimator and standard error under its assumptions.
rcbd_fit <- lm(
bp_reduction ~ clinic + coaching * home_monitoring,
data = doe_data
)
treatment_terms <- c(
"coachingYes", "home_monitoringYes",
"coachingYes:home_monitoringYes"
)
coefficient_table <- coef(summary(rcbd_fit))[treatment_terms, , drop = FALSE]
coefficient_table <- data.frame(
Term = c(
"Coaching simple effect when monitoring = No",
"Monitoring simple effect when coaching = No",
"Interaction: difference in differences"
),
Estimate = coefficient_table[, "Estimate"],
SE = coefficient_table[, "Std. Error"],
t = coefficient_table[, "t value"],
p_value = format_p(coefficient_table[, "Pr(>|t|)"]), row.names = NULL
)
knitr::kable(coefficient_table, digits = 3,
caption = "Treatment-coded coefficients from the clinic-blocked factorial model")| Term | Estimate | SE | t | p_value |
|---|---|---|---|---|
| Coaching simple effect when monitoring = No | 2.81 | 0.96 | 2.92 | 0.00392 |
| Monitoring simple effect when coaching = No | 3.21 | 0.96 | 3.35 | < 0.001 |
| Interaction: difference in differences | 2.25 | 1.36 | 1.66 | 0.09860 |
The first two coefficient rows are simple effects at reference levels, not marginal main effects. Their scientific meaning changes if the reference level changes. The next section estimates named contrasts that do not depend on a reader remembering the coding convention.
aov_fit <- aov(
bp_reduction ~ clinic + coaching * home_monitoring,
data = doe_data
)
anova_table <- anova(aov_fit)
anova_display <- data.frame(
Term = rownames(anova_table), anova_table,
row.names = NULL, check.names = FALSE
)
anova_display[["Pr(>F)"]] <- format_p(anova_display[["Pr(>F)"]])
knitr::kable(anova_display, digits = 3,
caption = "ANOVA decomposition for clinic blocks and the 2 × 2 factorial treatment")| Term | Df | Sum Sq | Mean Sq | F value | Pr(>F) |
|---|---|---|---|---|---|
| clinic | 11 | 1617 | 147.0 | 6.64 | <0.001 |
| coaching | 1 | 743 | 742.7 | 33.57 | <0.001 |
| home_monitoring | 1 | 904 | 904.5 | 40.88 | <0.001 |
| coaching:home_monitoring | 1 | 61 | 61.0 | 2.76 | 0.0986 |
| Residuals | 177 | 3917 | 22.1 | NA | NA |
Because every clinic contains equal replication of all four combinations, the clinic, marginal factor, and interaction comparisons are orthogonal in this complete balanced design. Consequently, the treatment decomposition has a clean design-based interpretation. In an unbalanced dataset—especially after missing outcomes—sequential sums of squares can depend on model order. Return to estimand-based cell-mean contrasts rather than selecting a “Type” of sums of squares mechanically.
An intercept describes the reference clinic and control cell, which is usually not the desired population summary. Construct one design-matrix row for every clinic at each treatment combination, average those 12 rows, and propagate the full covariance matrix. This standardizes every cell to the same empirical distribution of clinics.
cell_profiles <- data.frame(
coaching = factor(c("No", "Yes", "No", "Yes"), levels = c("No", "Yes")),
home_monitoring = factor(c("No", "No", "Yes", "Yes"), levels = c("No", "Yes")),
Cell = c("Control", "Coaching only", "Monitoring only", "Both")
)
average_model_row <- function(coaching_value, monitoring_value) {
profile <- data.frame(
clinic = factor(clinic_levels, levels = clinic_levels),
coaching = factor(rep(coaching_value, n_clinics), levels = c("No", "Yes")),
home_monitoring = factor(rep(monitoring_value, n_clinics), levels = c("No", "Yes"))
)
colMeans(model.matrix(delete.response(terms(rcbd_fit)), data = profile))
}
cell_X <- do.call(rbind, lapply(seq_len(nrow(cell_profiles)), function(j) {
average_model_row(
as.character(cell_profiles[["coaching"]][j]),
as.character(cell_profiles[["home_monitoring"]][j])
)
}))
cell_estimate <- drop(cell_X %*% coef(rcbd_fit))
cell_se <- sqrt(diag(cell_X %*% vcov(rcbd_fit) %*% t(cell_X)))
t_critical <- qt(0.975, df = df.residual(rcbd_fit))
cell_results <- data.frame(
Cell = cell_profiles[["Cell"]],
Estimated_mean = cell_estimate,
SE = cell_se,
CI_low = cell_estimate - t_critical * cell_se,
CI_high = cell_estimate + t_critical * cell_se
)
knitr::kable(cell_results, digits = 2,
caption = "Model-based cell means standardized equally across all 12 clinics")| Cell | Estimated_mean | SE | CI_low | CI_high |
|---|---|---|---|---|
| Control | 2.96 | 0.68 | 1.62 | 4.30 |
| Coaching only | 5.77 | 0.68 | 4.43 | 7.11 |
| Monitoring only | 6.18 | 0.68 | 4.84 | 7.52 |
| Both | 11.24 | 0.68 | 9.90 | 12.58 |
The intervals describe uncertainty in mean outcomes under this model. They are not prediction intervals for an individual, which must include individual residual variation, and they do not quantify transport uncertainty to clinics outside the study.
Any scientifically meaningful factorial estimand can be written , where . We use the model-matrix rows above so estimates, standard errors, and covariances all remain aligned.
contrast_weights <- rbind(
"Marginal coaching effect" = c(-0.5, 0.5, -0.5, 0.5),
"Marginal monitoring effect" = c(-0.5, -0.5, 0.5, 0.5),
"Interaction (difference in differences)" = c(1, -1, -1, 1),
"Coaching effect when monitoring = No" = c(-1, 1, 0, 0),
"Coaching effect when monitoring = Yes" = c(0, 0, -1, 1)
)
contrast_X <- contrast_weights %*% cell_X
contrast_estimate <- drop(contrast_X %*% coef(rcbd_fit))
contrast_se <- sqrt(diag(contrast_X %*% vcov(rcbd_fit) %*% t(contrast_X)))
contrast_t <- contrast_estimate / contrast_se
contrast_results <- data.frame(
Contrast = rownames(contrast_weights),
Estimate = contrast_estimate,
SE = contrast_se,
CI_low = contrast_estimate - t_critical * contrast_se,
CI_high = contrast_estimate + t_critical * contrast_se,
nominal_unadjusted_p = format_p(
2 * pt(abs(contrast_t), df = df.residual(rcbd_fit), lower.tail = FALSE)
),
row.names = NULL
)
knitr::kable(
contrast_results, digits = 3,
caption = "Prespecified factorial effects and simple effects with 95% confidence intervals; p-values are nominal and must follow the prespecified multiplicity plan"
)| Contrast | Estimate | SE | CI_low | CI_high | nominal_unadjusted_p |
|---|---|---|---|---|---|
| Marginal coaching effect | 3.93 | 0.679 | 2.594 | 5.27 | < 0.001 |
| Marginal monitoring effect | 4.34 | 0.679 | 3.001 | 5.68 | < 0.001 |
| Interaction (difference in differences) | 2.25 | 1.358 | -0.425 | 4.93 | 0.09860 |
| Coaching effect when monitoring = No | 2.81 | 0.960 | 0.911 | 4.70 | 0.00392 |
| Coaching effect when monitoring = Yes | 5.06 | 0.960 | 3.166 | 6.96 | < 0.001 |
The marginal coaching estimate averages coaching’s effect equally across monitoring levels, as prespecified. If the target population would use monitoring at another prevalence, replace 0.5/0.5 with justified target weights and label the estimand accordingly. When interaction is substantial, lead with cell means and simple effects; a single marginal effect can hide decision-relevant heterogeneity.
Interpretation in the simulated experiment The estimated marginal coaching effect is 3.93 mmHg (95% CI 2.59 to 5.27). The estimated interaction is 2.25 mmHg (95% CI -0.43 to 4.93). These estimates quantify assigned-intervention contrasts under the simulated design. Their intervals should be read as ranges of values compatible with the model and data, not as binary declarations that an effect exists or does not exist.
Prespecified primary contrasts should drive sample size and
interpretation. If all six pairwise cell comparisons are explored,
simultaneous coverage matters. TukeyHSD() provides
familywise-error-controlled intervals for pairwise comparisons in this
balanced Gaussian ANOVA; it does not decide which comparisons are
scientifically important.
tukey_cells <- TukeyHSD(aov_fit, which = "coaching:home_monitoring")[[1]]
tukey_display <- data.frame(
Comparison = rownames(tukey_cells), tukey_cells,
row.names = NULL, check.names = FALSE
)
tukey_display[["p adj"]] <- format_p(tukey_display[["p adj"]])
knitr::kable(tukey_display, digits = 3,
caption = "Tukey-adjusted pairwise comparisons among the four factorial cells")| Comparison | diff | lwr | upr | p adj |
|---|---|---|---|---|
| Yes:No-No:No | 2.806 | 0.316 | 5.30 | 0.02031 |
| No:Yes-No:No | 3.214 | 0.723 | 5.70 | 0.00547 |
| Yes:Yes-No:No | 8.275 | 5.784 | 10.77 | < 0.001 |
| No:Yes-Yes:No | 0.407 | -2.083 | 2.90 | 0.97430 |
| Yes:Yes-Yes:No | 5.468 | 2.978 | 7.96 | < 0.001 |
| Yes:Yes-No:Yes | 5.061 | 2.571 | 7.55 | < 0.001 |
Do not use a global ANOVA as a gatekeeper that forbids reporting a prespecified contrast. Conversely, do not call one of many data-selected comparisons “confirmatory.” Report the family of hypotheses, adjustment, and whether an analysis was planned or exploratory.
Ignoring clinic does not undo randomization, but it leaves systematic clinic variation in the residual and can reduce precision. Because treatment is balanced within every clinic, adjustment is prespecified and cannot be driven by observed outcome imbalance.
crd_fit <- lm(bp_reduction ~ coaching * home_monitoring, data = doe_data)
linear_combo_se <- function(model, weights) {
coefficient_weights <- setNames(rep(0, length(coef(model))), names(coef(model)))
coefficient_weights[names(weights)] <- weights
sqrt(drop(t(coefficient_weights) %*% vcov(model) %*% coefficient_weights))
}
marginal_coaching_weights <- c(
coachingYes = 1,
"coachingYes:home_monitoringYes" = 0.5
)
se_coaching_crd <- linear_combo_se(crd_fit, marginal_coaching_weights)
se_coaching_rcbd <- linear_combo_se(rcbd_fit, marginal_coaching_weights)
blocking_comparison <- data.frame(
Analysis = c("Ignore clinic blocks", "Include clinic blocks"),
Residual_MSE = c(deviance(crd_fit) / df.residual(crd_fit),
deviance(rcbd_fit) / df.residual(rcbd_fit)),
Marginal_coaching_SE = c(se_coaching_crd, se_coaching_rcbd)
)
knitr::kable(blocking_comparison, digits = 3,
caption = "Residual variation and coaching-effect precision with and without clinic blocks")| Analysis | Residual_MSE | Marginal_coaching_SE |
|---|---|---|
| Ignore clinic blocks | 29.4 | 0.783 |
| Include clinic blocks | 22.1 | 0.679 |
The estimated relative efficiency from residual mean squares is 1.33: under these assumptions, the unblocked analysis would need roughly that multiplier of information to attain comparable residual precision. This is a descriptive comparison for the simulated design, not a universal property of blocking.
clinic_means <- aggregate(bp_reduction ~ clinic, data = doe_data, FUN = mean)
dotchart(
clinic_means[["bp_reduction"]], labels = clinic_means[["clinic"]],
pch = 19, color = palette_doe["teal"],
xlab = "Clinic mean BP reduction (mmHg)",
main = "Outcome differences across clinic blocks"
)
abline(v = mean(doe_data[["bp_reduction"]]), lty = 2, lwd = 2,
col = palette_doe["orange"])Mean BP reduction by clinic, with an overall mean reference line. Between-clinic variation is a nuisance source absorbed by clinic blocks, leaving treatment comparisons to be made locally within every clinic.
The linear-model standard errors assume independent, approximately homoscedastic errors and a correctly specified mean. Randomization protects the assignment mechanism, but it does not make every model-based standard error valid. Inspect residual patterns, tails, influence, and the level at which dependence can arise.
standardized_residuals <- rstandard(rcbd_fit)
old_par <- par(mfrow = c(2, 2), mar = c(4, 4, 2.5, 1))
plot(
fitted(rcbd_fit), resid(rcbd_fit), pch = 19,
col = grDevices::adjustcolor(palette_doe["teal"], alpha.f = 0.55),
xlab = "Fitted value", ylab = "Residual", main = "Residuals vs fitted"
)
abline(h = 0, lty = 2, col = palette_doe["gray"])
qqnorm(standardized_residuals, pch = 19, col = palette_doe["blue"],
main = "Normal Q-Q")
qqline(standardized_residuals, col = palette_doe["orange"], lwd = 2)
plot(
fitted(rcbd_fit), sqrt(abs(standardized_residuals)), pch = 19,
col = grDevices::adjustcolor(palette_doe["vermillion"], alpha.f = 0.55),
xlab = "Fitted value", ylab = expression(sqrt("|standardized residual|")),
main = "Scale-location"
)
plot(
cooks.distance(rcbd_fit), type = "h", col = palette_doe["navy"],
xlab = "Participant row", ylab = "Cook's distance", main = "Influence"
)Four diagnostics for the clinic-blocked factorial linear model: residuals versus fitted values, a normal quantile plot, scale-location behavior, and Cook’s distance. They assess mean structure, variance, tail behavior, and influential experimental units.
With moderate balanced cells, mean contrasts can be reasonably robust to mild nonnormality, but heavy tails, unequal variances, protocol errors, or informative missingness deserve sensitivity analyses. A transformation changes the effect scale. Heteroskedasticity-consistent or cluster-robust standard errors can be useful in other settings, but they do not repair the wrong experimental-unit definition, and reliable cluster-robust inference needs enough independent clusters.
A randomization test can analyze the treatment assignment mechanism directly. An exact test of the global sharp null would enumerate every allowed assignment while keeping outcomes and clinics fixed and jointly permuting the four-level treatment-combination labels within each clinic. Below, we approximate that distribution with 999 Monte Carlo draws and a plus-one -value. We prespecify two statistics: the interaction difference in differences and a three-degree-of-freedom omnibus treatment statistic.
omnibus_treatment_f <- function(data) {
reduced_fit <- lm(bp_reduction ~ clinic, data = data)
full_fit <- lm(bp_reduction ~ clinic + treatment_combination, data = data)
unname(anova(reduced_fit, full_fit)[["F"]][2])
}
interaction_statistic <- function(labels, outcome) {
means <- tapply(outcome, factor(labels, levels = 1:4), mean)
unname((means[4] - means[3]) - (means[2] - means[1]))
}
observed_f <- omnibus_treatment_f(doe_data)
observed_interaction <- interaction_statistic(
doe_data[["combination"]], doe_data[["bp_reduction"]]
)
set.seed(20260915)
B_permutations <- 999
clinic_indices <- split(seq_len(nrow(doe_data)), doe_data[["clinic"]])
permuted_f <- replicate(B_permutations, {
permuted_data <- doe_data
permuted_labels <- as.character(doe_data[["treatment_combination"]])
for (indices in clinic_indices) {
permuted_labels[indices] <- sample(permuted_labels[indices], replace = FALSE)
}
permuted_data[["treatment_combination"]] <- factor(
permuted_labels, levels = levels(doe_data[["treatment_combination"]])
)
omnibus_treatment_f(permuted_data)
})
# Reset the seed so the focused statistic uses exactly the same allowed
# assignment sequence as the omnibus statistic and the Chinese companion page.
set.seed(20260915)
permuted_interaction <- numeric(B_permutations)
for (b in seq_len(B_permutations)) {
permuted_labels <- integer(nrow(doe_data))
for (indices in clinic_indices) {
permuted_labels[indices] <- sample(doe_data[["combination"]][indices])
}
permuted_interaction[b] <- interaction_statistic(
permuted_labels, doe_data[["bp_reduction"]]
)
}
omnibus_randomization_p <- (1 + sum(permuted_f >= observed_f)) /
(B_permutations + 1)
interaction_randomization_p <-
(1 + sum(abs(permuted_interaction) >= abs(observed_interaction))) /
(B_permutations + 1)
randomization_result <- data.frame(
Prespecified_statistic = c(
"Interaction difference in differences (two-sided)",
"Clinic-adjusted 3-df omnibus treatment F"
),
Observed = c(observed_interaction, observed_f),
Permutations = c(B_permutations, B_permutations),
Fisher_sharp_null_p = c(interaction_randomization_p, omnibus_randomization_p)
)
knitr::kable(randomization_result, digits = 4,
caption = "Two prespecified statistics under the same restricted randomization test of the global sharp null")| Prespecified_statistic | Observed | Permutations | Fisher_sharp_null_p |
|---|---|---|---|
| Interaction difference in differences (two-sided) | 2.25 | 999 | 0.158 |
| Clinic-adjusted 3-df omnibus treatment F | 25.73 | 999 | 0.001 |
The plus-one calculation avoids a zero Monte Carlo -value. We permuted the four treatment labels jointly, not coaching and monitoring independently, and never moved a label across clinics. Both rows test the same sharp global null, but their sensitivity differs: the interaction statistic focuses on additive interaction, whereas the omnibus responds to any cell-mean difference and readily detects the large main effects here. Neither row is an exact test of an isolated weak or average-zero interaction null when main effects may exist.
old_par <- par(mfrow = c(1, 2), mar = c(4, 4, 3, 1))
hist(
permuted_interaction, breaks = 28, col = palette_doe["light"],
border = "white", xlab = "Permuted interaction (mmHg)",
main = "Interaction statistic"
)
abline(v = c(-abs(observed_interaction), abs(observed_interaction)),
col = palette_doe["orange"], lwd = 2, lty = 2)
hist(
permuted_f, breaks = 28, col = palette_doe["light"],
border = "white", xlab = "Permuted omnibus F statistic",
main = "Omnibus treatment F"
)
usr <- par("usr")
arrows(
x0 = usr[2] - 0.04 * diff(usr[1:2]), y0 = usr[3] + 0.12 * diff(usr[3:4]),
x1 = usr[2], y1 = usr[3] + 0.12 * diff(usr[3:4]),
col = palette_doe["orange"], lwd = 2, length = 0.08
)
text(
usr[2] - 0.04 * diff(usr[1:2]), usr[3] + 0.22 * diff(usr[3:4]),
labels = sprintf("Observed F = %.2f (off scale)", observed_f),
col = palette_doe["orange"], adj = 1, cex = 0.82
)Restricted randomization distributions for the focused interaction contrast and the three-degree-of-freedom omnibus treatment F under the same global sharp null.
Power is a property of a proposed design under assumptions—not a quality score computed after seeing a -value. A defensible plan names:
power.anova.test() treats the four combinations as
independent groups with a common within-group variance. It can screen an
omnibus four-cell question, but it ignores clinic blocking and does not
target a particular factorial contrast. The rough calculation adds the
prespecified clinic variance to the individual variance so it does not
silently omit clinic-level outcome variation; it still misrepresents
within-clinic correlation and the precision gain from blocking.
anova_power <- power.anova.test(
groups = 4, n = NULL,
between.var = var(unname(true_cell_means)),
within.var = 4.5^2 + 2.2^2,
sig.level = 0.05, power = 0.80
)
anova_power_summary <- data.frame(
Target = "Four-group omnibus ANOVA approximation",
Required_per_cell = ceiling(anova_power[["n"]]),
Approximate_total = 4 * ceiling(anova_power[["n"]]),
Assumed_within_SD = sqrt(4.5^2 + 2.2^2)
)
knitr::kable(anova_power_summary, digits = 2,
caption = "Coarse four-group power approximation using prespecified simulation values")| Target | Required_per_cell | Approximate_total | Assumed_within_SD |
|---|---|---|---|
| Four-group omnibus ANOVA approximation | 10 | 40 | 5.01 |
Because the result does not enforce multiples of 12 clinics or participants per clinic-cell, it cannot be copied directly into this RCBD protocol. Design feasibility and the exact primary contrast must determine rounding.
For a design-aligned calculation, simulate the proposed 12-clinic
complete-block schedule, clinic and participant variation, the
prespecified cell means, and the intended
clinic + coaching * home_monitoring analysis. The target
below is the 2.8-mmHg interaction. These assumptions are fixed in
advance; this is not post hoc power based on the observed estimate.
simulate_interaction_power <- function(replicates_per_clinic_cell,
simulations = 400) {
simulated_schedule <- expand.grid(
replicate = seq_len(replicates_per_clinic_cell),
combination = seq_len(4),
clinic_id = seq_len(n_clinics)
)
simulated_schedule[["coaching"]] <- factor(
c(0, 1, 0, 1)[simulated_schedule[["combination"]]],
levels = 0:1, labels = c("No", "Yes")
)
simulated_schedule[["monitoring"]] <- factor(
c(0, 0, 1, 1)[simulated_schedule[["combination"]]],
levels = 0:1, labels = c("No", "Yes")
)
simulated_schedule[["clinic"]] <- factor(simulated_schedule[["clinic_id"]])
coach <- as.integer(simulated_schedule[["coaching"]] == "Yes")
monitor <- as.integer(simulated_schedule[["monitoring"]] == "Yes")
p_values <- numeric(simulations)
for (s in seq_len(simulations)) {
simulated_clinic_effect <- rnorm(n_clinics, 0, 2.2)
simulated_outcome <- 4.0 + 3.0 * coach + 2.2 * monitor +
2.8 * coach * monitor +
simulated_clinic_effect[simulated_schedule[["clinic_id"]]] +
rnorm(nrow(simulated_schedule), 0, 4.5)
simulated_fit <- lm(
simulated_outcome ~ clinic + coaching * monitoring,
data = simulated_schedule
)
p_values[s] <- coef(summary(simulated_fit))[
"coachingYes:monitoringYes", "Pr(>|t|)"
]
}
mean(p_values < 0.05)
}
set.seed(20260916)
replication_grid <- c(2, 4, 6, 8)
simulated_power <- vapply(
replication_grid, simulate_interaction_power, numeric(1)
)
n_per_cell <- n_clinics * replication_grid
interaction_se_normal <- 2 * 4.5 / sqrt(n_per_cell)
noncentrality <- 2.8 / interaction_se_normal
z_critical <- qnorm(0.975)
normal_power <- pnorm(-z_critical - noncentrality) +
1 - pnorm(z_critical - noncentrality)
factorial_power <- data.frame(
Replicates_per_clinic_cell = replication_grid,
Total_participants = 4 * n_per_cell,
Simulated_power = simulated_power,
Monte_Carlo_SE = sqrt(simulated_power * (1 - simulated_power) / 400),
Normal_approximation = normal_power
)
knitr::kable(factorial_power, digits = 3,
caption = "Interaction power under a 12-clinic balanced factorial design")| Replicates_per_clinic_cell | Total_participants | Simulated_power | Monte_Carlo_SE | Normal_approximation |
|---|---|---|---|---|
| 2 | 96 | 0.348 | 0.024 | 0.332 |
| 4 | 192 | 0.565 | 0.025 | 0.578 |
| 6 | 288 | 0.760 | 0.021 | 0.752 |
| 8 | 384 | 0.843 | 0.018 | 0.862 |
plot(
factorial_power[["Total_participants"]], factorial_power[["Simulated_power"]],
type = "b", pch = 19, lwd = 2, ylim = c(0, 1),
col = palette_doe["teal"], xlab = "Total participants",
ylab = "Power", main = "Power for the factorial interaction"
)
lines(
factorial_power[["Total_participants"]], factorial_power[["Normal_approximation"]],
type = "b", pch = 1, lwd = 2, col = palette_doe["orange"]
)
abline(h = 0.80, lty = 2, col = palette_doe["gray"])
legend(
"bottomright", legend = c("Simulation", "Normal approximation", "80% target"),
col = c(palette_doe["teal"], palette_doe["orange"], palette_doe["gray"]),
pch = c(19, 1, NA), lty = c(1, 1, 2), bty = "n"
)Simulated and normal-approximation power for the prespecified 2.8-mmHg interaction as total sample size increases in multiples allowed by 12 complete clinic blocks.
Simulation uncertainty remains: with 400 repetitions, a power estimate near 0.80 has Monte Carlo standard error about . A protocol calculation should use more repetitions, vary all uncertain inputs, inflate recruitment for realistic outcome loss, and consider whether the interaction or a marginal main effect is truly primary.
The randomized assignment is a baseline variable and must never be overwritten by treatment received. The primary intention-to-treat (ITT) comparison estimates the effect of assignment under real adherence in the trial. Per-protocol and as-treated analyses condition on postrandomization behavior and can be confounded.
set.seed(20260917)
adherence_example <- doe_data
adherence_example[["received_coaching"]] <- factor(
ifelse(
adherence_example[["coaching"]] == "Yes",
rbinom(nrow(adherence_example), 1, 0.86),
rbinom(nrow(adherence_example), 1, 0.08)
),
0:1, c("No", "Yes")
)
adherence_table <- xtabs(~ coaching + received_coaching, data = adherence_example)
knitr::kable(adherence_table,
caption = "Illustrative assigned versus received coaching; the ITT model retains assignment")| No | Yes | |
|---|---|---|
| No | 89 | 7 |
| Yes | 6 | 90 |
Nonadherence is an outcome of the implementation process, not a reason to delete participants. If a per-protocol causal estimand is important, specify it in advance and use methods whose assumptions address postrandomization confounding, such as inverse-probability weighting or instrumental-variable approaches where justified.
Missing outcomes are different from nonadherence. Prevention begins with follow-up procedures and masked outcome collection. Report the number and reasons by randomized arm, define the missing-data estimand, and use sensitivity analyses for plausible departures from missing-at-random assumptions. Complete-case analysis can lose precision and be biased; simple last-observation-carried-forward or best/worst imputation does not generally solve the problem.
Ethics and design quality reinforce each other Clinical equipoise, a meaningful comparator, proportionate burden, informed consent, privacy protections, adverse-event monitoring, and a justified sample size are design requirements. Underpowered studies can expose participants without answering the question; excessive sample size can expose more participants than necessary. Subgroup and equity analyses should be planned with adequate representation and careful interpretation.
Suppose 12 clinics, rather than 192 participants, were randomized to a clinic-wide coaching policy. Outcomes from patients in the same clinic share one treatment assignment. Treating every patient as independently randomized would inflate the effective sample size: pseudoreplication. The treatment degrees of freedom and uncertainty must reflect 12 randomized clinics, along with intracluster correlation and small-cluster corrections where appropriate.
Technical replicates, repeated blood-pressure readings, bilateral organs, multiple time points, and multiple specimens can improve measurement but do not create new independent treatment assignments. Average or model subsamples at their proper level; never use them to manufacture experimental replication.
| Design | Assignment structure | Best use | Essential analysis boundary |
|---|---|---|---|
| Cluster-randomized | Groups receive treatments | Contamination, policy, or group delivery | Clusters are experimental units; plan ICC and enough clusters |
| Split-plot | Hard-to-change factor assigned to whole plots; another within plots | Multistage implementation or laboratory processing | Each effect uses its correct whole-plot or subplot error stratum |
| Crossover | Units receive sequences of treatments | Stable chronic conditions with reversible effects | Model period and within-person correlation; address carryover and washout |
| Latin square | Treatments balanced across two blocking dimensions | Two strong nuisance gradients | Basic square estimates treatment after rows/columns; interactions need replication |
| Repeated measures | Outcomes recur after one assignment | Trajectories and durability | Model within-unit correlation and time-by-treatment; visits are not new assignments |
| Incomplete block | Blocks cannot contain every treatment | Many treatments or capacity limits | Use the actual incidence structure and connected-design estimability |
In crossover studies, dropout, period effects, and carryover can compromise a simple paired analysis. In split-plot studies, testing a whole-plot intervention against participant-level residual error is pseudoreplication. In Latin squares, a single unreplicated square cannot freely estimate all treatment-by-block interactions. Design diagrams and randomization records should accompany the analysis plan.
The randomization principles remain, but the estimand and model scale change. A binary outcome may use a risk difference, risk ratio, or odds ratio; a count outcome may use a rate ratio with exposure time; repeated or clustered versions need appropriate correlation methods.
# Binary teaching outcome: at least an 8-mmHg reduction. Dichotomizing a
# continuous primary outcome loses information, so this would need a rationale.
outcome_example <- doe_data
outcome_example[["clinically_meaningful"]] <- outcome_example[["bp_reduction"]] >= 8
binary_fit <- glm(
clinically_meaningful ~ clinic + coaching * home_monitoring,
family = binomial, data = outcome_example
)
# Count teaching outcome with unequal observation time.
set.seed(20260918)
outcome_example[["followup_years"]] <- runif(nrow(outcome_example), 0.75, 1.00)
event_rate <- exp(-0.1 - 0.25 * (outcome_example[["coaching"]] == "Yes") -
0.15 * (outcome_example[["home_monitoring"]] == "Yes"))
outcome_example[["urgent_visits"]] <- rpois(
nrow(outcome_example), outcome_example[["followup_years"]] * event_rate
)
count_fit <- glm(
urgent_visits ~ clinic + coaching * home_monitoring +
offset(log(followup_years)),
family = poisson, data = outcome_example
)
outcome_model_examples <- data.frame(
Outcome = c("Continuous BP reduction", "Binary meaningful reduction", "Urgent-visit count"),
Model = c("Gaussian linear model", "Binomial logistic model", "Poisson log-rate model"),
Natural_effect_scale = c("Mean difference", "Odds ratio in this model", "Rate ratio")
)
knitr::kable(outcome_model_examples,
caption = "Outcome type changes the model and effect scale, not the randomization record")| Outcome | Model | Natural_effect_scale |
|---|---|---|
| Continuous BP reduction | Gaussian linear model | Mean difference |
| Binary meaningful reduction | Binomial logistic model | Odds ratio in this model |
| Urgent-visit count | Poisson log-rate model | Rate ratio |
For common binary outcomes, odds ratios can be far from risk ratios; adjusted probabilities or risk differences may be easier to interpret. For counts, inspect overdispersion and use the log of person-time as an offset. For all generalized models, interaction is scale-specific, and model coefficients may not equal marginal causal contrasts. Standardize predicted outcomes across the planned target population when marginal effects are required.
| Application | Factors and blocks | Primary design concern |
|---|---|---|
| Community hypertension program | Coaching × home monitoring; clinic blocks | Implementation fidelity and contamination |
| Vaccine formulation study | Antigen dose × adjuvant; site or production-lot blocks | Safety, dose-response, and multiplicity |
| Health-message experiment | Message frame × delivery channel; demographic strata | Interference and measurement of engagement |
| Laboratory assay optimization | Temperature × reagent concentration; plate/day blocks | Random run order and technical versus biological replication |
| Hospital implementation trial | Decision support × audit feedback; hospital clusters | Cluster-level assignment and few-cluster inference |
| Environmental intervention | Filtration × education; neighborhood blocks | Spillover, exposure measurement, and seasonal blocking |
Factorial designs are especially attractive when interventions can be combined and resources must answer more than one question. They are inappropriate when combinations are unsafe, operationally impossible, or scientifically meaningless. Fractional factorial and response-surface designs can screen many factors or optimize doses, but aliasing, curvature, sequential experimentation, and confirmatory validation must be planned explicitly.
case_summary <- data.frame(
Result = c(
"Participants", "Clinics", "Participants per factorial cell",
"Estimated marginal coaching effect (mmHg)",
"Estimated marginal monitoring effect (mmHg)",
"Estimated interaction (mmHg)",
"Global sharp-null 3-df randomization p-value"
),
Value = c(
nrow(doe_data), nlevels(doe_data[["clinic"]]),
min(as.vector(table(doe_data[["treatment_combination"]]))),
contrast_results[["Estimate"]][1], contrast_results[["Estimate"]][2],
contrast_results[["Estimate"]][3], omnibus_randomization_p
)
)
knitr::kable(case_summary, digits = 3,
caption = "Selected design and analysis results from the simulated experiment")| Result | Value |
|---|---|
| Participants | 192.000 |
| Clinics | 12.000 |
| Participants per factorial cell | 48.000 |
| Estimated marginal coaching effect (mmHg) | 3.934 |
| Estimated marginal monitoring effect (mmHg) | 4.341 |
| Estimated interaction (mmHg) | 2.255 |
| Global sharp-null 3-df randomization p-value | 0.001 |
We conducted a [CRD/RCBD/factorial/cluster/split-plot/crossover] experiment among [population, setting, and dates]. The experimental unit was [unit]; [factor levels] were assigned using [randomization procedure, restrictions, allocation ratio, and concealment], with [block/stratum] defined before assignment. The primary outcome was [definition, timing, direction], and the primary estimand was [explicit cell-mean contrast and population weighting]. We analyzed participants according to [assignment/other strategy] using [model], including [blocks, factors, interactions], and reported [effect scale] with 95% confidence intervals. Multiplicity was handled using [plan]. We assessed [missingness, adherence, residuals, dependence, influence, randomization-based sensitivity]. Limitations include [measurement, attrition, interference, generalizability, power, and design-specific constraints].
| Error | Why it is wrong | Better approach |
|---|---|---|
| Call every measured row an experimental unit | Measurements can share one assignment | Trace the level of independent randomization |
| Randomize globally but analyze as blocked | Analysis cannot invent restrictions not used | Record and reproduce the actual assignment mechanism |
| Shuffle coaching and monitoring separately in a randomization test | This may create assignments the design never allowed | Jointly permute four-arm labels within clinic |
Interpret coachingYes as a marginal main effect |
Under 0/1 coding it is simple at monitoring = No | Estimate an explicit four-cell contrast |
| Predict every cell at the reference clinic | The intercept encodes an arbitrary clinic | Average model-matrix rows across all target clinics |
| Drop the interaction after seeing its -value | Data-dependent selection changes the estimand and uncertainty | Follow the prespecified hierarchy and report cell effects |
| Treat as proof of no effect | Imprecision is not equivalence | Report estimates, intervals, and meaningful bounds |
| Use repeated readings as independent replicates | They do not receive independent assignments | Model or summarize subsamples within experimental units |
| Exclude nonadherent participants from the primary analysis | Adherence is postrandomization | Preserve ITT and label secondary causal estimands |
| Calculate observed power | It adds no information beyond estimate and -value | Plan prospective power across plausible scenarios |
| Ignore missing outcomes | Randomization does not eliminate attrition bias | Prevent loss, report flow, and perform sensitivity analyses |
| Generalize automatically from randomized participants | Randomization supports internal comparison, not sampling representativeness | Define the target population and assess transportability |
coachingYes estimate when
an interaction is in the model?power.anova.test() only a rough approximation
for the primary interaction here?| Target | Formula or code | Interpretation |
|---|---|---|
| RCBD factorial model | lm(y ~ block + A * B) |
Block-adjusted cells, main terms, and interaction |
| Coaching marginal effect | Coaching averaged equally over monitoring | |
| Monitoring marginal effect | Monitoring averaged equally over coaching | |
| Interaction | Difference between two simple effects | |
| Linear contrast variance | Uses full coefficient covariance | |
| Pairwise cell comparisons | TukeyHSD(aov_fit) |
Simultaneous pairwise intervals in a suitable ANOVA |
| Global randomization test | Permute joint arm labels within block | Tests the sharp null under the actual restriction |
| Four-group screening power | power.anova.test(...) |
Omnibus approximation, not a factorial contrast |
| Binary outcome | glm(..., family = binomial) |
Conditional odds on the logit scale |
| Count rate | glm(... + offset(log(time)), family = poisson) |
Conditional event-rate comparison |
Next topics include covariate-adjusted randomized-trial analysis, ANCOVA with baseline outcomes, response-surface methodology, fractional factorial screening, optimal designs, sequential and adaptive designs, noninferiority and equivalence, group-sequential monitoring, stepped-wedge cluster trials, randomization-based confidence intervals, treatment-effect heterogeneity, mediation, principal stratification, and transportability.
## R version 4.6.1 (2026-06-24)
## Platform: aarch64-apple-darwin23
## Running under: macOS Tahoe 26.5.1
##
## Matrix products: default
## BLAS: /Library/Frameworks/R.framework/Versions/4.6/Resources/lib/libRblas.0.dylib
## LAPACK: /Library/Frameworks/R.framework/Versions/4.6/Resources/lib/libRlapack.dylib; LAPACK version 3.12.1
##
## locale:
## [1] C.UTF-8/C.UTF-8/C.UTF-8/C/C.UTF-8/C.UTF-8
##
## time zone: America/Edmonton
## tzcode source: internal
##
## attached base packages:
## [1] stats graphics grDevices utils datasets methods base
##
## loaded via a namespace (and not attached):
## [1] digest_0.6.39 R6_2.6.1 fastmap_1.2.0 xfun_0.60 cachem_1.1.0
## [6] knitr_1.51 htmltools_0.5.9 rmarkdown_2.31 lifecycle_1.0.5 cli_3.6.6
## [11] sass_0.4.10 jquerylib_0.1.4 compiler_4.6.1 tools_4.6.1 evaluate_1.0.5
## [16] bslib_0.12.0 yaml_2.3.12 rlang_1.3.0 jsonlite_2.0.0