QScript Functions for Setting Up Experiments

From Q
Jump to navigation Jump to search

To make these functions available when writing a QScript or Rule see JavaScript Reference.

This page contains functions for setting up Experiment questions (e.g. Choice-based Conjoint).

CBCSetupFromDesign(design_type)

Main function to set up an Experiment question for a Choice-based Conjoint Experiment. Set design_type to "JMP" for JMP designs, or "Sawtooth" for Sawtooth CSV designs.

SetupError(message)

A function to generate error messages for problems with the design setup.

choiceModellingUtilityComputation(type)

A function to generate different measures of utility based on individual-level parameter estimates.

There are different types of calculation that can be used (passed in as a string):

  • Zero-Centered Diffs: Scores with an average of zero within each attribute, and which have the property that the average range (max - min) of each attribute is 100 for each respondent.
  • Utilities: Scores where the smallest coefficient within each attribute is subtracted from each coefficient in the attribute, so that the minimum score for any respondent in any attribute is 0.
  • Utilities with 0 to 100 Scaling: The same as "Utilties" except scaled to range from 0 to 100 for each respondent.
  • Within-Attribute Preference Shares: Preference shares for each attribute, obtained using the logit tranformation.

maxDiffUtilityCalculation(type)

A function to generate calculations based on individual-level parameter means for the coefficients in a Max-Diff experiment.

There are different types of calculation that can be used (passed in as a string):

  • Zero-Centered Utilities: Subtract the mean coefficient for each respondent.
  • Preference Shares: Generate shares using a logit transformation.
  • Sawtooth-Style Preference Shares: Generate shares from the zero-centered utilities using the Sawtooth formula.

scrapeLabelFile(label_file)

Generate an array of labels from the label file. The label file should have one column per attribute, with the labels in order. The names of the attributes themseves should be obtained separately by looking at the variable labels.

scrapeDataFromCHOFile(data_file)

Obtain the data from an experimental design in the Sawtooth CHO format.

Source Code

