Augmenting designs with controlled efficiency loss

library(optedr)

Motivation

In practice an experiment is rarely designed from scratch. A researcher may already have data collected at certain conditions and want to add new observations to improve estimation — without discarding what has already been measured. The key question is: where can new points be placed so that the efficiency of the augmented design stays above an acceptable threshold?

optedr answers this question with two functions used in sequence:

  1. get_augment_region() — computes the candidate region: the set of design points whose addition keeps the D-efficiency of the augmented design above a user-specified threshold delta_val.
  2. augment_design() — adds a chosen point to the initial design and rescales the weights.

Both functions support the same optimality criteria as opt_des() and work for any number of factors.

Key parameters

Parameter Role
init_design Current design (data frame with Point/Weight in 1D, or factor columns + Weight in multi-D)
alpha Fraction of total weight assigned to the new point after augmentation
delta_val Minimum acceptable D-efficiency of the augmented design
calc_optimal_design If TRUE, also computes the optimal design and uses it as the reference for efficiency
new_points Data frame of points to add (non-interactive mode); omit for interactive mode
par_int Indices of parameters of interest (Ds-Optimality only)
n_lhs Number of Latin-Hypercube candidates for the region search (multi-D)

One-factor augmentation

Step 1: compute the candidate region

We start with a uniform three-point design for Antoine’s equation and look for points that keep the D-efficiency of the augmented design above 85 %.

init_des <- data.frame(
  Point  = c(30, 60, 90),
  Weight = c(1/3, 1/3, 1/3)
)

region <- get_augment_region(
  criterion           = "D-Optimality",
  init_design         = init_des,
  alpha               = 0.25,
  model               = y ~ 10^(a - b / (c + x)),
  parameters          = c("a", "b", "c"),
  par_values          = c(8.07131, 1730.63, 233.426),
  design_space        = c(1, 100),
  calc_optimal_design = FALSE,
  delta_val           = 0.85
)

print(region)
#> Augment candidate region  (delta = 0.8500)
#>   Intervals: [5.361, 100]

region$region is a data frame of candidate intervals. Each row gives a lower and upper bound on the design space where the new point can be placed.

Step 2: choose a point and augment

new_pt <- mean(region$region[1:2])

augmented <- augment_design(
  criterion           = "D-Optimality",
  init_design         = init_des,
  alpha               = 0.25,
  model               = y ~ 10^(a - b / (c + x)),
  parameters          = c("a", "b", "c"),
  par_values          = c(8.07131, 1730.63, 233.426),
  design_space        = c(1, 100),
  calc_optimal_design = FALSE,
  delta_val           = 0.85,
  new_points          = data.frame(Point = new_pt, Weight = 1)
)

print(augmented)
#>      Point Weight
#> 1 30.00000   0.25
#> 2 60.00000   0.25
#> 3 90.00000   0.25
#> 4 52.68026   0.25
cat("Sum of weights:", sum(augmented$Weight), "\n")
#> Sum of weights: 1

Comparing efficiency before and after

result_opt <- opt_des(
  "D-Optimality",
  y ~ 10^(a - b / (c + x)), c("a", "b", "c"),
  c(8.07131, 1730.63, 233.426), c(1, 100)
)
#> 
#> ℹ Stop condition not reached, max iterations performed
#> ⠙ Calculating optimal design 22 done (33/s) | 660ms
#> ℹ The lower bound for efficiency is 99.9986187505804%

eff_before <- design_efficiency(init_des, result_opt)
#> ℹ The efficiency of the design is 38.5312233926718%
eff_after  <- design_efficiency(augmented, result_opt)
#> ℹ The efficiency of the design is 34.2933573563283%

cat("Efficiency before augmenting:", round(eff_before * 100, 2), "%\n")
#> Efficiency before augmenting: 38.53 %
cat("Efficiency after augmenting: ", round(eff_after  * 100, 2), "%\n")
#> Efficiency after augmenting:  34.29 %
cat("Gain:                        ", round((eff_after - eff_before) * 100, 2),
    "percentage points\n")
#> Gain:                         -4.24 percentage points

Using the optimal design as reference (calc_optimal_design = TRUE)

When calc_optimal_design = TRUE, the function internally computes the optimal design and uses it to define the efficiency threshold. This is the recommended mode when no optimal design has been computed yet:

