QScript Functions for Choice Modeling EXPERIMENTAL

From Q
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

See also