Example of a D-efficient RUM design using the Modified Federov algorithm
This notebook illustrates how to use ChoiceDesign with the Modified Federov optimisation algorithm. Unlike the random swapping and RSC algorithms — which make small moves within the current design — Modified Federov searches more systematically:
At each iteration, a random row is selected from the current design.
That row is compared against every row in the full factorial of attribute levels.
The candidate that yields the greatest D-error improvement is kept.
This makes each iteration more expensive than swap or RSC (one D-error evaluation per candidate row), but each accepted move is guaranteed to be the best available replacement for that row.
The design setup — attributes, utility functions, and stopping criteria — is identical to the d_efficient_rum_simple example. The only difference is the algorithm='federov' argument passed to optimise().
Step 1: Load modules, define design parameters and set attributes
[1]:
from choicedesign.design import EffDesign
from choicedesign.expressions import Attribute, Parameter
The following lines define 2 alternatives with 4 attributes each, named \(A\) to \(D\):
[2]:
alt1_A = Attribute('alt1_A', [1, 2, 3])
alt1_B = Attribute('alt1_B', [10, 15, 15.5])
alt1_C = Attribute('alt1_C', [0, 3, 5])
alt1_D = Attribute('alt1_D', [0, 1, 2])
alt2_A = Attribute('alt2_A', [1, 2, 3])
alt2_B = Attribute('alt2_B', [10, 15, 15.5])
alt2_C = Attribute('alt2_C', [0, 3, 5])
alt2_D = Attribute('alt2_D', [0, 1, 2])
Step 2: Construct the design object and generate the initial design matrix
[3]:
design = EffDesign(
X=[alt1_A, alt1_B, alt1_C, alt1_D,
alt2_A, alt2_B, alt2_C, alt2_D],
ncs=18)
[4]:
init_design = design.gen_initdesign(seed=42)
init_design
[4]:
| alt1_A | alt1_B | alt1_C | alt1_D | alt2_A | alt2_B | alt2_C | alt2_D | |
|---|---|---|---|---|---|---|---|---|
| 0 | 1.0 | 10.0 | 5.0 | 1.0 | 2.0 | 15.5 | 5.0 | 2.0 |
| 1 | 2.0 | 15.0 | 3.0 | 2.0 | 3.0 | 15.0 | 5.0 | 0.0 |
| 2 | 3.0 | 10.0 | 5.0 | 1.0 | 2.0 | 15.5 | 5.0 | 1.0 |
| 3 | 3.0 | 15.5 | 0.0 | 1.0 | 1.0 | 10.0 | 3.0 | 2.0 |
| 4 | 1.0 | 15.5 | 3.0 | 0.0 | 1.0 | 15.5 | 5.0 | 0.0 |
| 5 | 2.0 | 15.0 | 3.0 | 2.0 | 3.0 | 15.0 | 3.0 | 0.0 |
| 6 | 2.0 | 10.0 | 3.0 | 2.0 | 2.0 | 10.0 | 0.0 | 1.0 |
| 7 | 1.0 | 10.0 | 0.0 | 0.0 | 3.0 | 15.5 | 0.0 | 0.0 |
| 8 | 3.0 | 15.5 | 5.0 | 0.0 | 1.0 | 10.0 | 5.0 | 2.0 |
| 9 | 3.0 | 10.0 | 0.0 | 2.0 | 2.0 | 15.0 | 0.0 | 0.0 |
| 10 | 1.0 | 10.0 | 0.0 | 0.0 | 1.0 | 15.5 | 0.0 | 0.0 |
| 11 | 3.0 | 15.0 | 5.0 | 1.0 | 3.0 | 10.0 | 3.0 | 1.0 |
| 12 | 2.0 | 15.0 | 5.0 | 2.0 | 1.0 | 15.0 | 3.0 | 1.0 |
| 13 | 1.0 | 15.5 | 3.0 | 2.0 | 3.0 | 15.0 | 0.0 | 2.0 |
| 14 | 2.0 | 15.5 | 3.0 | 0.0 | 3.0 | 15.5 | 0.0 | 1.0 |
| 15 | 2.0 | 15.0 | 5.0 | 1.0 | 2.0 | 15.0 | 5.0 | 2.0 |
| 16 | 3.0 | 15.5 | 0.0 | 1.0 | 2.0 | 10.0 | 3.0 | 2.0 |
| 17 | 1.0 | 15.0 | 0.0 | 0.0 | 1.0 | 10.0 | 3.0 | 1.0 |
Step 3: Define the utility functions
[5]:
beta_A = Parameter('beta_A', -0.1)
beta_B = Parameter('beta_B', -0.02)
beta_C = Parameter('beta_C', 0.1)
beta_D = Parameter('beta_D', 0.15)
[6]:
V1 = beta_A * alt1_A + beta_B * alt1_B + beta_C * alt1_C + beta_D * alt1_D
V2 = beta_A * alt2_A + beta_B * alt2_B + beta_C * alt2_C + beta_D * alt2_D
V = {1: V1, 2: V2}
Step 4: Optimise using the Modified Federov algorithm
The algorithm='federov' argument selects the Modified Federov algorithm. Note that Federov performs fewer iterations per unit time than swap or RSC, since each iteration evaluates all rows in the full factorial as candidates. A shorter time_lim is often sufficient:
[7]:
optimal_design, init_perf, final_perf, final_iter, ubalance_ratio = design.optimise(
init_design=init_design,
V=V,
model='mnl',
algorithm='federov',
time_lim=1,
verbose=True
)
Evaluating initial design
Optimization complete 0:00:57 / D-error: 0.028581
Elapsed time: 0:01:00
D-error of initial design: 0.080551
D-error of last stored design: 0.028581
Utility Balance ratio: 93.61 %
Algorithm iterations: 24
Step 5: Block the design
[8]:
optimal_design_blocked, corr_history = design.gen_blocks(optimal_design, n_blocks=3)
optimal_design_blocked
[8]:
| CS | alt1_A | alt1_B | alt1_C | alt1_D | alt2_A | alt2_B | alt2_C | alt2_D | Block | |
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1.0 | 1.0 | 10.0 | 5.0 | 1.0 | 2.0 | 15.5 | 5.0 | 2.0 | 3 |
| 1 | 2.0 | 1.0 | 10.0 | 0.0 | 0.0 | 3.0 | 15.5 | 5.0 | 2.0 | 1 |
| 2 | 3.0 | 3.0 | 10.0 | 5.0 | 1.0 | 2.0 | 15.5 | 5.0 | 1.0 | 2 |
| 3 | 4.0 | 1.0 | 10.0 | 5.0 | 0.0 | 3.0 | 15.5 | 0.0 | 2.0 | 3 |
| 4 | 5.0 | 1.0 | 10.0 | 5.0 | 2.0 | 3.0 | 15.5 | 0.0 | 0.0 | 3 |
| 5 | 6.0 | 1.0 | 10.0 | 5.0 | 2.0 | 3.0 | 15.5 | 0.0 | 0.0 | 1 |
| 6 | 7.0 | 1.0 | 10.0 | 0.0 | 0.0 | 3.0 | 15.5 | 5.0 | 2.0 | 2 |
| 7 | 8.0 | 1.0 | 10.0 | 0.0 | 0.0 | 3.0 | 15.5 | 0.0 | 0.0 | 2 |
| 8 | 9.0 | 1.0 | 15.5 | 5.0 | 0.0 | 3.0 | 10.0 | 0.0 | 2.0 | 1 |
| 9 | 10.0 | 1.0 | 15.5 | 5.0 | 0.0 | 3.0 | 10.0 | 0.0 | 2.0 | 3 |
| 10 | 11.0 | 1.0 | 15.5 | 0.0 | 2.0 | 3.0 | 10.0 | 5.0 | 0.0 | 3 |
| 11 | 12.0 | 1.0 | 15.5 | 5.0 | 0.0 | 3.0 | 10.0 | 0.0 | 2.0 | 1 |
| 12 | 13.0 | 1.0 | 15.5 | 0.0 | 0.0 | 3.0 | 10.0 | 5.0 | 2.0 | 3 |
| 13 | 14.0 | 1.0 | 15.5 | 0.0 | 2.0 | 3.0 | 10.0 | 5.0 | 0.0 | 2 |
| 14 | 15.0 | 1.0 | 15.5 | 5.0 | 2.0 | 3.0 | 10.0 | 0.0 | 0.0 | 2 |
| 15 | 16.0 | 2.0 | 15.0 | 5.0 | 1.0 | 2.0 | 15.0 | 5.0 | 2.0 | 1 |
| 16 | 17.0 | 1.0 | 10.0 | 0.0 | 2.0 | 3.0 | 15.5 | 5.0 | 0.0 | 1 |
| 17 | 18.0 | 1.0 | 15.0 | 0.0 | 0.0 | 1.0 | 10.0 | 3.0 | 1.0 | 2 |
(Optional) Evaluate the design
[9]:
perf, ubalance = design.evaluate(optimal_design, V, model='mnl')
print(perf, ubalance)
0.02858103102201532 93.60915724865137
Export the design
Export the Federov-optimised design to Excel.
[ ]:
attr_names = {
'alt1_A': 'Attribute A', 'alt2_A': 'Attribute A',
'alt1_B': 'Attribute B', 'alt2_B': 'Attribute B',
'alt1_C': 'Attribute C', 'alt2_C': 'Attribute C',
'alt1_D': 'Attribute D', 'alt2_D': 'Attribute D',
}
design.export_design(optimal_design, attr_names, 'rum_federov_design.xlsx')
Save the optimisation summary
After calling optimise(), the method export_output() writes a plain-text summary of the optimisation run — design configuration, stopping criteria, criterion values, utility balance, elapsed time, and iteration count — to a file.
[ ]:
design.export_output('rum_federov_output.txt')
References
[1] Quan, W., Rose, J. M., Collins, A. T., & Bliemer, M. C. (2011). A comparison of algorithms for generating efficient choice experiments.