Skip to contents

A fixed-sample-size DesignFixed that first partitions subjects into B approximately equal-sized, covariate-homogeneous blocks by (approximately or exactly) minimizing total within-block pairwise covariate distance \(\sum_{k=1}^{B} \sum_{i, j \in \text{block } k, i < j} D(x_i, x_j)\), then randomizes treatment independently within each resulting block at probability prob_T (via block_ra, the same mechanism as DesignFixedBlocking). Unlike DesignFixedBlocking, which forms blocks from user-specified column-wise strata (categorical levels / quantile bins), here blocks are formed directly from a multivariate distance/clustering criterion over all covariates jointly, so this design generalizes matched-pair designs (DesignFixedBinaryMatch) from block size 2 to arbitrary block size \(B\). When B is omitted and n is known at initialization, the default is floor(sqrt(n)), truncated below at 1 (the usual heuristic block count for balancing within-block homogeneity against within-block sample size).

Block-formation algorithms. method selects among three block construction strategies with different exactness/scalability trade-offs (see the new() argument documentation for details): "K-way" (default, balanced k-means-style anticlustering via anticlust, fast and empirically close to optimal), "greedy" (nearest-neighbor greedy matching via blockTools, fast even for large \(n\)), and "ompr" (an exact mixed-integer program solved with GLPK via ompr/ompr.roi, globally optimal but scaling as \(O(n^2 B)\) in the number of decision variables — practical only for small \(n\)). Block membership is computed lazily (on first draw, via get_or_compute_block_ids()) and cached in private$block_ids for reuse across replicates.

Distance specification ("ompr" only). dist selects the pairwise distance \(D(x_i, x_j)\) the exact solver minimizes: "euclidean", "sum_abs_diff" (sum of absolute coordinate differences), "mahal" (default, Mahalanobis distance accounting for covariate correlation/scale), or a user-supplied distance function. The "K-way" and "greedy" methods use their own respective packages' built-in distance conventions and do not consult dist.

No-covariate fallback. If the covariate matrix has zero columns, block membership is assigned by simple round-robin (rep(seq_len(B), length.out = n)) rather than by any of the three clustering algorithms, since there is no covariate information to cluster on.

Solver backend (method = "ompr" only). roi_solver selects the MILP backend ompr.roi dispatches to, a closed set c("glpk", "gurobi", "cplex") (default "glpk") – validated against this set, not passed through to ROI::ROI_registered_solvers() unchecked, so a typo or an unsupported solver name fails fast with a clear message rather than an opaque ompr error three layers down. Gurobi and CPLEX are supported because they extend which block-formation problems stay practical, not just which are expressible: GLPK's branch-and-bound is single-threaded with no commercial-grade presolve/cutting-plane machinery, while Gurobi/CPLEX are typically an order of magnitude faster on the same MILP and solve in parallel, meaningfully extending the \(n\) for which the exact "ompr" method stays practical. Scope is deliberately closed to these two for now – not because other ROI plugins (CBC, SYMPHONY, ...) wouldn't work mechanically (the dispatch is generic), but because Gurobi and CPLEX are the two most widely used commercial solvers and the only ones worth a maintained step-by-step guide at this point; extending the closed set is a small, low-risk addition later if a real need for a third backend appears, not a reason to leave the set open-ended now.