// Setup a Choice-based Conjoint Experiment using an externally-supplied design
function CBCSetupFromDesign(design_type) {
    includeWeb("QScript Functions to Generate Outputs");
    // Make a copy of the specified choice variables and relabel them to the specified label.
    // Return an array of the new variables.
    function prepareChoiceVariables(choice_variables, label) {
        var new_choice_variables = choice_variables.map(function (v) { return v.duplicate();} );
        new_choice_variables.forEach(function (v) { v.label = label;});
        return new_choice_variables;
    }
 
    // Obtain the necessary information from the design data file, assuming it was written in
    // the CSV format specified by Sawtooth
    function scrapeConjointDesignFromDataFile(data_file, design_type) {
        if (["JMP", "Sawtooth"].indexOf(design_type) == -1)
            throw new SetupError("Unknown design type: " + design_type);
 
        var design_headers;
        if (design_type == "JMP")
            design_headers = ["Survey", "ChoiceSet", "ChoiceID"];
        else if (design_type == "Sawtooth")
            design_headers = ["Version#", "Task#", "Concept#"];
 
        var has_noneofthese = null;
 
        var variables = data_file.variables;
        // Check for and handle text variables
        var text_variables = variables.filter(function (v) { return v.variableType == "Text"; });
        var level_labels_in_design = text_variables.length > 0; 
        if (design_type == "Sawtooth" && level_labels_in_design)
            throw new SetupError("The attribute levels should be described by numbers only. There is some text information in the design.")
        else if (design_type == "JMP" && level_labels_in_design) {
            variables = variables.map(function (v) {
                if (v.variableType == "Text") {
                    var q_below = data_file.getVariableByName(v.name);
                    var new_linked = preventDuplicateVariableName(data_file, v.name); 
                    var new_var = data_file.newJavaScriptVariable(v.name, true, new_linked, v.name, q_below);
                    new_var.variableType = "Categorical";
                    var cur_label = v.label;
                    v.label = cur_label + " - TEXT";
                    v.question.name = cur_label + " - TEXT";
                    new_var.label = cur_label;
                    new_var.question.name = cur_label;
                    v.question.isHidden = true;
                    return new_var;
                } else if (design_headers.indexOf(v.name) == -1)
                    throw new SetupError("When using a design with labels instead of values you cannot have labels that are purely numbers. \"" + v.label + "\" has labels that are all numbers. Please edit your file using Search/Replace in Excel and add some text to the label.");
                else
                    return v;   
            });
        }
 
 
 
        // Obtain relevant elements of the design file.
        var raw_data = variables.map(function (v) { return v.rawValues; });
        var variable_names = variables.map(function (v) { return v.name; });
        var variable_labels = variables.map(function (v) { return v.label; });
        if (variable_names[0] != design_headers[0] || variable_names[1] != design_headers[1] || variable_names[2] != design_headers[2])
            throw new SetupError("Expected to see variables named: " + design_headers.join(", ") + " at the start of the design file. Please check the format of the file.");
 
        var attribute_names = variable_names.splice(3, variable_names.length);
        var attribute_labels = variable_labels.splice(3, variable_labels.length);
        var choice_set = raw_data[1];
        var attribute_data = raw_data.splice(3, raw_data.length);
        var num_alts_per_task = choice_set.filter(function (s) {return s == choice_set[0]; }).length;
        var num_sets = choice_set[choice_set.length - 1];
 
        // If the labels are in the design then store them in an array (JMP designs)
        var label_array;
        if (design_type == "JMP" && level_labels_in_design)
            label_array = attribute_names.map(function (s) {
                var cur_var = data_file.getVariableByName(s);
                return cur_var.uniqueValues.map(function (v) { return cur_var.valueAttributes.getLabel(v);});
            });
 
        // Check that the same number of alternatives are present for each task
        // Also ensures that the choice sets are numbered sequentially with no gaps
        for (var j = 0; j < num_sets; j++) {
            var alts = choice_set.filter(function (x) { return x == j+1; }).length;
            if (alts != num_alts_per_task)
                throw new SetupError("The first task contains " + num_alts_per_task + " alternatives, but task " + (j+1) + " contains " + alts + " alternatives." +
                                     " All tasks must have the same number of alternatives.");
        }
 
        // Check to see if there is a 'none of these' option
        // which is indicated by alternatives with a value of '0'
        // for each attribute. (Sawtooth designs)
        if (design_type == "Sawtooth") {
            has_noneofthese = attribute_data[0].indexOf(0) > -1;
            if (has_noneofthese) {  
                // Make sure that the pattern of 0's is consistent with what we expect
                var zero_indices = [];
                for (var j = 0; j < attribute_data[0].length; j++)
                    if (attribute_data[0][j] == 0)
                        zero_indices.push(j);
                for (var k = 1; k < attribute_data.length; k++) {
                    for (var j = 0; j < attribute_data[k].length; j++) {
                        if (attribute_data[k][j] == 0)
                            if (zero_indices.indexOf(j) == -1)
                                throw new SetupError("There is a problem with the specification of the \'None of these\' options in attribute: " 
                                                     + attribute_labels[k] + ", in task #" + choice_set[j] + ". All attributes for a \'None of these\' alternative must have a value of 0.")
                    }
                }
            }
        }
 
        return {attributeNames: attribute_names, 
                attributeLabels: attribute_labels, 
                choiceSet: choice_set, 
                attributeData: attribute_data, 
                numAltsPerTask: num_alts_per_task, 
                numSets: num_sets,
                hasNoneOfThese: has_noneofthese,
                levelLabels: label_array};
    }
 
    // Generate an array of new variables for the attributes in the choice model
    function generateAttributeVariables(design_data_object, data_file, task_variable_names, after_var, label_file, has_noneofthese, add_asc, design_type) {
        if (["JMP", "Sawtooth"].indexOf(design_type) == -1)
            throw new SetupError("Unknown design type: " + design_type);
 
        var attribute_vars = [];
        var num_attributes = design_data_object.attributeNames.length;
        var num_alts_per_task = design_data_object.numAltsPerTask;
        var num_tasks_per_respondent = task_variable_names.length;
        var num_sets = design_data_object.numSets;
        var last_var = after_var;
 
        var attribute_labels;
        // Get labels from label file if given or from design object if present
        if (label_file != null) {
            if (label_file.variables.length != num_attributes)
                throw new SetupError("The number of columns in the label file (" + label_file.variables.length + ") does not match the number of attributes in the design (" + num_attributes + ").");
            // There may not be the same number of labels for each attribute.
            // So strip out any missing labels.
            attribute_labels = label_file.variables.map(function (v) {
                return v.rawValues.map(function (x) { return x.toString(); })
                                  .filter(function (s) { return s.search(/\S/) > -1 && s != "NaN"; });
            });
        } else 
            attribute_labels = design_data_object.levelLabels;
 
        // (Optional) add variables to represent alrternative-specific constants
        if (add_asc) {
            for (var j = 0; j < num_tasks_per_respondent; j++) {
                for (var alt = 1; alt < num_alts_per_task + 1; alt++) {
                    var new_var_name = preventDuplicateVariableName(data_file, "alternative_task" + (j+1) + "_alt" + alt);
                    var new_var = data_file.newJavaScriptVariable(alt, false, new_var_name, "Alternative", last_var);
                    new_var.variableType = "Categorical";
                    if (design_type == "Sawtooth" && has_noneofthese && alt == num_alts_per_task) {
                        var value_attributes = new_var.valueAttributes;
                        value_attributes.setLabel(num_alts_per_task, "None of these");
                    }
                    attribute_vars.push(new_var);
                    last_var = new_var;
                }
                if (design_type == "JMP" && has_noneofthese) {
                    var new_var_name = preventDuplicateVariableName(data_file, "alternative_task" + (j+1) + "_alt" + (num_alts_per_task + 1));
                    var new_var = data_file.newJavaScriptVariable((num_alts_per_task + 1), false, new_var_name, "Alternative", last_var);
                    new_var.variableType = "Categorical";
                    var value_attributes = new_var.valueAttributes;
                    value_attributes.setLabel((num_alts_per_task + 1), "None of these");
                    attribute_vars.push(new_var);
                    last_var = new_var;
                }
            }
        }
 
        // Generate each new variable in turn.
        // This is done for each attribute in turn, then within each attribute is each choice task, and within each task is placed each alternative
        for (var j = 0; j < num_attributes; j++) {
            // Split the variable for this attribute into a separate array for each alternative.
            // Each of the resuting arrays will have one element for each choice task.
            var levels = [];
            for (var m = 0; m < num_alts_per_task; m++)
                levels.push([]);
            var counter = 0;
            for (var k = 0; k < num_sets; k++) {
                for  (var m = 0; m < num_alts_per_task; m++) {
                    levels[m].push(design_data_object.attributeData[j][counter]);
                    counter ++;
                }
            }
            var new_var_label = design_data_object.attributeLabels[j];
            for (var k = 0; k < num_tasks_per_respondent; k++) {
                var current_task_variable_name = task_variable_names[k];
                for (var m = 0; m < num_alts_per_task; m++) {
                    var new_var_name = preventDuplicateVariableName(data_file, design_data_object.attributeNames[j] + "_task" + (k+1) + "_alt" + (m+1));
 
                    // Generate the expression that identifies the level of this alternative seen by the respondent in this choice
                    var attribute_levels = levels[m];
 
                    var expression = "var _current_task = " + current_task_variable_name + ";\r\n"
                                        + "var _attribute_levels = [" + attribute_levels.join(", ") + "];\r\n"
                                        + "_attribute_levels[_current_task - 1]";
                    var new_var = data_file.newJavaScriptVariable(expression, false, new_var_name, new_var_label, last_var);
                    new_var.variableType = "Categorical";
 
                    // Set labels if label file specified
                    if (attribute_labels != null) {
                        var unique_values = new_var.uniqueValues;
                        unique_values = unique_values.filter(function (x) { return !isNaN(x); });
                        if (unique_values.length > attribute_labels[j].length)
                            throw new SetupError("The number of values in task " + (k+1) + " for " + new_var_label + " is " + unique_values.length +
                                                 " but the number of labels for this attribute is " + attribute_labels[j].length +
                                                 ". Please check that the label file is set up correctly.");
                        else {
                            var value_attributes = new_var.valueAttributes;
                            for (var vl = 0; vl < attribute_labels[j].length; vl++) {
                                if (value_attributes.getLabel(vl + 1) != null)
                                    value_attributes.setLabel(vl + 1, attribute_labels[j][vl]);
                            }
                        }
                    }
                    // Set 0 as missing for 'None of these' options in Sawtooth files
                    if (design_type == "Sawtooth" && has_noneofthese && m == num_alts_per_task - 1) {
                        var value_attributes = new_var.valueAttributes;
                        value_attributes.setIsMissingData(0, true);
                    }
                    attribute_vars.push(new_var);
                    last_var = new_var;
                }
                // For JMP files only
                // If the there is a "None of these" option then an extra blank alternative variable must be added for this attribute
                if (design_type == "JMP" && has_noneofthese) {
                    var new_var_name = preventDuplicateVariableName(data_file, design_data_object.attributeNames[j] + "_task" + (k+1) + "_alt" + (num_alts_per_task+1));
                    var expression = "NaN";
                    var new_var = data_file.newJavaScriptVariable(expression, false, new_var_name, new_var_label, last_var);
                    new_var.variableType = "Categorical";
                    attribute_vars.push(new_var);
                    last_var = new_var;
                }
            }   
        }
        return attribute_vars;
    }
 
    function checkAllTasksInDesign(task_variables, design_data) {
        var all_task_numbers_in_data = [];
        task_variables.forEach(function (v) {
            all_task_numbers_in_data = all_task_numbers_in_data.concat(v.rawValues);
        });
        all_task_numbers_in_data = uniqueElementsInArray(all_task_numbers_in_data);
        all_task_numbers_in_data = all_task_numbers_in_data.filter(function (x) { return !isNaN(x); });
        var task_numbers_in_design = uniqueElementsInArray(design_data.choiceSet);
        var in_design_not_in_data = task_numbers_in_design.filter(function (x) { return all_task_numbers_in_data.indexOf(x) == -1; });
        var in_data_not_in_design = all_task_numbers_in_data.filter(function (x) { return task_numbers_in_design.indexOf(x) == -1; });
        if (in_design_not_in_data.length > 0)
            log("Warning: some tasks in the design are not present in the data file: " + in_design_not_in_data.join(", "));
        if (in_data_not_in_design.length > 0)
            throw new SetupError("The data file refers to tasks that do not exist in the experimental design: " + in_data_not_in_design.join(", ") + ". All tasks in the data must be contained in the design.");
    }
 
    // User interface, data import, and variable creation.
 
    if (project.dataFiles.length == 0)
        throw new SetupError("This script requires that your project has a data file containing the choices made by the respondents in your CBC experiment.");
    var data_file = project.dataFiles[0];
    var all_variables = data_file.variables.filter(function (v) { return !v.question.isHidden && !v.question.isBanner; });
    var keep_going = true;
    var num_respondents = all_variables[0].rawValues.length;
    var version = fileFormatVersion();
 
    // Prompt the user to specify the variables containing the choice data as well as which tasks were shown to each respondent
    var task_variables = selectManyVariablesByName("Please select the variables that contain the numbers of the choice tasks that were shown to each respondent. There should be one variable for each choice task.", all_variables, false).variables;
    var task_variable_names = task_variables.map(function (v) { return v.name; }); 
    var choice_variables = selectManyVariablesByName("Please select the variables that contain the choices made by each respondent in each task. There should be one variable for each choice task.", all_variables, false).variables;
    var choice_variable_names = choice_variables.map(function (v) { return v.name; }); 
    var num_tasks = task_variables.length;
 
    if (task_variables.length != choice_variables.length)
        throw new SetupError("The number of variables for the tasks (" + task_variables.length + ") does not match the number of variables for the choices (" + choice_variables.length + ")");
 
    // Import the design file
    var design_file;
    if (version < 8.39) {
        design_file = filePromptWithVerification("Please paste the path and filename of the CSV/Excel file that contains your experimental design. For example: C:\\Chris\\Documents\\design.csv", "Experimental Design", {col_names_included: true});
    } else {
        design_file = project.addDataFileDialog("Select Excel/CSV File Containing Experimental Design", {col_names_included: true});
    }
    var design_data = scrapeConjointDesignFromDataFile(design_file, design_type);
 
    // Check that the tasks in the data and the design are the same
    checkAllTasksInDesign(task_variables, design_data);
 
 
    // Specify a label file (optional for JMP, required for Sawtooth)
    var label_filename;
    var label_file = null;
    var import_labels = false;
    if (design_type == "JMP") 
        import_labels = askYesNo("Do you want to import attribute labels from an external file?");
    else if (design_type == "Sawtooth") 
        import_labels = true;
 
    if (import_labels) {
        if (version < 8.39) {
            label_file = filePromptWithVerification("Please paste the path and filename of the CSV/Excel file that contains the labels for the attribute levels.", "Attribute Labels", {col_names_included: true});
        } else {
            label_file = project.addDataFileDialog("Select Excel/CSV File Containing Labels", {col_names_included: true});
        }
    }
 
    // Do the choice tasks include a "None of these" option
    var has_noneofthese;
    if (design_type == "Sawtooth")
        has_noneofthese = design_data.hasNoneOfThese;
    else if (design_type == "JMP")
        has_noneofthese = askYesNo("Do the choice tasks include a \"None of these\" option?");
 
    // Do you want to add variables for alternative-specific constants?
    var add_asc = askYesNo("Do you want to include alternative-specific constants?");
 
    var new_choice_variables = prepareChoiceVariables(choice_variables, "Choice");
    data_file.moveAfter(new_choice_variables, null);
    var attribute_variables = generateAttributeVariables(design_data, data_file, task_variable_names, new_choice_variables[new_choice_variables.length - 1], label_file, has_noneofthese, add_asc, design_type);
    var experiment_variables = new_choice_variables.concat(attribute_variables);
    var experiment_name = preventDuplicateQuestionName(data_file, "Conjoint Experiment");
    var new_question = data_file.setQuestion(experiment_name, "Experiment", experiment_variables);
    var new_group = project.report.appendGroup();
    new_group.name = experiment_name;
    var new_text = new_group.appendText();
    var new_html = Q.htmlBuilder();
    new_html.appendParagraph("The experiment has " + design_data.attributeNames.length + " attributes, " + num_tasks + " tasks for each respondent, and " + design_data.numAltsPerTask + " alternatives per task.\r\n", { font: 'Times New Roman', size: 12 })
    new_text.content = new_html;
    var html_title = Q.htmlBuilder();
    html_title.appendParagraph("Experiment");
    new_text.title = html_title;
    var new_table = new_group.appendTable();
    new_table.primary = new_question;
        new_table.changeDecimalPlacesBy(3);
 
    // More recent Q versions can point the user to the new items.
    if (fileFormatVersion() > 8.65)
        project.report.setSelectedRaw([new_group.subItems[1]]);
 
    conditionallyEmptyLog("A new Experiment question has been created and a table showing the question has been added to your report. The experiment has " + design_data.attributeNames.length + " attributes, " + num_tasks + " tasks for each respondent, and " + design_data.numAltsPerTask + " alternatives per task.\r\n");
 
    design_file.remove();
    if (label_file != null)
        label_file.remove();
 
} 

