Tables - Crosstabs

From Q
Jump to: navigation, search

Related Videos
Part 4 of Creating Tables in Q (Video)
 

Generate multiple crosstabs at the same time This QScript generates multiple crosstabs at the same time (also, see Tables - Banner Tables). This is an alternative to using the Basic Tables feature, which automatically flattens any two-dimensional (grid) questions.

Technical detail

You will be asked to select:

  1. The questions use in the rows of the tables (these will be placed in the Blue Drop-down Menu).
  2. The questions use in the columns of the tables (these will be placed in the Brown Drop-down Menu).
  3. Whether you would like to sort the tables based on their significance (p-values) and whether you would like to only show tables with p-values below the selected threshold.
  4. The statistic(s) to be shown on the generated tables. Statistics will only be shown where they make sense for the data selected in the rows and columns of the tables.
  5. (In Displayr only) An (optional) variable to use as a filter and an (optional) variable to use as a weight.

Any Pick One - Multi, Pick Any - Grid and Number - Grid questions will be copied and flattened before using them in a crosstab. The flattened copy will contain all of the cells from the original question in a single column or row.

How to apply this QScript

  • Start typing the name of the QScript into the Search features and data box in the top right of the Q window.
  • Click on the QScript when it appears in the QScripts and Rules section of the search results.

OR

  • Select Automate > Browse Online Library.
  • Select this QScript from the list.

Customizing the QScript

This QScript is written in JavaScript and can be customized by copying and modifying the JavaScript.

Customizing QScripts in Q4.11 and more recent versions

  • Start typing the name of the QScript into the Search features and data box in the top right of the Q window.
  • Hover your mouse over the QScript when it appears in the QScripts and Rules section of the search results.
  • Press Edit a Copy (bottom-left corner of the preview).
  • Modify the JavaScript (see QScripts for more detail on this).
  • Either:
    • Run the QScript, by pressing the blue triangle button.
    • Save the QScript and run it at a later time, using Automate > Run QScript (Macro) from File.

Customizing QScripts in older versions

  • Copy the JavaScript shown on this page.
  • Create a new text file, giving it a file extension of .QScript. See here for more information about how to do this.
  • Modify the JavaScript (see QScripts for more detail on this).
  • Run the file using Automate > Run QScript (Macro) from File.

JavaScript

includeWeb("QScript Table Functions"); // for largeCrosstabsAllowed
// Also includes QScript Utility Functions for inDisplayr()
includeWeb('QScript Selection Functions');  // for getAllUserSelections
includeWeb("QScript Functions to Generate Outputs");  // for createReport
 
if (!Crosstabs())
    conditionallyEmptyLog("QScript cancelled.");
else 
    conditionallyEmptyLog("QScript finished.");
 
function Crosstabs() {
    let ask_statistics = !inDisplayr();
    let user_inputs = userInputsForCrosstabs(ask_statistics, false);
    if (!user_inputs)
        return false;
    let page_names = user_inputs.questions_array.map(qs => crosstabPageName(qs.primary, qs.secondary)); 
    let report = createReport("Crosstabs",
                              user_inputs.questions_array,
                              page_names,
                              user_inputs.statistics,
                              user_inputs.weight,
                              user_inputs.filters,
                              user_inputs.delete_tables,
                              user_inputs.p_value_cutoff,
                              user_inputs.sort_tables,
                              false);
    if (!report)  // All crosstabs not significant at cutoff
        return false;
    conditionallyEmptyLog('Crosstabs have been added to the "' +
                          report.name + '" group.');
    return true;
} 

function userInputsForCrosstabs(ask_statistics, ask_filters_and_weight) {
    let web_mode = inDisplayr();
    let ALLOWED_STATS_Q = ["%", "Column %", "Row %", "Total %", "Average",
                   "Median", "Index", "n", "Column n", "Row n", "Base n"];
    let allowed_stats;
    if (web_mode)
        allowed_stats = ALLOWED_STATS_Q.map(function(s){ return project.translations.get(s); });
    else
        allowed_stats = ALLOWED_STATS_Q;

    const data_file = requestOneDataFileFromProject(true);
    
    let row_col_questions = askForCrosstabQuestions(data_file);
    let row_questions = row_col_questions.row;
    let column_questions = row_col_questions.column;
    if (row_questions.length === 0)  // Abort QScript due to large crosstabs
        return false;
    
    flattenSelectedQuestions(row_questions);
    flattenSelectedQuestions(column_questions);

    let questions_array = row_questions.flatMap(rq =>
                                                        column_questions.map(
                                                            cq => ({primary: rq, secondary: cq})));
    let delete_tables = false;
    let sort_tables = false;
    let p_value_cutoff = Infinity;
    // In Displayr user can sort and delete easily via the toolbar
    if (!web_mode) {
        var show_opts = ["All tables",
                         "Delete tables not significant at the 0.1 level",
                         "Delete tables not significant at the 0.05 level",
                         "Delete tables not significant at the 0.01 level",
                         "Delete tables not significant at the 0.001 level",
                         "Delete tables not significant at the 0.0001 level"];
        // Sorting happens within each row by column variables, so if only one
        // column variable, nothing to sort
        if (column_questions.length > 1) {
	    show_opts.splice(1, 0, "Sort tables by statistical significance of column data");
            show_opts = show_opts.concat(
                             ["Sort and delete tables not significant at 0.1",
                             "Sort and delete tables not significant at 0.05",
                             "Sort and delete tables not significant at 0.01",
                             "Sort and delete tables not significant at 0.001",
                              "Sort and delete tables not significant at 0.0001"]);
	}
        var show_opt_idx = selectOne("Show", show_opts, null, 0);
        var selected_opt = show_opts[show_opt_idx];
        sort_tables = selected_opt.match("^Sort|^All tables sorted") !== null;
        delete_tables = selected_opt.match("[Dd]elete") !== null;
        if (delete_tables)
            p_value_cutoff = selected_opt.match("0[.][0]{0,3}[15]")[0];    
    }
    
    if (ask_statistics) {
        var stat_selection_text = "Which statistic would you like to show on the table? " + 
                      "(Statistics will only be shown where they make sense " +
                      "for the data selected in the rows and columns of the tables.)";
        var selected_stats_idx = selectMany(stat_selection_text, allowed_stats);
        allowed_stats = ALLOWED_STATS_Q.filter((s,i) => selected_stats_idx.includes(i));
    }else
        allowed_stats = null;
    
    let selected_weight = null;
    let selected_filters = [];
    if (ask_filters_and_weight) {
        selected_filters = askForFilters(data_file);
        selected_weight = askForWeightVariable(data_file);
    }
        
    return {questions_array: questions_array, 
            sort_tables: sort_tables, delete_tables: delete_tables,
            p_value_cutoff: p_value_cutoff, statistics: allowed_stats,
            filters: selected_filters, weight: selected_weight};
}

