QScript Functions for Choice Modeling EXPERIMENTAL
Jump to navigation
Jump to search
This page contains functions used by QScript which work with choice models.
Source Code
function createChoiceSimulator(userSpecifiedNumberAlternatives, multiple_selection) {
includeWeb('QScript R Output Functions');
includeWeb('QScript Selection Functions');
includeWeb('QScript Functions to Generate Outputs');
function generateUniqueComboBoxName(name) {
var combo_boxes = [];
recursiveGetAllComboBoxNamesInGroup(project.report, combo_boxes);
if (combo_boxes.indexOf(name) == -1)
return name;
var nonce = 1;
while (combo_boxes.indexOf(name + "." + nonce.toString()) != -1)
++nonce;
return name + "." + nonce.toString();
}
function recursiveGetAllComboBoxNamesInGroup(group_item, objects_array) {
var cur_sub_items = group_item.subItems;
for (var j = 0; j < cur_sub_items.length; j++) {
if (cur_sub_items[j].type == 'ReportGroup') {
recursiveGetAllComboBoxNamesInGroup(cur_sub_items[j], objects_array);
}
else if (cur_sub_items[j].type == 'Control') {
objects_array.push(cur_sub_items[j].name);
}
}
}
function createArray(length) {
var arr = new Array(length || 0),
i = length;
if (arguments.length > 1) {
var args = Array.prototype.slice.call(arguments, 1);
while (i--) arr[length - 1 - i] = createArray.apply(this, args);
}
return arr;
}
function getChoiceModelOptions(choiceModel, userSpecifiedNumberAlternatives, multiple_selection) {
this_item = choiceModel;
var n_alternatives = this_item.data.get("n.alternatives");
var n_attributes = this_item.data.get("n.attributes");
var attributes = this_item.data.getAttribute("attribute.levels", "names");
var none_options_labels = this_item.data.getAttribute("none.alternatives", "names");
var exclude_alternative_attr_requested = false;
var exclude_none = false;
script_type = multiple_selection ? "optimizer" : "simulator";
var none_with_no_ASCs = this_item.data.get(["attribute.levels", attributes[0]]).filter(e => none_options_labels.indexOf(e) == -1).length == 1;
if(this_item.data.getAttribute("attribute.levels", "names")[0] === "Alternative" && !none_with_no_ASCs)
exclude_alternative_attr_requested = !askYesNo("Would you like to include the Alternative attribute?");
if(none_options_labels.length)
exclude_none = !askYesNo("Would you like to include the 'None' alternative?");
var alternatives = [];
for (var j = 0; j < userSpecifiedNumberAlternatives; j++)
alternatives.push("Alternative " + (j+1));
levels = [];
var level_count = 0;
var exclude_first_attribute = false;
for (var i = 0; i < attributes.length; i++) {
var currentLevels = this_item.data.get(["attribute.levels", attributes[i]]);
if (currentLevels.length > 0) {
if (i == 0)
{
// Remove "none of these" alternatives from levels if requested, or in the special case of
// "none of these" alternatives with no ASCs (in which case "Others" appears in attribute.levels)
currentLevels = currentLevels.filter(e => none_options_labels.indexOf(e) == -1);
exclude_first_attribute = exclude_alternative_attr_requested || currentLevels.length == 1;
}
levels.push(currentLevels);
level_count = level_count + currentLevels.length - 1;
} else {
// Numeric attribute
// Work out max and min values, then generate four levels at (roughly)
// even intervals, rounded to 2 decimal places because
// values usually represent dollars.
level_count ++;
var min;
var max;
try {
var range = this_item.data.get(["processed.data", "parameter.range"]);
min = range[i][0];
max = range[i][1];
} catch (e) {
var min_array = this_item.data.get(["processed.data", "parameter.min"]);
var max_array = this_item.data.get(["processed.data", "parameter.max"]);
min = min_array[level_count - 1];
max = max_array[level_count - 1];
}
min = Math.ceil(100 * min) / 100;
max = Math.floor(100 * max) / 100;
var interval = (max - min) / 4;
levels.push([min, Math.floor(100 * (min + interval)) / 100, Math.floor( 100 * (min + 2 * interval)) / 100, max]);
}
}
if (exclude_none)
none_options_labels = [];
var simulatorOptions = {attributes: attributes,
alternatives: alternatives,
noneOptionsLabels: none_options_labels,
levels: levels,
choiceOutput: this_item,
excludeFirstAttribute: exclude_first_attribute
}
return simulatorOptions;
}
function buildSimulator(options, multiple_selection) {
const attributes = options.attributes;
const choiceAlternatives = options.alternatives
const noneAlternatives = options.noneOptionsLabels;
const levels = options.levels;
const selected_item_name = options.choiceOutput.referenceName;
const exclude_first_attribute = options.excludeFirstAttribute;
const alternatives = choiceAlternatives.concat(noneAlternatives);
const numberOfColumns = alternatives.length;
const numberOfRows = exclude_first_attribute ? attributes.length - 1 : attributes.length;
// Create page and title
const pageName = "Simulator"
const page = options.choiceOutput.group.group.appendPage('TitleOnly');
page.group.moveAfter(page, options.choiceOutput.group);
page.name = pageName;
var titleText = page.subItems[0];
titleText.text = pageName;
// Figure out layout values.
// Work out height of rows
var testText = page.appendText();
testText.text = "Text";
const optionRowHeight = testText.height;
testText.deleteItem();
const rowPad = 10;
// Work out height needed for simulator options
const optionHeightNeeded = (numberOfRows + 3) * (optionRowHeight + rowPad);
const titleHeightNeeded = titleText.height + rowPad;
// If going over page
var topMargin
if (page.height < (optionHeightNeeded + titleHeightNeeded)) {
titleText.top = 0;
topMargin = titleText.top + titleText.height + 5;
} else {
topMargin = titleText.top + titleText.height + 10 + (optionRowHeight + rowPad);
}
// Width
const leftMargin = 7;
const rightMargin = 7;
const wUnit = (page.width - leftMargin - rightMargin) / (numberOfColumns + 1);
// Create a grid of combos
const combos = createArray(numberOfColumns, numberOfRows);
let lastRowTop = 0;
let lastRowHeight = 0;
for (let x = 0; x < numberOfColumns; x++) {
let above;
const text = page.appendText();
text.left = wUnit * (x + 1) + 5 + leftMargin;
text.top = topMargin + 5;
text.width = wUnit - 10;
text.text = isNaN(alternatives[x]) ? alternatives[x] : ("Alternative " + alternatives[x]);
above = text;
if (x < choiceAlternatives.length) {
for (let y = 0; y < numberOfRows; y++) {
let attribute_index = exclude_first_attribute ? y + 1 : y;
combo = page.appendControl("Combobox");
combo.selectionMode = multiple_selection ? 'MultipleSelection' : 'SingleSelection';
combo.whenItemListChanges = 'SelectFirst';
combo.itemList = levels[attribute_index];
combo.selectedItems = [levels[attribute_index][0]];
combo.placeholderText = levels[attribute_index][0];
combo.width = wUnit - 10;
combo.left = wUnit * (x + 1) + 5 + leftMargin;
combo.top = above.top + above.height + rowPad;
combo.name = generateUniqueComboBoxName("c" + attributes[attribute_index].replace(/\W/g,"_") + "." + (x + 1));
combos[x][y] = combo;
if (x === 0) {
const text = page.appendText();
text.left = 5 + leftMargin;
text.top = combo.top;
text.width = wUnit - 10;
text.text = attributes[attribute_index];
if (y == numberOfRows - 1) {
lastRowTop = combo.top + combo.height + rowPad;
lastRowHeight = combo.height;
}
}
above = combo;
}
}
}
const text = page.appendText();
text.left = 5 + leftMargin;
text.top = lastRowTop;
text.width = wUnit - 10;
text.text = 'Preference Share';
above = text;
var controls = combos.map(function(a1) {
return a1.map(function(a2) {
return a2.name;
})
});
// Create R output to compute market shares
var n_alternatives = alternatives.length;
var r_temp = "scenario = list(";
for (var i = 0; i < n_alternatives; i++) {
r_temp += "'" + alternatives[i] + "' = list("
if (i >= choiceAlternatives.length) {// This is a none-of-these alternative
r_temp += "'" + attributes[0] + "' = '" + noneAlternatives[i - choiceAlternatives.length] + "'";
} else {
for (var j = 0; j < numberOfRows; j++) {
let attribute_index = exclude_first_attribute ? j + 1 : j;
r_temp += "'" + attributes[attribute_index] + "' = " + controls[i][j];
if (j < numberOfRows - 1)
r_temp += ",";
}
}
if (i < n_alternatives-1)
r_temp += "),\r\n";
}
var pref_name = generateUniqueRObjectName('preference.shares');
var r_expression = r_temp + "))\n scenario <- lapply(scenario, function(x) x[vapply(x, length, 0L) != 0])\r\n"; // rm empty comboboxes
r_expression += 'preferences.by.respondents' + " = predict(" + selected_item_name + ", scenario)\r\n";
r_expression += pref_name + " = apply(preferences.by.respondents, c(1, 3), mean, na.rm = TRUE) * 100";
var pref_shares = page.appendR(r_expression);
if (!multiple_selection) {
pref_shares.top = page.height + 5;
pref_shares.left = 0;
pref_shares.height = 100;
pref_shares.width = 70;
pref_shares.hiddenFromExportedViews = true;
pref_shares.update();
// Adding R outputs to display the shares under each column
for (let x = 0; x < numberOfColumns; x++) {
const rOutput = page.appendR(`${pref_shares.name}[, ${x + 1}]`);
rOutput.left = wUnit * (x + 1) + 5 + leftMargin;
rOutput.top = lastRowTop;
rOutput.width = wUnit - 10;
rOutput.height = lastRowHeight * 10;
}
}else {
pref_shares.top = lastRowTop;
pref_shares.left = wUnit + 5 + leftMargin;
pref_shares.height = lastRowHeight * 10;
pref_shares.width = numberOfColumns*wUnit;
pref_shares.update();
}
project.report.setSelectedRaw([combos[0][0]]);
}
var selected_item = checkSelectedItemClassCustomMessage("FitChoice", "Select an output that has been created with Insert > Choice Modeling.");
if (selected_item === null)
return false;
var simulatorOptions = getChoiceModelOptions(selected_item, userSpecifiedNumberAlternatives, multiple_selection);
buildSimulator(simulatorOptions, multiple_selection);
return true;
}
See also
- QScript for more general information about QScripts.
- QScript Examples Library for other examples.
- Online JavaScript Libraries for the libraries of functions that can be used when writing QScripts.
- QScript Reference for information about how QScript can manipulate the different elements of a project.
- JavaScript for information about the JavaScript programming language.
- Table JavaScript and Plot JavaScript for tools for using JavaScript to modify the appearance of tables and charts.