// A custom error object so we can abort the setup of the choice model,
// and catch this error, presenting the message to the user without
// causing the QScript to crash and show an error report.
function SetupError(message) {
 	this.message = message;
}

// Generates a New Number - Multi question with computations peformed on
// a set of Individual-Level Parameter Means. Constructed using JavaScript variables.
// The types of calculation are:
// - Zero-Centered Diffs: Scores with an average of zero within each attribute, and which have the property that 
//                        the average range (max - min) of each attribute is 100 for each respondent.
//
// - Utilities: Scores where the smallest coefficient within each attribute is subtracted from each coefficient
//              in the attribute, so that the minimum score for any respondent in any attribute is 0.
//
// - Utilities with 0 to 100 Scaling: The same as "Utilties" except scaled to range from 0 to 100 for each respondent.
//
// - Within-Attribute Preference Shares: Preference shares for each attribute, obtained using the logit tranformation.
//
// - Importance: Compute one new variable for each attribute which shows the maximum value of the 0 to 100 utilities
//               for each respondent

function choiceModellingUtilityComputation(type) {
    includeWeb("QScript Selection Functions");
    includeWeb("QScript Utility Functions");

    // JavaScript functions as strings

    var get_utils_string = function _getUtilities(_coefficient_array) {
        return _coefficient_array.map(function (_array) {
           var _cur_min = _arrayMin(_array);
           return _array.map(function (_x) { return _x - _cur_min; });
        });
    }.toString() + "\r\n";

    var scale_utilities_string = function _scaleUtilities(_utils, _max) {
        return _utils.map(function (_arr) {
            return _arr.map(function (_x) { return _x / _max * 100; });
        });
    }.toString() + "\r\n";

    var get_shares_string = function _getShares(_utils) {
        var _exp_u = _utils.map(function (_arr) {
            return _arr.map(function (_x) {
                return Math.exp(_x);
            });
        });
        return _exp_u.map(function (_arr) {
            var _sum_exp = _arraySum(_arr);
            return _arr.map(function (_x) { return _x / _sum_exp * 100; });
        });
    }.toString() + "\r\n";

    var array_min_string = function _arrayMin(_arr) {
        return _arr.slice(0).sort(function (_a,_b) { return _a - _b; })[0];
    }.toString() + "\r\n";

    var array_max_string = function _arrayMax(_arr) {
        return _arr.slice(0).sort(function (_a,_b) { return _b - _a; })[0];
    }.toString() + "\r\n";

    var array_sum_string = function _arraySum(_arr) {
        var _sum = 0;
        _arr.forEach(function (_x) {
            _sum += _x;
        });
        return _sum;
    }.toString() + "\r\n";

    var get_zero_centred_diffs_string = function _getZeroCentredDiffs(_utils) {
        var _mc_utils = _utils.map(function (_arr) {
            var _a_mean = _arraySum(_arr) / _arr.length;
            return _arr.map(function (_x) { return _x - _a_mean; });
        });
        var _diffs = _mc_utils.map(function (_arr) { return _arrayMax(_arr) - _arrayMin(_arr); });
        var _diff_multiplier = 100 * _mc_utils.length / _arraySum(_diffs);
        return _mc_utils.map(function (_arr) {
            return _arr.map(function (_x) { return _x * _diff_multiplier; });
        });
    }.toString() + "\r\n";

    var data_file = requestOneDataFileFromProject(false);
    if (data_file == null)
        return false;


    // Only use questions called "Individual-Level Parameter Means" and don't contain references to
    // "Utilities" or "Shares" or "Diffs" or "Importance"
    var forbidden_strings = ["Utilities", "Shares", "Diffs", "Importance"];
    var candidate_questions = data_file.getQuestionsByName("Individual-Level Parameter Means");
    candidate_questions = candidate_questions.filter(function (q) { 
        return forbidden_strings.every(function (string) {
                                           return q.name.indexOf(string) == -1;
                                       });
    });
    if (candidate_questions.length < 1) {
        log("Could not find any questions called 'Individual-Level Parameter Means'.");
        return false;
    }

    var selected_question = selectOneQuestion("Select the question containing your individual-level parameter means:", candidate_questions);

    // Extract information about which variables match each attribute in the experiment
    var attribute_variables = getAttributeVariables(selected_question, ":");

    // Turn the attribute information into a string that references the variables in Q
    var source_variable_expression = attributeVariablesToStringExpression(attribute_variables);




    // Generate a variable for each level of each attribute
    var new_variables = [];
    var last_var = null;
    var prefix = makeid();
    for (var attribute = 0; attribute < attribute_variables.length; attribute ++) {

        // Get properties of current attribute
        var attribute_label = attribute_variables[attribute].label;
        var attribute_levels = attribute_variables[attribute].levels.length;
        var is_linear = attribute_variables[attribute].variables.length == 1;


        if (type == "Importance") { 
            // Importance works differently from the others as there is only one variable per attribute
            var new_name = preventDuplicateVariableName(data_file, "importance" + prefix + "_" + attribute);
            var new_label = attribute_label;

            // Expression to go at the top (functions, etc)
            var header_expression = get_utils_string;
            header_expression += array_min_string;

            // Expression to go at bottom (function calls, etc)
            var main_expression = source_variable_expression;
            main_expression +=  "var _cur_att = " + attribute + ";\r\n"
                              + "var _utils = _getUtilities(_av);\r\n";
            
            header_expression += scale_utilities_string;
            header_expression += array_max_string;
            main_expression += "var _au = [];\r\n"
                            + "_au = _au.concat.apply(_au, _utils);\r\n"
                            + "var _max_util = _arrayMax(_au);\r\n"
                            + "var _su = _scaleUtilities(_utils, _max_util);\r\n"
                            + "var _imp = _su.map(function (_arr) { return _arrayMax(_arr); });\r\n"
                            + "_imp[_cur_att];";

            var expression = header_expression + "\r\n\r\n" + main_expression;

            // Create new variable
            var new_var = data_file.newJavaScriptVariable(expression, false, new_name, new_label, last_var, false);
            new_variables.push(new_var);
            last_var = new_var;

        } else {
            // Create a new JavaScript variable for each level
            for (var level = 0; level < attribute_levels; level++) {
                var level_label = attribute_variables[attribute].levels[level];

                var new_name = preventDuplicateVariableName(data_file, "utils" + prefix + "_" + attribute + "_" + level);
                var new_label = attribute_label + ": " + level_label;
                
                // Build the expression
                
                // Expression to go at the top (functions, etc)
                var header_expression = get_utils_string;
                header_expression += array_min_string;

                // Expression to go at bottom (function calls, etc)
                var main_expression = source_variable_expression;
                main_expression +=  "var _cur_att = " + attribute + ";\r\nvar _cur_level = " + level + ";\r\n"
                                  + "var _utils = _getUtilities(_av);\r\n";
                
                // Add other code depending on which type of analysis is required
                if (type == "Utilities")
                    main_expression += "_utils[_cur_att][_cur_level];";
                else if (type == "Utilities with 0 to 100 Scaling") {
                    header_expression += scale_utilities_string;
                    header_expression += array_max_string;
                    main_expression += "var _au = [];\r\n"
                                     + "_au = _au.concat.apply(_au, _utils);\r\n"
                                     + "var _max_util = _arrayMax(_au);\r\n"
                                     + "var _su = _scaleUtilities(_utils, _max_util);\r\n"
                                     + "_su[_cur_att][_cur_level];";
                } else if (type == "Within-Attribute Preference Shares") {
                    header_expression += array_sum_string;
                    header_expression += get_shares_string;
                    main_expression += "var _ps = _getShares(_utils);\r\n"
                                     + "_ps[_cur_att][_cur_level];";
                } else if (type == "Zero-Centered Diffs") {
                    header_expression += array_sum_string;
                    header_expression += get_zero_centred_diffs_string;
                    header_expression += array_max_string;
                    main_expression += "var _zcd = _getZeroCentredDiffs(_utils);\r\n"
                                     + "_zcd[_cur_att][_cur_level];";
                }
                var expression = header_expression + "\r\n\r\n" + main_expression;
                
                // Create new variable
                var new_var = data_file.newJavaScriptVariable(expression, false, new_name, new_label, last_var, false);
                new_variables.push(new_var);
                last_var = new_var;
            }
        }
    }

    // Set question and create table
    var new_q = data_file.setQuestion(preventDuplicateQuestionName(data_file, selected_question.name + " - " + type), "Number - Multi", new_variables);
    var new_group = project.report.appendGroup();
    new_group.name = type;
    var new_t = new_group.appendTable();
    new_t.primary = new_q;

    // Create spans for each attribute (except for Importance)
    if (type != "Importance") {
        var new_data_reduction = new_q.dataReduction;
        var var_labels = new_variables.map(function (v) { return v.label; });
        attribute_variables.forEach(function (obj) {
            var span_labels = var_labels.filter(function (label) {
                return label.indexOf(obj.label) == 0;
            })
            new_data_reduction.span(span_labels, obj.label);
        });
        removeRowLabelPrefix(new_data_reduction, ": ");
    }
 
    // More recent Q versions can point the user to the new items.
    if (fileFormatVersion() > 8.65)
        project.report.setSelectedRaw([new_group.subItems[0]]);



    return true;


    // Determine which variables in the input question correspond to each attribute
    function getAttributeVariables(question, split_char) {
        var variables = question.variables;

        // Obtain list of attribute names from variable labels.
        // Expecting to see variable labels of the form:
        // <Attribute Name> : <Level Label>
        // eg
        // Weight: 55g
        var attribute_labels = variables.map(function(v) { return v.label.split(split_char)[0]; });
        var attribute_list = uniqueElementsInArray(attribute_labels);
        
        // For each attribute, return an object containing:
        // - label: The attribute label (as above)
        // - variables: An array of variables that match this attribute
        // - levels: The labels of the levels for this attribute (eg 55g, 60g, 65g, 75g for Egg weights)
        //           For numeric attributes, this will be undefined because they have 1 level.
        var attribute_variables = attribute_list.map(function (att) {
            var cur_vars = variables.filter(function (v) {
                return v.label.indexOf(att) == 0;
            });
            var levels;
            if (cur_vars.length > 1) // Single-level attributes don't have level labels
                levels = cur_vars.map(function (v) { return v.label.substring(v.label.indexOf(split_char) + split_char.length).trim(); });
            else
                levels = promptUserForLinearAttributeLevels(att);
            return { label: att, 
                     variables: cur_vars, 
                     levels: levels };
        });
        return attribute_variables;
    }

    // Return an array where each element is an array of variable names corresponding to the levels
    // for an attribute.
    function attributeVariablesToStringExpression(attribute_variables) {
        var string = "var _av = [";
        attribute_variables.forEach(function (obj) {
            if (obj.variables.length > 1) {
                // Categorical attributes (many levels)
                string += "[" + obj.variables.map(function (v) { return v.name; }).join(", ") + "],\r\n";
            } else {
                // Linear attribute
                var cur_name = obj.variables[0].name;
                // Look up the user-entered levels.
                var level_array = obj.levels;
                string += "[" + level_array.map(function (level) { return level + "*" + cur_name}).join(", ") + "],\r\n";
            }
        }); 
        string = string.substring(0, string.length - 3); // Trim the last comma
        string += "];\r\n";
        return string;
    }

    // Looks for split_char in the row labels and removes it and everything that
    // precedes it.
    function removeRowLabelPrefix(data_reduction, split_char) {
        var labels = data_reduction.rowLabels;
        labels.forEach(function (label) {
            var i = label.indexOf(split_char);
            if (i != -1)
                data_reduction.rename(label, label.substring(i + split_char.length).trim());
        });
    }


    // Ask the user to tell us which levels to use when converting a linear attribute to a categorical one
    function promptUserForLinearAttributeLevels(label) {
        var not_numbers = null;
        do {
            var prompt_prefix;
            if (not_numbers == null)
                prompt_prefix = "The attribute '" + label + "' is numeric and must be treated as categorical for this analysis.";
            else
                prompt_prefix = not_numbers[0] + " is not a number.";

            var input_string = prompt(prompt_prefix + " Specify the levels that you want to analyze (as numbers), separated by commas. For example: 1, 1.5, 2...");

            var vals = input_string.split(",");
            vals = vals.map(function (str) { return str.trim(); });
            not_numbers = vals.filter(function (x) { return !isNumber(x); });

            // If the user enters something that is not a number, keep asking them until they enter a number
        } while (not_numbers.length != 0)

        return vals.map(parseFloat);
    }

    // Check if something is a number
    function isNumber(n) {
        return !isNaN(parseFloat(n)) && isFinite(n);
    }
}