region_opt <- get_augment_region(
  criterion           = "D-Optimality",
  init_design         = init_des,
  alpha               = 0.25,
  model               = y ~ 10^(a - b / (c + x)),
  parameters          = c("a", "b", "c"),
  par_values          = c(8.07131, 1730.63, 233.426),
  design_space        = c(1, 100),
  calc_optimal_design = TRUE,
  delta_val           = 0.85
)

Two-factor augmentation

In multi-dimensional spaces get_augment_region() samples candidate points with a Latin Hypercube (controlled by n_lhs) and returns a data frame of candidates together with their estimated efficiency gain. A heatmap of the efficiency function is displayed automatically.

Initial design and candidate region

init_2d <- data.frame(
  x1     = c(0.8, 10, 5),
  x2     = c(10, 0.8, 5),
  Weight = c(1/3, 1/3, 1/3)
)

result_2D <- opt_des(
  criterion    = "D-Optimality",
  model        = y ~ Vmax * x1 * x2 / ((K1 + x1) * (K2 + x2)),
  parameters   = c("Vmax", "K1", "K2"),
  par_values   = c(1, 1, 1),
  design_space = list(x1 = c(0.1, 10), x2 = c(0.1, 10))
)
#> 
#> ℹ Stop condition reached: difference between sensitivity and criterion < 1e-05
#> ⠙ Calculating optimal design 19 done (19/s) | 983ms
#> ℹ The lower bound for efficiency is 99.9990058762859%

region_2d <- get_augment_region(
  criterion           = "D-Optimality",
  init_design         = init_2d,
  alpha               = 0.25,
  model               = y ~ Vmax * x1 * x2 / ((K1 + x1) * (K2 + x2)),
  parameters          = c("Vmax", "K1", "K2"),
  par_values          = c(1, 1, 1),
  design_space        = list(x1 = c(0.1, 10), x2 = c(0.1, 10)),
  calc_optimal_design = FALSE,
  delta_val           = 0.85
)

#> ℹ 1911 candidate points with efficiency >= 0.85 (from LHS sample of 2000)

region_2d$region is a data frame of sampled candidates, each with an efficiency column. Pick the candidate that maximises efficiency:

best_2d <- region_2d$region[which.max(region_2d$region$efficiency), ]

eff_antes <- suppressMessages(design_efficiency(init_2d, result_2D))

aug_2d <- augment_design(
  criterion           = "D-Optimality",
  init_design         = init_2d,
  alpha               = 0.25,
  model               = y ~ Vmax * x1 * x2 / ((K1 + x1) * (K2 + x2)),
  parameters          = c("Vmax", "K1", "K2"),
  par_values          = c(1, 1, 1),
  design_space        = list(x1 = c(0.1, 10), x2 = c(0.1, 10)),
  calc_optimal_design = FALSE,
  delta_val           = 0.85,
  new_points          = data.frame(x1 = best_2d$x1, x2 = best_2d$x2, Weight = 1)
)

#> ℹ 1905 candidate points with efficiency >= 0.85 (from LHS sample of 2000)
#> Sample of candidate points:
#>           x1        x2 efficiency
#> 1  0.7611782 1.8747102  0.9477295
#> 2  1.5770442 4.1902073  0.8810413
#> 3  5.0766615 4.8306139  0.9412320
#> 4  1.3090452 6.2035373  0.9028696
#> 5  7.0637485 7.2161732  1.0996926
#> 6  3.9557854 0.2860621  0.8832119
#> 7  9.3979549 4.0914459  1.0178637
#> 8  7.1685236 2.4781115  0.8925406
#> 9  0.8498649 6.5480135  0.9370562
#> 10 9.1585168 3.3176372  0.9660453
#> 11 9.8404978 2.5341046  0.9290985
#> 12 4.7510415 4.9451374  0.9332216
#> 13 6.7557968 5.2888438  1.0179250
#> 14 5.1546876 6.6373871  1.0083672
#> 15 7.1764757 6.3282685  1.0729088

eff_despues <- suppressMessages(design_efficiency(aug_2d, result_2D))

cat("Efficiency before:", round(eff_antes  * 100, 2), "%\n")
#> Efficiency before: 68.4 %
cat("Efficiency after: ", round(eff_despues * 100, 2), "%\n")
#> Efficiency after:  84.5 %
print(aug_2d)
#>          x1        x2 Weight
#> 1  0.800000 10.000000   0.25
#> 2 10.000000  0.800000   0.25
#> 3  5.000000  5.000000   0.25
#> 4  9.763098  9.637105   0.25

