How to force my own DIY experimental design for conjoint analysis?


This note is prepared for those familiar with the specifics of discrete choice experimentation. If you need us to help, please feel free to contact us for a quote to customise your experimental design.
DIY experimental design

With Conjointly, you can set up experimental designs of your choosing and perform data collection. Once data collection is complete, you can access the standard reports that do not take your design restrictions into account and download the files for your own analysis that takes your specific design into account.

The experimental design for the Claims Test, Product Variant Selector, Generic Conjoint, and Brand Specific Conjoint are generated on the fly. There are special JavaScript hooks that allow you to interfere in the experimental design process and modify the resultant design using customisations. These hooks are useful if you want to force specific restrictions and even a predetermined design.

This note covers all you need to know about customising your DIY experimental design.

For Generic Conjoint

initializeSurvey

When the survey page is opened, the initializeSurvey method is called. In this method, we initialise the experiment settings. We load the scheme of attributes, restrictions, and weights.

renderFrames

When switching to the frame of the first conjoint block, the renderFrames method is called. This method deletes the old generated frames with answers (if we returned to this frame again with the back button).

This method calls the genericConjointUpdateSettings hook, where you can override the settings of the questionnaire.

The genericConjointUpdateSettings hook

This hook requires the parameter settings. Some properties cannot be changed in the customisation, and will result in the inability to save the survey:

  • settings.attributes
  • settings.nAlternatives
  • settings.enableNothingOption

The settings that can be changed are:

  • settings.prohibitions is a matrix [levels x levels] where if the intersection is 0, then this pair is prohibited. It is completed based on prohibited pairs. You can redefine it and set new pairs.
  • settings.maxQuestions is the maximum number of conjoint questions (can be overridden).
  • settings.userConditions is a [levels] array - you can set weights for certain levels:
    • The greater the weight, the more often it will be shown,
    • 0 will completely hide the level for the user.

Representing levels in generation methods

All levels are represented by a one-dimensional array, i.e. collapsed into a single level. For example, in an experiment with the following attributes:

  • Attribute 1:

    • white,
    • yellow,
    • red
  • Attribute 2:

    • small,
    • large
  • Attribute 3:

    • 1000,
    • 4000,
    • 5000,
    • 6000

Then they will be flattened into one array with the meaning of [white, yellow, red, small, large, 1000, 4000, 5000, 6000]. For the design, it will be just a one-dimensional zero-indexed array of numbers, where 6 corresponds to 4000.

The generateNewBlock method

The method returns an array of sets, each set is an array of alternatives, and the alternative is an array of level numbers. During the operation of the method, the levels are named based on the position in the collapsed version (in the above example, the second attribute of the third level will have the number 6).

But at the end of the generateNewBlock method, the level numbers are recoded, and the level numbers are recorded as 1-indexed values within the attribute (i.e. the 6 from the example above becomes 2).

generateNewBlock method

Generic Conjoint and Claims Test return different response structures. There is only one attribute for Claims Test, hence the array has one less nesting level.

You can get into the generation mechanism with several hooks.

The genericConjointIsAlternativeValid hook

It is called when a new alternative is added to the set. Parameters are passed:

  • newAlternative is an array with level numbers (example [0,6])
  • candidate is an array with alternatives already added to the set (example [[0,4],[2,5]])

The method works like a filter. If you return an empty array (return []), then the alternative will not be added to the set (there will be an attempt to generate a new one).

If the alternative is suitable, then you need to return it in the method (return newAlternative).

Example usages:
  • You can make it so that they do not fall into one set with a minimum and maximum price.

  • It is possible to make the color yellow and the size big not shown in the same alternative.

The genericConjointIsSCUValid hook

The method works similarly to genericConjointIsAlternativeValid but works on a processed version of the alternative, in which all price levels are replaced by 0.

This is done in order to check and not allow alternatives that differ only in price.

The genericConjointIsCandidateValid hook