function askForCrosstabQuestions(data_file) {
    var column_q_choices = [];
    var row_q_choices = [];

    var row_q_choices = data_file.questions.filter(function (q) { return q.isValid && !q.isHidden && q.questionType.indexOf("Text") == -1 && !questionHasOnlyMissingValues(q)});
    var column_q_choices = row_q_choices.filter(function (q) { return q.questionType != 'Ranking' && q.questionType != 'Experiment'});

    if (row_q_choices.length == 0 || column_q_choices.length == 0) {
        log("Not enough questions available to make crosstabs.")
    }

    var row_questions, column_questions;
    while (!row_questions || !oneOrMoreQuestions(row_questions))
        row_questions = selectManyQuestions('Please select data to place in the rows:',
                                            row_q_choices).questions;

    while (!column_questions || !oneOrMoreQuestions(column_questions))
        column_questions = selectManyQuestions('Please select data to place in the columns:',
                                               column_q_choices).questions;
    
    if (!largeCrosstabsAllowed(row_questions, column_questions)) {
            return {row: [], column: []};
    }
    return {row: row_questions, column: column_questions};
}

function askForFilters(data_file) {
    let selected_filters = [];  // "Total Sample"
    const filter_questions = data_file.questions.filter(v => v.isFilter);
    if (filter_questions.length > 0) {
        let filter_variables = filter_questions[0].variables
        for (i = 1; i < filter_questions.length; i++)
            filter_variables = filter_variables.concat(filter_questions[i].variables);
        let filter_opts = ["Total Sample"];
        filter_opts = filter_opts.concat(filter_variables.map(v =>v.label));

        selected_filters = selectMany("Please select filter(s) to apply to each crosstab.",
                                          filter_opts, null, [0]);
        if (selected_filters == 0)
            selected_filters = [];
        else {
            selected_filters = selected_filters.filter(function gt0(x){ return x > 0; });
            selected_filters = selected_filters.map(function idx(x){return filter_variables[x-1];});
        }
    }
    return selected_filters;
}

function askForWeightVariable(data_file) {
    let selected_weight = null;
    const weight_questions = data_file.questions.filter(q => q.isWeight);
    if (weight_questions.length > 0) {        
        let weight_variables = weight_questions[0].variables;
        for (i = 1; i < weight_questions.length; i++)
            weight_variables = weight_variables.concat(weight_questions[i].variables);

        let weight_opts = ["None"];
        weight_opts = weight_opts.concat(weight_variables.map(v => v.label));

        selected_weight = selectOne("Please select a weight to apply to each crosstab.",
                                        weight_opts, null, 0);
        if (selected_weight == 0)
            selected_weight = null;
        else
            selected_weight = weight_variables[selected_weight-1];
    }
    return selected_weight;
}

/// primary - A QScript Question, the primary question in the crosstab
/// secondary - A QScript Question representing  the secondary question in the
/// crosstab or either of the strings "RAW DATA" or "SUMMARY".
function crosstabPageName(primary, secondary) {
    let page_name = primary.name || "";
    if (secondary === "RAW DATA") {
        page_name += "- Raw Data";
    } else if (secondary.hasOwnProperty("name")){
        page_name += " by " + secondary.name;
    }
    return page_name;
}

// replicate R's order() function
// e.g. sortWithIndices(['b','c','a']) = [2,1,0]
function order(arr) {
    var arr_with_index = [];
    for (var i in arr) {
        arr_with_index.push([arr[i], i]);
    }
    arr_with_index.sort(function(left, right) {
      return left[0] - right[0];
    });

    var idxs = [];
    for (var j in arr_with_index) {
        // arr.push(arr_with_index[j][0]);
        idxs.push(arr_with_index[j][1]);
    }
    return idxs;
}
 
function addStat(table, stat) {
    cell_stats = table.cellStatistics;
    cell_stats.push(stat);
    table.cellStatistics = cell_stats;
    return table.cellStatistics.indexOf(stat) > -1;
}
 
function oneOrMoreQuestions(questions) {
    if (questions.length == 0) {
        alert('Select one or more questions.');
        return false;
    } else
        return true;
}

function questionHasOnlyMissingValues(q) {
    var v = q.variables[0];
    try {
        var raw = v.rawValues;
    }
    catch(e) {
        return true;
    }
    var all_missing = true;
    for (var i = 0; i < raw.length; i++) { 
        if (!v.valueAttributes.getIsMissingData(raw[i])) {
            all_missing = false;
            break;
        }
    }
    return all_missing;  
}

See also