// Valid types are:
//
// - Zero-Centered Utilities
// - Preference Shares
// - Sawtooth-Style Preference Shares
function maxDiffUtilityCalculation(type) {
	includeWeb("QScript Selection Functions");
    includeWeb("QScript Utility Functions");

    // JavaScript functions as strings

    var array_sum_string = function _arraySum(_arr) {
        var _sum = 0;
        _arr.forEach(function (_x) {
            _sum += _x;
        });
        return _sum;
    }.toString() + "\r\n";

    var data_file = requestOneDataFileFromProject(false);
    if (data_file == null)
        return false;

    // Only use questions called "Individual-Level Parameter Means" and don't contain references to
    // "Utilities" or "Shares"
    var forbidden_strings = ["Utilities", "Shares"];
    var candidate_questions = data_file.getQuestionsByName("Individual-Level Parameter Means");
    candidate_questions = candidate_questions.filter(function (q) { 
        return forbidden_strings.every(function (string) {
                                           return q.name.indexOf(string) == -1;
                                       });
    });

    if (candidate_questions.length < 1) {
        log("Could not find any questions called 'Individual-Level Parameter Means'.");
        return false;
    }

    var selected_question = selectOneQuestion("Select the question containing your individual-level parameter means:", candidate_questions);

    var K;
    if (type == "Sawtooth-Style Preference Shares") {
    	var is_valid = null;
    	do {
    		var message;
    		if (is_valid == null)
    			message = "";
    		else
    			message = K + " is not valid selection. Enter an integer greater than 2. ";
   			K = prompt(message + "How many alternatives were shown in each task?");
   			var pk = parseInt(K, 10);
   			// K == pk ensures that K is an integer
   			is_valid = (K == pk && pk > 2);
    	} while (!is_valid)
    }

    var variables = selected_question.variables;
    var source_variable_expression = "var _coeff = [" + variables.map(function (v) { return v.name; }).join(", ") + "];";

    // Generate new variables
    var new_variables = [];
    var last_var = null;
    var prefix = makeid();
    for (var j = 0; j < variables.length; j++) {

    	var new_name = preventDuplicateVariableName(data_file, "utils" + prefix + "_" + (j + 1));

    	// JavaScript for top (eg functions)
    	var header_expression = array_sum_string;

    	// JavaScript for bottom (eg function calls)
    	var main_expression = source_variable_expression;
    	main_expression += "var _cur_index = " + j + ";\r\n"

    	if (type == "Zero-Centered Utilities") {
    		main_expression += "var _c_mean = _arraySum(_coeff) / _coeff.length;\r\n"
    						 + "_coeff[_cur_index] - _c_mean;";
    	} else if (type == "Preference Shares") {
    		main_expression += "var _exp_c = _coeff.map(function (_x) { return Math.exp(_x); });\r\n"
    						 + "_exp_c[_cur_index] / _arraySum(_exp_c);";
    	} else if (type == "Sawtooth-Style Preference Shares") {
    		main_expression += "var _K = " + K + ";\r\n"
    						 + "var _c_mean = _arraySum(_coeff) / _coeff.length;\r\n"
    						 + "var _z_c_coeff = _coeff.map(function (_x) { return _x - _c_mean; });\r\n"
    						 + "var _u_prefs = _z_c_coeff.map(function (_x) { return Math.exp(_x) / (Math.exp(_x) + _K - 1); });\r\n"
    						 + "_u_prefs[_cur_index] / _arraySum(_u_prefs);\r\n";
    	}

    	var expression = header_expression + "\r\n\r\n" + main_expression;
    	var new_var = data_file.newJavaScriptVariable(expression, false, new_name, variables[j].label, last_var, false);
    	new_variables.push(new_var);
        last_var = new_var;
    }

    // Set question and create table
    var new_q_name = preventDuplicateQuestionName(data_file, selected_question.name + " - " + type +
    		 								      (type == "Sawtooth-Style Preference Shares" ? " (with " + K + " alternatives per task)" : ""));
    
    var new_q = data_file.setQuestion(new_q_name, "Number - Multi", new_variables);
    var new_group = project.report.appendGroup();
    new_group.name = type;
    var new_t = new_group.appendTable();
    new_t.primary = new_q;

    return true;
}

