Covariate-Constrained Randomization in CRTs

Author

Solomon Eshun

Published

November 5, 2025

Intro

Cluster-randomized trials (CRTs) randomize intact groups (like clinics, schools, villages) rather than individuals, and are widely used when interventions are delivered at the group level or when individual randomization may lead to contamination between study arms (Murray 1998; Hayes and Moulton 2017). CRTs often involve a relatively small number of randomized units, with some trials allocating as few as ten or twenty clusters across two arms. When the number of clusters is small, simple randomization can, by chance alone, produce substantial differences between study arms in important cluster-level covariates (Ivers et al. 2012; Moulton 2004).

This imbalance can confound estimated intervention effects, and analyses that adjust for many cluster-level covariates quickly exhaust the limited degrees of freedom available in a small-cluster trial. Unlike individually randomized studies, where the law of large numbers tends to balance covariates across arms, CRTs offer no such guarantee, precisely because the unit of randomization is scarce (Li et al. 2016).

Covariate-constrained randomization (CCR) addresses this problem directly. Rather than accepting whatever allocation simple randomization produces, CCR restricts the randomization to the subset of allocations that achieve acceptable balance on prespecified covariates (Raab and Butcher 2001; Moulton 2004). The procedure enumerates (or samples) the space of possible allocations, scores each one with a balance criterion, retains only those allocations meeting the criterion, and then randomly selects the final allocation from this constrained set. This preserves randomization while excluding the highly imbalanced allocations that can arise by chance in small trials (Carter and Hood 2008; Li et al. 2016).

A central design question in CCR is how tightly to constrain. Restrict too little and imbalance remains; restrict too aggressively and the constrained set becomes small, which can compromise the validity of randomization-based inference and inflate the correlation between allocation and covariates (Bailey and Rowley 1987; Li et al. 2017). This post implements CCR for a two-arm cluster trial using a small example. Cluster-level covariates are generated, a balance metric is defined, candidate allocations are enumerated, the constrained randomization space is constructed, and the balance achieved under constrained and simple randomization is compared. The resulting workflow provides a reusable framework for designing a covariate-constrained allocation and illustrates how the choice of constraint affects the resulting balance.

Generating Cluster-Level Data

We begin by simulating a two-arm cluster trial with a modest number of clusters. For each cluster we generate several baseline covariates that we would want balanced across arms: the cluster size, a baseline outcome rate, a continuous socioeconomic index, and a binary urban/rural indicator. These are exactly the kinds of cluster-level characteristics that, left to chance, can end up imbalanced in a small trial.

library(dplyr)
library(tidyr)
library(ggplot2)
set.seed(42)

# Number of clusters (small, as is typical in CRTs)
K <- 16          # total clusters
n_treat <- K / 2 # clusters assigned to treatment

# Generate cluster-level covariates
clusters <- data.frame(
  cluster_id  = 1:K,
  size        = round(rnorm(K, mean = 250, sd = 60)),        # cluster size
  base_rate   = round(rbeta(K, 2, 8), 3),                    # baseline outcome rate
  ses_index   = round(rnorm(K, mean = 0, sd = 1), 2),        # socioeconomic index
  urban       = rbinom(K, 1, 0.4)                            # 1 = urban, 0 = rural
)

head(clusters)
  cluster_id size base_rate ses_index urban
1          1  332     0.160     -2.41     0
2          2  216     0.013      0.04     0
3          3  272     0.157      0.21     1
4          4  288     0.175     -0.36     1
5          5  274     0.141      0.76     1
6          6  244     0.163     -0.73     0

The covariates we want to balance are the four cluster-level characteristics. Because they are measured on very different scales—cluster size in the hundreds, the baseline rate between 0 and 1, the socioeconomic index roughly standard normal, and the urban indicator binary—any sensible balance metric must put them on a common footing before combining them.

covars <- c("size", "base_rate", "ses_index", "urban")

