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

References

[1] Quan, W., Rose, J. M., Collins, A. T., & Bliemer, M. C. (2011). A comparison of algorithms for generating efficient choice experiments.