The method is called when the set is added to the block:

  • candidate is an array with alternatives (example [[0,4],[2,5],[0,6]])

  • block is an array with included candidates (example [[[1,4],[2,4],[2,6]],[[0,5],[1,],[2,6]],[[ 2,4],[2,5],[1,4]])

For example, you can look at the prices in the previous block, and make sure that they do not match.

The genericConjointNewAlternativeMaxAttempts and genericConjointNewCandidateMaxAttempts hooks

This sets the number of attempts to generate a set. The default is 100 attempts.

If you set some complex filters that will reject many options, then it might be worth increasing the maximum number of attempts for generation.

If it not possible to generate a set in the maximum number of attempts set, then the algorithm stops, displaying the number of sets it was possible to generate.

The genericConjointUserFilter hook

This method is called before generating a new alternative, allowing you to disable or reduce the weighs of some levels independently for each alternative.

For example, you can disable level A, if level A was shown in the previous set. Or you can disable a level if level B or C is already presented in this set.

This method gets parameters:

  • userFilter - array for each level, you can modify it
  • candidate - current set candidate (not complete)
  • block - array of block, already included to design

The genericConjointChangeCandidate hook

This hook allows you to manually fill every set. It is called after a set is generated, meaning you can make changes to it, even completely change all alternatives.

This method gets parameters:

  • candidate - current set candidate (not inserted to the block)
  • block - array of block, already included to design

For Claims Test and Product Variant Selector

For Claims Test, the principle is similar, but the names of the hooks are slightly different, and the structure of the parameters

  • claimsTestUpdateSettings
  • claimsTestIsCandidateValid
    • candidate is an array with alternatives (example [[0,4,5,7]])
    • block is an array with included candidates (example [[1,4,3,5],[0,1,2,3],[2,6,7,8]])
  • claimsTestNewCandidateMaxAttempts
  • claimsTestNewAlternativeMaxAttempts
  • claimsTestUserFilter
  • claimsTestChangeCandidate

For Brand-Specific Conjoint

While similar, the way that alternatives are presented changes for brand-specific conjoint.

For each brand we include all acceptable levels by applicability. E.g., we have 3 attributes:

  • brand [‘A’, ‘B’, ‘C’]
  • feature [‘f1’, ‘f2’, ‘f3’]
  • price [10, 20, 30]

And following applicability table:

  • brand A could have f1, f2 and price 20 and 30
  • brand B could have f2, f3 and price 10
  • brand C could have f3 and price 10 and 30

The levels for dynamic design would be numerated as follows:

  • 1 - A, f1
  • 2 - A, f2
  • 3 - A, 20
  • 4 - A, 30
  • 5 - B, f2
  • 6 - B, f3
  • 7 - B, 10
  • 8 - C, f3
  • 9 - C, 10
  • 10 - C, 30

An example alternative of brand A, feature f1, price 20 would be represented by the array [1,3]. To represent an alternative of brand C, feature f3, price 30 would be [8,10].

The brand-specific hooks are as follows:

  • brandSpecificConjointUpdateSettings
  • brandSpecificConjointIsCandidateValid
  • brandSpecificConjointNewCandidateMaxAttempts
  • brandSpecificConjointNewAlternativeMaxAttempts
  • brandSpecificConjointUserFilter
  • brandSpecificConjointChangeCandidate

Example customisations

Example 1: Remove the combination of the first and fifth levels in the Generic Conjoint

This example does not actually require a customisation because you can do this with a forbidden pair.

<script>
  function myFilterDeclineCandidate(newAlternative,candidate) {
    if (newAlternative.includes(0) && newAlternative.includes(4)) {
      return [];
    };
    
    return newAlternative;
  }
  
  window.hooks.addFilter('genericConjointIsAlternativeValid', 'survey', myFilterDeclineCandidate, 20);
  window.hooks.addFilter('genericConjointNewAlternativeMaxAttempts', 'survey', function() {return 200;}, 20);
</script>

Example 2: Remove the first 3 claims from the user if they answered a certain way to the preliminary additional question

Set the maximum number of questions from the answer to the additional question. Remove certain combinations of brands in one set.

<script>
  function myFilterDeclineCandidate(candidate, block) 
    // Remove claims 7 and 8 in a single set
    if (candidate.includes(6) && candidate.includes(7)) {
      return [];
    };
    
        
    return candidate;
  }
  function myUpdateSettings(settings) {
    settings.maxQuestions = $('#additionalQuestions3-short-answer').get(0).value;
    if ($('#additionalQuestions4-multiple-options-9203').get(0).checked) {
      settings.userConditions = [0,0,0,1,1,1,1,1];
    } else {
      settings.userConditions = [1,1,1,1,1,1,1,1];
    };

  }
  
  window.hooks.addFilter('claimsTestIsCandidateValid', 'survey', myFilterDeclineCandidate, 20);
  window.hooks.addFilter('claimsTestNewCandidateMaxAttempts', 'survey', function() {return 200;}, 20);
  window.hooks.addAction('claimsTestUpdateSettings', 'survey', myUpdateSettings, 20);
</script>

Example 3: Make sure that all alternatives in the same set have the same level for the first attribute

Imagine there are only three levels in the first attribute.

<script>
  
  function myFilterDeclineSet(newSet,block) {
     
    if (
      !newSet.every( x => x[0] == 0) && 
      !newSet.every( x => x[0] == 1) &&  
      !newSet.every( x => x[0] == 2) 
    ) { 
      return []
    } 

    
    return newSet;
  }
  
  window.hooks.addFilter('genericConjointIsCandidateValid', 'survey', myFilterDeclineSet, 20);
  
  window.hooks.addFilter('genericConjointNewAlternativeMaxAttempts', 'survey', function() {return 200;}, 20);
  window.hooks.addFilter('genericConjointNewCandidateMaxAttempts', 'survey', function() {return 20000;}, 20);
</script> 

Example 4: Make sure that levels 1 and 2 are not shown together in a one-attribute MaxDiff

<script>
  
  var prohibited_combination = [1,2];
  
  function myFilterDeclineCandidate(candidate, block) {
    if (candidate[0].includes(prohibited_combination[0]-1) &&
        candidate[0].includes(prohibited_combination[1]-1)) {
      return [];
    };        
    return candidate;
  }
  
  window.hooks.addFilter('genericConjointIsCandidateValid', 'survey', myFilterDeclineCandidate, 20);
  window.hooks.addFilter('genericConjointNewAlternativeMaxAttempts', 'survey', function() {return 200;}, 20);
</script>

Example 5: Don’t show level 1, if it was show in previous set

<script>
  
  var TARGET_LEVEL = 1;
  
  function myUserFilter(filter, candidate, block) {
    if (block.length > 0) {
      lastSet = block[block.length-1];
      if (lastSet.flat().includes(TARGET_LEVEL)) {
        filter[TARGET_LEVEL] = 0;
        console.log(filter);
      }
    }
    return filter;
  }
  
  window.hooks.addFilter('genericConjointUserFilter', 'survey', myUserFilter, 20);

</script>

Example 6: Manually define desirable first set to the survey

<script>
  
  function mySetChanges(candidate, block) {
    if (block.length === 0) {
      candidate = [
        [1,3,7,9],
        [0,2,7,8]
        ]
    }
    return candidate;
  }
  
  window.hooks.addFilter('genericConjointChangeCandidate', 'survey', mySetChanges, 20);

</script>