# Standardize covariates so they contribute comparably to the balance score
clusters_std <- clusters
clusters_std[covars] <- scale(clusters[covars])

head(clusters_std)
  cluster_id        size   base_rate   ses_index      urban
1          1  0.88258163 -0.04391999 -2.27864521 -0.6527912
2          2 -1.06982779 -1.35151274  0.08752412 -0.6527912
3          3 -0.12728531 -0.07060556  0.25170730  1.4361407
4          4  0.14201254  0.08950784 -0.29878924  1.4361407
5          5 -0.09362308 -0.21292858  0.78288817  1.4361407
6          6 -0.59855655 -0.01723443 -0.65612910 -0.6527912

A Balance Criterion

The heart of CCR is the rule used to decide whether a candidate allocation is acceptable. A common and interpretable choice is a balance score built from the standardized difference in covariate means between the two arms (Raab and Butcher 2001; Gallis et al. 2018). For a given allocation, we compute the difference in means for each standardized covariate and summarize these differences into a single number—here, the sum of squared standardized mean differences. Smaller scores indicate better balance, and the allocation with a score of zero would be perfectly balanced on the means of all covariates. The choice of metric is not unique, however: any reasonable summary of imbalance can serve, and the framework accommodates alternatives such as the sum of absolute standardized differences, a Mahalanobis-type distance that accounts for correlations among covariates, or a weighted score that prioritizes covariates believed to be more strongly prognostic (Raab and Butcher 2001; Gallis et al. 2018). What matters is that the metric orders allocations sensibly from well- to poorly balanced; the sum of squared standardized differences is simply a transparent and widely used default.

Concretely, for covariate \(j\) with treatment-arm mean \(\bar{x}_{1j}\) and control-arm mean \(\bar{x}_{0j}\), the balance score \(B\) for an allocation is

\[ B = \sum_{j=1}^{p} \left( \bar{x}_{1j} - \bar{x}_{0j} \right)^2, \]

where the \(x_{ij}\) are standardized so that each covariate contributes on a comparable scale (Raab and Butcher 2001; Li et al. 2016). Other criteria exist—weighted versions that prioritize covariates believed to be more prognostic, or the \(l_1\) distance instead of the squared distance—but the squared standardized difference is a natural default and is what the cvcrand framework uses by default (Gallis et al. 2018). We implement the score as a small reusable function.

# Balance score: sum of squared standardized mean differences between arms.
# `assignment` is a 0/1 vector indicating treatment for each cluster.
balance_score <- function(assignment, data, covars) {
  treated  <- data[assignment == 1, covars, drop = FALSE]
  control  <- data[assignment == 0, covars, drop = FALSE]

  diffs <- colMeans(treated) - colMeans(control)
  sum(diffs^2)
}

Enumerating the Allocation Space

With a balance score in hand, the next step is to build the space of candidate allocations. For a two-arm trial that assigns exactly n_treat of the K clusters to treatment, the number of possible allocations is \(\binom{K}{n_{\text{treat}}}\). With 16 clusters split evenly, this is \(\binom{16}{8} = 12{,}870\) allocations—small enough to enumerate completely. For larger trials where full enumeration is infeasible, the standard approach is to draw a large random sample of allocations instead, which the same machinery supports (Gallis et al. 2018; Li et al. 2016).

# Enumerate every way to choose n_treat treated clusters out of K
combos <- combn(K, n_treat)
n_allocations <- ncol(combos)
n_allocations
[1] 12870

We now score every candidate allocation. Each column of combos lists the clusters assigned to treatment; we convert it to a 0/1 assignment vector and compute its balance score.

# Score all candidate allocations
scores <- apply(combos, 2, function(treated_ids) {
  assignment <- as.integer(1:K %in% treated_ids)
  balance_score(assignment, clusters_std, covars)
})

summary(scores)
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
0.07117 0.49713 0.87141 1.00000 1.35142 4.60707 

