Temper market share data

Respondents can overstate behavior changes in an anticipated future share distribution when one or more new products are offered. In this tutorial we demonstrate how a scale question can be used to adjust overstated market share distributions in an exercise called "tempering".

In this example, we use a data process to calculate a tempered share by reducing the new product shares, and redistributing the reduced shares back into the products that respondents currently use. The following examples all assume that the code below is executing within a data process (aka "green card") under Project Settings ... > Data or in dynamic precalculate under Project Settings > Precalculate

Example questionnaire

A survey asks for current prescribing behavior in Q100. Then in Q200 the survey asks for a new distribution assuming that Product C is available for prescribing. If Product C receives a larger market share than expected, you can use a scale question such as Q210 to temper down the stated shares.

Q100. Approximately what percentage of the following products do you currently prescribe:

  • Q100_1 Product A ____ %
  • Q100_2 Product B ____ %

Q200. Should Product C be available for prescribing, what will your future prescribing distribution be:

  • Q200_1 Product A ____ %
  • Q200_2 Product B ____ %
  • Q200_3 Product C ____ %

Q210. What is your overall likelihood of using Product C? 1: Not at all likely 2: Somewhat likely 3: Fairly likely 4: Very likely

Choose a factor to weight stated shares by

Determine a likelihood factor (%) for each value in Q210. How you do this is up to you. It might be based on a question like Q210 (What is your overall likelihood of using Product C?). Your firm might establish the following tempering factors:

1 (Not at all likely): 0% 2 (Somewhat likely): 33% 3 (Fairly likely): 50% 4 (Very likely): 67%

Temper and reallocate shares

Our standard tempering algorithm works as follows. For each respondent:

  • Calculate a temper factor ε for the new product
  • Reduce or "temper" stated share of the new product by that temper factor.
  • Reallocate that reduced share to products based on change in stated share from prior scenario.

This is illustrated in the diagram at the top of this page:

Protobi tempering algorithm

Protobi provides a built in algorithm temper() for tempering. It works at the respondent or "row" level. It takes the stated shares for the prior scenario and new scenario, plus a temper factor for the newly introduced product.

Example dataset

First let's create an example respondent dataset with two rows:

let rows = [
  {
	  respondent: 1,
    Q100_1: 0.6,  // product A share before
    Q100_2: 0.4,  // product B share before
    Q100_3: null,  // product C share before
    Q200_1: 0.4,  // product A share in new scenario
    Q200_2: 0.3, // product B share in new scenario
    Q200_3: 0.3, // product C share in new scenario,
    Q210: 4
  },
   {
	 respondent: 2,
    Q100_1: 0.6, 
    Q100_2: 0.4, 
    Q100_3: null, 
    Q200_1: 0.6,   // example where existing Product A shares stay the same
    Q200_2: 0.2, 
    Q200_3: 0.2,
    Q210: 4
  }
]

Apply tempering

  // define temper factors per your organization's standards
  let temper_factors = {
	   1:  0.00,
		 2: 0.33,
		 3: 0.50,
		 4: 0.67
	}

 // temper each row
for (let row of rows) {
  let prior = Protobi.pick(row, ["Q100_1", "Q100_2", "Q100_3"])
  let next = Protobi.pick(row, ["Q200_1", "Q200_2", "Q200_3"])

  let temper_factor = temper_factors[row.Q210]   // row.Q210 is 4 so temper factor is 0.67 or a 0.33 reduction
  let eps = [null, null, 1 - temper_factor]
  let result = Protobi.temper(prior, next, eps) 

  Protobi.apply(row, result, ["tQ200_1", "tQ200_2", "tQ200_3"])
  $("#out").append($("<pre>").html(JSON.stringify(row,null,4)))
}

Result

The rows now include tempered share values:

[
  {
    "respondent": 1,
    "Q100_1": 0.6,
    "Q100_2": 0.4,
    "Q100_3": null,
    "Q200_1": 0.4,
    "Q200_2": 0.3,
    "Q200_3": 0.3,
		"Q210": 4,
    "tQ200_1": 0.46666,
    "tQ200_2": 0.33333,
    "tQ200_3": 0.2000
},
{
    "respondent": 2,
    "Q100_1": 0.6,
    "Q100_2": 0.4,
    "Q100_3": null,
    "Q200_1": 0.6,
    "Q200_2": 0.2,
    "Q200_3": 0.2,
		"Q210": 4,
    "tQ200_1": 0.6,
    "tQ200_2": 0.26666,
    "tQ200_3": 0.13333
  }
]

Appendix: Source code

Below is the source code for the tempering algorithm built into Protobi data process tools.
You don't actually need to read or use the code below, it's built in to Protobi. But it does illustrate how you can create custom methods of your own to add or override those in Protobi.


/**
 * Returns an array of values from a row, one for each column name
 */
Protobi.pick =function(row,cols) {
  return cols.map(col=> row[col])
}

/**
 * Applies an array of values to a row, one for each column name
 */
Protobi.apply = function(row, vals, cols) {
  cols.forEach((col, idx) => row[col] = vals[idx])
}

/**
 * Tempers the transition from prior shares to next shares based on epsilon factors.
 * 
 * This function adjusts share transitions by applying a tempering factor (epsilon) to 
 * selected items, reducing their change magnitude, and redistributing the "tempered away" 
 * amount to items without epsilon factors (null epsilon values) proportionally based on 
 * their absolute change magnitudes.
 * 
 * @param {number[]} prior - Array of prior shares that sum to 1.0
 * @param {number[]} next - Array of target future shares that sum to 1.0
 * @param {(number|null)[]} epsilon - Array of temper factors (0-1) or null. 
 *                                    Non-null values reduce the change for that item.
 *                                    Null values indicate items that receive redistributed amounts.
 * @returns {number[]} Array of tempered shares that sum to 1.0
 * 
 * @example
 * // Product 0 decreases by 0.2, Product 1 decreases by 0.1, Product 2 increases by 0.3
 * // Product 2's increase is tempered by 0.333, reducing it by 0.1
 * // The 0.1 is redistributed to Products 0 and 1 proportional to their change magnitudes
 * const prior = [0.6, 0.4, 0];
 * const next = [0.4, 0.3, 0.3];
 * const epsilon = [null, null, 0.333];
 * const result = temper(prior, next, epsilon);
 * // Returns: [0.4667, 0.3333, 0.2000]
 */
Protobi.temper =function (prior, next, epsilon) {
  const n = prior.length;
  const changes = next.map((val, i) => val - prior[i]);
  const result = [...next]; // start with next values
  
  // Calculate total reduction from tempered items
  let totalReduction = 0;
  for (let i = 0; i < n; i++) {
    if (epsilon[i] !== null) {
      // The reduction is the change multiplied by epsilon
      const reduction = changes[i] * epsilon[i];
      totalReduction += reduction;
      // Apply the tempered change: change * (1 - epsilon)
      result[i] = prior[i] + changes[i] * (1 - epsilon[i]);
    }
  }
  
  // Calculate total absolute change for non-tempered items (null epsilon)
  let totalAbsChange = 0;
  for (let i = 0; i < n; i++) {
    if (epsilon[i] === null) {
      totalAbsChange += Math.abs(changes[i]);
    }
  }
  
  // Redistribute the reduction proportionally to non-tempered items
  if (totalAbsChange > 0) {
    for (let i = 0; i < n; i++) {
      if (epsilon[i] === null) {
        const proportion = Math.abs(changes[i]) / totalAbsChange;
        result[i] += proportion * totalReduction;
      }
    }
  }
  
  return result;
}