TURF - TURF Analysis
Performs a TURF (Total Unduplicated Reach and Frequency) analysis.
Example
An example output is shown below for a TURF analysis on bubblegum flavor preferences:
Usage
In Displayr, go to Insert > More > TURF > TURF Analysis.
In Q, go to Automate > Browse Online Library > TURF > TURF Analysis.
To run this function, the data must consist of more than one variable in a binary format (1's and 0's), where the 1's represent selected values and 0's represent options that were not selected. It should look like the following in your raw data:
Variable sets or questions that contain binary data are represented with an icon of either two or four check-boxes.
In the object inspector on the right of the screen, under Inputs > Binary variables select one or more binary variable sets (questions), change any other settings as required.
More information
See our webinar here: TURF for New Product Development Awesome in Less than 20 Mins.
See also our detailed eBook: to Use TURF to Optimize Product Portfolios
Options
Alternatives A Pick-Any question or binary variables indicating respondents' choices of alternatives.
Portfolio size The size of the portfolios to construct.
Must include Alternatives that must appear in all portfolios. These are selected from comboboxes.
Mutually exclusive sets Sets of alternatives that cannot appear together. These are selected from comboboxes.
Minimum alternatives per case Reach is computed based on cases liking at least this number of the alternatives from the portfolio.
Maximum alternatives per case Reach is computed based on cases liking at most this number of the alternatives from the portfolio.
Maximum portfolios to investigate The maximum number of portfolios to investigate before the Monte Carlo algorithm is used instead of the exhaustive search algorithm. The Monte Carlo algorithm is much faster for large problems and almost always provides the same answer as an exhaustive search. The Monte Carlo algorithm starts with a randomly generated portfolio and then uses a "hill climbing" approach to find an optimal portfolio. For each alternative in this portfolio, candidate portfolios are generated by swapping it with alternatives not in the portfolio to find a portfolio with better reach, breaking ties with frequency. It does this while respecting the constraints set by the user, e.g. mutually exclusive alternatives. The algorithm repeats this with the best portfolio from the previous step and only stops when it has reach a local maxima, i.e., no better portfolio is found after exploring all replacements. This is done for 100 randomly generated portfolios and outputs the best portfolios that it finds.
Number of portfolios to keep The number of optimal portfolios to show in the output.
Additional Properties
When using this feature you can obtain additional information that is stored by the R code which produces the output.
- To do so, select Create > R Output.
- In the R CODE, paste: item = YourReferenceName
- Replace YourReferenceName with the reference name of your item. Find this in the Report tree or by selecting the item and then going to Properties > General > Name from the object inspector on the right.
- Below the first line of code, you can paste in snippets from below or type in str(item) to see a list of available information.
For a more in depth discussion on extracting information from objects in R, checkout our blog post here.
Properties which may be of interest are:
- turf$output.table # raw numeric table of the output data
Code
function extractAlternatives(selected_data)
{
let alternatives = [];
let n_data_files = project.dataFiles.length;
selected_data.forEach(dat => {
let alt_guid = dat.guid;
let alt_type = dat.type;
for (let j = 0; j < n_data_files; j++)
{
let df = project.dataFiles[j];
if (["variableset-BinaryMulti", "variableset-BinaryMultiCompact"].includes(alt_type))
{
let matched_question = df.questions.find(q => q.guid == alt_guid);
if (matched_question)
{
let data_rd = matched_question.dataReduction;
alternatives = alternatives.concat(data_rd.rowLabels.filter((x, i) => !data_rd.netRows.includes(i)));
break;
}
}
else if (alt_type == "variableset-BinaryGrid")
{
let matched_question = df.questions.find(q => q.guid == alt_guid);
if (matched_question)
{
let data_rd = matched_question.dataReduction;
let row_labels = data_rd.rowLabels.filter((x, i) => !data_rd.netRows.includes(i));
let col_labels = data_rd.columnLabels.filter((x, i) => !data_rd.netColumns.includes(i));
col_labels.forEach(col_lbl => alternatives = alternatives.concat(row_labels.map(row_lbl => col_lbl + ", " + row_lbl)));
}
}
else if (alt_type.match("^variableset") != null)
{
let matched_question = df.questions.find(q => q.guid == alt_guid);
if (matched_question)
{
alternatives = alternatives.concat(matched_question.variables.map(v => v.label));
break;
}
}
else // variable
{
let matched_variable = df.variables.find(v => v.guid == alt_guid);
if (matched_variable)
{
alternatives = alternatives.concat(matched_variable.label);
break;
}
}
}
});
return alternatives;
}
var heading_text = "TURF Analysis";
if (!!form.setObjectInspectorTitle)
form.setObjectInspectorTitle(heading_text, "TURF Analyses");
else
form.setHeading(heading_text);
let selected_data = form.dropBox({name: "formData",
label: "Alternatives:",
types: ["Question", "Variable"],
multi: true,
required: true,
prompt: "Select data containing the alternatives"}).getValues();
form.numericUpDown({name: "formSize",
label: "Portfolio size",
increment: 1,
minimum: 1,
maximum: Number.MAX_SAFE_INTEGER,
default_value: 2,
prompt: "Higher values will take longer to investigate"});
form.group({label: "CONSTRAINTS", expanded: false});
let cb_alternatives = [""].concat(extractAlternatives(selected_data));
// Must include comboboxes
let cb_count = 0;
let remaining_alternatives = cb_alternatives;
do
{
cb_count++
var cb = form.comboBox({name: "formMustInclude" + cb_count,
label: "Must include",
alternatives: remaining_alternatives,
default_value: "",
required: false,
prompt: "Alternatives that must be included in portfolios"}).getValue();
remaining_alternatives = remaining_alternatives.filter(x => x != cb);
} while(cb != "" && remaining_alternatives.length > 1);
// Mutually exclusive set comboboxes
let set_count = 0;
do
{
remaining_alternatives = cb_alternatives;
cb_count = 0;
set_count++;
do
{
cb_count++
var cb = form.comboBox({name: "formMutuallyExclusive" + set_count + "alt" + cb_count,
label: "Mutually exclusive set " + set_count,
alternatives: remaining_alternatives,
default_value: "",
prompt: "Set of alternatives that cannot appear together in portfolios"}).getValue();
remaining_alternatives = remaining_alternatives.filter(x => x != cb);
} while(cb != "" && remaining_alternatives.length > 1);
} while(cb_count > 2);
form.numericUpDown({name: "formMinAlternatives",
label: "Minimum alternatives per case",
increment: 1,
minimum: 1,
maximum: Number.MAX_SAFE_INTEGER,
default_value: 1,
prompt: "Reach is computed based on cases liking at least this number of the alternatives from the portfolio."});
form.numericUpDown({name: "formMaxAlternatives",
label: "Maximum alternatives per case",
increment: 1,
minimum: 1,
maximum: Number.MAX_SAFE_INTEGER,
default_value: 100000,
prompt: "Reach is computed based on cases liking at most this number of the alternatives from the portfolio."});
form.group({label: "ADVANCED", expanded: false});
form.numericUpDown({name: "formMaxPortfolios",
label: "Maximum porfolios to investigate",
increment: 1,
minimum: 1,
maximum: Number.MAX_SAFE_INTEGER,
default_value: 100000,
prompt: "Higher values will take longer to investigate."});
form.group({label: "OUTPUTS", expanded: false});
form.numericUpDown({name: "formPorfoliosToKeep",
label: "Number of portfolios to keep",
increment: 1,
minimum: 1,
maximum: Number.MAX_SAFE_INTEGER,
default_value: 10,
prompt: "Higher values will take longer to investigate."});
output <- flipData::SplitFormQuestions(formData, include.grid.flag = TRUE)
dat <- output$dat
alternative.names <- names(dat)
i <- 1
must.include <- integer(0)
while (TRUE)
{
matched.index <- match(get0(paste0("formMustInclude", i)), alternative.names)
if (length(matched.index) > 0 && !is.na(matched.index))
{
must.include <- c(must.include, matched.index)
i <- i + 1
}
else
break
}
mutually.exclusive.sets <- list()
i <- 1
while (TRUE)
{
j <- 1
set <- integer(0)
while (TRUE)
{
matched.index <- match(get0(paste0("formMutuallyExclusive", i, "alt", j)), alternative.names)
if (length(matched.index) > 0 && !is.na(matched.index))
{
set <- c(set, matched.index)
j <- j + 1
}
else
break
}
if (length(set) > 1)
mutually.exclusive.sets[[i]] <- set
else if (length(set) == 1)
{
warning("The Mutually Exclusive Set ", i, " has been specified with ",
"only one alternative when at least two are required. It has been ignored.")
break
}
else
break
i <- i + 1
}
turf <- flipTURF::TURF(data = dat,
subset = QFilter,
weight = QPopulationWeight,
portfolio.size = formSize,
max.iterations = formMaxPortfolios,
min.alternatives.per.case = formMinAlternatives,
max.alternatives.per.case = formMaxAlternatives,
n.portfolios.to.keep = formPorfoliosToKeep,
must.include = must.include,
mutually.exclusive.sets = mutually.exclusive.sets,
seed = 123)