The distribution of balance scores across all possible allocations tells us how much imbalance simple randomization risks. Many allocations achieve good balance, but a non-trivial fraction are poorly balanced—these are precisely the allocations we want to exclude.

Forming the Constrained Randomization Space

The constraint is applied by keeping only the allocations whose balance score falls below a chosen threshold. A widely used convention is to retain the best-balanced fraction of allocations—commonly the top 10%—which keeps the constrained set large enough to preserve randomization validity while removing the worst imbalances (Li et al. 2016; Gallis et al. 2018). We adopt that convention here, retaining the allocations in the lowest decile of balance scores.

# Retain the best-balanced 10% of allocations
cutoff <- quantile(scores, probs = 0.10)
constrained_idx <- which(scores <= cutoff)

length(constrained_idx)          # size of the constrained space
[1] 1288
cutoff                           # balance-score threshold
      10% 
0.2675201 

From this constrained set, the final allocation is chosen at random. This is the step that preserves randomization: the trialist does not pick the single most-balanced allocation (which would be a deterministic, non-random choice), but rather draws uniformly from the many well-balanced allocations that satisfy the constraint.

# Randomly select the final allocation from the constrained set
set.seed(2024)
chosen <- sample(constrained_idx, 1)
final_treated_ids <- combos[, chosen]

final_assignment <- as.integer(1:K %in% final_treated_ids)
clusters$arm <- ifelse(final_assignment == 1, "Treatment", "Control")

clusters[, c("cluster_id", "arm", covars)]
   cluster_id       arm size base_rate ses_index urban
1           1 Treatment  332     0.160     -2.41     0
2           2   Control  216     0.013      0.04     0
3           3   Control  272     0.157      0.21     1
4           4 Treatment  288     0.175     -0.36     1
5           5 Treatment  274     0.141      0.76     1
6           6   Control  244     0.163     -0.73     0
7           7   Control  341     0.035     -1.37     0
8           8 Treatment  244     0.281      0.43     0
9           9   Control  371     0.118     -0.81     0
10         10   Control  246     0.333      1.44     0
11         11 Treatment  328     0.415     -0.43     1
12         12   Control  387     0.121      0.66     1
13         13   Control  167     0.290      0.32     0
14         14 Treatment  233     0.037     -0.78     0
15         15 Treatment  242     0.103      1.58     0
16         16 Treatment  288     0.097      0.64     0

Comparing Constrained and Simple Randomization

To see what the constraint buys us, we compare the balance achieved under CCR with the balance we would expect from simple randomization. We draw a large number of simple-randomization allocations, compute their balance scores, and contrast that distribution with the constrained set.

# Balance scores under simple randomization vs the constrained set
set.seed(7)
simple_draws <- replicate(5000, {
  assignment <- sample(rep(c(1, 0), each = n_treat))
  balance_score(assignment, clusters_std, covars)
})

plot_df <- bind_rows(
  data.frame(score = simple_draws,           method = "Simple"),
  data.frame(score = scores[constrained_idx], method = "Constrained")
)

ggplot(plot_df, aes(x = score, fill = method)) +
  geom_density(alpha = 0.5) +
  labs(x = "Balance score (sum of squared standardized mean differences)",
       y = "Density", fill = NULL) +
  theme_minimal()

The constrained distribution is concentrated near zero, whereas the simple-randomization distribution has a long right tail of poorly balanced allocations. In other words, CCR removes exactly the allocations that would have left the arms imbalanced, while still leaving a rich set of allocations to randomize among.

We can also quantify the improvement directly by comparing the mean balance score under each approach.

comparison <- data.frame(
  Method = c("Simple randomization", "Constrained randomization"),
  `Mean balance score` = c(mean(simple_draws),
                           mean(scores[constrained_idx])),
  `Worst balance score` = c(max(simple_draws),
                            max(scores[constrained_idx])),
  check.names = FALSE
)