Three-factor augmentation

For three or more factors the candidate region is displayed as a scatter-matrix coloured by candidate/non-candidate status, with the current design shown as triangles.

init_3d <- data.frame(
  x1     = c(0.8, 10,  10,  0.8, 10),
  x2     = c(10,  0.8, 10,  10,  0.8),
  x3     = c(10,  10,  0.8, 0.8, 10),
  Weight = rep(0.2, 5)
)

region_3d <- get_augment_region(
  criterion           = "D-Optimality",
  init_design         = init_3d,
  alpha               = 0.45,
  model               = y ~ Vmax * x1 * x2 * x3 / ((K1+x1) * (K2+x2) * (K3+x3)),
  parameters          = c("Vmax", "K1", "K2", "K3"),
  par_values          = c(1, 1, 1, 1),
  design_space        = list(x1 = c(0.1, 10), x2 = c(0.1, 10), x3 = c(0.1, 10)),
  calc_optimal_design = FALSE,
  delta_val           = 0.93
)

#> ℹ 989 candidate points with efficiency >= 0.93 (from LHS sample of 2000)
cat("Number of candidate points:", nrow(region_3d$region), "\n")
#> Number of candidate points: 989
plot(region_3d$plot)

Augmenting with Ds-Optimality

When the goal is to augment while preserving estimation quality for a subset of parameters, use criterion = "Ds-Optimality" and pass par_int:

region_ds <- get_augment_region(
  criterion           = "Ds-Optimality",
  init_design         = init_2d,
  alpha               = 0.25,
  model               = y ~ Vmax * x1 * x2 / ((K1 + x1) * (K2 + x2)),
  parameters          = c("Vmax", "K1", "K2"),
  par_values          = c(1, 1, 1),
  design_space        = list(x1 = c(0.1, 10), x2 = c(0.1, 10)),
  calc_optimal_design = FALSE,
  par_int             = c(1),
  delta_val           = 0.85,
  n_lhs               = 5000
)

#> ℹ 3528 candidate points with efficiency >= 0.85 (from LHS sample of 5000)

best_ds <- region_ds$region[which.max(region_ds$region$efficiency), ]

aug_ds <- augment_design(
  criterion           = "Ds-Optimality",
  init_design         = init_2d,
  alpha               = 0.25,
  model               = y ~ Vmax * x1 * x2 / ((K1 + x1) * (K2 + x2)),
  parameters          = c("Vmax", "K1", "K2"),
  par_values          = c(1, 1, 1),
  design_space        = list(x1 = c(0.1, 10), x2 = c(0.1, 10)),
  calc_optimal_design = FALSE,
  par_int             = c(1),
  delta_val           = 0.85,
  new_points          = data.frame(x1 = best_ds$x1, x2 = best_ds$x2, Weight = 1),
  n_lhs               = 5000
)

#> ℹ 3522 candidate points with efficiency >= 0.85 (from LHS sample of 5000)
#> Sample of candidate points:
#>           x1       x2 efficiency
#> 1  6.7835260 0.402285  0.9071073
#> 2  6.7474203 3.590032  1.0783303
#> 3  7.4717268 9.361849  2.4541269
#> 4  0.5344098 9.185204  0.8655550
#> 5  3.2656891 5.063149  0.8815170
#> 6  5.9696669 4.167820  1.1425919
#> 7  0.2454912 3.297296  0.9301367
#> 8  0.1982677 5.470957  0.8785634
#> 9  7.4017492 9.145874  2.4039867
#> 10 9.7450771 5.072476  1.7978839
#> 11 4.6115665 5.714111  1.2064730
#> 12 0.5320061 8.010434  0.8772578
#> 13 6.9280476 9.716881  2.3647339
#> 14 8.2884997 9.759583  2.7066870
#> 15 7.4194035 9.800505  2.5053478
print(aug_ds)
#>          x1        x2 Weight
#> 1  0.800000 10.000000   0.25
#> 2 10.000000  0.800000   0.25
#> 3  5.000000  5.000000   0.25
#> 4  9.879799  9.766438   0.25

Interactive mode

Omitting new_points (and delta_val) from both functions triggers an interactive session where the package plots the candidate region and asks the user to type a point. This mode is documented in ?augment_design.