// Get the attribute level labels
function scrapeLabelFile(label_file) {
    attribute_labels = label_file.variables.map(function (v) {
        return v.rawValues.map(function (x) { return x.toString(); })
                          .filter(function (s) { return s.search(/\S/) > -1 && s != "NaN"; });
    });
    return attribute_labels;
}

function scrapeDataFromCHOFile(data_file) {
    
    var variables = data_file.variables;
    var raw_data = variables.map(function (v) { return v.rawValues; });
    raw_data = Q.transpose(raw_data);

    var design_data = [];
    var row = 0;

    // Each iteration of this outer loop should process one respondent's 
    // set of data.
    while (row < raw_data.length) {
        // Here we must always begin at the first row for the respondent's
        // data.
        var id = raw_data[row][0];
        var num_extra_vars = raw_data[row][1];
        var num_tasks = raw_data[row][3];

        var num_attributes = raw_data[row][2];
        var has_none = raw_data[row][4];
        if (has_none == 2)
            throw new SetupError("This QScript does not currently support the dual-response none option.");
        var alts_per_task = [];
        var choices = [];
        var level_data = [];

        row++;
        // Obtain data for any extra variables
        var extra_vars = [];
        for (var col = 0; col < num_extra_vars; col++) {
            extra_vars.push(raw_data[row][col]);
        }


        // Get the data for this task
        for (var task = 0; task < num_tasks; task++) {
            // Skip ahead to start of task
            row++;
            var num_alts = raw_data[row][0];
            alts_per_task.push(num_alts);

            // Eventually we will do something with the
            // attribute levels at this point
            var end_row = row + num_alts;
            var task_data = [];
            while (row < end_row) {
                row++;
                task_data = task_data.concat(raw_data[row]);
            }
            level_data = level_data.concat(task_data);
            // Add a row of NaNs to the data when there is a 'None of these'
            // alternative included. This alternative is not represented by
            // a normal row in the CHO file
            if (has_none) {
                for (var j = 0; j < num_attributes; j++)
                    level_data.push(NaN);
            }

            // Skip to line after level info where choice
            // info is found
            row ++;
            choices.push(raw_data[row][0]);
        }
        design_data.push({ id: id, numTasks: num_tasks, numAttributes: num_attributes, 
                           altsPerTask: alts_per_task, levelData: level_data, choices: choices, 
                           hasNone: has_none, extraVars:extra_vars });
        row++;
    }

    return design_data;
}

See also