comparison
                     Method Mean balance score Worst balance score
1      Simple randomization          1.0102564           3.8668416
2 Constrained randomization          0.1826744           0.2675201

The constrained approach delivers a substantially lower mean balance score and, by construction, a far lower worst-case score. The practical payoff is that the analysis of the trial begins from arms that are comparable on the covariates we cared about, reducing reliance on model-based adjustment that a small trial can ill afford.

A design caution is worth restating. The tighter the constraint—the smaller the retained fraction—the better the balance, but the smaller the constrained randomization space, and the stronger the induced dependence between the allocation and the covariates. This dependence has implications for the analysis: randomization-based inference should account for the constrained design, and model-based analyses generally should adjust for the covariates used in the constraint (Li et al. 2016, 2017; Gallis et al. 2018). Choosing the constraint is therefore a balance in its own right, between covariate balance and the validity of inference.

Conclusion

This post walked through covariate-constrained randomization for a two-arm cluster trial, from motivation to a working simulation. We generated cluster-level covariates, defined a balance criterion based on standardized mean differences, enumerated the allocation space, formed a constrained randomization set from the best-balanced allocations, and selected a final allocation at random from that set. Comparing constrained with simple randomization made the benefit concrete: CCR concentrates the design on well-balanced allocations while preserving the randomness that underpins valid inference. For trials with few clusters—where chance imbalance is a real threat and degrees of freedom are scarce—CCR is a principled and practical tool. The cvcrand package implements this workflow with additional features for analysis and is a natural next step for applied use (Gallis et al. 2018).

Thank you for reading, and I welcome your feedback and any experiences you have had designing cluster-randomized trials!

References

Bailey, R. A., and C. A. Rowley. 1987. “Restricted Randomization: A Practical Example.” Journal of the American Statistical Association 82 (399): 712–19.
Carter, Bruce R., and Kerenza Hood. 2008. “Constrained Randomization for Comparing Multiple Management Strategies in a Single Large Cluster.” BMC Medical Research Methodology 8 (1): 77.
Gallis, John A., Fan Li, Hengshi Yu, and Elizabeth L. Turner. 2018. “Cvcrand: A Package for Covariate-Constrained Randomization and the Clustered Permutation Test for Cluster Randomized Trials.” The Stata Journal 18 (2): 357–78.
Hayes, Richard J., and Lawrence H. Moulton. 2017. Cluster Randomised Trials. 2nd ed. Boca Raton, FL: Chapman; Hall/CRC.
Ivers, Noah M., Ilana J. Halperin, Jan Barnsley, Jeremy M. Grimshaw, Baiju R. Shah, Karen Tu, Ross Upshur, and Merrick Zwarenstein. 2012. “Allocation Techniques for Balance at Baseline in Cluster Randomized Trials: A Methodological Review.” Trials 13 (1): 120.
Li, Fan, Yuliya Lokhnygina, David M. Murray, Patrick J. Heagerty, and Elizabeth R. DeLong. 2016. “Evaluation of the Covariate-Constrained Randomization Design for Cluster Randomized Trials.” Statistics in Medicine 35 (10): 1565–79.
Li, Fan, Elizabeth L. Turner, Patrick J. Heagerty, David M. Murray, William M. Vollmer, and Elizabeth R. DeLong. 2017. “An Evaluation of Constrained Randomization for the Design and Analysis of Group-Randomized Trials with Binary Outcomes.” Statistics in Medicine 36 (24): 3791–3806.
Moulton, Lawrence H. 2004. “Covariate-Based Constrained Randomization of Group-Randomized Trials.” Clinical Trials 1 (3): 297–305.
Murray, David M. 1998. Design and Analysis of Group-Randomized Trials. New York: Oxford University Press.
Raab, Gillian M., and Isabella Butcher. 2001. “Balance in Cluster Randomized Trials.” Statistics in Medicine 20 (3): 351–65.