Wiring up Gurobi:

  1. Obtain a Gurobi license (a free academic license is available from Gurobi for non-commercial use) and install the Gurobi Optimizer itself. This sets up GUROBI_HOME and the license file (gurobi.lic, discoverable via the GRB_LICENSE_FILE environment variable or Gurobi's default search path) – entirely outside this package's control or dependency graph.

  2. Install Gurobi's own R package. Not available via CRAN – it ships inside the Gurobi installation itself: R CMD INSTALL "$GUROBI_HOME/R/gurobi_<version>_R_<Rmajor.minor>.tar.gz" (exact filename/path depends on your Gurobi version and platform; see the R/ subdirectory of your Gurobi install). This is the vendor interface ROI.plugin.gurobi wraps – required even though it's not what you call directly.

  3. Install the ROI bridge package from CRAN: install.packages("ROI.plugin.gurobi"). This package is on CRAN (it only depends on ROI + the gurobi R package from step 2 being present at load time) and is the only new artifact this class's own dependency graph ever touches.

  4. Verify: after library(ROI.plugin.gurobi), "gurobi" %in% ROI::ROI_registered_solvers() should be TRUE.

  5. Pass roi_solver = "gurobi" to the constructor.

Wiring up CPLEX:

  1. Obtain an IBM CPLEX license (a free academic license is available from IBM) and install IBM ILOG CPLEX Optimization Studio.

  2. Install Rcplex (CRAN), CPLEX's R interface. Unlike ROI.plugin.gurobi, Rcplex is a source package that compiles against your local CPLEX installation – it needs to be pointed at your CPLEX SDK's include/lib directories at install time (typically via configure.args to install.packages(), naming your CPLEX version's cplex/include/cplex/lib/<platform> paths). The exact flag names and paths are CPLEX-version- and platform-specific – follow Rcplex's own INSTALL/README instructions for your installed CPLEX version rather than a fixed command copied from here, since this changes across CPLEX releases.

  3. Install the ROI bridge package from CRAN: install.packages("ROI.plugin.cplex") (depends on Rcplex from step 2 being present and working).

  4. Verify: after library(ROI.plugin.cplex), "cplex" %in% ROI::ROI_registered_solvers() should be TRUE.

  5. Pass roi_solver = "cplex" to the constructor.

Dependency-graph consequence: ROI.plugin.gurobi/ROI.plugin.cplex (and Rcplex) are never added to Suggests – they're free/CRAN- available themselves, but declaring them would misrepresent the dependency as something install.packages("EDI", dependencies = TRUE) could satisfy, when the vendor package/license underneath cannot be. The lazy-check pattern already used for ompr/ompr.roi/ROI.plugin.glpk extends naturally: check requireNamespace("ROI.plugin.gurobi"/"ROI.plugin.cplex") at solve time (not at package load or class-definition time) for whichever roi_solver was requested, and error informatively – naming the missing package and, if that's present but the solve still fails, noting the likely cause is a missing vendor license/installation, not something this class can diagnose further.

Bootstrap. draw_bootstrap_indices() resamples within blocks by default (bootstrap_type = "within_blocks" or NULL, via stratified_bootstrap_indices_cpp()) or resamples whole blocks otherwise (via resample_group_rows_cpp()), mirroring the block structure in the resampling scheme, as in DesignFixedBlocking.

References

Higgins, M. J., Sävje, F., and Sekhon, J. S. (2016). "Improving massive experiments with threshold blocking." Proceedings of the National Academy of Sciences, 113(27), 7369-7376, doi:10.1073/pnas.1510504113 , for optimal/near-optimal covariate-based blocking prior to randomization. See also randomized block design for orientation and Mahalanobis distance for the default "ompr" distance.

Super classes

Design -> DesignFixed -> DesignFixedOptimalBlocks

Methods

+ inherited public methods from DesignFixed
+ inherited public methods from Design


DesignFixedOptimalBlocks$supports_batch_w_pregeneration()

Returns TRUE so the calling framework pre-generates all replicate w vectors for a simulation cell in one batch, paying the one-time block-formation cost (K-way anticlustering, greedy matching, or the exact ompr/GLPK solve) once per cell and reusing the resulting block assignment across replicates, rather than recomputing it per replicate.

Usage

DesignFixedOptimalBlocks$supports_batch_w_pregeneration()

Returns

Always TRUE for this class.


DesignFixedOptimalBlocks$new()

Initialize a fixed optimal-blocks design. Block formation itself is deferred until the first draw (see class documentation); this constructor only validates and records configuration, including checking that B (or its floor(sqrt(n)) default) admits a feasible block-size partition of n when n is already known.

Usage

DesignFixedOptimalBlocks$new(
  B = NULL,
  method = "K-way",
  dist = "mahal",
  roi_solver = "glpk",
  response_type,
  prob_T = 0.5,
  include_is_missing_as_a_new_feature = TRUE,
  n = NULL,
  verbose = FALSE,
  missingness_method = "impute",
  design_formula = ~.,
  seed = NULL
)

Arguments

B

Number of blocks to form. If omitted and n is supplied, defaults to floor(sqrt(n)), with a minimum of 1.

method

Algorithm used to partition subjects into blocks.

"K-way" (default)

Balanced k-means anticlustering via anticlust::balanced_clustering. Requires the anticlust package. Produces well-spread blocks and is significantly faster than "greedy" (e.g., ~10x faster for \(n=200, p=10, B=10\)) while achieving a better within-block distance objective (e.g., ~4% lower).

"greedy"

Greedy nearest-neighbour matching via blockTools::block. Requires the blockTools package. Fast even for large \(n\).

"ompr"

Exact mixed-integer programme solved with GLPK via ompr. Globally optimal but scales as \(O(n^2 B)\) in variables and is only practical for small \(n\).

dist

Distance specification used only when method = "ompr". Either a function or one of "euclidean", "sum_abs_diff", or "mahal". Default is "mahal".

roi_solver

MILP backend used only when method = "ompr". A closed set c("glpk", "gurobi", "cplex"), default "glpk". See the class documentation's "Solver backend" and wiring-guide sections for the Gurobi/CPLEX setup steps.

response_type

The response type for the design.

prob_T

Treatment assignment probability within each block.

include_is_missing_as_a_new_feature

Whether to include missingness indicators.

n

Planned sample size.

verbose

Whether to print progress messages.

missingness_method

How to handle missing values in covariates.

design_formula

A formula object.

seed

Integer seed for reproducibility.

Returns

A new DesignFixedOptimalBlocks object.


DesignFixedOptimalBlocks$clone()

The objects of this class are cloneable with this method.

Usage

DesignFixedOptimalBlocks$clone(deep = FALSE)

Arguments

deep

Whether to make a deep clone.

Examples

des = DesignFixedOptimalBlocks$new(n = 9, response_type = 'continuous')
des$add_all_subjects_to_experiment(data.frame(x = rnorm(9)))
des$assign_w_to_all_subjects()