QScript Functions for Choice Modeling
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'); // also imports QScript Utility Functions > correctTerminology
includeWeb('QScript Functions to Generate Outputs');
function rCodeAsString(scenario, pref_name) {
let r_expr = `${scenario}))
${pref_name} <- predict(formChoiceModel,
scenario = scenario,
rule = formRule,
share.adjustment = c(formScaleToShares, formCalibrateToShares),
shares = get0("formShares"),
scale = get0("formScale"),
offset = get0("formOffset"),
availability = formAvailability,
subset = QFilter,
weights = QPopulationWeight,
optimx.controls = list(abstol = 1e-7, maxit = 1000),
warn.once = TRUE,
output.type = formOutputType)`;
return r_expr
}
function guiControlsAsString(multiple_selection) {
return `controls = [];
var db = form.dropBox({name: "formChoiceModel", label: "Choice model",
required: true, multi: false,
prompt: "Select an output from Choice Modeling - Hierarchical Bayes, Latent Class Analysis, or Multinomial Logit",
types: ["RItem:FitChoice"]});
controls.push(db);
let rule_types = ['Logit respondent', 'Logit draw',
'First choice respondent', 'First choice draw'];
var rule = form.comboBox({name: 'formRule', label: "Rule",
prompt: "Type of choice model to fit",
alternatives: rule_types,
default_value: "Logit respondent"});
controls.push(rule);
var availability = form.dropBox({label: "Availability",
types:["Table", "RItem:matrix,data.frame"],
prompt: "A matrix of TRUE/FALSE or 0/1 values of respondents to exclude from each scenario",
name: "formAvailability", required: false,
multi: false});
controls.push(availability);
var calibrate = form.checkBox({label: "Calibrate to shares",
prompt: "Adds constants to the utilities computed for each scenario, " +
"such that the simulators shares add up to the shares provided",
name: "formCalibrateToShares", default_value: false});
var scale = form.checkBox({label: "Scale to shares",
prompt: "Calculates the scale factor (also known as lambda, and the exponent) " +
"that best predicts shares",
name: "formScaleToShares", default_value: false});
if (calibrate.getValue() || scale.getValue())
var tb = form.textBox({name: "formShares",
label: "Shares",
prompt: "Enter shares, separate by commas, that sum to 1",
required: true})
var utilities = form.textBox({name: "formOffset",
label: "Calibration factors",
prompt: "(Optional) Enter utilities to be added to the scenarios.",
required: false});
if (scale.getValue()) {
controls.push(tb);
controls.push(scale);
controls.push(calibrate);
if (!calibrate.getValue())
controls.push(utilities);
}else {
controls.push(scale);
var nup = form.numericUpDown({name: "formScale",
label: "Scale factor",
default_value: 1,
increment: 0.0001,
minimum: 0});
controls.push(nup);
if (calibrate.getValue()) {
controls.push(tb);
controls.push(calibrate);
}else {
controls.push(calibrate);
controls.push(utilities);
}
}
let ot = form.comboBox({name: 'formOutputType', label: 'Output type',
prompt: 'Choose the level of aggregation in the output shares; average/market shares, shares per respondent, or shares per HB draw',
alternatives: ['Average shares', 'Respondent shares', 'HB draw shares'],
default_value: 'Average shares'});
controls.push(ot);
form.setInputControls(controls);
` + (multiple_selection ? `form.setHeading('Choice Optimizer');` : `form.setHeading('Choice Simulator');`);
}
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 script_type = multiple_selection ? "optimizer" : "simulator";
var none_options_labels = this_item.data.getAttribute("none.alternatives", "names");
if (userSpecifiedNumberAlternatives == 1 && !none_options_labels.length) {
alert("Cannot create a one alternative " + script_type + " for a choice model with no 'None' alternative");
return false;
}
var exclude_alternative_attr_requested = false;
var exclude_none = false;
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 && userSpecifiedNumberAlternatives > 1)
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]]);
// level_count needs to account for 'None' parameters to properly
// index into the processed.data components below; however, we don't
// want to show them as an option in the combo box if the user
// requests the alternative attribute be shown, so we filter
// them from levels
if (currentLevels.length > 0) {
level_count = level_count + currentLevels.length - 1;
if (i == 0)
{
currentLevels = currentLevels.filter(e => none_options_labels.indexOf(e) == -1);
exclude_first_attribute = exclude_alternative_attr_requested || currentLevels.length == 1; // Others (appears when there are "none of these" alternatives with no ASCs)
}
levels.push(currentLevels);
} 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, selected_item, 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 = multiple_selection ? "Optimizer" : "Simulator";
const choiceOutputIsOnPage = options.choiceOutput.IsOnPage;
const page = choiceOutputIsOnPage ? options.choiceOutput.group.group.appendPage('TitleOnly') : options.choiceOutput.group.appendPage('TitleOnly');
if (choiceOutputIsOnPage) {
page.group.moveAfter(page, options.choiceOutput.group);
}
page.name = pageName;
let titleText = page.subItems[0];
// Fallback if user has removed title placeholder from TitleOnly page
// - first item is undefined if no objects present on master page.
// - first item is not a text box if some other object has been added to the page and title removed
// - first item is a text box other than the title text if it contains anything other than a non-breaking space
if (typeof titleText == "undefined" || titleText.type != "Text" || titleText.text.length != 1 || titleText.text.charCodeAt(0) != 160) {
titleText = page.appendText();
// Looks like defaults for TitleOnly layout
titleText.top = 17;
titleText.height = 85;
titleText.width = 250;
titleText.left = Math.ceil((page.width - 250) / 2);
let new_html = Q.htmlBuilder();
new_html.append(pageName, {size: 24 });
titleText.content = new_html;
} else
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 = rCodeAsString(r_temp, pref_name);
var pref_shares = page.appendR(r_expression);
pref_shares.setCodeForGuiControls(guiControlsAsString(multiple_selection));
pref_shares.setGuiControlInputRaw("formChoiceModel", selected_item.guid);
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(`attr(${pref_shares.name}, "AverageShares")[${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([pref_shares]);
}
let msg = correctTerminology('Select an output that has been created with Anything > Advanced Analysis > Choice Modeling.');
var selected_item = checkSelectedItemClassCustomMessage("FitChoice", msg);
if (selected_item === null)
return false;
var simulatorOptions = getChoiceModelOptions(selected_item, userSpecifiedNumberAlternatives, multiple_selection);
if (simulatorOptions === false) // error from requesting one-alternative simulator with no 'None'
return false;
buildSimulator(simulatorOptions, selected_item, 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.